whyteboard-0.41.1/0000755000175000017500000000000011444710654013011 5ustar stevestevewhyteboard-0.41.1/whyteboard-help/0000755000175000017500000000000011444710654016107 5ustar stevestevewhyteboard-0.41.1/whyteboard/0000777000175000017500000000000011444710654015165 5ustar stevestevewhyteboard-0.41.1/locale/0000755000175000017500000000000011443353600014241 5ustar stevestevewhyteboard-0.41.1/images/0000755000175000017500000000000011323430531014243 5ustar stevestevewhyteboard-0.41.1/whyteboard/lib/0000777000175000017500000000000011444710654015733 5ustar stevestevewhyteboard-0.41.1/whyteboard/gui/0000755000175000017500000000000011444710654015745 5ustar stevestevewhyteboard-0.41.1/whyteboard/misc/0000755000175000017500000000000011444710654016114 5ustar stevestevewhyteboard-0.41.1/locale/zh_TW/0000755000175000017500000000000011444710653015302 5ustar stevestevewhyteboard-0.41.1/locale/cy/0000777000175000017500000000000011444710653014666 5ustar stevestevewhyteboard-0.41.1/locale/es/0000777000175000017500000000000011444710653014662 5ustar stevestevewhyteboard-0.41.1/locale/de/0000777000175000017500000000000011444710653014643 5ustar stevestevewhyteboard-0.41.1/locale/it/0000777000175000017500000000000011444710653014667 5ustar stevestevewhyteboard-0.41.1/locale/nl/0000777000175000017500000000000011444710653014664 5ustar stevestevewhyteboard-0.41.1/locale/cs/0000777000175000017500000000000011444710653014660 5ustar stevestevewhyteboard-0.41.1/locale/en_GB/0000777000175000017500000000000011444710653015225 5ustar stevestevewhyteboard-0.41.1/locale/fr/0000777000175000017500000000000011444710653014662 5ustar stevestevewhyteboard-0.41.1/locale/hi/0000777000175000017500000000000011444710653014653 5ustar stevestevewhyteboard-0.41.1/locale/ja/0000777000175000017500000000000011444710653014645 5ustar stevestevewhyteboard-0.41.1/locale/pt/0000777000175000017500000000000011444710653014676 5ustar stevestevewhyteboard-0.41.1/locale/ru/0000777000175000017500000000000011444710653014701 5ustar stevestevewhyteboard-0.41.1/locale/ar/0000777000175000017500000000000011444710653014655 5ustar stevestevewhyteboard-0.41.1/locale/gl/0000755000175000017500000000000011444710653014651 5ustar stevestevewhyteboard-0.41.1/images/tools/0000755000175000017500000000000011444710654015416 5ustar stevestevewhyteboard-0.41.1/images/cursors/0000777000175000017500000000000011444710654015762 5ustar stevestevewhyteboard-0.41.1/images/icons/0000777000175000017500000000000011444710654015375 5ustar stevestevewhyteboard-0.41.1/whyteboard/lib/pubsub/0000777000175000017500000000000011444710654017233 5ustar stevestevewhyteboard-0.41.1/locale/zh_TW/LC_MESSAGES/0000755000175000017500000000000011444710654017070 5ustar stevestevewhyteboard-0.41.1/locale/cy/LC_MESSAGES/0000777000175000017500000000000011444710654016454 5ustar stevestevewhyteboard-0.41.1/locale/es/LC_MESSAGES/0000777000175000017500000000000011444710654016450 5ustar stevestevewhyteboard-0.41.1/locale/de/LC_MESSAGES/0000777000175000017500000000000011444710654016431 5ustar stevestevewhyteboard-0.41.1/locale/it/LC_MESSAGES/0000777000175000017500000000000011444710654016455 5ustar stevestevewhyteboard-0.41.1/locale/nl/LC_MESSAGES/0000777000175000017500000000000011444710654016452 5ustar stevestevewhyteboard-0.41.1/locale/cs/LC_MESSAGES/0000777000175000017500000000000011444710654016446 5ustar stevestevewhyteboard-0.41.1/locale/en_GB/LC_MESSAGES/0000777000175000017500000000000011444710654017013 5ustar stevestevewhyteboard-0.41.1/locale/fr/LC_MESSAGES/0000777000175000017500000000000011444710654016450 5ustar stevestevewhyteboard-0.41.1/locale/hi/LC_MESSAGES/0000777000175000017500000000000011444710654016441 5ustar stevestevewhyteboard-0.41.1/locale/ja/LC_MESSAGES/0000777000175000017500000000000011444710654016433 5ustar stevestevewhyteboard-0.41.1/locale/pt/LC_MESSAGES/0000777000175000017500000000000011444710654016464 5ustar stevestevewhyteboard-0.41.1/locale/ru/LC_MESSAGES/0000777000175000017500000000000011444710654016467 5ustar stevestevewhyteboard-0.41.1/locale/ar/LC_MESSAGES/0000777000175000017500000000000011444710654016443 5ustar stevestevewhyteboard-0.41.1/locale/gl/LC_MESSAGES/0000755000175000017500000000000011444710654016437 5ustar stevestevewhyteboard-0.41.1/whyteboard/lib/pubsub/utils/0000777000175000017500000000000011444710654020373 5ustar stevestevewhyteboard-0.41.1/whyteboard/lib/pubsub/core/0000777000175000017500000000000011444710654020163 5ustar stevestevewhyteboard-0.41.1/whyteboard.py0000777000175000017500000000303011444710556015537 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ This is the main module which fires up Whyteboard. It checks if the installed wxPython version is recent enough, as Whyteboard uses newer versions' features. """ import sys import webbrowser import locale locale.setlocale(locale.LC_ALL) if not hasattr(sys, 'frozen'): WXVER = '2.8.9' import wxversion if not wxversion.checkInstalled(WXVER): import wx app = wx.App(False) wx.MessageBox(u"The minimum required version of wxPython, \n%s is not installed." % WXVER, u"wxPython Version Error") app.MainLoop() webbrowser.open(u"http://www.wxpython.org/download.php") sys.exit() import wx from whyteboard import WhyteboardApp WhyteboardApp().MainLoop()whyteboard-0.41.1/TODO.txt0000777000175000017500000000432511444706312014326 0ustar stevesteveWhyteboard 0.41.1 - To do list https://launchpad.net/whyteboard -- http://code.google.com/p/whyteboard/ Fri 17 September 2010 ---- NEW FEATURES ---- see: https://blueprints.launchpad.net/whyteboard * Selecting drawn Pen items with the select tool, to move/re-order, like other tools * Improvements for whiteboard pen usage: accepting certain commands to perform program shortcuts (e.g. new/close tab, clear sheet etc; sent via a tool like ZScreen) * Drag / drop items in the Shape Viewer to re-arrange them * Rich Text Control, allowing any part of text to be bolded/coloured differently to other parts of the text, as opposed to a single colour/font/style. * "Zoom" capability; zoom in and out of the canvas. *Will* be difficult to make * UI changes: - the left panel takes up too much vertical space - remove the text panel, so you're writing on the canvas (with floating "text edit" toolbar, would be cool) * Hyperlinks to web resources * Slider bar to "move" back/forwards through a drawing's timeline in a replay, as well as being able to jump to the start/end * Select tool: group items together to move all items together * Select tool: copy/paste shapes Eventually, long term: * Network support with Twisted (a Python network library) * Poppler support for better PDF rendering - no need to use ImageMagick. -> Problems with Windows here though, Linux is more compatible. Research is ongoing into Poppler + Windows * Improved drawing with anti-aliased graphics, including transparency -> Performance will be much worse, guaranteed * Improved user interface with the possibility of docking/floating panels ------------ At this point, I'm happy with the direction Whyteboard is taking. It is well translated and slowly becoming feature rich, full of small, hidden functionality that makes using the program easier. Despite slow development over the past few months, Whyteboard will continue to expand and will grow a whole new bunch of features. Contributions via the means of feedback, translations, code additions, patches, bug reports, feature ideas are always welcome - they help improve the program's quality, usability and usefulness. - Steven Sproat, April 8 2010whyteboard-0.41.1/README.txt0000777000175000017500000000742711444706256014533 0ustar stevesteveWhyteboard 0.41.1 - A simple image, PDF and postscript file annotator https://launchpad.net/whyteboard -- http://code.google.com/p/whyteboard/ Fri 17 September 2010 ---- TO RUN WHYTEBOARD ---- If you have installed the Windows .exe file, simply run Whyteboard from the start menu. If you have the stand-alone exe, please run whyteboard.exe To run Whyteboard from source, here are the requirements: * Python - 2.5, 2.6, 2.7 (untested on other major versions; should work on 2.4) Whyteboard does not work with Python 3 http://www.python.org/download * wxPython - the latest version is -always- recommended (currently 2.8.11.0). You will want the unicode build. wxPython 2.8.9.0 needed at minimum http://www.wxpython.org/download.php * ImageMagick, possibly GhostScript for Windows users (optional) http://www.imagemagick.net http://pages.cs.wisc.edu/~ghost/ - Windows users may need this for ImageMagick *** Run whyteboard.py to launch the application. If nothing happens, try executing the following command from the console/terminals: python whyteboard.py A bunch of errors should be printed - let me know at, , or report a bug through Launchpad at https://bugs.launchpad.net/whyteboard If a bug occurs while the program is running, the built-in error reporter will appear. Use this to send me an e-mail directly, which will contain relevant system information and a log of the error. This is the best method to report errors. Please fill in as much detail about what you were doing so that I can reproduce the error. Giving your e-mail address will allow me to get back in touch with you to follow up on the bug, and to confirm a fix. ** Feedback can be sent from the program, click on "Help", then "Send Feedback", which will bring up a dialog. Please enter an e-mail address so I can reply about any issues/feature suggestions. ** If you are getting an error starting the program on Windows, "Application failed to start because the application configuration is incorrect. Reinstalling the application may fix the problem." ...then download the C++ Runtime. 4.0 MB: http://www.microsoft.com/downloads/details.aspx?familyid=A5C84275-3B97-4AB7-A40D-3802B2AF5FC2 - or use this 64 bit version if you have Windows 64. 4.7MB http://www.microsoft.com/downloads/details.aspx?familyid=BA9257CA-337F-4B40-8C14-157CFDFFEE4E ---- KNOWN BUGS ---- * Printing quality may be bad * Exporting PDFs creates an image of your sheets and then places that image into the PDF. Be careful with overwriting. * Media Tool may take several file loads to actually load the file correctly. * Media Tool may not resize the video/control panel properly * Dragging and dropping text from Firefox will 'hang' the program until the text dialog is closed * Copying/pasting text with tab characters do not display the tabs on Windows See up to date reports at https://bugs.launchpad.net/whyteboard Identified and confirmed bugs are always fixed before a new release. ---- LIBRARIES / SOFTWARE USED ---- Python, core programmling language - http://www.python.org/ wxPython, GUI framework - http://www.wxpython.org ImageMagick, image editing suite - http://www.imagemagick.net ConfigObj, Python configuration files - http://www.voidspace.org.uk/python/configobj.html Editra Control Library, extra wxPython widgets - http://editra.org/eclib Pubsub, publish/subscribe API -- http://pubsub.sourceforge.net Tango Icon Library - http://tango.freedesktop.org/Tango_Icon_Library py2exe, helps compile python files to windows .exe - http://www.py2exe.org/ GUI2Exe, python compiler front-end - http://code.google.com/p/gui2exe/ InnoSetup - windows installer creator - http://www.jrsoftware.org/isinfo.php UPX, .exe compressor - http://upx.sourceforge.net/whyteboard-0.41.1/LICENSE.txt0000666000175000017500000010451311146270707014643 0ustar stevesteve GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 3 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, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . whyteboard-0.41.1/DEVELOPING.txt0000777000175000017500000001534411441301645015215 0ustar stevesteveWhyteboard - Help for developers Version 0.41.1 - document last updated: 1 May 2010 --------------------------------- This document serves to help a developer who wishes to contribute / understand Whyteboard's code. You'll probably need some knowledge of Python + the wxPython GUI toolkit, as Whyteboard is developed with these, using some advanced-ish concepts. ---------------------------------- The main class is GUI in gui.py. It consists of a wx.Frame - a "window" to the general end-user, containing the title and close/minimise button etc. It has references to its contained panels - the control panel on the left, menu bar, tool bar, the canvas itself and the panel on the right. The gui contains the global event bindings, e.g. undo/redo, copy/paste. It dispatches function calls to relevant classes, for example undo will tell the current tab to undo its last operation, Or, saving a file will involve prompting the user for the location to save the file to, and then call a function in the utility class to actually save the data. Since the program is tabbed, the gui keeps a reference to its active canvas. The gui updates its canvas reference when opening/closing/changing tabs. Often, we want to perform operations only on the current tab, e.g paste an image into a tab, export the current tab as an image and so on. Thus knowing the current tab is useful. The Canvas contains the drawable canvas - a scrollable window. It has its own event bindings and no title bar. Multiple Canvases will exists at a given time, and the GUI presents these views to the user through its tab list, thumbnails panel and the Notes tree. The Canvas class contains a list of shapes that has been drawn upon it. Each instance of this class will have its own unique shapes list. This is how each Whyteboard tab can perform its own undo and redo without affecting any other Whyteboard instances. Tools.py defines the shapes - different drawable tools that the program uses. At the bottom of the file is a list named "items" -- this is the list of tools the user can draw with. From this, buttons, icons, translated names, hotkeys are generated. Here are some interesting methods/properties each shape provides: - name - translated name - hotkey - keyboard shortcut key - left_up() - called when drawing - left button pressed - left_down() - left button up - double_click() - double clicked - right_up() - right button up - motion() - mouse motion while left mouse button is held down - hit_test() - a check if an x/y point is "inside" the shape - load() - called when loading the shape - save() - when saving - properties() - a text description of the shapes' properties, e.g. x/y coords - preview() - tool preview drawn in the left panel These allow you to customise your tools easily. There is an object hierarchy, as so: Tool | --------------------------------------------- | | | | Eyedrop OverlayShape Select Media | ------------------------------------------------------------------------- | | | | | | Image Rectangle Circle Polygon Text Line | | | | -------------------------------------- Pen Note Arrow | | | | BitmapSelect RoundedRectangle Ellipse ---------- | | Eraser Highlighter Many of the classes at the bottom of the list simply extend the base classes' behaviour - nested deeply in the hierarchy, they have many inherited methods. Most of these classes invoke their superclasses' methods. For example, draw() in OverlayShape will draw the shape overlayed on the canvas, meaning it is not permanently drawn on the canvas as the user moves their mouse around. Note needs to extend this behaviour by also drawing a background to the shape -- so first calls draw() on its superclass, Text, which will draw the text, and invokes draw() on OverlayShape. So, to create your own tool you'd need to extend from an existing class (most likely OverlayShape) and override the appropriate methods, remembering to call super() on its parent. Also, add your class to the items list at the bottom of tools.py, making sure the filename defined in its "icon" attribute exists under /images/tools/[icon].png The file "meta" contains read-only metadata used by the program. This may seem a nasty way of having global data, but in Python, but the values are read-only and the module isn't tied to any classes. It contains common data such as the version number, configuration scheme, list of translators, whether the host system supports transparency etc. ---------------------------------- The source files are organised as follows: lib/ - third party modules fakewidgets/ - mock GUI classes for unit testing canvas.py - canvas class dialogs.py - dialogs and their events, e.g. Progresss bar / resize / history event_ids.py - custom wxPython IDs for events functions.py - misc. stand-alone functions that can be used anywhere gui.py - main program frame, wx.App meta.py - meta data, meant to be dependent on wxPython only panels.py - control panel, notes, thumnbails preferences.py - preferences dialog tools.py - class definitions for the drawing tools unitests.py - unit tests utility.py - a class the GUI references that performs saving/loading files, getting file paths and misc. things ------------------------------------------ CODING PRACTICES * Try and follow Python's Pep 8 guide http://www.python.org/dev/peps/pep-0008 * 80 characters per line can be hard to use at times and can be broken in some cases. * Name methods like_this, not AsSuch. As wxPython uses CamelCased method names, it easily allows you to differentiate between wx and Whyteboard methods. Very useful for dialogs and custom panels. * Try and write a unit test for your functionality to assert it works correctly * Learn to use PubSub. It will help to decouple objects and even modules' dependencies on each other which is always a very good thing, and allows for easier testing * Be wary of performance issues when performing loops. * Python has a high overhead for functions calls so please be wary of that, too. * Translate every string like _("so"). Make sure to use variable placeholders instead of string concatination as this gives translators more context _("The value is %i and its name is %s") % (x, name) is better than _("The value is ") + x + _(" and its name is ") + namewhyteboard-0.41.1/CHANGELOG.txt0000777000175000017500000011133311444706663015057 0ustar stevesteveWhyteboard 0.41.1 - A simple image, PDF and postscript file annotator https://launchpad.net/whyteboard -- http://code.google.com/p/whyteboard/ Fri 17 September 2010 ---- VERSION HISTORY ---- ---------------------------------------------------------------------- 17 September 2010 - 0.41.1 * New icon * Windows Vista and 7 - icon now shown in Explorer and the taskbar * New About Dialog for Windows: looks like GTK, and less crappy than before * Program translated into Portuguese, latest translations applied. * PDF Cache viewer's width increased; delete key also removes the selected item * Bugfix: close button on a sheet allowing the first sheet to be closed, causing crashes * Bugfix with closing the "import image" dialog * Bugfix: unicode error on loading a file from the operating system "explorer" * Bugfix: crash when pressing "okay" in Preferences dialog after changing number of closed sheets to remember Bugfix: misc. items not being translated: ok/cancel buttons, text in colour, save and font dialogs * Bugfix: file types in the "open" dialog not being translated * Bugfix: languages in the preference dialog not being translated ---------------------------------------------------------------------- 30 August 2010 -- 0.41 * "Recent Files" list under the File menu is synchronised across all Whyteboard instances * Improved program updating; removed a dependency on an external library * Fixed possible Unicode errors on any saving operation to a filepath containing unicode characters >255 (e.g. ø) * Program language defaults to the system language if possible * Change Foreground / Background options added to Shapes menu and to a shape's right-click pop-up menu. Background's changeable when shape is not transparent * Removed code that was handling loading older .wtbd save files from around a year back, as it's no longer needed * Removed version number from application title (see About Dialog for it) * Little "*" shown in program title when application has unsaved data * Removed shortcut key information from popup menus * Shape viewer dialog handles delete key. * Deleting shape from shape viewer selects the previous shape; not the first shape in the list. * Removed "focus" rectangle from the currently selected Tab * Bugfix: possible permission error on Linux when saving a file * Bugfix - trying to load a file that's been deleted from the Recent Files list didn't save the file list, so the file would still be shown * Fix bug on Mac where wrong version of wxPython was being used * Fix bug with crash on startup with an older install of wxPython * Fix bug where program was writing the Language preference as the translated language; the program was expecting the language in English * Bugfix: Using the mouse wheel on the "thickness" selection drop-down with a shape selected would create an undo point for each "scroll" of the mousewheel. Now it creates an undo point after you have finished scrolling. * Fix "transparent" checkbox under Shape menu and on the toolbox displaying wrong values at times. * Fix possible "division by zero" bug with Ellipse tool * Fix tab ordering on many dialogs * Fix cancelling the Save As dialog in save changes prompt closing the dialog * Fix the eraser tool appearing as a black square * Fix undo/redo not making the "sure you want to quit" prompt appear * Fix preference dialog "view" tab always scrolling to the bottom of the dialog * Fix bug in opening help files if they had been removed after the program had been run * Fix opening a save file not clearing the undo tab list * Fix "Delete" in Shape Viewer actually deleting the shape; and not on "okay" * Fix the selected item being de-selected in the Shape Viewer, and overriding any resized columns headers after performing an action * Fix "Gdk-CRITICAL **: gdk_gc_set_foreground: assertion `color != NULL' failed" GTK bug when drawing with a Pen * Bugfix: Error dialog wouldn't show with unicode characters in "recently saved" preference option * Major changes to the program's file structure. Lots of code cleanup and refactorings ---------------------------------------------------------------------- 17 May 2010 -- 0.40.1 -- Bugfixes: * Possible crash with deleting shapes * Shape Viewer's buttons not being properly disabled on changing sheets * Opening a .wtbd file from a file explorer wouldn't work * Windows: function and other shortcut keys not working (e.g. F2/F11/Shift+Tab) * Windows: Shape Viewer dialog drawing its bitmaps as enabled when they were disabled * Windows: Ctrl+Enter in text input dialog not acting like "okay" button press ---------------------------------------------------------------------- 16 May 2010 -- 0.40.0 -- New features/changes: * New tool: Highlighter. It behaves as the Pen, but draws in a semi-transparent ink; perfect for drawing attention to portions of a PDF image * Image rotation is now performed from a special "handle" for rotating. Rotating draws a transparent rectangle "over" the image to show where its rotation will end up, instead of drawing the image as it rotates (which was slow) * Image re-scaling using its selection handles. Also draws a transparent rectangle to indicate its new size * PDF Cache Viewer - view the list of PDF files that Whyteboard is caching. From here you can remove cache entries, meaning that the PDF will be re-converted. * Improvements to rotating an image with the mouse: now "follows" the mouse's position, and can be rotated both clockwise and counter-clockwise. Thanks to Zack Buhman and members of Stack Overflow for help with the coding * Scaling of Polygon shapes - increase or decrease their size! * Rotating Polygon shapes. Note that all scaling/rotating operations are from the *center* of the shape * 8-way selection handle on rectangle, rounded rectangle and ellipse, allowing vertical-only / horizontal-only resizing * Right clicking a shape with the select tool pops up a menu with select/delete/ edit/move operations. Also, from here, new points may be added to a polygon * Changing sheets scrolls the new sheet's thumbnail into view * Few UI changes, program now looks neater with less borders, also takes up less vertical space (takes up ~700 pixels vertically at a minimum) * Removed many Message Boxes, tried to present more helpful error text to the user when an error occurs * Programs remembers last opened file's directory the next time the program runs * Foreground/background colour swap button added * "Swap Colours" menu item for when you select a shape that's not transparent * New toolbar items for Move Shape Up/Down/To Top/To Bottom * Recently Closed Sheets as a sub-menu to allow you to choose a sheet to re-open * Improved "save changes?" dialog when you are exiting the program. Now tells you how long it was since you last saved, and the buttons have better text than "yes/no/cancel" * Thick rectangles now don't have a "roundness" to their edges * Help files updated and improved with more detail * Improved a few icons (circle/ellipse/polygon/rounded rectangle, move up/down * Little "close" button drawn on the current tab * Up/down/left/right arrow keys can pan around the canvas. * The selected shape may be moved around the canvas using the arrow keys too * Escape key will de-select the currently selected shape (as well as Ctrl-D) * Cancelling PDF Conversion on Linux takes a few seconds, so the progress bar no longer "pumps", and the dialog title changes from "Converting" to "Cancelling" * The "Shape Viewer" is now in sync with the canvas: adding, deleting, undoing, redoing, editing and changing shapes' order is now reflected when the Shape Viewer is open * Minimise button added to the Shape Viewer * Undo closed tabs remembers the "viewport" of the canvas when closed * Removed the "Rotate Image" dialog * Ctrl+Enter in text input dialog will "submit" the text * Restructured source code directories -- now just run whyteboard.py from the main directory to start the program * Started improving code: removing module dependencies and tight coupling, to create more readable/maintainable code. -- New Preferences / Options: * Under "View", number of toolbox columns, can be set to 2 or 3 to decrease the vertical space used by the toolbox * Toggle tool preview on/off (to gain more vertical space) * Toggle colour grid on/off * "Send Feedback" added to the Help menu * Command-line extras added (inc. help). Call whyteboard -f FILE to load a file; whyteboard -l LANG to set a language e.g. es, spanish, nl, dutch, de, german -- Bugfixes: * Important Windows bugfix: drawing a shape would create extra Windows "GDI" objects; which was taking up unneeded resources. This bug has been present since version 0.25 (3rd release) - released back in January 2009! * Recently closed tab list was deleting incorrect entries when it had more items than the "number of closed tabs" preference * Bitmap Select could cause some visual anomalies * Possible crash when loading/saving .wtbd files containing text * After selecting a shape (and being brought to the front), any shapes that were on top of the shape would prevent the user from selecting the handles of the selected shape. * "Recent Files" menu wasn't working on Linux * Rotated images weren't being saved as rotated * Shape Viewer's column widths would not take up the full space * Fixed shape viewer crashing when an item isn't selected * Using next/previous sheet buttons in the Shape Viewer wouldn't set the drop-down control to the changed-to sheet * "End" key would not jump to the end of the document correctly * Menu were icons appearing for 3 menu items on Windows when no other menu items had icons - now removed * Stopped a crash when wx.GCDC was not implemented on the system, due to a misconfigured wxPython install. Now, the program just doesn't use transparency for these users (new highlighter tool is also not available) * Hopefully stopped a visual oddity/black square appearing underneath the close and maximise button on Windows * Hopefully fixed problem with various "UnicodeError" (the problem doesn't happen on my machine, so it's hard to test if the issue is fixed) * Fixed unit tests so that they at least run -- need to improve the test's code coverage, which will come with time. ---------------------------------------------------------------------- 31 Dec 2009 -- 0.39.35 * Bugfix: crash with loading fonts saved on Windows into Linux that don't exist there on Linux, and vice versa * Bugfix: bitmap select, and pasting images were broken * Bugfix: pasting the same image multiple times would create multiple copies of the image when saving; now saves one file and refers all copies to that file * Bugfix: toolbox acting weird when changing between text/icons twice or more from the preferences * Few translation improvements ---------------------------------------------------------------------- 28 Dec 2009 -- 0.39.3 * New save format. ".wtbd" files are now renamed ".zip" files. The zip files now contain all loaded/imported images (and images from a PDF conversion) to allow the save file to be transferred from one computer to another. The "old" .wtbd files are also compatible, and will be converted to the new format when they are saved with this version of the program. Of course, this new change means that the .wtbd files become larger as more images are loaded Another downside is there is a performance hit to loading/saving files - I haven't benchmarked the difference. It will be more noticeable with saves with many open sheets, and with many images. * Open file dialog defaults to the new "all supported files" rather than .wtbd files - so all supported image types, PDF/PS/SVG and .wtbd files are "matched" * Open file dialog remembers the last opened directory * Recently opened files sub-menu * "Reload preferences" added to File menu * Support for drag and dropping a file onto a Media panel to load it * Select/deselect and Delete options added to a Note's right-click menu in the Note tree * ImageMagick's location can be set from the preferences * User's preferences are added to the bug reporter to help debugging * Many "keyboard accessors" for menu items added where missing * "Save As" fills in the filename with the current loaded filename * Latest translations * Bugfix: exiting the program with multiple Whyteboard instances through "new window" would close *all* instances instead of just one * Bugfix: moving the media panel will maintain a mouse "offset" (not place the panel's top-left corner at the mouse position) * Bugfix: deleting a Note wouldn't remove it from the Note tree * Bugfix: crash when selecting the "shapes" menu with a Media panel selected * Bugfix: undoing closed tabs will re-create any Media panels * Bugfix: possible crash with the clipboard being already open * Bugfix: possible crash on the "open file" dialog * Bugfix: Polygon's preview was drawing incorrectly * Bugfix: image "filter" on open file dialog wouldn't recognise upper case files on Linux * Bugfix: "import image" wouldn't set the correct dialog filter when set to a language other than English ---------------------------------------------------------------------- 16 Dec 2009 -- 0.39.2 * New Media tool - embed audio and video files into Whyteboard! Each sheet may have as many Media panels embedded as you wish * New Polygon tool - draw a shape with [x] many points, as complex as you wish. Each point can then be adjusted separately * Paste text from the clipboard. Doesn't preserve any font/colour data, though * Pasted images "into a new sheet" will resize the canvas to the image size * Support for drag/drop of multiple files; e.g. drag/drop 3 .jpg files will open 3 new sheets, each containing one of the images * Support for drag and dropping text. However, dragging text from a web browser will not work currently, as it is not recognised as "plain" text * Stopped a crash when PDF conversion with ImageMagick fails (due to no install of GhostScript) * Saving a new file suggests the current date/time as the filename * The Arrow tool's arrows are drawn at a smaller degree * Middle clicking a sheet in the tab list will close it * A check for a minimum wxPython version of 2.8.9.0 * The uninstaller on Windows will prompt for feedback on why Whyteboard is being uninstalled * Updated help files * Bugfix: the program's process could sometimes remain after closing the program * Bugfix: text objects were being re-drawn twice after using the Select tool * Bugfix: can now drag/drop files onto the canvas once again (0.39.0 broke it) * Bugfix: Changing background colour with a shape selected would change its background colour, even with "transparent" checked * Bugfix: Exporting as PDF on Linux without typing ".pdf" into the filename now works * Bugfix: Updating the EXE was broken on Windows, now fixed * Bugfix: ensured some strings were displayed in their translated form * Bugfix: sheet counts were appearing wrong, sometimes. * Bugfix: pasting with the menu/toolbar buttons could position the image wrongly * Bugfix: copying a Bitmap Select "outside" of the canvas could cause a crash * Fixed a few spelling mistakes * Latest translations. New translation: - Galician ---------------------------------------------------------------------- 17 Nov 2009 -- 0.39.1 * Arrow tool * Bugfix: "cancel" in the "do you want to save before quitting" dialog would quit, not cancel the dialog * Bugfix: keyboard shortcuts for selecting tools were not working on Windows ---------------------------------------------------------------------- 16 Nov 2009 -- 0.39.0 * Background colour added - fills in shapes with solid colour. Right click a colour in the grid or with the Eyedropper to set that colour as the background * Checkbox below the colours for transparency - ignores the background colour * Export as PDF functionality (exports all sheets as images; requires Image Magick) * Import/export Whyteboard preference files * Bitmap Select now uses a fancy transparent blue fill (default is off - may be slow on older machines. Toggle it on in the preferences) * Drag and drop sheets to reposition them as well as the notes tree / thumbnails * The currently selected tab is highlighted with the transparent fill * Move shape up / down / to top / to bottom feature, with keyboard shortcuts * Shape viewer - see a list of each tab's shapes and their properties, and move their drawing order around. * Bugfix: When resizing the canvas to the right, the cursor was misaligned * Bugfix: Cancel button for PDF conversion on Windows would take a long time to actually cancel, it is now practically instant * Bugfix: Default font preferences's label was messed up when a default font preference was not set (on Windows) * Bugfix: Some crashes when loading older .wtbd files fixed * Bugfix / UI: Current shape de-selects with bitmap select / export / print, so the handles are never copied/drawn into the PNG/print * Bugfix: Rotate cursor on Linux looked bad * Bugfix: Incompatibilities between Windows and Linux saved .wtbd files. Should now be able to load files saved on Windows that was saved on Linux; vice versa * Bugfix: Printout title was aligned too low on the page * Bugfix: Pasting to a new sheet now places the image at the top-left corner * Bugfix: Select tool causing "flickering" when deselecting -- I said this was fixed last release, but the problem remained. NOTE: Whyteboard will draw the most recently drawn shape first. If you draw a rectangle, draw 20 other things on top of it, then move the rectangle - the rectangle will still be drawn last (and thus, be covered by other shapes if they're on top of it) * UI: "Apply" buttons placed where appropriate (resize canvas, rotate image) * UI: Resize Canvas dialog now shows the memory size of the new canvas in MBs * UI: Keyboard hotkeys for selecting tools - e.g. P for Pen, R for Rectangle. See each item's tooltip for specifics * Hotkeys for navigating the canvas with keyboard: home, end, page up, page down. Use to jump to the very left (home) and very right (end) of the canvas. Hold Ctrl as you press the keys to jump to the top and bottom of the document. * UI: Wordwrapped texts that could become too long in different languages * UI: Repositioned some menu items; new menu category -- Shapes * UI: Preference to toggle the page title in a printout * UI: Export sub-menu in File, instead of 3 separate entries * UI: Escape key closes full screen view * UI: Black border drawn around thumbnails on Windows, looked weird without * Latest translations. New language: - Arabic ---------------------------------------------------------------------- 28 Oct 2009 -- 0.38.8 * Resize canvas through a menu dialog, or by dragging the edges of the canvas to resize (like in ms paint). Many thanks to Michael H. * Support for printing, print preview, page setup * Rotate an image via its selection handles, or through a dialog * An error handler, which will gracefully handle any errors, and allow them to be submitted to me by loading a simple website * The toolbox in the left-hand pane can be viewed as images instead of text buttons (default: icon view) * Delete shape functionality (select a shape with Select tool, press Delete), or Edit->Delete Shape, or via the new toolbar icon * Additional preference tab: "View". Moved statusbar/toolbar options here, as well as allowing the user to change between text/icon views * Bugfix: Preferences' "Select Language" list was always shown in English * Bugfix: Bold headings in "Font/Colour" preferences had a large font in Windows * Bugfix: Eraser's cursor was offset slightly, and erasing the wrong area * Bugfix: Pasting now pastes the image under the cursor, not at the top-left * Bugfix: Translations not loading on all versions! * Bugfix: Select tool causing "flickering" when deselecting * More translations; now complete in Russian, Spanish, Italian and Welsh. Over 85% done in Dutch and German. Thanks to all contributors! New languages: - Traditional Chinese - Portugese - Japanese - Russian - Hindi ---------------------------------------------------------------------- 17 Oct 2009 -- 0.38.5 * Support for internationalization - Whyteboard will be available in other languages once translated. Currently (almost) fully translated into: - Dutch - English - Italian - Welsh and, partially: - Czech - French - German - Spanish * User preferences, allowing customisation of the program: - Language - Preferred default font - Your 9 colours in the left-hand panel - PDF conversion quality options (normal/high/highest) - How many sheets to remember for "undo closed sheets" - Toggle toolbar / statusbar * Whyteboard remembers your converted PDF files' image locations, so when you load that PDF again, it will not need to convert it. * Export all sheets (as a series of images) functionality * Cancel button added to the PDF conversion progress bar * Added right-click pop-up menus to each thumbnail / Note tree item. From here you can also rename, close or export that sheet * User Interface improvements * Help Files improvements * Eraser now erases a larger surface * Shift + Tab / Shift + Ctrl + Tab shortcuts now work on Linux (GTK at least) * Bugfix: the "Paste" menu was always active, even with nothing to paste (fixed) ---------------------------------------------------------------------- 06 Oct 2009 -- 0.38.1 * Emergency Bugfix - closing any sheet would always close the first sheet * Bugfix - closing the current tab would not remove its Notes from the tree view * Improvements and fixes to renaming sheets - the note tree item and thumbnail text are renamed * Hopefully (?!) fixed a bug on Windows where the same drawing would appear on every sheet * Moved this file into CHANGELOG.txt, formatted it nicely ---------------------------------------------------------------------- 04 Oct 2009 -- 0.38.0 * Select tool: can select shapes to alter their colour/thickness, edit text and notes, move shapes and resize them. Selected shapes are drawn with an outline "handle" at their corners -- Images and text can also be repositioned -- can double click text/notes to edit them -- moveable shapes are shown by the cursor changing to a hand * Text input dialogs remembers the last font used, which is selected by default when creating new Text/Notes -- the chosen font is also saved into the .wtbd save file * Improved undo/redo to support editing, moving and resizing of objects as described above. Text edits can also be undone * Button to change colour in text input dialog, instead of always drawing with the user's chosen palette colour * Thumbnail label shows the selected thumb label in bold * The Pen tool now draws in response to a single mouse click; before the mouse needed to be moved to draw * Each sheet can have its own BitmapSelect at anytime. Undoing and redoing will not remove the selection; drawing a new shape will * Help files updated to reflect new changes and clarify any issues before * Mouse x/y position tracked in the status bar * Over 10 bug fixes * Misc existing code improvements and performance increases * More/better unit testing to help with adding new features, tracking down potential bugs and increased code confidence ---------------------------------------------------------------------- 06 May 2009 -- 0.37.0 * 'Check for Updates'- Whyteboard can update itself -- will download an .exe / .tar.gz as appropriate -- on Windows, running via source will download the tar, which is cool because Windows doesn't support .tar.gz by default -- shows progress of downloaded file - program restarts with new version loaded, also re-loads the current .wtbd * HTML Help system/manual built into the application -- well, via a folder containing HTML help files -- if they are not present, they can be downloaded (optional) * 'About Box' standardised * Exit dialog more like other apps: "sure you want to save?" (yes/no/cancel), instead of "sure you want to quit?" (yes/no) -- also asks when opening a new .wtbd file with an unsaved .wtbd file open -- and after downloading the update, before the program restarts it will prompt for save: yes/no ---------------------------------------------------------------------- 20 Apr 2009 -- 0.36.7 * Memory use improved from undo closed sheet improved * Bugfix: paste / paste as new sheet - not updating the thumbnail * Backwards/forwards compability: from this version onwards, Whyteboard will not change the version inside saved .wtbd files that are created in a newer version of Whyteboard. i.e. version 0.36.7 won't change the version inside a file saved in 0.36.9 (before, it would). It will update the version from a file saved in < 0.36.7 to 0.36.7 ---------------------------------------------------------------------- 13 Apr 2009 -- 0.36.6 * Undo closed tabs, last 10 tabs are stored * Bugfix: closing a sheet would make all other sheet display the closed sheets' image until drawn on * Bugfix: Saving a document which has Notes would result in duplicate notes being visible in the Notes tree view. * Windows exe filesize reduced: 14.2MB -> 4.78MB (!) ---------------------------------------------------------------------- 08 Apr 2009 -- 0.36.5 * Bugfix with drawing "outlined" shapes having inverted colours lines when drawing over other colours. * Bugfix with opening new window with Windows EXE * Bugfix: thumbnails not being drawn white initially on Windows * Added icon into the EXE ---------------------------------------------------------------------- 08 Apr 2009 -- 0.36.4 * Paste image from clipboard into a new sheet * Toggle full screen view * Tool panel is now collapsible to give (a little bit) more room in full screen * Bugfix with eraser cursor in Windows * Bugfix with Windows not drawing new lines from text input * Misc. code improvements * Performance increase from drawing shape 'fix' added in 0.36.2 ---------------------------------------------------------------------- 07 Apr 2009 -- 0.36.3 * Tooltips for each item on the toolbox * "New Window" menu item to launch a new Whyteboard instance. * Important Windows bugfix: .wtbd drawings being "overwritten" on load. * Important Windows bugfix: .wtbd drawings not restoring the saved selected tool ---------------------------------------------------------------------- 05 Apr 2009 -- 0.36.2 * Paste image support. Pasted images will be saved to a temporary directory; duplicated pasted images will be saved in one file. * Copy selection as bitmap with new tool: RectSelect * Popup menu on the sheet bar. Added "rename" option for sheets, which are saved into a save file. Can also close a sheet, open a new sheet or export the selected one from the menu * Cleaner code for managing UI button enabling/disabling * Bugfix: Editing a note and pressing backspace updates the note properly * Bugfix: drawing outlined shapes weirdness outside the default scrollbar region (introduced in 0.35.8) * Bugfix: exporting image saves whole sheet, not just the visible area * Bugfix: Adding a line sometimes wasn't being actually added ---------------------------------------------------------------------- 02 Apr 2009 -- 0.36.1 * Fixed an issue with 'flickering' on Windows * Bugfix: "edit" right-click popup menu on the Note root node. * Preview for the eyedrop (just shows current colour) ---------------------------------------------------------------------- 30 Mar 2009 -- 0.36.0 * Windows UI improvement: change the thickness by scrolling the mousewheel on the drop-down box, no need to click it. (this is the default behaviour under GNOME) * Next/Previous sheet in the "Sheets" menu (which previously was "Image") - Ctrl+Tab / Ctrl-Shift+Tab shortcut keys * Side panel is now "collapsible" instead of a "toggle" menu item * Menus renamed, organised differently * Popup context menu on Notes tree: Edit (note), Switch To (sheet) * Text/notes appear on the canvas as you type * Eraser has a custom rectangle cursor, depending on its size * Escape button exits "History" dialog * Updated "About" menu * Bugfix: can no longer add a blank text/note object ---------------------------------------------------------------------- 25 Mar 2009 -- 0.35.9 * Misc. UI improvements: more labels/small grid of drawing colours * Added eraser tool * Flood fill temporarily removed, was too buggy * Tabs renamed to Sheets * Import -> PDF/PS/Image menu items (as well as through Open) ---------------------------------------------------------------------- 20 Mar 2009 -- 0.35.8 * Windows bugfix: outlined shapes not drawing properly * Windows bugfix: wxPython errors on closing the application with loaded images * Bugfix with the eyedropper not updating the colour label. ---------------------------------------------------------------------- 18 Mar 2009 -- 0.35.7 * Bugfix: toggling side panel on/off caused a lot of lag, should only be noticeable now with around 65+ tabs open. * Drag and drop support: drop any file that Whyteboard supports into the drawing panel to load it * Hold down the middle mouse button to scroll (was the right btn) ---------------------------------------------------------------------- 18 Mar 2009 -- 0.35.6 * Improved undo/redo functionality. It is now possible to undo and redo the clear all drawings/clear all tabs' drawings functionality * Redo is also fixed to more like other applications now: when you undo twice and then draw a new shape, your redo history is lost. Before you could redo the two undone shapes after the new drawing * The ImageMagick folder locator will only pop-up when trying to convert a file and when the directory is not set, not at application start-up ---------------------------------------------------------------------- 17 Mar 2009 -- 0.35.5 * Added dragging around Whyteboard by holding down the right button and moving the mouse ---------------------------------------------------------------------- 15 Mar 2009 -- 0.35.4 * Big bugfix on Windows: ImageMagick's convert program not being found. Whyteboard prompts for its installed location and remembers it. (will also notify Linux users if ImageMagick isn't installed) ---------------------------------------------------------------------- 11 Mar 2009 -- 0.35.3 * Bugfix: thumbnails getting cut off with too many tabs * Can load in a file from the commmand line when running Whyteboard ---------------------------------------------------------------------- 10 Mar 2009 -- 0.35.2 * Bugfix: Can no longer cancel file load progress dialogs. * Performance increase: loading large .wtbd files * Notification of "updating thumbnails" after load/converting a file ---------------------------------------------------------------------- 09 Mar 2009 -- 0.35.1 * Bugfix: thumbnails not updating on file load ---------------------------------------------------------------------- 08 Mar 2009 -- 0.35.0 * Bugfix: a thumbnail's text label not being removed when the thumbnail was removed. * Side panel is now tabbed to select between thumbnails and a "tree" view of all Notes (new feature) for each tab. Notes are similar to how text is input, except a light yellow background is drawn around it to indicate it's a note. In the tree control, a note item can be double clicked upon to bring up the text input dialog to change a note's text ---------------------------------------------------------------------- 04 Mar 2009 -- 0.34.6 * Bugfix under Windows: toggling thumbnails would cause an error. ---------------------------------------------------------------------- 22 Feb 2009 -- 0.34.5 * Bugfix with drawing 'outlined' shapes. Began work on text 'notes'. * F5 will refresh all thumbnails since they're not refreshed upon loading a save, PDF or PostScript file. ---------------------------------------------------------------------- 21 Feb 2009 -- 0.34.0 * Export current tab's view as an image. * Live thumbnails, get updated when the selected tab is drawn upon * Thumbnail panel toggleable on/off * Drawing Preview window now shows a preview of the actual shape instead of always showing a line. ---------------------------------------------------------------------- 21 Feb 2009 -- 0.33.0 * Text input overhaul, before the text was rendered as a wxPython widget - now it's a drawing of a String, with a custom dialog for inputting the text and selecting a font. This new method fixes many problems with having the text as widget, such as text being un-undoable and drawings not overwriting the text * Long text updates the scrollbars, vertically or horizontally * Program maximises on startup. * History improvements: draw all shapes back in the correct order * Save files store the version number; loading an older save file into a newer Whteboard version which has added save data will say the older save file's version ...only problem with that is that the previous version's savefile versions aren't known since only know they're being stored ---------------------------------------------------------------------- 20 Feb 2009 -- 0.32.6 * Small fix of a silly bug introduced below on accident where a pen would not render in its selected colour/thickness. ---------------------------------------------------------------------- 19 Feb 2009 -- 0.32.5 * Added GPL.txt + fixed a bug with multiple images loaded into one tab. * Fixed 'sure you want to open this file?' message dialog so that it only appears if you're loading in a Whyteboard file, not a PDF or PNG, for instance * Fixed a bug on Windows involving rectangles/circles, yet another persists. ---------------------------------------------------------------------- 14 Feb 2009 -- 0.32.0 * Added line drawing tool. * "Sure you want to quit?" dialog when user hasn't saved. * Only allowing one .wtbd file to be loaded at once * Last Selected tab is stored in the .wtdb file * Undo/redo tool/menu bar items are disabled/enabled as appropriate ---------------------------------------------------------------------- 12 Feb 2009 -- 0.31.0 * Fixed saving/loading text controls, scrollbars adjust to screen resolution on resize. * Fixed undo/redo visual & clear/clear all. Change clear/clear all to 4 options * remove all drawings from current tab, keeping images * from all from current tab * remove all drawings from all tabs, keeping images * clear all tabs ---------------------------------------------------------------------- 12 Feb 2009 -- 0.30.0 * Scrolling works properly. Loading an image will expand the scroll bars to the image size if the image is too big. Improvements to the history replaying, rewrote most of the drawing code again. ---------------------------------------------------------------------- 11 Feb 2009 -- 0.29.0 * Saving/loading working. Currently keeping tmp files from PDF/PS conversions to make loading a saved file faster (would need to convert any 'linked' PDF/PS files for every .wtdb load) Saving text works, program also saves its settings ---------------------------------------------------------------------- 11 Feb 2009 -- 0.28.0 * Fixed history replaying, added pause/ stop the replay. * Fixed a bug with the converting progress where the bar increased in response mouse movement - now it's increased by a timer. ---------------------------------------------------------------------- 10 Feb 2009 -- 0.27.0 * Converting PDF/PS pops up a "converting" progress bar ---------------------------------------------------------------------- 10 Feb 2009 -- 0.26.0 * More code unification, cleaner code, deleting tmp files, loading multiple PDF/PS/SVG files ---------------------------------------------------------------------- 09 Feb 2009 -- 0.25.0 * Code refactored, performance increased ten-fold, some bug fixes ---------------------------------------------------------------------- 03 Feb 2009 -- 0.20.5 * Minor code cleanup ---------------------------------------------------------------------- 02 Feb 2009 -- 0.20.0 * Added a toolbar, each whyteboard tab has its own undo/redo history ---------------------------------------------------------------------- 31 Jan 2009 -- 0.15.0 * Closing the program removes temporary PNG files from PDF convert ---------------------------------------------------------------------- I wish I kept a longer version history.whyteboard-0.41.1/whyteboard-help/Index.hhk0000777000175000017500000000337411344761140017663 0ustar stevesteve
whyteboard-0.41.1/whyteboard-help/preferences.htm0000777000175000017500000000725611322273274021140 0ustar stevestevePreferences

Preferences

Setting up the program how you like it

Whyteboard has a number of configurable parameters, allowing you to influence how the program behaves. They are split into several categories:


General

  • Language: Whyteboard has been translated into a number of languages: select your preferred one here. Please note that many translations are half-complete.

  • Number of Recently Closed Sheets: When a sheet is closed, its data is saved internally so that the closing action can be undone, restoring the sheet. This setting controls how many sheets Whyteboard will store - the higher the number, the more memory Whyteboard will use.

  • Selection Handle Size: When you select a shape with the Select tool, several selection handles are displayed. This setting controls the size of these handles, as larger handles are easier to click on.

  • Canvas Border: When the canvas is larger than the program's size, scrollbars are shown. A small border is always visible - this is the area you can grab to resize the canvas. This controls the size of the border - larger values are easier to grab.


Fonts and Colours

  • Choose Your Custom Colours: You can click any of the colour buttons to select the 9 colours that are shown in the grid on the left-hand pane.

  • Default Font: The font that the Text and Note tool automatically use when you first start the program.

  • Transparent Bitmap Selection: The Bitmap Select is drawn with a fancy transparency. This may perform slow on older computers with low-end graphics cards. Is turned off by default.

View

  • Toolbox View: View the toolbox as text, or as icons. The text view takes up more space vertically, and when other languages are used, may also take up more horizontal space.

  • Default Canvas Width: The default width of the canvas when a new tab is opened.

  • Default Canvas Height: The default height of the canvas when a new tab is opened.

  • View the statusbar: Toggles the statusbar on/off at program startup. Can be turned on/off temporarily from the "View" menu..

  • View the toolbar: Toggles the toolbar on/off at program startup. Can be turned on/off temporarily from the "View" menu..

  • Show the title when printing: Toggles the file name being displayed when printing.

  • Show the tool preview: Toggles the tool preview at the bottom of the control panel.

  • Show the colour grid: Toggles the colour grid in the control panel.


PDF Conversion

  • Conversion Quality: Specifies how good the images from a converted PDF are. At standard quality, most PDFs should be readable, but better quality options will create a clearer, larger and overall better conversion of the PDF. This comes at the expense of more computing power, and the process will take longer, especially with PDFs that have many pages.

  • ImageMagick Location: (Windows-only) Brings up the ImageMagick locator dialog.

whyteboard-0.41.1/whyteboard-help/pdfs.htm0000777000175000017500000000411711353753420017564 0ustar stevesteve

Importing PDF, Postscript and SVG files

Overview

Whyteboard can load in PDF, PS and SVG files by using ImageMagick.

Importing

Windows

When you first try and load in a PDF/PS or SVG file, Whyteboard prompts for the location of the ImageMagick folder. This is usually in the C:/Program Files/ImageMagick-[version] directory; a directory selector dialog will allow you to choose its location. Until you specify a directory that contains convert.exe then you cannot import these file formats.

Once found, Whyteboard saves the location so you don't have to specify it each time. This can be changed from the Preferences dialog.


Linux / Mac

Whyteboard will perform the which convert shell command to see if the ImageMagick application, convert is installed.


Exporting

Be careful when exporting and overwriting a PDF. Whyteboard will save your tabs into the PDF as images, overwriting any meta content/links etc in the PDF.


PDF Caching

Once a PDF has been converted, Whyteboard will remember the converted images, and will load the PDF much faster next time it is loaded.
However, if the PDF changes, then the images that were previously converted will still be loaded.

Using the PDF Cache Viewer (in the View menu), documents may be removed from the cache, and Whyteboard will re-convert the file the next time it is loaded. This allows changes to the PDF to be reflected in Whyteboard.


Issues

When you load in a PDF, the file is passed off to the convert application in the background, and you must wait until the operation finishes.

The trouble here is that there is no way to gauge the progress of the convert, as the application does not return any information about its progress, nor can the PDF be queried to determine how many pages it has. The more pages inside a PDF, the longer it will take to convert and to refresh Whyteboard's thumbnails for every sheet.

whyteboard-0.41.1/whyteboard-help/whyteboard.hhp0000777000175000017500000000045411324524300020756 0ustar stevesteve[OPTIONS] Compatibility=1.1 Compiled file=whyteboard.chm Contents file=contents.hhc Display compile progress=No Index file=Index.hhk Language=0x405 �esky Title=Whyteboard Default topic=main.htm [FILES] main.htm drawing.htm menu.htm pdfs.htm notes.htm misc.htm saving.htm select.htm preferences.htmwhyteboard-0.41.1/whyteboard-help/saving.htm0000777000175000017500000000265611320607270020120 0ustar stevesteveSaving

Saving Annotations

Overview

Whyteboard saves .wtbd files as a renamed zip file. This contains any images that were loaded into the program, and a "save.data" file.

Zip Files

The file format .wtbd is essentially a renamed zip file. This contains any images that were saved inside the program, allowing a file saved on one computer to be opened, and edited on another. Versions prior to 0.39.3 did not do this.

The zip also contains a "save.data" file - this contains various information about the program state. This includes:

  • Currently selected tool
  • Current colour
  • Current thickness
  • Currently selected tab
  • Last used font/size/style (if any)
  • All sheets' names (preserving renamed sheets)
  • Version of the Whyteboard the save was created with

This allows a user to carry on from where they were last editing their file.

When you load in .wtbd file, it may take a while to load as all the thumbnails need to be refreshed, which can take a few seconds; depending mainly on how many sheets are contained in the saved file.


Zip Performance Implications

There are a few issues with the zip file implementation, it will take longer to read and write larger .wtbd files containing many images/sheets/shapes.

whyteboard-0.41.1/whyteboard-help/notes.htm0000777000175000017500000000164711265736211017766 0ustar stevesteveNotes

The Note Tool

Overview

Whyteboard provides a Note tool, which can be used to annotate PDFs or the current sheet

Usage

When you select the Note tool, and click on the canvas, a text input dialog pops up. You can enter multi-line notes, as well as change the font. Note that currently the font applies to the entire Note, and not only to highlighted text.
The currently selected drawing colour is used as the font colour.

Viewing Note outlines and editing Notes

When you create a Note, it appears in the "notes" tab on the right-hand panel. This shows a tree overview of every sheet open in Whyteboard, along with any associated notes for each sheet. A note in the tree view can be double clicked or right clicked to bring up the text input screen to edit the notes' text. Pressing cancel will not edit the note. whyteboard-0.41.1/whyteboard-help/misc.htm0000777000175000017500000001056411371567510017571 0ustar stevesteve

Miscellaneous Tips

Things you may not know

 

Resizing the Canvas

The grey area in each tab may be clicked upon to begin resizing the canvas. As the mouse is dragged out, the canvas' size updates.

The canvas may also be resized from the Sheets->Resize Canvas dialog (Ctrl-R), where the height and width can be specified in pixels. The memory that this bitmap size will use is also shown.

Note: Highlighter shapes aren't drawn while resizing.

Shape Shortcut Keys

Each shape has an associated shortcut key, e.g. P for Pen, R for rectangle. Hover your mouse over each tool to view its shortcut.

The History Viewer

From the View menu, selecting the "History viewer" will bring up the replay dialog. Pressing play will start to re-play your drawing history from the currently selected sheet. This is most noticeable with the Pen tool.

Drag and Drop

Any file type Whyteboard supports can be dragged and dropped into the drawing panel to be loaded, rather than through the Open file dialog. Media files may also be dropped onto the Media tool's panel.

Scrolling the Canvas with the Mouse

By holding down your mouse's middle button, you can scroll around the canvas without needing to use the scrollbars.

Navigating the Canvas with the Keyboard

The Home/End/Page Up/Page Down keys can nagivate the canvas, as in a text editor. Using the Ctrl key in combination with Home and End takes you to the top and bottom of the canvas.
The Up/Down/Left/Right keys may be used to move around the canvas, too.

Collapsing the Side Panels

The tool bar and thumbnail/notes panels have a button at their top with an arrow on it; clicking this folds the panel into the side of the application, showing more room in the drawing panel.

Copy and Paste

Using the Bitmap Select tool, regions may be selected to be copied as a bitmap. This can then be pasted into other applications. Similarly, Whyteboard can paste in bitmap data stored on the clipboard. Text may also be pasted into Whyteboard, creating a Text shape.

Renaming your Sheets

A sheet can be right-clicked upon in the tab view (under the tool bar icons) and renamed.

Undoing a Closed Sheet

By default, your last 10 closed sheets are remembered - if you accidentally closed a sheet then you may open it again from the Sheets menu.

Rearranging Sheets

You can "drag and drop" the tabs to change their order. This is useful to restore a closed tab to its original position

Editing Text

Using the Select tool, you can double click on drawn text to bring up the text edit dialog.

Editing a Note

From the Notes tab in the right-hand panel, a note can be edited by double clicking upon it, or right clicking and selecting "edit". The edit can be cancelled at any time. Also, a Sheet in the tree view can be "jumped" to by double clicking it, or right clicking and selecting "switch to".

Exporting your Image

From the File menu, the Export menu item will save the current sheet's drawing as an image to your hard drive. This can be also accessed by right clicking a sheet in the tab view.
Additionally, each sheet can be exported as a series of images - choosing a filename "image.jpg" with 3 open tabs creates image-1.jpg, image-2.jpg, image-3.jpg

Updating Whyteboard

From the "Help" menu, the Update option will check online for the latest program version, and can download and install it automatically.

Clearing Sheets

There are 4 sheet clearing options: clear drawings/clear sheet/clear all drawings/clear all sheets.

  • Clear drawings: This will remove any annotations, but keep any images.
  • Clear sheet: Removes everything.


The "all" option applies the clearing to every sheet. These can be undone, but the "all" option must have each sheet selectively restored; undoing clearing all will only restore the currently selected sheet.

Translate Whyteboard

From the "Help" menu, the Translate option will take you to a website where you help translate Whyteboard to your local language, or to improve an existing translation. whyteboard-0.41.1/whyteboard-help/menu.htm0000777000175000017500000001375311444710112017572 0ustar stevesteveThe Menu Commands

Menu Commands

This page describes Whyteboard's menu items



File

  • New Window: Opens a new, empty instance of Whyteboard.
  • New Sheet: A new sheet is created, along with a Thumbnail and a new item in the Notes tree.
  • Open: Open an image, PDF/PS/SVG file, or a .wtbd save file.
  • Open Recent: Your last 8 opened documents are shown.
  • Save: saves your current document, overwriting any changes.
  • Save As: Prompts for where to save your document, allowing you to save your changes to a new file without editing the original.
  • Import File:
    • Image: Opens image files (bmp/jpg/png/tiff)
    • PDF: PDF files.
    • PostScript: PostScript files (.ps)
    • Preferences: Loads in Whyteboard preferences from a .pref file.

  • Export :
    • Export Sheet: Saves the currently selected sheet as an image file
    • Export All Sheets: Saves each sheet into a series of image files.
    • PDF: Uses ImageMagick to save each sheet into a PDF
    • Preferences: Saves your Whyteboard .pref file somewhere, so it can be imported.

  • Reload Preferences: Reads your preferences file and re-configures the application against it.
  • Page Setup: Set up the page for printing.
  • Print Preview: Preview the printed document.
  • Print: Print the document.
  • Quit: Exits Whyteboard. If any changes have been made to the document, then you are promted to save your file.

Edit

  • Undo: Undoes the last action that was performed (i.e, draw a new shape). Note that some actions are undoable, such as resizing the canvas, or adding a Media panel
  • Redo: Re-does the last action that was undone.
  • Copy: With an active "Bitmap Select" selection, this copies the area as an image, that can be pasted.
  • Paste: Any text/image data on the clipboard will be pasted into the document.
  • Paste to a New Sheet: Adds a new sheet containing the pasted data.
  • Preferences: Opens the preferences dialog.

View

  • Shape Viewer: Opens the shape viewer dialog.
  • History Viewer: Brings up the history dialog, allowing you to replay your drawings.
  • PDF Cache: View a list of PDF documents that Whyteboard is caching
  • Toolbar: Toggles the toolbar on/off.
  • Statusbar: Toggles the statusbar on/off.
  • Tool Preview: Toggles the tool preview at the bottom of the control panel.
  • Colour Grid: Toggles the colour grid in the control panel.
  • Full Screen: Runs Whyteboard in full screen. Hit escape, F11 or View->Fullscreen to return to normal.

Shapes

  • Move Shape Up: Moves the currently selected shape up one step in the shapes' drawing order
  • Move Shape Down: Moves the currently selected shape down one step in the shapes' drawing order
  • Move Shape to Top: Moves the currently selected shape to the top of the shapes' drawing order
  • Move Shape to Bottom: Moves the currently selected shape to the bottom of the shapes' drawing order
  • Delete Shape: Deletes the currently selected shape.
  • Deselect Shape: Removes the selection from the selected shape
  • Color...: Change selectedshape's colour
  • Background Color...: Change the selected shape's background colour, if it is not transparent
  • Swap Colors: If the selected shape isn't transparent, this swaps the shape's foreground and background colours
  • Transparent: Toggles the selected shape's transparency

Sheets

  • Remove Sheet: Closes the currently selected sheet
  • Close All Sheets: Closes every sheeet, leaving a blank new sheet
  • Rename Sheet: Changes the sheet's name (defaults to "Sheet [x]")
  • Resize Canvas: Opens a dialog to allow you to change the current sheet's canvas' width and height.
  • Next Sheet: Selects the next sheet
  • Previous Sheet: Selects the previous sheet.
  • Undo Last Closed Sheet: Restores the last sheet that was closed
  • Recently Closed Sheets: A list of the recently closed sheets to undo. This way you can pick a sheet to undo instead of only being able to close the most recently closed sheet.
  • Clear Sheet's Drawings: Removes everything from the current sheet
  • Clear Sheet: Removes everything but Images from the current sheet
  • Clear All Sheets' Drawings: Clears all sheets
  • Clear All Sheets: Clears all sheets, keeping their images

Help

  • Contents: Opens this help viewer
  • Check for Updates: Whyteboard will check if there's a new version available and download and intall it, if you choose.
  • Report a Problem: Opens a webpage to allow you to submit a bug report
  • Translate: Opens a webpage to allow you to translate the prorgram
  • Send Feedback: Send feedback by email to the developer directly
  • About: View the developer and translator credits
whyteboard-0.41.1/whyteboard-help/contents.hhc0000777000175000017500000000353011344761146020441 0ustar stevesteve
whyteboard-0.41.1/whyteboard-help/main.htm0000777000175000017500000000051511176740156017557 0ustar stevesteve

Whyteboard Manual

A simple PDF annotator and image editor

This help manual serves to outline the usage of Whyteboard, detail miscellaneous tips and tricks and generally help you feel comfortable using Whyteboard. To get started, please select a topic from the left-hand pane.

whyteboard-0.41.1/whyteboard-help/drawing.htm0000777000175000017500000000651611344761044020271 0ustar stevesteveDrawing

Drawing with Whyteboard

Whyteboard provides a number of different drawing tools. This page will catalogue them and their usage.



  • Pen: A free-style pen. "Ink" will appear wherever you drag your mouse while holding down the left button.
  • Hightlighter: Very similar to the Pen, but draws in a transparent ink, useful for highlighting documents.
  • Rectangle: A transparent rectangle. Clicking the mouse will set the initial starting point for the rectangle; holding the mouse button down and dragging the mouse will resize the rectangle in the direction the mouse is going. So, if you drag the mouse downwards and to the left, you will extend the rectangle in that direction. Dragging it to the top right would make the rectangle smaller until you reach the original location you clicked, and then the rectangle would increase in size to the top right.
  • Line: as above, but with a straight line.
  • Arrow: like a line, but with an arrow head pointing in the direction of the line.
  • Ellipse: A "squashed" rectangle, creating an oval. This behaves exactly as drawing a rectangle.
  • Circle: The initial mouse click sets the position for the centre of the circle, and dragging the mouse out will increase its radius; dragging the mouse in decreases it.
  • Polygon: Draws a polygon with n many points. To finish drawing, double click, or press the right mouse button. The polygon must have a minimum of 3 points.
  • Text: Clicking will pop up a text input dialog which you can enter text into, and select your font. Text can be multi-lined and as long as you want; Whyteboard will update its scrollbars' length/height if the text is longer/wider than Whyteboard's current size.
  • Note: Similar to text, yet is displayed with a light yellow background, to mimic a sticky/"post-it" note. There are further details on notes here.
  • Rounded Rectangle: As a rectangle, but with rounded edges.
  • Eyedropper: This will set the current drawing colour as the colour underneath the mouse click. Be careful, often you can set your colour to white (the background colour) and get the appearance that you're drawing nothing (since you're drawing white onto white)
  • Media: Allows audio and video to be embedded into Whyteboard. Standard play controls are provided: open file/play/pause/stop/volume slider/media duration slider.
    Note: Adding, deleting and clearing the Media panel from the canvas cannot be undone - simply add another media panel and load in the file again.
  • Bitmap Select: Selects a region of the canvas to be copied. Once a selection has been made, it can be copied (ctrl+C / edit->copy), and then pasted (ctrl-V/ / edit->paste) as an image. The selection may also be pasted into a new sheet. If anything is drawn after a selection has been made, but not copied, then the selection is removed.
  • Select Tool: Used for manipulating existing shapes. View more detail about the Select tool.
whyteboard-0.41.1/whyteboard-help/select.htm0000777000175000017500000000461511363161773020117 0ustar stevesteve

The Select Tool

Overview

Whyteboard has a Select tool, which is used to manipulate already-drawn shapes.

Moving the mouse over a drawn shape will change the cursor to indicate different actions.

Clicking "inside" a shape will select it: handles are drawn at the shape's edges. This helps to see the actions to perform.



  • Hand: The shape may be moved. Holding the mouse will allow you to reposition the shape.
  • Multiple Arrows: Used by the Polygon and Line tools, this shows a point may be repositioned anywhere
  • Diagonal Arrow: Shape may be resized in any direction, the opposite corner is "anchored"
  • Vertical Double-ended Arrow: Shape may be resized vertically, no changes will be made horizontally
  • Horizontal Double-ended Arrow: Shape may be resized horizontally, no changes will be made vertically
  • Rotate Icon: Shape may be rotated.

Items selected with the select tool can be deleted, by pressing the Delete key, from the Toolbar or the Shapes menu.

A shape's position in the "drawing order" can also be adjusted by selecting it, and using the items in the Shapes menu, or the shortcut keys ctrl+up/down. Using ctrl+shift moves the shape to the top or bottom of the order.

When you select an item with the Select tool, it is brought to the front of the list temporarily.
When you deselect, draw or select a new shape, the old shape is drawn back in its correct position.

A shape will be de-selected when:

  • New shape is drawn
  • Different shape is selected
  • Shapes->Deselect menu item (or Ctrl-D)
  • The escape key is pressed

Images

With an image shape, the diagonal arrows will scale the image, adjusting its size. There will be a green handle drawn in the middle of the image, which can be used to rotate the image around its center point.

Moving Selected Shapes

Using the Up/Down/Left/Right keyboard arrow keys when a shape is moved will move it. Keep the key held in to move the shape continiously.

When you move a shape towards the edge of the canvas, it will automatically scroll the canvas in the direction you are dragging to. This helps you move a shape around the canvas easily.

whyteboard-0.41.1/whyteboard-help/shape_viewer.htm0000777000175000017500000000243411363161645021314 0ustar stevesteveShape Viewer

Shape Viewer

Overview

The Shape Viewer allows you to see and manage the order, and properties of each sheet's drawn items.

Usage

From the View Menu, select Shape Viewer to bring up a dialog displaying a list of all drawn items for the current sheet.
Items at the top of the list are drawn on top of other items below it.

The buttons can be used to move the shapes' position in the list: they are "move to top", "move up", "move down", "move to bottom".
There is also buttons for changing to the next/previous sheet, and a drop-down menu for quickly jumping to any sheet.

A list of each drawn item is shown, as well as its thickness and colour (may be an RGB value) attributes. Additional properties are displayed, for example a text entry will display its text.

Buttons are enabled/disabled as appropriate - for example, selecting the shape at the bottom of the list disables the "move to bottom" and "move down" buttons, since they cannot be moved down any further.


Other Things

The shape viewer is also synchronised with operations on the canvas. When you draw, undo, redo, edit a shape, change sheet, add/close a sheet, the viewer is updated.

whyteboard-0.41.1/whyteboard-help/index.html0000777000175000017500000000135011425137141020103 0ustar stevesteve whyteboard-0.41.1/whyteboard/tools.py0000777000175000017500000016641411443222121016700 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ This module contains classes which can be drawn onto a Whyteboard frame Note: the list "items" at the bottom contains all the classes that can be drawn with by the user (e.g. they can't draw an image directly) """ from __future__ import division import os import time import math import cStringIO import ntpath import wx from whyteboard.lib import pub from whyteboard.misc import meta, get_image_path _ = wx.GetTranslation # constants for selection handles HANDLE_SIZE = 6 # square pixels HANDLE_ROTATE = -1 TOP_LEFT = 1 TOP_RIGHT = 2 BOTTOM_LEFT = 3 BOTTOM_RIGHT = 4 CENTER_TOP = 5 CENTER_RIGHT = 6 CENTER_BOTTOM = 7 CENTER_LEFT = 8 EDGE_TOP = 9 EDGE_RIGHT = 10 EDGE_BOTTOM = 11 EDGE_LEFT = 12 def set_handle_size(handle_size): global HANDLE_SIZE HANDLE_SIZE = handle_size pub.subscribe(set_handle_size, 'tools.set_handle_size') #---------------------------------------------------------------------- class Tool(object): """ Abstract class representing a tool """ tooltip = u"" name = u"" icon = u"" hotkey = u"" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT, cursor=wx.CURSOR_PENCIL, join=wx.JOIN_ROUND): self.canvas = canvas self.colour = colour self.background = background self.thickness = thickness self.cursor = cursor self.join = join self.brush = None self.selected = False self.drawing = False self.edges = {} self.x = 0 self.y = 0 self.make_pen() def left_down(self, x, y): pass def left_up(self, x, y): pass def double_click(self, x, y): pass def right_up(self, x, y): pass def motion(self, x, y): pass def draw(self, dc, replay=True): """ Draws itself. """ pass def hit_test(self, x, y): """ Returns True/False if a mouseclick in "inside" the shape """ pass def handle_hit_test(self, x, y): """ Returns the position of the handle the user has clicked on """ pass def start_select_action(self, handle): """Do something before being resized/moved/scaled""" pass def end_select_action(self, handle): """ Gives the shape a chance to do cleanup when it finishes moving/resizing """ pass def make_pen(self, dc=None): """ Creates a pen, usually after loading in a save file """ if self.background == wx.TRANSPARENT: self.brush = wx.TRANSPARENT_BRUSH else: self.brush = wx.Brush(self.background) def preview(self, dc, width, height): """ Tools' preview in the left-hand panel """ pass def properties(self): """ Text description of this Tool's properties (for Shape Viewer) """ pass def save(self): """ Defines how this class will pickle itself """ self.canvas = None self.brush = None def load(self): """ Defines how this class will unpickle itself """ if not hasattr(self, "background"): self.background = wx.TRANSPARENT if not hasattr(self, "drawing"): self.drawing = False if not hasattr(self, "join"): self.join = wx.JOIN_ROUND #---------------------------------------------------------------------- class OverlayShape(Tool): """ Contains methods for drawing an overlayed shape. Has some general method implementations for drawing handles and drawing the shape. """ def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT, cursor=wx.CURSOR_CROSS, join=wx.JOIN_ROUND): Tool.__init__(self, canvas, colour, thickness, background, cursor, join) self.handles = [] self.canvas.overlay = wx.Overlay() def left_down(self, x, y): self.x = x self.y = y def left_up(self, x, y): """ Only adds the shape if it was actually dragged out """ if x != self.x and y != self.y: pub.sendMessage('shape.add', shape=self) self.sort_handles() def draw(self, dc, replay=False, _type=u"Rectangle"): """ Draws a shape polymorphically, using Python's introspection; is called by any sub-class that needs to be overlayed. When called with replay=True it doesn't draw a temp outline Avoids excess calls to make_pen - better performance """ if not replay: odc = wx.DCOverlay(self.canvas.overlay, dc) odc.Clear() self.make_pen(dc) # Note object needs a DC to draw its outline here pen = wx.Pen(self.colour, self.thickness, wx.SOLID) pen.SetJoin(self.join) dc.SetPen(pen) dc.SetBrush(self.brush) getattr(dc, u"Draw" + _type)(*self.get_args()) if self.selected: self.draw_selected(dc) if not replay: del odc def get_args(self): """The drawing arguments that this class uses to draw itself""" pass def get_handles(self): """Returns the handle positions: top-lef, top-rig, btm-lef, btm-rig""" pass def find_edges(self): """Finds the x/y/width/height edges of a shape""" pass def resize(self, x, y, handle=None): """When the shape is being resized with Select tool""" self.motion(x, y) def move(self, x, y, offset): """Being moved with Select. Offset is to keep the cursor centered""" self.x = x - offset[0] self.y = y - offset[1] def sort_handles(self): """Sets the shape's handles""" self.handles = [] for x in self.get_handles(): self.handles.append(wx.Rect(x[0], x[1], HANDLE_SIZE, HANDLE_SIZE)) def handle_hit_test(self, x, y): """Returns which handle has been clicked on""" if not hasattr(self, "handles"): self.sort_handles() self.handle_hit_test(x, y) if self.handles[0].InsideXY(x, y): return TOP_LEFT if self.handles[1].InsideXY(x, y): return TOP_RIGHT if len(self.handles) > 2: if self.handles[2].InsideXY(x, y): return BOTTOM_LEFT if self.handles[3].InsideXY(x, y): return BOTTOM_RIGHT return False # nothing hit def anchor(self, handle): """ Avoids an issue when resizing, anchors shape's x/y point to the opposite side of the handle that is being dragged """ pass def end_select_action(self, handle): self.sort_handles() def draw_selected(self, dc): """Draws each handle that an object has""" dc.SetBrush(find_inverse(self.colour)) dc.SetPen(wx.Pen(wx.BLACK, 1, wx.SOLID)) draw = lambda dc, x, y: dc.DrawRectangle(x, y, HANDLE_SIZE, HANDLE_SIZE) [draw(dc, x, y) for x, y in self.get_handles()] def offset(self, x, y): """Used when moving the shape, to keep the cursor in the same place""" return (x - self.x, y - self.y) def load(self): super(OverlayShape, self).load() self.selected = False #---------------------------------------------------------------------- class Polygon(OverlayShape): """ Draws a polygon with [x] number of points, each of which can be repositioned Due to it working different to every other shape it has to do some canvas manipulation here """ tooltip = _("Draw a polygon") name = _("Polygon") icon = u"polygon" hotkey = u"y" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT, cursor=wx.CURSOR_CROSS, join=wx.JOIN_ROUND): OverlayShape.__init__(self, canvas, colour, thickness, background, cursor, join) self.points = [] self.drawing = False # class keeps track of its drawing, not whyteboard self.center = None self.scale_factor = 0 self.operation = None # scaling/rotating self.original_points = [] # when scaling, we scale vs these self.orig_click = None # when scaling - x/y of original click def left_up(self, x, y): pass def left_down(self, x, y): if not self.drawing: pub.sendMessage('canvas.capture_mouse') self.drawing = True self.points.append((x, y)) if not self.x or not self.y: self.x = x self.y = y self.points.append((x, y)) self.canvas.draw_shape(self) def motion(self, x, y): if self.drawing: if self.points: pos = len(self.points) - 1 if pos < 0: pos = 0 self.points[pos] = (x, y) self.canvas.draw_shape(self) def double_click(self, x, y): if len(self.points) == 2: return del self.points[len(self.points) - 1] # dbl clicking fires 2 click evts self.right_up(x, y) def right_up(self, x, y): if len(self.points) > 2: self.drawing = False self.sort_handles() pub.sendMessage('shape.add', shape=self) pub.sendMessage('canvas.release_mouse') pub.sendMessage('canvas.change_tool') pub.sendMessage('thumbs.update_current') def start_select_action(self, handle): self.points = list(self.points) if wx.GetKeyState(wx.WXK_CONTROL): self.operation = u"rotate" elif wx.GetKeyState(wx.WXK_SHIFT): self.operation = u"rescale" def end_select_action(self, handle): super(Polygon, self).end_select_action(handle) self.operation = None def find_center(self): a = sum([x for x, y in self.points]) / len(self.points) b = sum([y for x, y in self.points]) / len(self.points) self.center = (a, b) def find_edges(self): """Get the bounding rectangle for the polygon""" xmin = min(x for x, y in self.points) ymin = min(y for x, y in self.points) xmax = max(x for x, y in self.points) ymax = max(y for x, y in self.points) self.edges = {EDGE_TOP: ymin, EDGE_RIGHT: xmax, EDGE_BOTTOM: ymax, EDGE_LEFT: xmin} def hit_test(self, x, y): """http://ariel.com.au/a/python-point-int-poly.html""" if (x < self.edges[EDGE_LEFT] or x > self.edges[EDGE_RIGHT] or y < self.edges[EDGE_TOP] or y > self.edges[EDGE_BOTTOM]): return False n = len(self.points) inside = False p1x, p1y = self.points[0] for i in xrange(n + 1): p2x, p2y = self.points[i % n] if y > min(p1y, p2y): if y <= max(p1y, p2y): if x <= max(p1x, p2x): if p1y != p2y: xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x if p1x == p2x or x <= xinters: inside = not inside p1x, p1y = p2x, p2y return inside def get_handles(self): d = lambda x, y: (x - 2, y - 2) handles = [d(x[0], x[1]) for x in self.points] return handles def handle_hit_test(self, x, y): for count, handle in enumerate(self.handles): if handle.ContainsXY(x, y): return count + 1 return False # nothing hit def resize(self, x, y, handle=None): pos = handle - 1 if pos < 0: pos = 0 if self.operation == u"rotate": self.rotate((x, y)) self.x, self.y = self.points[0] # for the correct offset when moving elif self.operation == u"rescale": self.rescale(x, y) self.x, self.y = self.points[0] else: self.points[pos] = (x, y) if pos == 0: # first point self.x, self.y = x, y def rescale(self, x, y): """ Thanks to Mark Ransom -- http://stackoverflow.com/questions/2014859/ """ if not self.orig_click: self.orig_click = (x, y) orig_click = self.orig_click original_distance = math.sqrt((orig_click[0] - self.center[0]) ** 2 + (orig_click[1] - self.center[1]) ** 2) current_distance = math.sqrt((x - self.center[0]) ** 2 + (y - self.center[1]) ** 2) self.scale_factor = current_distance / original_distance for count, point in enumerate(self.original_points): dist = (point[0] - self.center[0], point[1] - self.center[1]) self.points[count] = (self.scale_factor * dist[0] + self.center[0], self.scale_factor * dist[1] + self.center[1]) def rotate(self, position): """ http://stackoverflow.com/questions/786472/rotate-a-point-by-an-angle """ if not self.orig_click: self.orig_click = position knobangle = self.find_angle(self.orig_click, self.center) mouseangle = self.find_angle(position, self.center) angle = knobangle - mouseangle self.do_rotate(angle) def do_rotate(self, angle): """ Rotate the points. Can be called by Image as a rotate preview """ for x, p in enumerate(self.original_points): a = (math.cos(angle) * (p[0] - self.center[0]) - math.sin(angle) * (p[1] - self.center[1]) + self.center[0]) b = (math.sin(angle) * (p[0] - self.center[0]) + math.cos(angle) * (p[1] - self.center[1]) + self.center[1]) self.points[x] = (a, b) def move(self, x, y, offset): """Gotta update every point relative to how much the first has moved""" super(Polygon, self).move(x, y, offset) diff = (x - self.points[0][0] - offset[0], y - self.points[0][1] - offset[1]) for count, point in enumerate(self.points): self.points[count] = (point[0] + diff[0], point[1] + diff[1]) def sort_handles(self): super(Polygon, self).sort_handles() self.points = [(float(x), float(y)) for x, y in self.points] self.find_edges() self.find_center() self.original_points = list(self.points) self.orig_click = None def find_angle(self, a, b): return math.atan2((a[0] - b[0]) , (a[1] - b[1])) def get_args(self): return [self.points] def properties(self): return _("Number of points: %s") % len(self.points) def draw(self, dc, replay=False, _type=u"Polygon"): super(Polygon, self).draw(dc, replay, _type) def preview(self, dc, width, height): dc.DrawPolygon(((7, 13), (54, 9), (60, 38), (27, 34))) def load(self): super(Polygon, self).load() self.sort_handles() #---------------------------------------------------------------------- class Pen(Polygon): """ A free-hand pen. Has been turned into an OverlayShape to allow it to be selected and moved. """ tooltip = _("Draw strokes with a brush") name = _("Pen") icon = u"pen" hotkey = u"p" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT, cursor=wx.CURSOR_PENCIL, join=wx.JOIN_ROUND): Polygon.__init__(self, canvas, colour, thickness, background, cursor, join) self.time = [] # list of times for each point, for redrawing self.background = wx.TRANSPARENT self.x_tmp = 0 self.y_tmp = 0 def left_down(self, x, y): self.x = x # original mouse coords self.y = y self.x_tmp = x self.y_tmp = y def left_up(self, x, y): if self.points: pub.sendMessage('shape.add', shape=self) self.sort_handles() if len(self.points) == 1: # a single click self.canvas.redraw_all() def motion(self, x, y): self.points.append([self.x_tmp, self.y_tmp, x, y]) self.time.append(time.time()) self.x_tmp = x self.y_tmp = y # swap for the next call to this function def double_click(self, x, y): pass def right_up(self, x, y): pass def sort_handles(self): pass def handle_hit_test(self, x, y): pass def hit_test(self, x, y): pass def draw(self, dc, replay=True, _type=u"LineList"): super(Pen, self).draw(dc, replay, _type) def get_args(self): return [self.points] def preview(self, dc, width, height): """Points below make a curly line to show an example Pen drawing""" dc.DrawSpline([(52, 10), (51, 10), (50, 10), (49, 10), (49, 9), (48, 9), (47, 9), (46, 9), (46, 8), (45, 8), (44, 8), (43, 8), (42, 8), (41, 8), (40, 8), (39, 8), (38, 8), (37, 8), (36, 8), (35, 8), (34, 8), (33, 8), (32, 8), (31, 8), (30, 8), (29, 8), (28, 8), (27, 8), (27, 10), (26, 10), (26, 11), (26, 12), (26, 13), (26, 14), (26, 15), (26, 16), (28, 18), (30, 19), (31, 21), (34, 22), (36, 24), (37, 26), (38, 27), (40, 28), (40, 29), (40, 30), (40, 31), (38, 31), (37, 32), (35, 33), (33, 33), (31, 34), (28, 35), (25, 36), (22, 36), (20, 37), (17, 37), (14, 37), (12, 37), (10, 37), (9, 37), (8, 37), (7, 37)]) #---------------------------------------------------------------------- class Highlighter(Pen): tooltip = _("Highlight with a transparent pen") name = _("Highlighter") icon = u"highlighter" hotkey = u"h" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT, cursor=wx.CURSOR_PENCIL, join=wx.JOIN_ROUND): Pen.__init__(self, canvas, colour, thickness + 6) self.current = (0, 0) def left_down(self, x, y): super(Highlighter, self).left_down(x, y) self.current = (x, y) def left_up(self, x, y): super(Highlighter, self).left_up(x, y) self.canvas.redraw_all() def motion(self, x, y): self.points.append( [self.x_tmp, self.y_tmp, x, y] ) self.time.append(time.time()) self.current = (x, y) self.draw(None) self.x_tmp = x self.y_tmp = y def draw(self, dc, replay=False, _type=u"LineList"): if not dc: dc = self.canvas.get_dc() gc = wx.GraphicsContext.Create(dc) path = gc.CreatePath() colour = (self.colour[0], self.colour[1], self.colour[2], 50) gc.SetPen(wx.Pen(colour, self.thickness, wx.SOLID)) if not replay: path.MoveToPoint(*self.current) path.AddLineToPoint(self.x_tmp, self.y_tmp) else: for line in self.points: path.MoveToPoint(line[0], line[1]) path.AddLineToPoint(line[2], line[3]) gc.StrokePath(path) def preview(self, dc, width, height): """Points below make a curly line to show an example Pen drawing""" gc = wx.GraphicsContext.Create(dc) path = gc.CreatePath() colour = (self.colour[0], self.colour[1], self.colour[2], 30) gc.SetPen(wx.Pen(colour, self.thickness, wx.SOLID)) path.MoveToPoint(10, 25) path.AddLineToPoint(70, 25) gc.StrokePath(path) #---------------------------------------------------------------------- class Rectangle(OverlayShape): """ The rectangle and its descended classes (ellipse/rounded rect) use an overlay as a rubber banding method of drawing itself over other shapes. """ tooltip = _("Draw a rectangle") name = _("Rectangle") icon = u"rectangle" hotkey = u"r" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT): OverlayShape.__init__(self, canvas, colour, thickness, background, join=wx.JOIN_MITER) self.width = 0 self.height = 0 self.rect = None def motion(self, x, y): self.width = x - self.x self.height = y - self.y def resize(self, x, y, handle=None): if handle < CENTER_TOP: self.motion(x, y) elif handle in [CENTER_TOP, CENTER_BOTTOM]: self.height = y - self.y elif handle in [CENTER_LEFT, CENTER_RIGHT]: self.width = x - self.x def get_args(self): x, y, w, h = self.x, self.y, self.width, self.height args = [min(x, w + x), min(y, h + y), abs(w), abs(h)] return args def get_handles(self): """ ugh. """ t = round(self.thickness / 2) s = HANDLE_SIZE / 2 x, y, w, h = self.get_args()[:4] # RoundedRect has 5 args return [(x - t - 6, y - t - 6), # top left (x + w + t - 2, y - t - 6), # top right (x - t - 6, y + h + t - 2), # bottom left (x + w + t - 2, y + h + t - 2), # bottom right (x - t - s + (w / 2), y - t - 6), # top center (x + w + t - 2, y - t - s + (h / 2)), # right center (x - t - s + (w / 2), y + h + t - 2), # bottom center (x - t - 6, y - t - s + (h / 2))] # left center def handle_hit_test(self, x, y): """Returns which handle has been clicked on""" result = super(Rectangle, self).handle_hit_test(x, y) if not result: keys = {2: BOTTOM_LEFT, 3: BOTTOM_RIGHT, 4: CENTER_TOP, 5: CENTER_RIGHT, 6: CENTER_BOTTOM, 7: CENTER_LEFT} for k, v in keys.items(): if self.handles[k].InsideXY(x, y): return v return False return result def anchor(self, handle): """ Avoids an issue when resizing, anchors shape's x/y point to the opposite corner of the corner being dragged. I'll be honest - I can't remember what's going on here - it just works! """ r = self.get_args()[:4] if handle == TOP_LEFT: self.x = r[0] + r[2] self.y = r[1] + r[3] self.width = -(r[2] - r[0]) self.height = -(r[1] - r[1]) elif handle == BOTTOM_LEFT: self.x = r[0] + r[2] self.y = r[1] self.width = -r[0] self.height = -r[1] elif handle == TOP_RIGHT: self.x = r[0] self.y = r[1] + r[3] self.width = -(r[2] - r[0]) self.height = -(r[1] - r[1]) elif handle == BOTTOM_RIGHT: self.x = r[0] self.y = r[1] self.width = r[2] self.height = r[3] elif handle == CENTER_TOP: self.y = r[1] + r[3] self.height = -(r[1] - r[1]) elif handle == CENTER_BOTTOM: self.y = r[1] self.height = -r[1] elif handle == CENTER_LEFT: self.x = r[0] + r[2] self.width = -r[0] elif handle == CENTER_RIGHT: self.x = r[0] self.width = r[2] self.sort_handles() def sort_handles(self): super(Rectangle, self).sort_handles() self.update_rect() def find_edges(self): x, y, w, h = self.get_args()[:4] self.edges = {EDGE_TOP: y, EDGE_RIGHT: x + w, EDGE_BOTTOM: y + h, EDGE_LEFT: x} def update_rect(self): """Need to pad out the rectangle with the line thickness""" x, y, w, h = self.get_args()[:4] t = math.ceil(self.thickness / 2) w = w + self.thickness h = h + self.thickness self.rect = wx.Rect(x - t, y - t, w, h) def hit_test(self, x, y): if not hasattr(self, "rect"): self.sort_handles() return self.rect.InsideXY(x, y) def properties(self): x, y, w, h = self.get_args()[:4] return u"X: %i, Y: %i, %s %i, %s %i" % (x, y, _("Width:"), w, _("Height:"), h) def load(self): super(Rectangle, self).load() self.sort_handles() def preview(self, dc, width, height): dc.DrawRectangle(5, 5, width - 15, height - 15) #---------------------------------------------------------------------- class Ellipse(Rectangle): """ Easily extends from Rectangle. """ tooltip = _("Draw an oval shape") name = _("Ellipse") icon = u"ellipse" hotkey = u"o" def draw(self, dc, replay=False): super(Ellipse, self).draw(dc, replay, u"Ellipse") def preview(self, dc, width, height): dc.DrawEllipse(5, 5, width - 12, height - 12) def find_center(self): x, y, w, h = self.get_args() return (x + w / 2, y + h/ 2) def hit_test(self, x, y): """ http://www.conandalton.net/2009/01/how-to-draw-ellipse.html """ center = self.find_center() try: dx = int((x - center[0]) / (self.width / 2)) except ZeroDivisionError: dx = 0 try: dy = int((y - center[1]) / (self.height / 2)) except ZeroDivisionError: dy = 0 if dx * dx + dy * dy < 1: return True return False #---------------------------------------------------------------------- class Circle(OverlayShape): """ Draws a circle. Uses its radius to calculate handle position """ tooltip = _("Draw a circle") name = _("Circle") icon = u"circle" hotkey = u"c" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT): OverlayShape.__init__(self, canvas, colour, thickness, background) self.radius = 1 def motion(self, x, y): self.radius = ((self.x - x) ** 2 + (self.y - y) ** 2) ** 0.5 def find_edges(self): x, y, r = self.get_args() self.edges = {EDGE_TOP: y - r, EDGE_RIGHT: x + r, EDGE_BOTTOM: y + r, EDGE_LEFT: x - r} def draw(self, dc, replay=False): super(Circle, self).draw(dc, replay, u"Circle") def preview(self, dc, width, height): dc.DrawCircle(width/2, height/2, 15) def get_args(self): return [self.x, self.y, self.radius] def get_handles(self): d = lambda x, y: (x - 2, y - 2) x, y, r = self.get_args() return d(x - r, y - r), d(x- r, y + r), d(x + r, y - r), d(x + r, y + r) def properties(self): r = _("Radius") return u"X: %i, Y: %i, %s: %i" % (self.x, self.y, r, self.radius) def hit_test(self, x, y): val = ((x - self.x) * (x - self.x)) + ((y - self.y) * (y - self.y)) if val <= (self.radius * self.radius): return True return False #---------------------------------------------------------------------- class RoundedRect(Rectangle): """ Easily extends from Rectangle. """ tooltip = _("Draw a rectangle with rounded edges") name = _("Rounded Rect") icon = u"rounded-rect" hotkey = u"u" def draw(self, dc, replay=False): super(RoundedRect, self).draw(dc, replay, u"RoundedRectangle") def preview(self, dc, width, height): dc.DrawRoundedRectangle(5, 5, width - 10, height - 7, 8) def get_args(self): args = super(RoundedRect, self).get_args() args.append(35) return args #---------------------------------------------------------------------- class Line(OverlayShape): """ Draws a line - methods for its own handles, resizing/moving and line length """ tooltip = _("Draw a straight line") name = _("Line") icon = u"line" hotkey = u"l" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT): OverlayShape.__init__(self, canvas, colour, thickness, background) self.x2 = 0 self.y2 = 0 def left_down(self, x, y): super(Line, self).left_down(x, y) self.x2 = x self.y2 = y def motion(self, x, y): self.x2 = x self.y2 = y def left_up(self, x, y): """ Don't add a 'blank' line """ if self.x != self.x2 or self.y != self.y2: pub.sendMessage('shape.add', shape=self) self.sort_handles() def offset(self, x, y): """Returns two tupples (unlike the others) - one for each point""" return ((x - self.x, y - self.y), (x - self.x2, y - self.y2)) def find_edges(self): self.edges = {EDGE_TOP: min(self.y, self.y2), EDGE_RIGHT: max(self.x, self.x2), EDGE_BOTTOM: max(self.y, self.y2), EDGE_LEFT: min(self. x, self.x2)} def draw(self, dc, replay=False): super(Line, self).draw(dc, replay, u"Line") def get_args(self): return [self.x, self.y, self.x2, self.y2] def properties(self): return u"X: %i, Y: %i, X2: %i, Y2: %i" % (self.x, self.y, self.x2, self.y2) def move(self, x, y, offset): self.x = x - offset[0][0] self.y = y - offset[0][1] self.x2 = x - offset[1][0] self.y2 = y - offset[1][1] def resize(self, x, y, handle=None): if handle == TOP_LEFT: self.x = x self.y = y else: self.x2 = x self.y2 = y def get_handles(self): d = lambda x, y: (x - 2, y - 2) return d(self.x, self.y), d(self.x2, self.y2) def preview(self, dc, width, height): dc.DrawLine(10, height / 2, width - 10, height / 2) def point_distance(self, a, b, c): """Taken/reformatted from http://stackoverflow.com/questions/849211/""" t = b[0] - a[0], b[1] - a[1] # Vector ab dd = math.sqrt(t[0] ** 2 + t[1] ** 2) # Length of ab t = t[0] / dd, t[1] / dd # unit vector of ab n = -t[1], t[0] # normal unit vect. to ab ac = c[0] - a[0], c[1] - a[1] # vector ac return math.fabs(ac[0] * n[0] + ac[1] * n[1]) # the minimum distance def hit_test(self, x, y): """ The above function calculates further than the length of the line. This stops it from even performing that calculation """ if self.x < self.x2: if x < self.x or x > self.x2: return False else: if x > self.x or x < self.x2: return False val = self.point_distance((self.x, self.y), (self.x2, self.y2), (x, y)) return (val < 3 + round(self.thickness / 2)) #--------------------------------------------------------------------- class Arrow(Line): tooltip = _("Draw an arrow") name = _("Arrow") hotkey = u"a" icon = u"arrow" def draw(self, dc, replay=False): """ From http://lifshitz.ucdavis.edu/~dmartin/teach_java/slope/arrows.html """ if not replay: odc = wx.DCOverlay(self.canvas.overlay, dc) odc.Clear() self.make_pen(dc) dc.SetPen(wx.Pen(self.colour, self.thickness)) dc.SetBrush(self.brush) x0, x1, y0, y1 = self.x, self.x2, self.y, self.y2 deltaX = self.x2 - self.x deltaY = self.y2 - self.y frac = 0.05 dc.DrawLine(*self.get_args()) dc.DrawLine(x0 + ((.75 - frac) * deltaX + frac * deltaY), y0 + ((.75 - frac) * deltaY - frac * deltaX), x1, y1) dc.DrawLine(x0 + ((.75 - frac) * deltaX - frac * deltaY), y0 + ((.75 - frac) * deltaY + frac * deltaX), x1, y1) if self.selected: self.draw_selected(dc) if not replay: del odc def preview(self, dc, width, height): dc.DrawLine(10, height / 2, width - 10, height / 2) dc.DrawLine(width - 10, height / 2, width - 20, (height / 2) - 6) dc.DrawLine(width - 10, height / 2, width - 20, (height / 2) + 6) #--------------------------------------------------------------------- class Media(Tool): tooltip = _("Insert media and audio") name = _("Media") hotkey = u"m" icon = u"media" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT, cursor=wx.CURSOR_ARROW): Tool.__init__(self, canvas, colour, thickness, background, cursor) self.filename = None self.mc = None # media panel def left_down(self, x, y): self.x = x self.y = y self.canvas.medias.append(self) self.make_panel() pub.sendMessage('canvas.change_tool') def make_panel(self): if not self.mc: pub.sendMessage('media.create_panel', size=(self.x, self.y), media=self) if self.filename: self.mc.do_load_file(self.filename) def properties(self): return _("Loaded file") + u": %s" % self.filename def save(self): super(Media, self).save() self.remove_panel() def remove_panel(self): if self.mc: self.mc.Destroy() self.mc = None def load(self): super(Media, self).load() self.make_panel() #--------------------------------------------------------------------- class Eraser(Pen): """ Erases stuff. Has a custom cursor from a drawn rectangle on a DC, turned into an image then into a cursor. """ tooltip = _("Erase a drawing to the background") name = _("Eraser") icon = u"eraser" hotkey = u"e" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT): cursor = self.make_cursor(thickness) Pen.__init__(self, canvas, (255, 255, 255), thickness + 6, background, cursor) def make_cursor(self, thickness): cursor = wx.EmptyBitmap(thickness + 7, thickness + 7) memory = wx.MemoryDC() memory.SelectObject(cursor) if os.name == "posix": memory.SetPen(wx.Pen((0, 0, 0), 1)) # border memory.SetBrush(wx.Brush((255, 255, 255))) else: memory.SetPen(wx.Pen((0, 0, 0), 1)) # border memory.SetBrush(wx.Brush((255, 255, 255))) memory.DrawRectangle(0, 0, thickness + 7, thickness + 7) memory.SelectObject(wx.NullBitmap) img = wx.ImageFromBitmap(cursor) img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, (thickness + 7) / 2) img.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, (thickness + 7) / 2) cursor = wx.CursorFromImage(img) return cursor def preview(self, dc, width, height): thickness = self.thickness + 1 dc.SetPen(wx.Pen((0, 0, 0), 1, wx.SOLID)) dc.DrawRectangle(15, 7, thickness + 1, thickness + 1) def save(self): super(Eraser, self).save() self.cursor = None #---------------------------------------------------------------------- class Eyedrop(Tool): """ Selects the colour at the specified x,y coords """ tooltip = _("Picks a color from the selected pixel") name = _("Eyedropper") icon = u"eyedrop" hotkey = u"d" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT): Tool.__init__(self, canvas, colour, thickness, background, wx.CURSOR_CROSS) def left_down(self, x, y): dc = wx.BufferedDC(None, self.canvas.buffer) pub.sendMessage('change_colour', colour=dc.GetPixel(x, y)) pub.sendMessage('canvas.change_tool') def right_up(self, x, y): dc = wx.BufferedDC(None, self.canvas.buffer) pub.sendMessage('change_background', colour=dc.GetPixel(x, y)) pub.sendMessage('canvas.change_tool') def preview(self, dc, width, height): dc.SetBrush(wx.Brush(self.canvas.gui.get_colour())) dc.DrawRectangle(20, 20, 5, 5) #---------------------------------------------------------------------- class Text(OverlayShape): """ Allows the input of text. When a save is pickled, the wx.Font and a string storing its values is stored. This string is then used to reconstruct the font. """ tooltip = _("Input text") name = _("Text") icon = u"text" hotkey = u"t" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT): OverlayShape.__init__(self, canvas, colour, thickness, background, wx.CURSOR_IBEAM) self.font = None self.text = u"" self.font_data = "" self.extent = (0, 0) def handle_hit_test(self, x, y): pass def resize(self, x, y, handle=None): pass def left_down(self, x, y): super(Text, self).left_down(x, y) self.canvas.text = self def left_up(self, x, y): """ Shows the text input dialog, creates a new Shape object if the cancel button was pressed, otherwise updates the object's text, checks that the text string contains any letters and if so, adds itself to the list """ self.x = x self.y = y pub.sendMessage('text.show_dialog', text=self.text) if not self.canvas.text: return False self.font_data = self.font.GetNativeFontInfoDesc() if self.text: pub.sendMessage('shape.add', shape=self) return True self.canvas.text = None return False def edit(self, event=None): """ Pops up the TextInput box to edit itself """ text = self.text # restore to these if cancelled/blank font = self.font font_data = self.font_data colour = self.colour self.canvas.add_undo() if not self.canvas.show_text_edit_dialog(self): self.text = text # restore attributes self.font = font self.colour = colour self.font_data = font_data self.find_extent() self.canvas.undo_list.pop() # undo "undo point" :) self.canvas.redraw_all() # get rid of any text else: self.font_data = self.font.GetNativeFontInfoDesc() if not self.text: self.text = text # don't want a blank item return False return True def draw(self, dc, replay=False): if not self.font: self.restore_font() dc.SetFont(self.font) dc.SetTextForeground(self.colour) if self.background != wx.TRANSPARENT: dc.SetBackground(wx.Brush(self.background)) super(Text, self).draw(dc, replay, u"Label") def restore_font(self): """Updates the text's font to the saved font data""" self.font = wx.FFont(1, 1) self.font.SetNativeFontInfoFromString(self.font_data) if not self.font.IsOk(): f = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT) self.font = wx.FFont(f.GetPointSize(), f.GetFamily()) def find_extent(self): """Finds the width/height of the object's text""" dc = wx.ClientDC(self.canvas) x = dc.GetMultiLineTextExtent(self.text, self.font) self.extent = x[0], x[1] def find_edges(self): self.edges = {EDGE_TOP: self.y, EDGE_RIGHT: self.x + self.extent[0], EDGE_BOTTOM: self.y + self.extent[1], EDGE_LEFT: self.x} def get_handles(self): x, y, w, h = self.x, self.y, self.extent[0], self.extent[1] d = lambda x, y: (x - 2, y - 2) return d(x, y), d(x + w, y), d(x, y + h), d(x + w, y + h) def get_args(self): w = self.x + self.extent[0] h = self.y + self.extent[1] return [self.text, wx.Rect(self.x, self.y, w, h)] def hit_test(self, x, y): width = self.x + self.extent[0] height = self.y + self.extent[1] if x > self.x and x < width and y > self.y and y < height: return True return False def properties(self): return "%s -- X: %i, Y: %i" % (self.text[:20], self.x, self.y) def preview(self, dc, width, height): dc.SetTextForeground(self.colour) dc.DrawText(u"abcdef", 15, height / 2 - 10) def save(self): super(Text, self).save() self.font = None def load(self): super(Text, self).load() self.restore_font() #---------------------------------------------------------------------- SIZE = 10 # border size for note class Note(Text): """ A special type of text input, in the style of a post-it/"sticky" notes. It is linked to the tab it's displayed on, and is drawn with a light yellow background (to show that it's a note). An overview of notes for each tab can be viewed on the side panel. """ tooltip = _("Insert a note") name = _("Note") icon = u"note" hotkey = u"n" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT): Text.__init__(self, canvas, colour, thickness, background) self.tree_id = None def left_up(self, x, y,): """ Don't add a blank note """ if super(Note, self).left_up(x, y): pub.sendMessage('note.add', note=self) def edit(self): if super(Note, self).edit(): pub.sendMessage('note.edit', tree_id=self.tree_id, text=self.text) def find_extent(self): """Overrides to add extra spacing to the extent for the rectangle.""" super(Note, self).find_extent() self.extent = (self.extent[0] + 20, self.extent[1] + 20) def make_pen(self, dc=None): """We first must draw the Note outline""" if dc: self.find_extent() dc.SetBrush(wx.Brush((255, 223, 120))) dc.SetPen(wx.Pen((0, 0, 0), 1)) dc.DrawRectangle(self.x - SIZE, self.y - SIZE, *self.extent) super(Note, self).make_pen() def hit_test(self, x, y): width = self.x + self.extent[0] - SIZE height = self.y + self.extent[1] - SIZE if x > self.x - SIZE and x < width and y > self.y - SIZE and y < height: return True return False def get_handles(self): x, y, w, h = self.x, self.y, self.extent[0], self.extent[1] d = lambda x, y: (x - 2, y - 2) return (d(x - SIZE, y - SIZE), d(x + w - SIZE, y - SIZE), d(x - SIZE, y + h - SIZE), d(x + w - SIZE, y + h - SIZE)) def preview(self, dc, width, height): dc.SetBrush(wx.Brush((255, 223, 120))) dc.SetPen(wx.Pen((0, 0, 0), 1)) dc.FloodFill(0, 0, (255, 255, 255)) dc.SetBrush(wx.TRANSPARENT_BRUSH) super(Note, self).preview(dc, width, height) def load(self, add_note=True): """Recreates the note in the tree""" super(Note, self).load() if add_note: pub.sendMessage('note.add', note=self) #---------------------------------------------------------------------- class Image(OverlayShape): """ When being pickled, the image reference will be removed. """ name = _("Image") def __init__(self, canvas, image, path): OverlayShape.__init__(self, canvas, wx.BLACK, 1) self.image = image # of type wx.Bitmap self.path = path # not really needed anymore self.filename = None # used to restore image on load if path: self.filename = os.path.basename(path) self.resizing = False self.img = wx.ImageFromBitmap(image) # original wx.Image to rotate/scale self.angle = 0 self.scale_size = (image.GetWidth(), image.GetHeight()) self.center = None self.outline = None # Rectangle/Polygon, used to rotate/resize self.dragging = False # controls whether to draw the outline self.orig_click = None self.rotate_handle = None # wx.Rect def left_down(self, x, y): self.x = x self.y = y pub.sendMessage('shape.add', shape=self) self.canvas.resize_if_large_image((self.image.GetWidth(), self.image.GetHeight())) self.sort_handles() dc = wx.BufferedDC(None, self.canvas.buffer) self.draw(dc) self.canvas.redraw_dirty(dc) def sort_handles(self): """Sets the internal image that will be used to rotate, and its mask""" super(Image, self).sort_handles() if not self.img: self.img = wx.ImageFromBitmap(self.image) if not self.img.HasAlpha(): # black background otherwise self.img.InitAlpha() self.find_center() self.rotate_handle = wx.Rect(self.x + self.image.GetWidth() / 2 - 6, self.y + self.image.GetHeight() / 2 - 6, 15, 15) def find_center(self): self.center = (self.x + self.image.GetWidth() / 2, self.y + self.image.GetHeight() / 2) def find_edges(self): self.edges = {EDGE_TOP: self.y, EDGE_RIGHT: self.x + self.image.GetWidth(), EDGE_BOTTOM: self.y + self.image.GetWidth(), EDGE_LEFT: self.x} def handle_hit_test(self, x, y): """Returns which handle has been clicked on""" result = super(Image, self).handle_hit_test(x, y) if not result: if self.rotate_handle.ContainsXY(x, y): return HANDLE_ROTATE return result # nothing hit def draw_selected(self, dc): super(Image, self).draw_selected(dc) dc.SetBrush(wx.Brush((0, 255, 0))) dc.DrawCircle(self.x + self.image.GetWidth() / 2, self.y + self.image.GetHeight() / 2, 6) def resize(self, x, y, handle=None): """Rotate the image""" if handle == HANDLE_ROTATE: self.rotate((x, y)) else: self.rescale(x, y, handle) def rescale(self, x, y, handle): outline = self.outline outline.resize(x, y, handle) if outline.width < 10: outline.width = 10 if outline.height < 10: outline.height = 10 self.scale_size = (outline.width, outline.height) def rotate(self, position): """Rotate the outline.""" if not self.orig_click: self.orig_click = position knob_angle = self.outline.find_angle(self.orig_click, self.outline.center) mouse_angle = self.outline.find_angle(position, self.outline.center) self.angle = knob_angle - mouse_angle self.outline.do_rotate(self.angle) def start_select_action(self, handle): if handle: self.dragging = True if not handle: overlay = self.canvas.overlay # init.ing the rect resets the overlay if handle == HANDLE_ROTATE: self.outline = Polygon(self.canvas, wx.BLACK, 2) self.outline.x = self.x self.outline.y = self.y self.outline.points.append((self.x, self.y)) self.outline.points.append((self.x + self.image.GetWidth(), self.y)) self.outline.points.append((self.x + self.image.GetWidth(), self.y + + self.image.GetHeight())) self.outline.points.append((self.x, self.y + self.image.GetHeight())) elif handle in [TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT]: self.outline = Rectangle(self.canvas, wx.BLACK, 2) self.outline.x = self.x self.outline.y = self.y self.outline.width = self.image.GetWidth() self.outline.height = self.image.GetHeight() if not handle: self.canvas.overlay = overlay # so restore it else: self.outline.sort_handles() def end_select_action(self, handle): """Performs the rescale/rotation, resets attributes""" if self.outline and self.dragging: img = wx.BitmapFromImage(self.img) img = wx.ImageFromBitmap(img) img.Rescale(self.scale_size[0], self.scale_size[1], wx.IMAGE_QUALITY_HIGH) img.SetMaskColour(255, 255, 255) # stop black border bug img = img.Rotate(-self.angle, self.center) self.image = wx.BitmapFromImage(img) self.dragging = False self.orig_click = None self.outline = None self.sort_handles() self.canvas.redraw_all() def draw(self, dc, replay=False): super(Image, self).draw(dc, replay, u"Bitmap") if self.dragging: self.outline.draw(dc, replay) def get_args(self): return [self.image, self.x, self.y] def properties(self): a, b = "", "" if self.filename: a, b = _("Filename:"), self.filename return u"X: %i, Y: %i %s %i %s %i %s %s" % (self.x, self.y, _("Width:"), self.image.GetWidth(), _("Height:"), self.image.GetHeight(), a, b) def get_handles(self): d = lambda x, y: (x - 2, y - 2) x, y, w, h = self.x, self.y, self.image.GetWidth(), self.image.GetHeight() return d(x, y), d(x + w, y), d(x, y + h), d(x + w, y + h) def save(self): super(Image, self).save() self.image = None self.img = None def load(self): super(Image, self).load() if not hasattr(self, "outline"): self.outline = None if not hasattr(self, "dragging"): self.dragging = False if not hasattr(self, "scale_size"): self.scale_size = (0, 0) if not hasattr(self, "filename") or not self.filename: self.filename = os.path.basename(self.path) if self.filename.find("\\"): # loading windows file on linux self.filename = ntpath.basename(self.path) if not self.canvas.gui.util.is_zipped: if self.path and os.path.exists(self.path): self.image = wx.Bitmap(self.path) else: self.image = wx.EmptyBitmap(1, 1) wx.MessageBox(_("Path for the image %s not found.") % self.path, u"Whyteboard") else: try: data = self.canvas.gui.util.zip.read("data/" + self.filename) stream = cStringIO.StringIO(data) self.image = wx.BitmapFromImage(wx.ImageFromStream(stream)) except KeyError: self.image = wx.EmptyBitmap(1, 1) wx.MessageBox(_("File %s not found in the save") % self.filename, u"Whyteboard") self.img = wx.ImageFromBitmap(self.image) self.colour = wx.BLACK self.sort_handles() def hit_test(self, x, y): width, height = self.image.GetSize() rect = wx.Rect(self.x, self.y, width, height) if rect.ContainsXY(x, y): return True return False #---------------------------------------------------------------------- class Select(Tool): """ Select an item to move it around/resize/change colour/thickness/edit text Only create an undo point when an item is selected and been moved/resized """ tooltip = _("Select a shape to move and resize it") name = _("Shape Select ") icon = u"select" hotkey = u"s" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT): Tool.__init__(self, canvas, (0, 0, 0), 1, background, cursor=wx.CURSOR_ARROW) self.shape = None self.dragging = False self.undone = False # Adds an undo point once per class self.anchored = False # Anchor shape's x point -once-, when resizing self.handle = None # handle that was clicked on (if any) self.offset = (0, 0) def left_down(self, x, y): """ First, check the selected shape (which will be drawn on top of the others) so that's selected first. """ self.canvas.redraw_all() if self.canvas.selected: if self.check_for_hit(self.canvas.selected, x, y): return for shape in reversed(self.canvas.shapes): if self.check_for_hit(shape, x, y): break # breaking is vital to selecting the correct shape else: self.canvas.deselect_shape() def check_for_hit(self, shape, x, y): """ Sees if a shape is underneath the mouse coords, and allows the shape to be re-dragged to place """ found = False handle = shape.handle_hit_test(x, y) # test handle before area if handle: self.handle = handle found = True elif shape.hit_test(x, y): found = True if found: self.shape = shape self.dragging = True self.offset = self.shape.offset(x, y) pub.sendMessage('shape.selected', shape=shape) return found def right_up(self, x, y): """Pops up a shape menu if a shape was clicked on""" found = None for shape in reversed(self.canvas.shapes): if shape.handle_hit_test(x, y): found = shape elif shape.hit_test(x, y): found = shape if found: break if not found: return selected = None if self.canvas.selected: selected = self.canvas.selected self.canvas.selected = found pub.sendMessage('shape.popup', shape=found) if selected: self.canvas.selected = selected if not found.selected: self.canvas.selected = None def double_click(self, x, y): if isinstance(self.canvas.selected, Text): self.dragging = False self.canvas.selected.edit() def motion(self, x, y): if self.dragging: if not self.undone: # add a single undo point, not one per call self.canvas.add_undo() self.undone = True self.shape.start_select_action(self.handle) if not self.handle: # moving self.shape.move(x, y, self.offset) #self.shape.find_edges() #direction = self.canvas.drag_direction(self.shape.edges[EDGE_LEFT], self.shape.edges[EDGE_TOP]) #self.canvas.shape_near_canvas_edge(self.shape.edges[EDGE_LEFT], # self.shape.edges[EDGE_TOP], direction, True) else: if not self.anchored: # don't want to keep anchoring self.shape.anchor(self.handle) self.anchored = True self.shape.resize(x, y, self.handle) #self.canvas.shape_near_canvas_edge(x, y, self.canvas.drag_direction(x, y)) def draw(self, dc, replay=False): if self.dragging: self.shape.draw(dc, False) def left_up(self, x, y): if self.dragging: self.shape.end_select_action(self.handle) pub.sendMessage('update_shape_viewer') pub.sendMessage('thumbs.update_current') pub.sendMessage('canvas.change_tool') def preview(self, dc, width, height): bmp = wx.Bitmap(get_image_path(u"icons", u"cursor")) dc.DrawBitmap(bmp, width / 2 - 5, 12) #---------------------------------------------------------------------- class BitmapSelect(Rectangle): """ Rectangle selection tool, used to select a region to copy/paste. When it is drawn it is stored inside the current tab as an instance attribute, not in the shapes list. It is then drawn separately from other shapes """ tooltip = _("Select a rectangle region to copy as a bitmap") name = _("Bitmap Select") icon = u"select-rectangular" hotkey = u"b" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT): Rectangle.__init__(self, canvas, (0, 0, 0), 1) def left_down(self, x, y): self.canvas.overlay = wx.Overlay() super(BitmapSelect, self).left_down(x, y) self.canvas.deselect_shape() self.canvas.copy = None self.canvas.redraw_all() self.canvas.copy = self def draw(self, dc, replay=False): if not replay: odc = wx.DCOverlay(self.canvas.overlay, dc) odc.Clear() if (not replay and self.canvas.gui.util.config['bmp_select_transparent'] and meta.transparent): dc = wx.GCDC(dc) dc.SetBrush(wx.Brush(wx.Color(0, 0, 255, 50))) # light blue dc.SetPen(wx.Pen(self.colour, self.thickness, wx.SOLID)) else: dc.SetPen(wx.Pen(self.colour, self.thickness, wx.SHORT_DASH)) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.DrawRectangle(*self.get_args()) if not replay: del odc def left_up(self, x, y): """ Doesn't affect the shape list """ if not (x != self.x and y != self.y): self.canvas.copy = None wx.CallAfter(pub.sendMessage, 'canvas.change_tool') self = BitmapSelect.__init__(self, self.canvas, self.colour, self.thickness) def preview(self, dc, width, height): dc.SetPen(wx.BLACK_DASHED_PEN) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.DrawRectangle(10, 10, width - 20, height - 20) #---------------------------------------------------------------------- class Zoom(Tool): """ Zooms in and out on the canvas, by setting the user scale in the Whyteboard tab """ tooltip = _("Zoom in and out of the canvas") name = _("Zoom") icon = u"zoom" hotkey = u"z" def __init__(self, canvas, colour, thickness, background=wx.TRANSPARENT): Tool.__init__(self, canvas, (0, 0, 0), 1, background, wx.CURSOR_MAGNIFIER) def left_up(self, x, y): self.set_scale(0.15) def right_up(self, x, y): self.set_scale(-0.15) def set_scale(self, amount): x = self.canvas.scale new = (x[0] + amount, x[1] + amount) self.canvas.scale = new self.canvas.redraw_all() #--------------------------------------------------------------------- class Flood(Tool): """ Zooms in and out on the canvas, by setting the user scale in the Whyteboard tab """ tooltip = _("Flood fill an area") name = _("Flood Fill") icon = u"flood" hotkey = u"f" def __init__(self, canvas, colour, thickness): Tool.__init__(self, canvas, colour, 1) def left_down(self, x, y): self.x = x self.y = y self.canvas.draw_shape(self) pub.sendMessage('shape.add', shape=self) def draw(self, dc, replay=False): dc.SetPen(self.pen) dc.SetBrush(wx.Brush(self.colour)) dc.FloodFill(self.x, self.y, dc.GetPixel(self.x, self.y), wx.FLOOD_SURFACE) def preview(self, dc, width, height): dc.SetBrush(wx.Brush(self.colour)) dc.DrawRectangle(10, 10, width - 20, height - 20) #--------------------------------------------------------------------- def find_inverse(colour): """ Returns a wx.Brush inverted the (R, G, B) colour """ if not isinstance(colour, wx.Colour): c = colour colour = wx.Colour() try: colour.SetFromName(c) except TypeError: colour.Set(*c) r = 255 - colour.Red() g = 255 - colour.Green() b = 255 - colour.Blue() return wx.Brush((r, g, b)) # Reference the correct classes for pickled files with old class names class RoundRect: self = RoundedRect class RectSelect: pass RoundRect = RoundedRect RectSelect = BitmapSelect # items to draw with. Note: the GUI inserts the highlighter items = [Pen, Eraser, Rectangle, RoundedRect, Ellipse, Circle, Polygon, Line, Arrow, Text, Note, Media, Eyedrop, BitmapSelect, Select] whyteboard-0.41.1/whyteboard/__init__.py0000777000175000017500000000012711443222121017263 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- from whyteboard.gui import WhyteboardAppwhyteboard-0.41.1/whyteboard/lib/dragscroller.py0000777000175000017500000000514311443222121020760 0ustar stevesteve#----------------------------------------------------------------------------- # Name: dragscroller.py # Purpose: Scrolls a wx.ScrollWindow by dragging # # Author: Riaan Booysen # # Created: 2006/09/05 # Copyright: (c) 2006 # Licence: wxPython #----------------------------------------------------------------------------- import wx class DragScroller: """ Scrolls a wx.ScrollWindow in the direction and speed of a mouse drag. Call Start with the position of the drag start. Call Stop on the drag release. """ def __init__(self, scrollwin, rate=30, sensitivity=0.75): self.scrollwin = scrollwin self.rate = rate self.sensitivity = sensitivity self.pos = None self.timer = None def GetScrollWindow(self): return self.scrollwin def SetScrollWindow(self, scrollwin): self.scrollwin = scrollwin def GetUpdateRate(self): return self.rate def SetUpdateRate(self, rate): self.rate = rate def GetSensitivity(self): return self.sensitivity def SetSensitivity(self, sensitivity): self.sensitivity = sensitivity def Start(self, pos): """ Start a drag scroll operation """ if not self.scrollwin: raise Exception, 'No ScrollWindow defined' self.pos = pos self.scrollwin.SetCursor(wx.StockCursor(wx.CURSOR_SIZING)) if not self.scrollwin.HasCapture(): self.scrollwin.CaptureMouse() self.timer = wx.Timer(self.scrollwin) self.scrollwin.Bind(wx.EVT_TIMER, self.OnTimerDoScroll, id=self.timer.GetId()) self.timer.Start(self.rate) def Stop(self): """ Stops a drag scroll operation """ if self.timer and self.scrollwin: self.timer.Stop() self.scrollwin.Disconnect(self.timer.GetId()) self.timer.Destroy() self.timer = None self.scrollwin.SetCursor(wx.STANDARD_CURSOR) if self.scrollwin.HasCapture(): self.scrollwin.ReleaseMouse() def OnTimerDoScroll(self, event): if self.pos is None or not self.timer or not self.scrollwin: return new = self.scrollwin.ScreenToClient(wx.GetMousePosition()) dx = int((new.x-self.pos.x)*self.sensitivity) dy = int((new.y-self.pos.y)*self.sensitivity) spx = self.scrollwin.GetScrollPos(wx.HORIZONTAL) spy = self.scrollwin.GetScrollPos(wx.VERTICAL) self.scrollwin.Scroll(spx+dx, spy+dy) whyteboard-0.41.1/whyteboard/lib/configobj.py0000777000175000017500000024771511443222121020252 0ustar stevesteve# configobj.py # A config file reader/writer that supports nested sections in config files. # Copyright (C) 2005-2010 Michael Foord, Nicola Larosa # E-mail: fuzzyman AT voidspace DOT org DOT uk # nico AT tekNico DOT net # ConfigObj 4 # http://www.voidspace.org.uk/python/configobj.html # Released subject to the BSD License # Please see http://www.voidspace.org.uk/python/license.shtml # Scripts maintained at http://www.voidspace.org.uk/python/index.shtml # For information about bugfixes, updates and support, please join the # ConfigObj mailing list: # http://lists.sourceforge.net/lists/listinfo/configobj-develop # Comments, suggestions and bug reports welcome. from __future__ import generators import os import re import sys from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE # imported lazily to avoid startup performance hit if it isn't used compiler = None # A dictionary mapping BOM to # the encoding to decode with, and what to set the # encoding attribute to. BOMS = { BOM_UTF8: ('utf_8', None), BOM_UTF16_BE: ('utf16_be', 'utf_16'), BOM_UTF16_LE: ('utf16_le', 'utf_16'), BOM_UTF16: ('utf_16', 'utf_16'), } # All legal variants of the BOM codecs. # TODO: the list of aliases is not meant to be exhaustive, is there a # better way ? BOM_LIST = { 'utf_16': 'utf_16', 'u16': 'utf_16', 'utf16': 'utf_16', 'utf-16': 'utf_16', 'utf16_be': 'utf16_be', 'utf_16_be': 'utf16_be', 'utf-16be': 'utf16_be', 'utf16_le': 'utf16_le', 'utf_16_le': 'utf16_le', 'utf-16le': 'utf16_le', 'utf_8': 'utf_8', 'u8': 'utf_8', 'utf': 'utf_8', 'utf8': 'utf_8', 'utf-8': 'utf_8', } # Map of encodings to the BOM to write. BOM_SET = { 'utf_8': BOM_UTF8, 'utf_16': BOM_UTF16, 'utf16_be': BOM_UTF16_BE, 'utf16_le': BOM_UTF16_LE, None: BOM_UTF8 } def match_utf8(encoding): return BOM_LIST.get(encoding.lower()) == 'utf_8' # Quote strings used for writing values squot = "'%s'" dquot = '"%s"' noquot = "%s" wspace_plus = ' \r\n\v\t\'"' tsquot = '"""%s"""' tdquot = "'''%s'''" # Sentinel for use in getattr calls to replace hasattr MISSING = object() __version__ = '4.7.0' try: any except NameError: def any(iterable): for entry in iterable: if entry: return True return False __all__ = ( '__version__', 'DEFAULT_INDENT_TYPE', 'DEFAULT_INTERPOLATION', 'ConfigObjError', 'NestingError', 'ParseError', 'DuplicateError', 'ConfigspecError', 'ConfigObj', 'SimpleVal', 'InterpolationError', 'InterpolationLoopError', 'MissingInterpolationOption', 'RepeatSectionError', 'ReloadError', 'UnreprError', 'UnknownType', 'flatten_errors', 'get_extra_values' ) DEFAULT_INTERPOLATION = 'configparser' DEFAULT_INDENT_TYPE = ' ' MAX_INTERPOL_DEPTH = 10 OPTION_DEFAULTS = { 'interpolation': True, 'raise_errors': False, 'list_values': True, 'create_empty': False, 'file_error': False, 'configspec': None, 'stringify': True, # option may be set to one of ('', ' ', '\t') 'indent_type': None, 'encoding': None, 'default_encoding': None, 'unrepr': False, 'write_empty_values': False, } def getObj(s): global compiler if compiler is None: import compiler s = "a=" + s p = compiler.parse(s) return p.getChildren()[1].getChildren()[0].getChildren()[1] class UnknownType(Exception): pass class Builder(object): def build(self, o): m = getattr(self, 'build_' + o.__class__.__name__, None) if m is None: raise UnknownType(o.__class__.__name__) return m(o) def build_List(self, o): return map(self.build, o.getChildren()) def build_Const(self, o): return o.value def build_Dict(self, o): d = {} i = iter(map(self.build, o.getChildren())) for el in i: d[el] = i.next() return d def build_Tuple(self, o): return tuple(self.build_List(o)) def build_Name(self, o): if o.name == 'None': return None if o.name == 'True': return True if o.name == 'False': return False # An undefined Name raise UnknownType('Undefined Name') def build_Add(self, o): real, imag = map(self.build_Const, o.getChildren()) try: real = float(real) except TypeError: raise UnknownType('Add') if not isinstance(imag, complex) or imag.real != 0.0: raise UnknownType('Add') return real+imag def build_Getattr(self, o): parent = self.build(o.expr) return getattr(parent, o.attrname) def build_UnarySub(self, o): return -self.build_Const(o.getChildren()[0]) def build_UnaryAdd(self, o): return self.build_Const(o.getChildren()[0]) _builder = Builder() def unrepr(s): if not s: return s return _builder.build(getObj(s)) class ConfigObjError(SyntaxError): """ This is the base class for all errors that ConfigObj raises. It is a subclass of SyntaxError. """ def __init__(self, message='', line_number=None, line=''): self.line = line self.line_number = line_number SyntaxError.__init__(self, message) class NestingError(ConfigObjError): """ This error indicates a level of nesting that doesn't match. """ class ParseError(ConfigObjError): """ This error indicates that a line is badly written. It is neither a valid ``key = value`` line, nor a valid section marker line. """ class ReloadError(IOError): """ A 'reload' operation failed. This exception is a subclass of ``IOError``. """ def __init__(self): IOError.__init__(self, 'reload failed, filename is not set.') class DuplicateError(ConfigObjError): """ The keyword or section specified already exists. """ class ConfigspecError(ConfigObjError): """ An error occured whilst parsing a configspec. """ class InterpolationError(ConfigObjError): """Base class for the two interpolation errors.""" class InterpolationLoopError(InterpolationError): """Maximum interpolation depth exceeded in string interpolation.""" def __init__(self, option): InterpolationError.__init__( self, 'interpolation loop detected in value "%s".' % option) class RepeatSectionError(ConfigObjError): """ This error indicates additional sections in a section with a ``__many__`` (repeated) section. """ class MissingInterpolationOption(InterpolationError): """A value specified for interpolation was missing.""" def __init__(self, option): msg = 'missing option "%s" in interpolation.' % option InterpolationError.__init__(self, msg) class UnreprError(ConfigObjError): """An error parsing in unrepr mode.""" class InterpolationEngine(object): """ A helper class to help perform string interpolation. This class is an abstract base class; its descendants perform the actual work. """ # compiled regexp to use in self.interpolate() _KEYCRE = re.compile(r"%\(([^)]*)\)s") _cookie = '%' def __init__(self, section): # the Section instance that "owns" this engine self.section = section def interpolate(self, key, value): # short-cut if not self._cookie in value: return value def recursive_interpolate(key, value, section, backtrail): """The function that does the actual work. ``value``: the string we're trying to interpolate. ``section``: the section in which that string was found ``backtrail``: a dict to keep track of where we've been, to detect and prevent infinite recursion loops This is similar to a depth-first-search algorithm. """ # Have we been here already? if (key, section.name) in backtrail: # Yes - infinite loop detected raise InterpolationLoopError(key) # Place a marker on our backtrail so we won't come back here again backtrail[(key, section.name)] = 1 # Now start the actual work match = self._KEYCRE.search(value) while match: # The actual parsing of the match is implementation-dependent, # so delegate to our helper function k, v, s = self._parse_match(match) if k is None: # That's the signal that no further interpolation is needed replacement = v else: # Further interpolation may be needed to obtain final value replacement = recursive_interpolate(k, v, s, backtrail) # Replace the matched string with its final value start, end = match.span() value = ''.join((value[:start], replacement, value[end:])) new_search_start = start + len(replacement) # Pick up the next interpolation key, if any, for next time # through the while loop match = self._KEYCRE.search(value, new_search_start) # Now safe to come back here again; remove marker from backtrail del backtrail[(key, section.name)] return value # Back in interpolate(), all we have to do is kick off the recursive # function with appropriate starting values value = recursive_interpolate(key, value, self.section, {}) return value def _fetch(self, key): """Helper function to fetch values from owning section. Returns a 2-tuple: the value, and the section where it was found. """ # switch off interpolation before we try and fetch anything ! save_interp = self.section.main.interpolation self.section.main.interpolation = False # Start at section that "owns" this InterpolationEngine current_section = self.section while True: # try the current section first val = current_section.get(key) if val is not None: break # try "DEFAULT" next val = current_section.get('DEFAULT', {}).get(key) if val is not None: break # move up to parent and try again # top-level's parent is itself if current_section.parent is current_section: # reached top level, time to give up break current_section = current_section.parent # restore interpolation to previous value before returning self.section.main.interpolation = save_interp if val is None: raise MissingInterpolationOption(key) return val, current_section def _parse_match(self, match): """Implementation-dependent helper function. Will be passed a match object corresponding to the interpolation key we just found (e.g., "%(foo)s" or "$foo"). Should look up that key in the appropriate config file section (using the ``_fetch()`` helper function) and return a 3-tuple: (key, value, section) ``key`` is the name of the key we're looking for ``value`` is the value found for that key ``section`` is a reference to the section where it was found ``key`` and ``section`` should be None if no further interpolation should be performed on the resulting value (e.g., if we interpolated "$$" and returned "$"). """ raise NotImplementedError() class ConfigParserInterpolation(InterpolationEngine): """Behaves like ConfigParser.""" _cookie = '%' _KEYCRE = re.compile(r"%\(([^)]*)\)s") def _parse_match(self, match): key = match.group(1) value, section = self._fetch(key) return key, value, section class TemplateInterpolation(InterpolationEngine): """Behaves like string.Template.""" _cookie = '$' _delimiter = '$' _KEYCRE = re.compile(r""" \$(?: (?P\$) | # Two $ signs (?P[_a-z][_a-z0-9]*) | # $name format {(?P[^}]*)} # ${name} format ) """, re.IGNORECASE | re.VERBOSE) def _parse_match(self, match): # Valid name (in or out of braces): fetch value from section key = match.group('named') or match.group('braced') if key is not None: value, section = self._fetch(key) return key, value, section # Escaped delimiter (e.g., $$): return single delimiter if match.group('escaped') is not None: # Return None for key and section to indicate it's time to stop return None, self._delimiter, None # Anything else: ignore completely, just return it unchanged return None, match.group(), None interpolation_engines = { 'configparser': ConfigParserInterpolation, 'template': TemplateInterpolation, } def __newobj__(cls, *args): # Hack for pickle return cls.__new__(cls, *args) class Section(dict): """ A dictionary-like object that represents a section in a config file. It does string interpolation if the 'interpolation' attribute of the 'main' object is set to True. Interpolation is tried first from this object, then from the 'DEFAULT' section of this object, next from the parent and its 'DEFAULT' section, and so on until the main object is reached. A Section will behave like an ordered dictionary - following the order of the ``scalars`` and ``sections`` attributes. You can use this to change the order of members. Iteration follows the order: scalars, then sections. """ def __setstate__(self, state): dict.update(self, state[0]) self.__dict__.update(state[1]) def __reduce__(self): state = (dict(self), self.__dict__) return (__newobj__, (self.__class__,), state) def __init__(self, parent, depth, main, indict=None, name=None): """ * parent is the section above * depth is the depth level of this section * main is the main ConfigObj * indict is a dictionary to initialise the section with """ if indict is None: indict = {} dict.__init__(self) # used for nesting level *and* interpolation self.parent = parent # used for the interpolation attribute self.main = main # level of nesting depth of this Section self.depth = depth # purely for information self.name = name # self._initialise() # we do this explicitly so that __setitem__ is used properly # (rather than just passing to ``dict.__init__``) for entry, value in indict.iteritems(): self[entry] = value def _initialise(self): # the sequence of scalar values in this Section self.scalars = [] # the sequence of sections in this Section self.sections = [] # for comments :-) self.comments = {} self.inline_comments = {} # the configspec self.configspec = None # for defaults self.defaults = [] self.default_values = {} self.extra_values = [] self._created = False def _interpolate(self, key, value): try: # do we already have an interpolation engine? engine = self._interpolation_engine except AttributeError: # not yet: first time running _interpolate(), so pick the engine name = self.main.interpolation if name == True: # note that "if name:" would be incorrect here # backwards-compatibility: interpolation=True means use default name = DEFAULT_INTERPOLATION name = name.lower() # so that "Template", "template", etc. all work class_ = interpolation_engines.get(name, None) if class_ is None: # invalid value for self.main.interpolation self.main.interpolation = False return value else: # save reference to engine so we don't have to do this again engine = self._interpolation_engine = class_(self) # let the engine do the actual work return engine.interpolate(key, value) def __getitem__(self, key): """Fetch the item and do string interpolation.""" val = dict.__getitem__(self, key) if self.main.interpolation: if isinstance(val, basestring): return self._interpolate(key, val) if isinstance(val, list): def _check(entry): if isinstance(entry, basestring): return self._interpolate(key, entry) return entry return [_check(entry) for entry in val] return val def __setitem__(self, key, value, unrepr=False): """ Correctly set a value. Making dictionary values Section instances. (We have to special case 'Section' instances - which are also dicts) Keys must be strings. Values need only be strings (or lists of strings) if ``main.stringify`` is set. ``unrepr`` must be set when setting a value to a dictionary, without creating a new sub-section. """ if not isinstance(key, basestring): raise ValueError('The key "%s" is not a string.' % key) # add the comment if key not in self.comments: self.comments[key] = [] self.inline_comments[key] = '' # remove the entry from defaults if key in self.defaults: self.defaults.remove(key) # if isinstance(value, Section): if key not in self: self.sections.append(key) dict.__setitem__(self, key, value) elif isinstance(value, dict) and not unrepr: # First create the new depth level, # then create the section if key not in self: self.sections.append(key) new_depth = self.depth + 1 dict.__setitem__( self, key, Section( self, new_depth, self.main, indict=value, name=key)) else: if key not in self: self.scalars.append(key) if not self.main.stringify: if isinstance(value, basestring): pass elif isinstance(value, (list, tuple)): for entry in value: if not isinstance(entry, basestring): raise TypeError('Value is not a string "%s".' % entry) else: raise TypeError('Value is not a string "%s".' % value) dict.__setitem__(self, key, value) def __delitem__(self, key): """Remove items from the sequence when deleting.""" dict. __delitem__(self, key) if key in self.scalars: self.scalars.remove(key) else: self.sections.remove(key) del self.comments[key] del self.inline_comments[key] def get(self, key, default=None): """A version of ``get`` that doesn't bypass string interpolation.""" try: return self[key] except KeyError: return default def update(self, indict): """ A version of update that uses our ``__setitem__``. """ for entry in indict: self[entry] = indict[entry] def pop(self, key, *args): """ 'D.pop(k[,d]) -> v, remove specified key and return the corresponding value. If key is not found, d is returned if given, otherwise KeyError is raised' """ val = dict.pop(self, key, *args) if key in self.scalars: del self.comments[key] del self.inline_comments[key] self.scalars.remove(key) elif key in self.sections: del self.comments[key] del self.inline_comments[key] self.sections.remove(key) if self.main.interpolation and isinstance(val, basestring): return self._interpolate(key, val) return val def popitem(self): """Pops the first (key,val)""" sequence = (self.scalars + self.sections) if not sequence: raise KeyError(": 'popitem(): dictionary is empty'") key = sequence[0] val = self[key] del self[key] return key, val def clear(self): """ A version of clear that also affects scalars/sections Also clears comments and configspec. Leaves other attributes alone : depth/main/parent are not affected """ dict.clear(self) self.scalars = [] self.sections = [] self.comments = {} self.inline_comments = {} self.configspec = None self.defaults = [] self.extra_values = [] def setdefault(self, key, default=None): """A version of setdefault that sets sequence if appropriate.""" try: return self[key] except KeyError: self[key] = default return self[key] def items(self): """D.items() -> list of D's (key, value) pairs, as 2-tuples""" return zip((self.scalars + self.sections), self.values()) def keys(self): """D.keys() -> list of D's keys""" return (self.scalars + self.sections) def values(self): """D.values() -> list of D's values""" return [self[key] for key in (self.scalars + self.sections)] def iteritems(self): """D.iteritems() -> an iterator over the (key, value) items of D""" return iter(self.items()) def iterkeys(self): """D.iterkeys() -> an iterator over the keys of D""" return iter((self.scalars + self.sections)) __iter__ = iterkeys def itervalues(self): """D.itervalues() -> an iterator over the values of D""" return iter(self.values()) def __repr__(self): """x.__repr__() <==> repr(x)""" return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(self[key]))) for key in (self.scalars + self.sections)]) __str__ = __repr__ __str__.__doc__ = "x.__str__() <==> str(x)" # Extra methods - not in a normal dictionary def dict(self): """ Return a deepcopy of self as a dictionary. All members that are ``Section`` instances are recursively turned to ordinary dictionaries - by calling their ``dict`` method. >>> n = a.dict() >>> n == a 1 >>> n is a 0 """ newdict = {} for entry in self: this_entry = self[entry] if isinstance(this_entry, Section): this_entry = this_entry.dict() elif isinstance(this_entry, list): # create a copy rather than a reference this_entry = list(this_entry) elif isinstance(this_entry, tuple): # create a copy rather than a reference this_entry = tuple(this_entry) newdict[entry] = this_entry return newdict def merge(self, indict): """ A recursive update - useful for merging config files. >>> a = '''[section1] ... option1 = True ... [[subsection]] ... more_options = False ... # end of file'''.splitlines() >>> b = '''# File is user.ini ... [section1] ... option1 = False ... # end of file'''.splitlines() >>> c1 = ConfigObj(b) >>> c2 = ConfigObj(a) >>> c2.merge(c1) >>> c2 ConfigObj({'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}}) """ for key, val in indict.items(): if (key in self and isinstance(self[key], dict) and isinstance(val, dict)): self[key].merge(val) else: self[key] = val def rename(self, oldkey, newkey): """ Change a keyname to another, without changing position in sequence. Implemented so that transformations can be made on keys, as well as on values. (used by encode and decode) Also renames comments. """ if oldkey in self.scalars: the_list = self.scalars elif oldkey in self.sections: the_list = self.sections else: raise KeyError('Key "%s" not found.' % oldkey) pos = the_list.index(oldkey) # val = self[oldkey] dict.__delitem__(self, oldkey) dict.__setitem__(self, newkey, val) the_list.remove(oldkey) the_list.insert(pos, newkey) comm = self.comments[oldkey] inline_comment = self.inline_comments[oldkey] del self.comments[oldkey] del self.inline_comments[oldkey] self.comments[newkey] = comm self.inline_comments[newkey] = inline_comment def walk(self, function, raise_errors=True, call_on_sections=False, **keywargs): """ Walk every member and call a function on the keyword and value. Return a dictionary of the return values If the function raises an exception, raise the errror unless ``raise_errors=False``, in which case set the return value to ``False``. Any unrecognised keyword arguments you pass to walk, will be pased on to the function you pass in. Note: if ``call_on_sections`` is ``True`` then - on encountering a subsection, *first* the function is called for the *whole* subsection, and then recurses into it's members. This means your function must be able to handle strings, dictionaries and lists. This allows you to change the key of subsections as well as for ordinary members. The return value when called on the whole subsection has to be discarded. See the encode and decode methods for examples, including functions. .. admonition:: caution You can use ``walk`` to transform the names of members of a section but you mustn't add or delete members. >>> config = '''[XXXXsection] ... XXXXkey = XXXXvalue'''.splitlines() >>> cfg = ConfigObj(config) >>> cfg ConfigObj({'XXXXsection': {'XXXXkey': 'XXXXvalue'}}) >>> def transform(section, key): ... val = section[key] ... newkey = key.replace('XXXX', 'CLIENT1') ... section.rename(key, newkey) ... if isinstance(val, (tuple, list, dict)): ... pass ... else: ... val = val.replace('XXXX', 'CLIENT1') ... section[newkey] = val >>> cfg.walk(transform, call_on_sections=True) {'CLIENT1section': {'CLIENT1key': None}} >>> cfg ConfigObj({'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}}) """ out = {} # scalars first for i in range(len(self.scalars)): entry = self.scalars[i] try: val = function(self, entry, **keywargs) # bound again in case name has changed entry = self.scalars[i] out[entry] = val except Exception: if raise_errors: raise else: entry = self.scalars[i] out[entry] = False # then sections for i in range(len(self.sections)): entry = self.sections[i] if call_on_sections: try: function(self, entry, **keywargs) except Exception: if raise_errors: raise else: entry = self.sections[i] out[entry] = False # bound again in case name has changed entry = self.sections[i] # previous result is discarded out[entry] = self[entry].walk( function, raise_errors=raise_errors, call_on_sections=call_on_sections, **keywargs) return out def as_bool(self, key): """ Accepts a key as input. The corresponding value must be a string or the objects (``True`` or 1) or (``False`` or 0). We allow 0 and 1 to retain compatibility with Python 2.2. If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns ``True``. If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns ``False``. ``as_bool`` is not case sensitive. Any other input will raise a ``ValueError``. >>> a = ConfigObj() >>> a['a'] = 'fish' >>> a.as_bool('a') Traceback (most recent call last): ValueError: Value "fish" is neither True nor False >>> a['b'] = 'True' >>> a.as_bool('b') 1 >>> a['b'] = 'off' >>> a.as_bool('b') 0 """ val = self[key] if val == True: return True elif val == False: return False else: try: if not isinstance(val, basestring): # TODO: Why do we raise a KeyError here? raise KeyError() else: return self.main._bools[val.lower()] except KeyError: raise ValueError('Value "%s" is neither True nor False' % val) def as_int(self, key): """ A convenience method which coerces the specified value to an integer. If the value is an invalid literal for ``int``, a ``ValueError`` will be raised. >>> a = ConfigObj() >>> a['a'] = 'fish' >>> a.as_int('a') Traceback (most recent call last): ValueError: invalid literal for int() with base 10: 'fish' >>> a['b'] = '1' >>> a.as_int('b') 1 >>> a['b'] = '3.2' >>> a.as_int('b') Traceback (most recent call last): ValueError: invalid literal for int() with base 10: '3.2' """ return int(self[key]) def as_float(self, key): """ A convenience method which coerces the specified value to a float. If the value is an invalid literal for ``float``, a ``ValueError`` will be raised. >>> a = ConfigObj() >>> a['a'] = 'fish' >>> a.as_float('a') Traceback (most recent call last): ValueError: invalid literal for float(): fish >>> a['b'] = '1' >>> a.as_float('b') 1.0 >>> a['b'] = '3.2' >>> a.as_float('b') 3.2000000000000002 """ return float(self[key]) def as_list(self, key): """ A convenience method which fetches the specified value, guaranteeing that it is a list. >>> a = ConfigObj() >>> a['a'] = 1 >>> a.as_list('a') [1] >>> a['a'] = (1,) >>> a.as_list('a') [1] >>> a['a'] = [1] >>> a.as_list('a') [1] """ result = self[key] if isinstance(result, (tuple, list)): return list(result) return [result] def restore_default(self, key): """ Restore (and return) default value for the specified key. This method will only work for a ConfigObj that was created with a configspec and has been validated. If there is no default value for this key, ``KeyError`` is raised. """ default = self.default_values[key] dict.__setitem__(self, key, default) if key not in self.defaults: self.defaults.append(key) return default def restore_defaults(self): """ Recursively restore default values to all members that have them. This method will only work for a ConfigObj that was created with a configspec and has been validated. It doesn't delete or modify entries without default values. """ for key in self.default_values: self.restore_default(key) for section in self.sections: self[section].restore_defaults() class ConfigObj(Section): """An object to read, create, and write config files.""" _keyword = re.compile(r'''^ # line start (\s*) # indentation ( # keyword (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'"=].*?) # no quotes ) \s*=\s* # divider (.*) # value (including list values and comments) $ # line end ''', re.VERBOSE) _sectionmarker = re.compile(r'''^ (\s*) # 1: indentation ((?:\[\s*)+) # 2: section marker open ( # 3: section name open (?:"\s*\S.*?\s*")| # at least one non-space with double quotes (?:'\s*\S.*?\s*')| # at least one non-space with single quotes (?:[^'"\s].*?) # at least one non-space unquoted ) # section name close ((?:\s*\])+) # 4: section marker close \s*(\#.*)? # 5: optional comment $''', re.VERBOSE) # this regexp pulls list values out as a single string # or single values and comments # FIXME: this regex adds a '' to the end of comma terminated lists # workaround in ``_handle_value`` _valueexp = re.compile(r'''^ (?: (?: ( (?: (?: (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\#][^,\#]*?) # unquoted ) \s*,\s* # comma )* # match all list items ending in a comma (if any) ) ( (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\#\s][^,]*?)| # unquoted (?:(? 1: msg = "Parsing failed with several errors.\nFirst error %s" % info error = ConfigObjError(msg) else: error = self._errors[0] # set the errors attribute; it's a list of tuples: # (error_type, message, line_number) error.errors = self._errors # set the config attribute error.config = self raise error # delete private attributes del self._errors if configspec is None: self.configspec = None else: self._handle_configspec(configspec) def _initialise(self, options=None): if options is None: options = OPTION_DEFAULTS # initialise a few variables self.filename = None self._errors = [] self.raise_errors = options['raise_errors'] self.interpolation = options['interpolation'] self.list_values = options['list_values'] self.create_empty = options['create_empty'] self.file_error = options['file_error'] self.stringify = options['stringify'] self.indent_type = options['indent_type'] self.encoding = options['encoding'] self.default_encoding = options['default_encoding'] self.BOM = False self.newlines = None self.write_empty_values = options['write_empty_values'] self.unrepr = options['unrepr'] self.initial_comment = [] self.final_comment = [] self.configspec = None if self._inspec: self.list_values = False # Clear section attributes as well Section._initialise(self) def __repr__(self): return ('ConfigObj({%s})' % ', '.join([('%s: %s' % (repr(key), repr(self[key]))) for key in (self.scalars + self.sections)])) def _handle_bom(self, infile): """ Handle any BOM, and decode if necessary. If an encoding is specified, that *must* be used - but the BOM should still be removed (and the BOM attribute set). (If the encoding is wrongly specified, then a BOM for an alternative encoding won't be discovered or removed.) If an encoding is not specified, UTF8 or UTF16 BOM will be detected and removed. The BOM attribute will be set. UTF16 will be decoded to unicode. NOTE: This method must not be called with an empty ``infile``. Specifying the *wrong* encoding is likely to cause a ``UnicodeDecodeError``. ``infile`` must always be returned as a list of lines, but may be passed in as a single string. """ if ((self.encoding is not None) and (self.encoding.lower() not in BOM_LIST)): # No need to check for a BOM # the encoding specified doesn't have one # just decode return self._decode(infile, self.encoding) if isinstance(infile, (list, tuple)): line = infile[0] else: line = infile if self.encoding is not None: # encoding explicitly supplied # And it could have an associated BOM # TODO: if encoding is just UTF16 - we ought to check for both # TODO: big endian and little endian versions. enc = BOM_LIST[self.encoding.lower()] if enc == 'utf_16': # For UTF16 we try big endian and little endian for BOM, (encoding, final_encoding) in BOMS.items(): if not final_encoding: # skip UTF8 continue if infile.startswith(BOM): ### BOM discovered ##self.BOM = True # Don't need to remove BOM return self._decode(infile, encoding) # If we get this far, will *probably* raise a DecodeError # As it doesn't appear to start with a BOM return self._decode(infile, self.encoding) # Must be UTF8 BOM = BOM_SET[enc] if not line.startswith(BOM): return self._decode(infile, self.encoding) newline = line[len(BOM):] # BOM removed if isinstance(infile, (list, tuple)): infile[0] = newline else: infile = newline self.BOM = True return self._decode(infile, self.encoding) # No encoding specified - so we need to check for UTF8/UTF16 for BOM, (encoding, final_encoding) in BOMS.items(): if not line.startswith(BOM): continue else: # BOM discovered self.encoding = final_encoding if not final_encoding: self.BOM = True # UTF8 # remove BOM newline = line[len(BOM):] if isinstance(infile, (list, tuple)): infile[0] = newline else: infile = newline # UTF8 - don't decode if isinstance(infile, basestring): return infile.splitlines(True) else: return infile # UTF16 - have to decode return self._decode(infile, encoding) # No BOM discovered and no encoding specified, just return if isinstance(infile, basestring): # infile read from a file will be a single string return infile.splitlines(True) return infile def _a_to_u(self, aString): """Decode ASCII strings to unicode if a self.encoding is specified.""" if self.encoding: return aString.decode('ascii') else: return aString def _decode(self, infile, encoding): """ Decode infile to unicode. Using the specified encoding. if is a string, it also needs converting to a list. """ if isinstance(infile, basestring): # can't be unicode # NOTE: Could raise a ``UnicodeDecodeError`` return infile.decode(encoding).splitlines(True) for i, line in enumerate(infile): if not isinstance(line, unicode): # NOTE: The isinstance test here handles mixed lists of unicode/string # NOTE: But the decode will break on any non-string values # NOTE: Or could raise a ``UnicodeDecodeError`` infile[i] = line.decode(encoding) return infile def _decode_element(self, line): """Decode element to unicode if necessary.""" if not self.encoding: return line if isinstance(line, str) and self.default_encoding: return line.decode(self.default_encoding) return line def _str(self, value): """ Used by ``stringify`` within validate, to turn non-string values into strings. """ if not isinstance(value, basestring): return str(value) else: return value def _parse(self, infile): """Actually parse the config file.""" temp_list_values = self.list_values if self.unrepr: self.list_values = False comment_list = [] done_start = False this_section = self maxline = len(infile) - 1 cur_index = -1 reset_comment = False while cur_index < maxline: if reset_comment: comment_list = [] cur_index += 1 line = infile[cur_index] sline = line.strip() # do we have anything on the line ? if not sline or sline.startswith('#'): reset_comment = False comment_list.append(line) continue if not done_start: # preserve initial comment self.initial_comment = comment_list comment_list = [] done_start = True reset_comment = True # first we check if it's a section marker mat = self._sectionmarker.match(line) if mat is not None: # is a section line (indent, sect_open, sect_name, sect_close, comment) = mat.groups() if indent and (self.indent_type is None): self.indent_type = indent cur_depth = sect_open.count('[') if cur_depth != sect_close.count(']'): self._handle_error("Cannot compute the section depth at line %s.", NestingError, infile, cur_index) continue if cur_depth < this_section.depth: # the new section is dropping back to a previous level try: parent = self._match_depth(this_section, cur_depth).parent except SyntaxError: self._handle_error("Cannot compute nesting level at line %s.", NestingError, infile, cur_index) continue elif cur_depth == this_section.depth: # the new section is a sibling of the current section parent = this_section.parent elif cur_depth == this_section.depth + 1: # the new section is a child the current section parent = this_section else: self._handle_error("Section too nested at line %s.", NestingError, infile, cur_index) sect_name = self._unquote(sect_name) if sect_name in parent: self._handle_error('Duplicate section name at line %s.', DuplicateError, infile, cur_index) continue # create the new section this_section = Section( parent, cur_depth, self, name=sect_name) parent[sect_name] = this_section parent.inline_comments[sect_name] = comment parent.comments[sect_name] = comment_list continue # # it's not a section marker, # so it should be a valid ``key = value`` line mat = self._keyword.match(line) if mat is None: # it neither matched as a keyword # or a section marker self._handle_error( 'Invalid line at line "%s".', ParseError, infile, cur_index) else: # is a keyword value # value will include any inline comment (indent, key, value) = mat.groups() if indent and (self.indent_type is None): self.indent_type = indent # check for a multiline value if value[:3] in ['"""', "'''"]: try: value, comment, cur_index = self._multiline( value, infile, cur_index, maxline) except SyntaxError: self._handle_error( 'Parse error in value at line %s.', ParseError, infile, cur_index) continue else: if self.unrepr: comment = '' try: value = unrepr(value) except Exception, e: if type(e) == UnknownType: msg = 'Unknown name or type in value at line %s.' else: msg = 'Parse error in value at line %s.' self._handle_error(msg, UnreprError, infile, cur_index) continue else: if self.unrepr: comment = '' try: value = unrepr(value) except Exception, e: if isinstance(e, UnknownType): msg = 'Unknown name or type in value at line %s.' else: msg = 'Parse error in value at line %s.' self._handle_error(msg, UnreprError, infile, cur_index) continue else: # extract comment and lists try: (value, comment) = self._handle_value(value) except SyntaxError: self._handle_error( 'Parse error in value at line %s.', ParseError, infile, cur_index) continue # key = self._unquote(key) if key in this_section: self._handle_error( 'Duplicate keyword name at line %s.', DuplicateError, infile, cur_index) continue # add the key. # we set unrepr because if we have got this far we will never # be creating a new section this_section.__setitem__(key, value, unrepr=True) this_section.inline_comments[key] = comment this_section.comments[key] = comment_list continue # if self.indent_type is None: # no indentation used, set the type accordingly self.indent_type = '' # preserve the final comment if not self and not self.initial_comment: self.initial_comment = comment_list elif not reset_comment: self.final_comment = comment_list self.list_values = temp_list_values def _match_depth(self, sect, depth): """ Given a section and a depth level, walk back through the sections parents to see if the depth level matches a previous section. Return a reference to the right section, or raise a SyntaxError. """ while depth < sect.depth: if sect is sect.parent: # we've reached the top level already raise SyntaxError() sect = sect.parent if sect.depth == depth: return sect # shouldn't get here raise SyntaxError() def _handle_error(self, text, ErrorClass, infile, cur_index): """ Handle an error according to the error settings. Either raise the error or store it. The error will have occured at ``cur_index`` """ line = infile[cur_index] cur_index += 1 message = text % cur_index error = ErrorClass(message, cur_index, line) if self.raise_errors: # raise the error - parsing stops here raise error # store the error # reraise when parsing has finished self._errors.append(error) def _unquote(self, value): """Return an unquoted version of a value""" if not value: # should only happen during parsing of lists raise SyntaxError if (value[0] == value[-1]) and (value[0] in ('"', "'")): value = value[1:-1] return value def _quote(self, value, multiline=True): """ Return a safely quoted version of a value. Raise a ConfigObjError if the value cannot be safely quoted. If multiline is ``True`` (default) then use triple quotes if necessary. * Don't quote values that don't need it. * Recursively quote members of a list and return a comma joined list. * Multiline is ``False`` for lists. * Obey list syntax for empty and single member lists. If ``list_values=False`` then the value is only quoted if it contains a ``\\n`` (is multiline) or '#'. If ``write_empty_values`` is set, and the value is an empty string, it won't be quoted. """ if multiline and self.write_empty_values and value == '': # Only if multiline is set, so that it is used for values not # keys, and not values that are part of a list return '' if multiline and isinstance(value, (list, tuple)): if not value: return ',' elif len(value) == 1: return self._quote(value[0], multiline=False) + ',' return ', '.join([self._quote(val, multiline=False) for val in value]) if not isinstance(value, basestring): if self.stringify: value = str(value) else: raise TypeError('Value "%s" is not a string.' % value) if not value: return '""' no_lists_no_quotes = not self.list_values and '\n' not in value and '#' not in value need_triple = multiline and ((("'" in value) and ('"' in value)) or ('\n' in value )) hash_triple_quote = multiline and not need_triple and ("'" in value) and ('"' in value) and ('#' in value) check_for_single = (no_lists_no_quotes or not need_triple) and not hash_triple_quote if check_for_single: if not self.list_values: # we don't quote if ``list_values=False`` quot = noquot # for normal values either single or double quotes will do elif '\n' in value: # will only happen if multiline is off - e.g. '\n' in key raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) elif ((value[0] not in wspace_plus) and (value[-1] not in wspace_plus) and (',' not in value)): quot = noquot else: quot = self._get_single_quote(value) else: # if value has '\n' or "'" *and* '"', it will need triple quotes quot = self._get_triple_quote(value) if quot == noquot and '#' in value and self.list_values: quot = self._get_single_quote(value) return quot % value def _get_single_quote(self, value): if ("'" in value) and ('"' in value): raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) elif '"' in value: quot = squot else: quot = dquot return quot def _get_triple_quote(self, value): if (value.find('"""') != -1) and (value.find("'''") != -1): raise ConfigObjError('Value "%s" cannot be safely quoted.' % value) if value.find('"""') == -1: quot = tdquot else: quot = tsquot return quot def _handle_value(self, value): """ Given a value string, unquote, remove comment, handle lists. (including empty and single member lists) """ if self._inspec: # Parsing a configspec so don't handle comments return (value, '') # do we look for lists in values ? if not self.list_values: mat = self._nolistvalue.match(value) if mat is None: raise SyntaxError() # NOTE: we don't unquote here return mat.groups() # mat = self._valueexp.match(value) if mat is None: # the value is badly constructed, probably badly quoted, # or an invalid list raise SyntaxError() (list_values, single, empty_list, comment) = mat.groups() if (list_values == '') and (single is None): # change this if you want to accept empty values raise SyntaxError() # NOTE: note there is no error handling from here if the regex # is wrong: then incorrect values will slip through if empty_list is not None: # the single comma - meaning an empty list return ([], comment) if single is not None: # handle empty values if list_values and not single: # FIXME: the '' is a workaround because our regex now matches # '' at the end of a list if it has a trailing comma single = None else: single = single or '""' single = self._unquote(single) if list_values == '': # not a list value return (single, comment) the_list = self._listvalueexp.findall(list_values) the_list = [self._unquote(val) for val in the_list] if single is not None: the_list += [single] return (the_list, comment) def _multiline(self, value, infile, cur_index, maxline): """Extract the value, where we are in a multiline situation.""" quot = value[:3] newvalue = value[3:] single_line = self._triple_quote[quot][0] multi_line = self._triple_quote[quot][1] mat = single_line.match(value) if mat is not None: retval = list(mat.groups()) retval.append(cur_index) return retval elif newvalue.find(quot) != -1: # somehow the triple quote is missing raise SyntaxError() # while cur_index < maxline: cur_index += 1 newvalue += '\n' line = infile[cur_index] if line.find(quot) == -1: newvalue += line else: # end of multiline, process it break else: # we've got to the end of the config, oops... raise SyntaxError() mat = multi_line.match(line) if mat is None: # a badly formed line raise SyntaxError() (value, comment) = mat.groups() return (newvalue + value, comment, cur_index) def _handle_configspec(self, configspec): """Parse the configspec.""" # FIXME: Should we check that the configspec was created with the # correct settings ? (i.e. ``list_values=False``) if not isinstance(configspec, ConfigObj): try: configspec = ConfigObj(configspec, raise_errors=True, file_error=True, _inspec=True) except ConfigObjError, e: # FIXME: Should these errors have a reference # to the already parsed ConfigObj ? raise ConfigspecError('Parsing configspec failed: %s' % e) except IOError, e: raise IOError('Reading configspec failed: %s' % e) self.configspec = configspec def _set_configspec(self, section, copy): """ Called by validate. Handles setting the configspec on subsections including sections to be validated by __many__ """ configspec = section.configspec many = configspec.get('__many__') if isinstance(many, dict): for entry in section.sections: if entry not in configspec: section[entry].configspec = many for entry in configspec.sections: if entry == '__many__': continue if entry not in section: section[entry] = {} section[entry]._created = True if copy: # copy comments section.comments[entry] = configspec.comments.get(entry, []) section.inline_comments[entry] = configspec.inline_comments.get(entry, '') # Could be a scalar when we expect a section if isinstance(section[entry], Section): section[entry].configspec = configspec[entry] def _write_line(self, indent_string, entry, this_entry, comment): """Write an individual line, for the write method""" # NOTE: the calls to self._quote here handles non-StringType values. if not self.unrepr: val = self._decode_element(self._quote(this_entry)) else: val = repr(this_entry) return '%s%s%s%s%s' % (indent_string, self._decode_element(self._quote(entry, multiline=False)), self._a_to_u(' = '), val, self._decode_element(comment)) def _write_marker(self, indent_string, depth, entry, comment): """Write a section marker line""" return '%s%s%s%s%s' % (indent_string, self._a_to_u('[' * depth), self._quote(self._decode_element(entry), multiline=False), self._a_to_u(']' * depth), self._decode_element(comment)) def _handle_comment(self, comment): """Deal with a comment.""" if not comment: return '' start = self.indent_type if not comment.startswith('#'): start += self._a_to_u(' # ') return (start + comment) # Public methods def write(self, outfile=None, section=None): """ Write the current ConfigObj as a file tekNico: FIXME: use StringIO instead of real files >>> filename = a.filename >>> a.filename = 'test.ini' >>> a.write() >>> a.filename = filename >>> a == ConfigObj('test.ini', raise_errors=True) 1 """ if self.indent_type is None: # this can be true if initialised from a dictionary self.indent_type = DEFAULT_INDENT_TYPE out = [] cs = self._a_to_u('#') csp = self._a_to_u('# ') if section is None: int_val = self.interpolation self.interpolation = False section = self for line in self.initial_comment: line = self._decode_element(line) stripped_line = line.strip() if stripped_line and not stripped_line.startswith(cs): line = csp + line out.append(line) indent_string = self.indent_type * section.depth for entry in (section.scalars + section.sections): if entry in section.defaults: # don't write out default values continue for comment_line in section.comments[entry]: comment_line = self._decode_element(comment_line.lstrip()) if comment_line and not comment_line.startswith(cs): comment_line = csp + comment_line out.append(indent_string + comment_line) this_entry = section[entry] comment = self._handle_comment(section.inline_comments[entry]) if isinstance(this_entry, dict): # a section out.append(self._write_marker( indent_string, this_entry.depth, entry, comment)) out.extend(self.write(section=this_entry)) else: out.append(self._write_line( indent_string, entry, this_entry, comment)) if section is self: for line in self.final_comment: line = self._decode_element(line) stripped_line = line.strip() if stripped_line and not stripped_line.startswith(cs): line = csp + line out.append(line) self.interpolation = int_val if section is not self: return out if (self.filename is None) and (outfile is None): # output a list of lines # might need to encode # NOTE: This will *screw* UTF16, each line will start with the BOM if self.encoding: out = [l.encode(self.encoding) for l in out] if (self.BOM and ((self.encoding is None) or (BOM_LIST.get(self.encoding.lower()) == 'utf_8'))): # Add the UTF8 BOM if not out: out.append('') out[0] = BOM_UTF8 + out[0] return out # Turn the list to a string, joined with correct newlines newline = self.newlines or os.linesep output = self._a_to_u(newline).join(out) if self.encoding: output = output.encode(self.encoding) if self.BOM and ((self.encoding is None) or match_utf8(self.encoding)): # Add the UTF8 BOM output = BOM_UTF8 + output if not output.endswith(newline): output += newline if outfile is not None: outfile.write(output) else: h = open(self.filename, 'wb') h.write(output) h.close() def validate(self, validator, preserve_errors=False, copy=False, section=None): """ Test the ConfigObj against a configspec. It uses the ``validator`` object from *validate.py*. To run ``validate`` on the current ConfigObj, call: :: test = config.validate(validator) (Normally having previously passed in the configspec when the ConfigObj was created - you can dynamically assign a dictionary of checks to the ``configspec`` attribute of a section though). It returns ``True`` if everything passes, or a dictionary of pass/fails (True/False). If every member of a subsection passes, it will just have the value ``True``. (It also returns ``False`` if all members fail). In addition, it converts the values from strings to their native types if their checks pass (and ``stringify`` is set). If ``preserve_errors`` is ``True`` (``False`` is default) then instead of a marking a fail with a ``False``, it will preserve the actual exception object. This can contain info about the reason for failure. For example the ``VdtValueTooSmallError`` indicates that the value supplied was too small. If a value (or section) is missing it will still be marked as ``False``. You must have the validate module to use ``preserve_errors=True``. You can then use the ``flatten_errors`` function to turn your nested results dictionary into a flattened list of failures - useful for displaying meaningful error messages. """ if section is None: if self.configspec is None: raise ValueError('No configspec supplied.') if preserve_errors: # We do this once to remove a top level dependency on the validate module # Which makes importing configobj faster from validate import VdtMissingValue self._vdtMissingValue = VdtMissingValue section = self if copy: section.initial_comment = section.configspec.initial_comment section.final_comment = section.configspec.final_comment section.encoding = section.configspec.encoding section.BOM = section.configspec.BOM section.newlines = section.configspec.newlines section.indent_type = section.configspec.indent_type # # section.default_values.clear() #?? configspec = section.configspec self._set_configspec(section, copy) def validate_entry(entry, spec, val, missing, ret_true, ret_false): section.default_values.pop(entry, None) try: section.default_values[entry] = validator.get_default_value(configspec[entry]) except (KeyError, AttributeError, validator.baseErrorClass): # No default, bad default or validator has no 'get_default_value' # (e.g. SimpleVal) pass try: check = validator.check(spec, val, missing=missing ) except validator.baseErrorClass, e: if not preserve_errors or isinstance(e, self._vdtMissingValue): out[entry] = False else: # preserve the error out[entry] = e ret_false = False ret_true = False else: ret_false = False out[entry] = True if self.stringify or missing: # if we are doing type conversion # or the value is a supplied default if not self.stringify: if isinstance(check, (list, tuple)): # preserve lists check = [self._str(item) for item in check] elif missing and check is None: # convert the None from a default to a '' check = '' else: check = self._str(check) if (check != val) or missing: section[entry] = check if not copy and missing and entry not in section.defaults: section.defaults.append(entry) return ret_true, ret_false # out = {} ret_true = True ret_false = True unvalidated = [k for k in section.scalars if k not in configspec] incorrect_sections = [k for k in configspec.sections if k in section.scalars] incorrect_scalars = [k for k in configspec.scalars if k in section.sections] for entry in configspec.scalars: if entry in ('__many__', '___many___'): # reserved names continue if (not entry in section.scalars) or (entry in section.defaults): # missing entries # or entries from defaults missing = True val = None if copy and entry not in section.scalars: # copy comments section.comments[entry] = ( configspec.comments.get(entry, [])) section.inline_comments[entry] = ( configspec.inline_comments.get(entry, '')) # else: missing = False val = section[entry] ret_true, ret_false = validate_entry(entry, configspec[entry], val, missing, ret_true, ret_false) many = None if '__many__' in configspec.scalars: many = configspec['__many__'] elif '___many___' in configspec.scalars: many = configspec['___many___'] if many is not None: for entry in unvalidated: val = section[entry] ret_true, ret_false = validate_entry(entry, many, val, False, ret_true, ret_false) unvalidated = [] for entry in incorrect_scalars: ret_true = False if not preserve_errors: out[entry] = False else: ret_false = False msg = 'Value %r was provided as a section' % entry out[entry] = validator.baseErrorClass(msg) for entry in incorrect_sections: ret_true = False if not preserve_errors: out[entry] = False else: ret_false = False msg = 'Section %r was provided as a single value' % entry out[entry] = validator.baseErrorClass(msg) # Missing sections will have been created as empty ones when the # configspec was read. for entry in section.sections: # FIXME: this means DEFAULT is not copied in copy mode if section is self and entry == 'DEFAULT': continue if section[entry].configspec is None: unvalidated.append(entry) continue if copy: section.comments[entry] = configspec.comments.get(entry, []) section.inline_comments[entry] = configspec.inline_comments.get(entry, '') check = self.validate(validator, preserve_errors=preserve_errors, copy=copy, section=section[entry]) out[entry] = check if check == False: ret_true = False elif check == True: ret_false = False else: ret_true = False section.extra_values = unvalidated if preserve_errors and not section._created: # If the section wasn't created (i.e. it wasn't missing) # then we can't return False, we need to preserve errors ret_false = False # if ret_false and preserve_errors and out: # If we are preserving errors, but all # the failures are from missing sections / values # then we can return False. Otherwise there is a # real failure that we need to preserve. ret_false = not any(out.values()) if ret_true: return True elif ret_false: return False return out def reset(self): """Clear ConfigObj instance and restore to 'freshly created' state.""" self.clear() self._initialise() # FIXME: Should be done by '_initialise', but ConfigObj constructor (and reload) # requires an empty dictionary self.configspec = None # Just to be sure ;-) self._original_configspec = None def reload(self): """ Reload a ConfigObj from file. This method raises a ``ReloadError`` if the ConfigObj doesn't have a filename attribute pointing to a file. """ if not isinstance(self.filename, basestring): raise ReloadError() filename = self.filename current_options = {} for entry in OPTION_DEFAULTS: if entry == 'configspec': continue current_options[entry] = getattr(self, entry) configspec = self._original_configspec current_options['configspec'] = configspec self.clear() self._initialise(current_options) self._load(filename, configspec) class SimpleVal(object): """ A simple validator. Can be used to check that all members expected are present. To use it, provide a configspec with all your members in (the value given will be ignored). Pass an instance of ``SimpleVal`` to the ``validate`` method of your ``ConfigObj``. ``validate`` will return ``True`` if all members are present, or a dictionary with True/False meaning present/missing. (Whole missing sections will be replaced with ``False``) """ def __init__(self): self.baseErrorClass = ConfigObjError def check(self, check, member, missing=False): """A dummy check method, always returns the value unchanged.""" if missing: raise self.baseErrorClass() return member def flatten_errors(cfg, res, levels=None, results=None): """ An example function that will turn a nested dictionary of results (as returned by ``ConfigObj.validate``) into a flat list. ``cfg`` is the ConfigObj instance being checked, ``res`` is the results dictionary returned by ``validate``. (This is a recursive function, so you shouldn't use the ``levels`` or ``results`` arguments - they are used by the function.) Returns a list of keys that failed. Each member of the list is a tuple:: ([list of sections...], key, result) If ``validate`` was called with ``preserve_errors=False`` (the default) then ``result`` will always be ``False``. *list of sections* is a flattened list of sections that the key was found in. If the section was missing (or a section was expected and a scalar provided - or vice-versa) then key will be ``None``. If the value (or section) was missing then ``result`` will be ``False``. If ``validate`` was called with ``preserve_errors=True`` and a value was present, but failed the check, then ``result`` will be the exception object returned. You can use this as a string that describes the failure. For example *The value "3" is of the wrong type*. """ if levels is None: # first time called levels = [] results = [] if res == True: return results if res == False or isinstance(res, Exception): results.append((levels[:], None, res)) if levels: levels.pop() return results for (key, val) in res.items(): if val == True: continue if isinstance(cfg.get(key), dict): # Go down one level levels.append(key) flatten_errors(cfg[key], val, levels, results) continue results.append((levels[:], key, val)) # # Go up one level if levels: levels.pop() # return results def get_extra_values(conf, _prepend=()): """ Find all the values and sections not in the configspec from a validated ConfigObj. ``get_extra_values`` returns a list of tuples where each tuple represents either an extra section, or an extra value. The tuples contain two values, a tuple representing the section the value is in and the name of the extra values. For extra values in the top level section the first member will be an empty tuple. For values in the 'foo' section the first member will be ``('foo',)``. For members in the 'bar' subsection of the 'foo' section the first member will be ``('foo', 'bar')``. NOTE: If you call ``get_extra_values`` on a ConfigObj instance that hasn't been validated it will return an empty list. """ out = [] out.extend((_prepend, name) for name in conf.extra_values) for name in conf.sections: if name not in conf.extra_values: out.extend(get_extra_values(conf[name], _prepend + (name,))) return out """*A programming language is a medium of expression.* - Paul Graham""" whyteboard-0.41.1/whyteboard/lib/__init__.py0000777000175000017500000000046211444534444020052 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- import flatnotebook as fnb from configobj import ConfigObj from dragscroller import DragScroller from errdlg import ErrorDialog as BaseErrorDialog from icon import whyteboard as icon from mock import Mock from pubsub import pub from validate import Validatorwhyteboard-0.41.1/whyteboard/lib/pubsubconf.py0000777000175000017500000001156711443222121020452 0ustar stevesteve""" Allows user to configure pubsub. Most important: - setVersion(N): State which version of pubsub should be used (N=1, 2 or 3; defaults to latest). E.g. to use version 1 of pubsub: # in your main script only: import pubsubconf pubsubconf.setVersion(1) # in main script and all other modules imported: from pubsub import pub # usual line - Several functions specific to version 3: - setListenerExcHandler(handler): set handling of exceptions raised in listeners (default: None). - setTopicUnspecifiedFatal(val=True): state whether unspecified topics should be creatable (default: False). - setNotificationhandler(notificationhandler): what class to instantiate for processing notification events (default: None). - transitionV1ToV3(commonName, stage=1): set policies that support migrating an application from pubsub version 1 to version 3. """ packageImported = False class Version: DEFAULT = 3 value = None output = None def setVersion(val, output=None): '''Set the version of package to be used when imported. If output is set to a file object (has write() method), a message will be written to that file indicating which version of pubsub has been imported. E.g. setVersion(2, sys.stdout).''' if val < 1 or val > 3: raise ValueError('val = %s invalid, need 1 <= val <= 3' % val) Version.value = val Version.output = output def getVersion(): '''Get version number selected for import (via setVersion, or default version if setVersion not called).''' return Version.value or Version.DEFAULT def isVersionChosen(): '''Return True if setVersion() was called at least once.''' return Version.value is not None def getDefaultVersion(): '''Get version number imported by default.''' return Version.default def getVersionOutput(): '''Return the file object to be used for messaging about imported version''' return Version.output class Policies: ''' Define the policies used by pubsub, when several alternatives exist. ''' _notificationHandler = None _listenerExcHandler = None _raiseOnTopicUnspecified = False _msgDataProtocol = 'kwargs' _msgDataArgName = None def setTopicUnspecifiedFatal(val=True): '''When called with val=True (default), causes pubsub to raise an UnspecifiedTopicError when attempting to create a topic that has no specification. This happens when pub.addTopicDefnProvider() was never called, or none of the given providers specify the topic (or a super topic of it) that was given to pub.subscribe(). If True, the topic will be created with argument specification inferred from first listener subscribed. ''' Policies._raiseOnTopicUnspecified = val def setNotificationHandler(notificationHandler): '''The notifier should be a class that follows the API of pubsub.utils.INotificationHandler. If no notifier is set, then the default will be used. ''' Policies._notificationHandler = notificationHandler def setListenerExcHandler(handler): '''Set the handler to call when a listener raises an exception during a sendMessage(). Without a handler, the send operation aborts, whereas with one, the exception information is sent to it (where it can be logged, printed, whatever), and sendMessage() continues to send messages to remaining listeners. ''' Policies._listenerExcHandler = handler def isPackageImported(): '''Can be used to determine if pubsub package has been imported by your application (or by any modules imported by it). ''' return packageImported def setMsgProtocol(protocol): '''Messaging protocol defaults to 'kwargs'. It can be set to 'dataArg' to support legacy code or simple pub-sub architectures. ''' if protocol not in ('dataArg', 'kwargs'): raise NotImplementedError('The protocol "%s" is not supported' % protocol) Policies._msgDataProtocol = protocol def transitionV1ToV3(commonName, stage=1): '''Use this to help with migrating code from protocol DATA_ARG to KW_ARGS. This only makes sense in an application that has been using setMsgProtocol('dataArg') and wants to move to the more robust 'kwargs'. This function is designed to support a three-stage process: (stage 1) make all listeners use the same argument name (commonName); (stage 2) make all senders use the kwargs protocol and all listeners use kwargs rather than Message.data. The third stage, for which you don't use this function, consists in splitting up your message data into more kwargs and further refining your topic specification tree. See the docs for more info. ''' Policies._msgDataArgName = commonName if stage <= 1: Policies._msgDataProtocol = 'dataArg' whyteboard-0.41.1/whyteboard/lib/mock.py0000777000175000017500000002011411443222121017221 0ustar stevesteve# mock.py # Test tools for mocking and patching. # Copyright (C) 2007-2009 Michael Foord # E-mail: fuzzyman AT voidspace DOT org DOT uk # mock 0.6.0 # http://www.voidspace.org.uk/python/mock/ # Released subject to the BSD License # Please see http://www.voidspace.org.uk/python/license.shtml # Scripts maintained at http://www.voidspace.org.uk/python/index.shtml # Comments, suggestions and bug reports welcome. __all__ = ( 'Mock', 'patch', 'patch_object', 'sentinel', 'DEFAULT' ) __version__ = '0.6.0' class SentinelObject(object): def __init__(self, name): self.name = name def __repr__(self): return '' % self.name class Sentinel(object): def __init__(self): self._sentinels = {} def __getattr__(self, name): return self._sentinels.setdefault(name, SentinelObject(name)) sentinel = Sentinel() DEFAULT = sentinel.DEFAULT class OldStyleClass: pass ClassType = type(OldStyleClass) def _is_magic(name): return '__%s__' % name[2:-2] == name def _copy(value): if type(value) in (dict, list, tuple, set): return type(value)(value) return value class Mock(object): def __init__(self, spec=None, side_effect=None, return_value=DEFAULT, name=None, parent=None, wraps=None): self._parent = parent self._name = name if spec is not None and not isinstance(spec, list): spec = [member for member in dir(spec) if not _is_magic(member)] self._methods = spec self._children = {} self._return_value = return_value self.side_effect = side_effect self._wraps = wraps self.reset_mock() def reset_mock(self): self.called = False self.call_args = None self.call_count = 0 self.call_args_list = [] self.method_calls = [] for child in self._children.itervalues(): child.reset_mock() if isinstance(self._return_value, Mock): self._return_value.reset_mock() def __get_return_value(self): if self._return_value is DEFAULT: self._return_value = Mock() return self._return_value def __set_return_value(self, value): self._return_value = value return_value = property(__get_return_value, __set_return_value) def __call__(self, *args, **kwargs): self.called = True self.call_count += 1 self.call_args = (args, kwargs) self.call_args_list.append((args, kwargs)) parent = self._parent name = self._name while parent is not None: parent.method_calls.append((name, args, kwargs)) if parent._parent is None: break name = parent._name + '.' + name parent = parent._parent ret_val = DEFAULT if self.side_effect is not None: if (isinstance(self.side_effect, Exception) or isinstance(self.side_effect, (type, ClassType)) and issubclass(self.side_effect, Exception)): raise self.side_effect ret_val = self.side_effect(*args, **kwargs) if ret_val is DEFAULT: ret_val = self.return_value if self._wraps is not None and self._return_value is DEFAULT: print self._wraps, args, kwargs return self._wraps(*args, **kwargs) if ret_val is DEFAULT: ret_val = self.return_value return ret_val def __getattr__(self, name): if self._methods is not None: if name not in self._methods: raise AttributeError("Mock object has no attribute '%s'" % name) elif _is_magic(name): raise AttributeError(name) if name not in self._children: wraps = None if self._wraps is not None: wraps = getattr(self._wraps, name) self._children[name] = Mock(parent=self, name=name, wraps=wraps) return self._children[name] def assert_called_with(self, *args, **kwargs): assert self.call_args == (args, kwargs), 'Expected: %s\nCalled with: %s' % ((args, kwargs), self.call_args) def _dot_lookup(thing, comp, import_path): try: return getattr(thing, comp) except AttributeError: __import__(import_path) return getattr(thing, comp) def _importer(target): components = target.split('.') import_path = components.pop(0) thing = __import__(import_path) for comp in components: import_path += ".%s" % comp thing = _dot_lookup(thing, comp, import_path) return thing class _patch(object): def __init__(self, target, attribute, new, spec, create): self.target = target self.attribute = attribute self.new = new self.spec = spec self.create = create self.has_local = False def __call__(self, func): if hasattr(func, 'patchings'): func.patchings.append(self) return func def patched(*args, **keywargs): # don't use a with here (backwards compatability with 2.5) extra_args = [] for patching in patched.patchings: arg = patching.__enter__() if patching.new is DEFAULT: extra_args.append(arg) args += tuple(extra_args) try: return func(*args, **keywargs) finally: for patching in getattr(patched, 'patchings', []): patching.__exit__() patched.patchings = [self] patched.__name__ = func.__name__ patched.compat_co_firstlineno = getattr(func, "compat_co_firstlineno", func.func_code.co_firstlineno) return patched def get_original(self): target = self.target name = self.attribute create = self.create original = DEFAULT if _has_local_attr(target, name): try: original = target.__dict__[name] except AttributeError: # for instances of classes with slots, they have no __dict__ original = getattr(target, name) elif not create and not hasattr(target, name): raise AttributeError("%s does not have the attribute %r" % (target, name)) return original def __enter__(self): new, spec, = self.new, self.spec original = self.get_original() if new is DEFAULT: # XXXX what if original is DEFAULT - shouldn't use it as a spec inherit = False if spec == True: # set spec to the object we are replacing spec = original if isinstance(spec, (type, ClassType)): inherit = True new = Mock(spec=spec) if inherit: new.return_value = Mock(spec=spec) self.temp_original = original setattr(self.target, self.attribute, new) return new def __exit__(self, *_): if self.temp_original is not DEFAULT: setattr(self.target, self.attribute, self.temp_original) else: delattr(self.target, self.attribute) del self.temp_original def patch_object(target, attribute, new=DEFAULT, spec=None, create=False): return _patch(target, attribute, new, spec, create) def patch(target, new=DEFAULT, spec=None, create=False): try: target, attribute = target.rsplit('.', 1) except (TypeError, ValueError): raise TypeError("Need a valid target to patch. You supplied: %r" % (target,)) target = _importer(target) return _patch(target, attribute, new, spec, create) def _has_local_attr(obj, name): try: return name in vars(obj) except TypeError: # objects without a __dict__ return hasattr(obj, name) whyteboard-0.41.1/whyteboard/lib/errdlg.py0000777000175000017500000003135611443222121017561 0ustar stevesteve############################################################################### # Name: errdlg.py # # Purpose: Error Reporter Dialog # # Author: Cody Precord # # Copyright: (c) 2009 Cody Precord # # License: wxWindows License # ############################################################################### """ Editra Control Library: Error Reporter Dialog Dialog for displaying exceptions and reporting errors to application maintainer. This dialog is intended as a base class and should be subclassed to fit the applications needs. This dialog should be initiated inside of a sys.excepthook handler. Example: sys.excepthook = ExceptHook ... def ExceptionHook(exctype, value, trace): # Format the traceback ftrace = ErrorDialog.FormatTrace(exctype, value, trace) # Ensure that error gets raised to console as well print ftrace # If abort has been set and we get here again do a more forcefull shutdown if ErrorDialog.ABORT: os._exit(1) # Prevent multiple reporter dialogs from opening at once if not ErrorDialog.REPORTER_ACTIVE and not ErrorDialog.ABORT: dlg = ErrorDialog(ftrace) dlg.ShowModal() dlg.Destroy() @summary: Error Reporter Dialog """ __author__ = "Cody Precord " __svnid__ = "$Id: errdlg.py 61808 2009-09-02 15:57:55Z CJP $" __revision__ = "$Revision: 61808 $" __all__ = [# Classes 'ErrorDialog', 'ErrorReporter', # Functions 'TimeStamp'] #----------------------------------------------------------------------------# # Dependancies import os import sys import platform import time import traceback import wx #----------------------------------------------------------------------------# # Globals _ = wx.GetTranslation #----------------------------------------------------------------------------# class ErrorReporter(object): """Crash/Error Reporter Service @summary: Stores all errors caught during the current session. @note: singleton class """ instance = None _first = True def __init__(self): """Initialize the reporter @note: The ErrorReporter is a singleton. """ # Ensure init only happens once if self._first: object.__init__(self) self._first = False self._sessionerr = list() else: pass def __new__(cls, *args, **kargs): """Maintain only a single instance of this object @return: instance of this class """ if not cls.instance: cls.instance = object.__new__(cls, *args, **kargs) return cls.instance def AddMessage(self, msg): """Adds a message to the reporters list of session errors @param msg: The Error Message to save """ if msg not in self._sessionerr: self._sessionerr.append(msg) def GetErrorStack(self): """Returns all the errors caught during this session @return: formatted log message of errors """ return (os.linesep * 2).join(self._sessionerr) def GetLastError(self): """Gets the last error from the current session @return: Error Message String """ if len(self._sessionerr): return self._sessionerr[-1] #-----------------------------------------------------------------------------# class ErrorDialog(wx.Dialog): """Dialog for showing errors and and notifying Editra.org should the user choose so. """ ID_SEND = wx.NewId() ABORT = False REPORTER_ACTIVE = False def __init__(self, parent, id=wx.ID_ANY, title=u'', pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, name="ErrorReporterDlg", message=u''): """Initialize the dialog @param message: Error message to display """ ErrorDialog.REPORTER_ACTIVE = True wx.Dialog.__init__(self, parent, id, title, pos, size, style, name) # Give message to ErrorReporter ErrorReporter().AddMessage(message) # Attributes self.err_msg = os.linesep.join((self.GetEnvironmentInfo(), u"#---- Traceback Info ----#\n", unicode(ErrorReporter().GetErrorStack(), "utf-8"), u"#---- End Traceback Info ----#\n")) # Layout self._panel = ErrorPanel(self, self.err_msg) self._DoLayout() self.SetMinSize(wx.Size(450, 300)) self.parent = parent # Event Handlers self.Bind(wx.EVT_BUTTON, self.OnButton) self.Bind(wx.EVT_CLOSE, self.OnClose) # Auto show at end of init self.CenterOnParent() def _DoLayout(self): """Layout the dialog and prepare it to be shown @note: Do not call this method in your code """ msizer = wx.BoxSizer(wx.VERTICAL) msizer.Add(self._panel, 1, wx.EXPAND) self.SetSizer(msizer) self.SetInitialSize() #---- Override in Subclass ----# def Abort(self): """Called to abort the application @note: needs to be overidden in sublcasses """ raise NotImplementedError("Abort must be implemented!") def GetEnvironmentInfo(self): """Get the enviromental info / Header of error report @return: string """ res = wx.Display().GetGeometry() info = list() info.append(self.GetProgramName()) info.append(u"Operating System: %s" % wx.GetOsDescription()) if sys.platform == 'darwin': info.append(u"Mac OSX: %s" % platform.mac_ver()[0]) info.append(u"Screen Resolution: %ix%i" % (res[2], res[3])) info.append(u"Python Version: %s" % sys.version) info.append(u"wxPython Version: %s" % wx.version()) info.append(u"wxPython Info: (%s)" % ", ".join(wx.PlatformInfo)) info.append(u"Python Encoding: Default=%s File=%s" % \ (sys.getdefaultencoding(), sys.getfilesystemencoding())) info.append(u"wxPython Encoding: %s" % wx.GetDefaultPyEncoding()) info.append(u"System Architecture: %s %s" % (platform.architecture()[0], \ platform.machine())) info.append(u"Byte order: %s" % sys.byteorder) info.append(u"Frozen: %s" % str(getattr(sys, 'frozen', 'False'))) info.append(u"#---- End System Information ----#") info.append(u"") info.append(u"") return info#os.linesep.join(info) def GetProgramName(self): """Get the program name/version info to include in error report @return: string """ return wx.GetApp().GetAppName() def Send(self): """Called to send error report @note: needs to be overridden in subclasses """ raise NotImplementedError("Send must be implemented!") #---- End Required overrides ----# @staticmethod def FormatTrace(exctype, value, trace): """Format the traceback @return: string """ exc = traceback.format_exception(exctype, value, trace) exc.insert(0, "*** %s ***%s" % (TimeStamp(), os.linesep)) ftrace = "".join(exc) return ftrace def SetDescriptionLabel(self, label): """Set the dialogs main description text @param label: string """ self._panel.SetDescriptionText(label) def ShowAbortButton(self, show=True): """Show/Hide the Abort button @keyword show: bool """ btn = self._panel.FindWindowById(wx.ID_ABORT) if btn is not None: btn.Show(show) self._panel.Layout() def ShowSendButton(self, show=True): """Show/Hide the Send button @keyword show: bool """ btn = self._panel.FindWindowById(ErrorDialog.ID_SEND) if btn is not None: btn.Show(show) self._panel.Layout() #---- Event Handlers ----# def OnButton(self, evt): """Handles button events @param evt: event that called this handler @postcondition: Dialog is closed @postcondition: If Report Event then email program is opened """ e_id = evt.GetId() if e_id == wx.ID_CLOSE: self.Close() elif e_id == ErrorDialog.ID_SEND: self.Send() self.Close() elif e_id == wx.ID_ABORT: ErrorDialog.ABORT = True self.Abort() self.Close() else: evt.Skip() def OnClose(self, evt): """Cleans up the dialog when it is closed @param evt: Event that called this handler """ ErrorDialog.REPORTER_ACTIVE = False evt.Skip() #-----------------------------------------------------------------------------# class ErrorPanel(wx.Panel): """Error Reporter panel""" def __init__(self, parent, msg): """Create the panel @param parent: wx.Window @param msg: Error message to display """ wx.Panel.__init__(self, parent) # Attributes self.err_msg = msg self.desc = wx.StaticText(self, label=u'') # Layout self.__DoLayout() def __DoLayout(self): """Layout the control""" icon = wx.StaticBitmap(self, bitmap=wx.ArtProvider.GetBitmap(wx.ART_ERROR)) d = _("Description of what you was doing before this error appeared") t_lbl = wx.StaticText(self, label=_("Error Traceback:")) email_label = wx.StaticText(self, label=_("E-mail Address (Optional)")) self.action = wx.TextCtrl(self, value=d, style=wx.TE_MULTILINE) tctrl = wx.TextCtrl(self, value=self.err_msg, style=wx.TE_MULTILINE | wx.TE_READONLY) abort_b = wx.Button(self, wx.ID_ABORT, _("Abort")) abort_b.SetToolTipString(_("Exit the application")) send_b = wx.Button(self, ErrorDialog.ID_SEND, _("Report Error")) send_b.SetDefault() close_b = wx.Button(self, wx.ID_CLOSE) self.email = wx.TextCtrl(self) font = t_lbl.GetClassDefaultAttributes().font font.SetWeight(wx.FONTWEIGHT_BOLD) font.SetPointSize(font.GetPointSize() + 1) self.desc.SetFont(font) order = (self.action, self.email, send_b, close_b, abort_b) # tab order for i in xrange(len(order) - 1): order[i+1].MoveAfterInTabOrder(order[i]) # Layout vsizer = wx.BoxSizer(wx.VERTICAL) hsizer1 = wx.BoxSizer(wx.HORIZONTAL) hsizer1.AddMany([((5, 40), 0), (icon, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10), ((12, 10), 0), (self.desc, 0, wx.ALIGN_CENTER_VERTICAL), ((5, 20), 0)]) hsizer2 = wx.BoxSizer(wx.HORIZONTAL) hsizer2.AddMany([((5, 150), 0), (tctrl, 1, wx.EXPAND | wx.BOTTOM, 10)]) hsizer3 = wx.BoxSizer(wx.HORIZONTAL) hsizer3.AddMany([((5, 80), 0), (self.action, 1, wx.EXPAND), ((5, 20), 0)]) hsizer4 = wx.BoxSizer(wx.VERTICAL) hsizer4.AddMany([((5, 5), 0), (email_label, 0,wx.BOTTOM | wx.LEFT, 10), (self.email, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 5), ((5, 20), 0)]) bsizer = wx.BoxSizer(wx.HORIZONTAL) bsizer.AddMany([((5, 5), 0), (abort_b, 0), ((-1, -1), 1, wx.EXPAND), (send_b, 0), ((5, 5), 0), (close_b, 0), ((5, 5), 0)]) vsizer.AddMany([((5, 5), 0), (hsizer1, 0), ((10, 10), 0), (t_lbl, 0, wx.ALIGN_LEFT | wx.LEFT | wx.BOTTOM, 5), ((3, 3), 0), (hsizer2, 1, wx.EXPAND), ((8, 8), 0), (hsizer3, 0, wx.EXPAND), ((8, 8), 0), (hsizer4, 0, wx.EXPAND), ((8, 4), 0), (bsizer, 0, wx.EXPAND), ((8, 8), 0)]) self.SetSizer(vsizer) self.SetAutoLayout(True) def SetDescriptionText(self, text): """Set the description label text @param text: string """ self.desc.SetLabel(text) self.Layout() #-----------------------------------------------------------------------------# def TimeStamp(): """Create a formatted time stamp of current time @return: Time stamp of the current time (Day Month Date HH:MM:SS Year) @rtype: string """ now = time.localtime(time.time()) now = time.asctime(now) return now whyteboard-0.41.1/whyteboard/lib/flatnotebook.py0000777000175000017500000062517311443222121020777 0ustar stevesteve# --------------------------------------------------------------------------- # # FLATNOTEBOOK Widget wxPython IMPLEMENTATION # # Original C++ Code From Eran. You Can Find It At: # # http://wxforum.shadonet.com/viewtopic.php?t=5761&start=0 # # License: wxWidgets license # # # Python Code By: # # Andrea Gavana, @ 02 Oct 2006 # Latest Revision: 25 Aug 2010, 10.00 GMT # # # For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please # Write To Me At: # # andrea.gavana@gmail.com # gavana@kpo.kz # # Or, Obviously, To The wxPython Mailing List!!! # # # End Of Comments # --------------------------------------------------------------------------- # """ FlatNotebook is a full, generic and owner-drawn implementation of `wx.Notebook`. Description =========== The FlatNotebook is a full implementation of the `wx.Notebook`, and designed to be a drop-in replacement for `wx.Notebook`. The API functions are similar so one can expect the function to behave in the same way. Some features: - The buttons are highlighted a la Firefox style; - The scrolling is done for bulks of tabs (so, the scrolling is faster and better); - The buttons area is never overdrawn by tabs (unlike many other implementations I saw); - It is a generic control; - Currently there are 6 different styles - VC8, VC 71, Standard, Fancy, Firefox 2 and Ribbon; - Mouse middle click can be used to close tabs; - A function to add right click menu for tabs (simple as L{SetRightClickMenu}); - All styles has bottom style as well (they can be drawn in the bottom of screen); - An option to hide 'X' button or navigation buttons (separately); - Gradient colouring of the selected tabs and border; - Support for drag 'n' drop of tabs, both in the same notebook or to another notebook; - Possibility to have closing button on the active tab directly; - Support for disabled tabs; - Colours for active/inactive tabs, and captions; - Background of tab area can be painted in gradient (VC8 style only); - Colourful tabs - a random gentle colour is generated for each new tab (very cool, VC8 style only); - Try setting the tab area color for the Ribbon Style And much more. Window Styles ============= This class supports the following window styles: ================================ =========== ================================================== Window Styles Hex Value Description ================================ =========== ================================================== ``FNB_VC71`` 0x1 Use Visual Studio 2003 (VC7.1) style for tabs. ``FNB_FANCY_TABS`` 0x2 Use fancy style - square tabs filled with gradient colouring. ``FNB_TABS_BORDER_SIMPLE`` 0x4 Draw thin border around the page. ``FNB_NO_X_BUTTON`` 0x8 Do not display the 'X' button. ``FNB_NO_NAV_BUTTONS`` 0x10 Do not display the right/left arrows. ``FNB_MOUSE_MIDDLE_CLOSES_TABS`` 0x20 Use the mouse middle button for cloing tabs. ``FNB_BOTTOM`` 0x40 Place tabs at bottom - the default is to place them at top. ``FNB_NODRAG`` 0x80 Disable dragging of tabs. ``FNB_VC8`` 0x100 Use Visual Studio 2005 (VC8) style for tabs. ``FNB_X_ON_TAB`` 0x200 Place 'X' close button on the active tab. ``FNB_BACKGROUND_GRADIENT`` 0x400 Use gradients to paint the tabs background. ``FNB_COLOURFUL_TABS`` 0x800 Use colourful tabs (VC8 style only). ``FNB_DCLICK_CLOSES_TABS`` 0x1000 Style to close tab using double click. ``FNB_SMART_TABS`` 0x2000 Use `Smart Tabbing`, like ``Alt`` + ``Tab`` on Windows. ``FNB_DROPDOWN_TABS_LIST`` 0x4000 Use a dropdown menu on the left in place of the arrows. ``FNB_ALLOW_FOREIGN_DND`` 0x8000 Allows drag 'n' drop operations between different FlatNotebooks. ``FNB_HIDE_ON_SINGLE_TAB`` 0x10000 Hides the Page Container when there is one or fewer tabs. ``FNB_DEFAULT_STYLE`` 0x10020 FlatNotebook default style. ``FNB_FF2`` 0x20000 Use Firefox 2 style for tabs. ``FNB_NO_TAB_FOCUS`` 0x40000 Does not allow tabs to have focus. ``FNB_RIBBON_TABS`` 0x80000 Use the Ribbon Tabs style ================================ =========== ================================================== Events Processing ================= This class processes the following events: ========================================= ================================================== Event Name Description ========================================= ================================================== ``EVT_FLATNOTEBOOK_PAGE_CHANGED`` Notify client objects when the active page in `FlatNotebook` has changed. ``EVT_FLATNOTEBOOK_PAGE_CHANGING`` Notify client objects when the active page in `FlatNotebook` is about to change. ``EVT_FLATNOTEBOOK_PAGE_CLOSED`` Notify client objects when a page in `FlatNotebook` has been closed. ``EVT_FLATNOTEBOOK_PAGE_CLOSING`` Notify client objects when a page in `FlatNotebook` is closing. ``EVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU`` Notify client objects when a pop-up menu should appear next to a tab. ``EVT_FLATNOTEBOOK_PAGE_DROPPED`` Notify client objects when a tab has been dropped and re-arranged (on the *same* notebook) ``EVT_FLATNOTEBOOK_PAGE_DROPPED_FOREIGN`` Notify client objects when a tab has been dropped and re-arranged (from a foreign notebook) ========================================= ================================================== License And Version =================== FlatNotebook is distributed under the wxPython license. Latest Revision: Andrea Gavana @ 25 Aug 2010, 10.00 GMT Version 3.1 """ __docformat__ = "epytext" #---------------------------------------------------------------------- # Beginning Of FLATNOTEBOOK wxPython Code #---------------------------------------------------------------------- import wx import wx.lib.colourutils as colourutils import random import math import weakref import cPickle # Used on OSX to get access to carbon api constants if wx.Platform == '__WXMAC__': import Carbon.Appearance # Check for the new method in 2.7 (not present in 2.6.3.3) if wx.VERSION_STRING < "2.7": wx.Rect.Contains = lambda self, point: wx.Rect.Inside(self, point) FNB_HEIGHT_SPACER = 10 # Use Visual Studio 2003 (VC7.1) style for tabs FNB_VC71 = 1 """Use Visual Studio 2003 (VC7.1) style for tabs""" # Use fancy style - square tabs filled with gradient colouring FNB_FANCY_TABS = 2 """Use fancy style - square tabs filled with gradient colouring""" # Draw thin border around the page FNB_TABS_BORDER_SIMPLE = 4 """Draw thin border around the page""" # Do not display the 'X' button FNB_NO_X_BUTTON = 8 """Do not display the 'X' button""" # Do not display the Right / Left arrows FNB_NO_NAV_BUTTONS = 16 """Do not display the right/left arrows""" # Use the mouse middle button for cloing tabs FNB_MOUSE_MIDDLE_CLOSES_TABS = 32 """Use the mouse middle button for cloing tabs""" # Place tabs at bottom - the default is to place them # at top FNB_BOTTOM = 64 """Place tabs at bottom - the default is to place them at top""" # Disable dragging of tabs FNB_NODRAG = 128 """Disable dragging of tabs""" # Use Visual Studio 2005 (VC8) style for tabs FNB_VC8 = 256 """Use Visual Studio 2005 (VC8) style for tabs""" # Firefox 2 tabs style FNB_FF2 = 131072 """Use Firefox 2 style for tabs""" # Place 'X' on a tab FNB_X_ON_TAB = 512 """Place 'X' close button on the active tab""" FNB_BACKGROUND_GRADIENT = 1024 """Use gradients to paint the tabs background""" FNB_COLOURFUL_TABS = 2048 """Use colourful tabs (VC8 style only)""" # Style to close tab using double click - styles 1024, 2048 are reserved FNB_DCLICK_CLOSES_TABS = 4096 """Style to close tab using double click""" FNB_SMART_TABS = 8192 """Use `Smart Tabbing`, like ``Alt`` + ``Tab`` on Windows""" FNB_DROPDOWN_TABS_LIST = 16384 """Use a dropdown menu on the left in place of the arrows""" FNB_ALLOW_FOREIGN_DND = 32768 """Allows drag 'n' drop operations between different L{FlatNotebook}s""" FNB_HIDE_ON_SINGLE_TAB = 65536 """Hides the Page Container when there is one or fewer tabs""" FNB_NO_TAB_FOCUS = 262144 """ Does not allow tabs to have focus""" # Use the Ribbon style for tabs FNB_RIBBON_TABS = 0x80000 """Use Ribbon style for tabs""" VERTICAL_BORDER_PADDING = 4 # Button size is a 16x16 xpm bitmap BUTTON_SPACE = 16 """Button size is a 16x16 xpm bitmap""" VC8_SHAPE_LEN = 16 MASK_COLOUR = wx.Colour(0, 128, 128) """Mask colour for the arrow bitmaps""" # Button status FNB_BTN_PRESSED = 2 """Navigation button is pressed""" FNB_BTN_HOVER = 1 """Navigation button is hovered""" FNB_BTN_NONE = 0 """No navigation""" # Hit Test results FNB_TAB = 1 # On a tab """Indicates mouse coordinates inside a tab""" FNB_X = 2 # On the X button """Indicates mouse coordinates inside the X region""" FNB_TAB_X = 3 # On the 'X' button (tab's X button) """Indicates mouse coordinates inside the X region in a tab""" FNB_LEFT_ARROW = 4 # On the rotate left arrow button """Indicates mouse coordinates inside the left arrow region""" FNB_RIGHT_ARROW = 5 # On the rotate right arrow button """Indicates mouse coordinates inside the right arrow region""" FNB_DROP_DOWN_ARROW = 6 # On the drop down arrow button """Indicates mouse coordinates inside the drop down arrow region""" FNB_NOWHERE = 0 # Anywhere else """Indicates mouse coordinates not on any tab of the notebook""" FNB_DEFAULT_STYLE = FNB_MOUSE_MIDDLE_CLOSES_TABS | FNB_HIDE_ON_SINGLE_TAB """L{FlatNotebook} default style""" # FlatNotebook Events: # wxEVT_FLATNOTEBOOK_PAGE_CHANGED: Event Fired When You Switch Page; # wxEVT_FLATNOTEBOOK_PAGE_CHANGING: Event Fired When You Are About To Switch # Pages, But You Can Still "Veto" The Page Changing By Avoiding To Call # event.Skip() In Your Event Handler; # wxEVT_FLATNOTEBOOK_PAGE_CLOSING: Event Fired When A Page Is Closing, But # You Can Still "Veto" The Page Changing By Avoiding To Call event.Skip() # In Your Event Handler; # wxEVT_FLATNOTEBOOK_PAGE_CLOSED: Event Fired When A Page Is Closed. # wxEVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU: Event Fired When A Menu Pops-up In A Tab. # wxEVT_FLATNOTEBOOK_PAGE_DROPPED: Event Fired When A Tab Is Dropped On The Same Notebook wxEVT_FLATNOTEBOOK_PAGE_CHANGED = wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGED wxEVT_FLATNOTEBOOK_PAGE_CHANGING = wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGING wxEVT_FLATNOTEBOOK_PAGE_CLOSING = wx.NewEventType() wxEVT_FLATNOTEBOOK_PAGE_CLOSED = wx.NewEventType() wxEVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU = wx.NewEventType() wxEVT_FLATNOTEBOOK_PAGE_DROPPED = wx.NewEventType() wxEVT_FLATNOTEBOOK_PAGE_DROPPED_FOREIGN = wx.NewEventType() #-----------------------------------# # FlatNotebookEvent #-----------------------------------# EVT_FLATNOTEBOOK_PAGE_CHANGED = wx.EVT_NOTEBOOK_PAGE_CHANGED """ Notify client objects when the active page in `FlatNotebook` has changed.""" EVT_FLATNOTEBOOK_PAGE_CHANGING = wx.EVT_NOTEBOOK_PAGE_CHANGING """ Notify client objects when the active page in `FlatNotebook` is about to change.""" EVT_FLATNOTEBOOK_PAGE_CLOSING = wx.PyEventBinder(wxEVT_FLATNOTEBOOK_PAGE_CLOSING, 1) """ Notify client objects when a page in `FlatNotebook` is closing.""" EVT_FLATNOTEBOOK_PAGE_CLOSED = wx.PyEventBinder(wxEVT_FLATNOTEBOOK_PAGE_CLOSED, 1) """ Notify client objects when a page in `FlatNotebook` has been closed.""" EVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU = wx.PyEventBinder(wxEVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU, 1) """ Notify client objects when a pop-up menu should appear next to a tab.""" EVT_FLATNOTEBOOK_PAGE_DROPPED = wx.PyEventBinder(wxEVT_FLATNOTEBOOK_PAGE_DROPPED, 1) """ Notify client objects when a tab has been dropped and re-arranged (on the *same* notebook).""" EVT_FLATNOTEBOOK_PAGE_DROPPED_FOREIGN = wx.PyEventBinder(wxEVT_FLATNOTEBOOK_PAGE_DROPPED_FOREIGN, 1) """ Notify client objects when a tab has been dropped and re-arranged (from a foreign notebook).""" # Some icons in XPM format left_arrow_disabled_xpm = [ " 16 16 8 1", "` c #008080", ". c #555555", "# c #000000", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "````````````````", "````````````````", "````````.```````", "```````..```````", "``````.`.```````", "`````.``.```````", "````.```.```````", "`````.``.```````", "``````.`.```````", "```````..```````", "````````.```````", "````````````````", "````````````````", "````````````````", "````````````````" ] x_button_pressed_xpm = [ " 16 16 8 1", "` c #008080", ". c #4766e0", "# c #9e9ede", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "`..............`", "`.############.`", "`.############.`", "`.############.`", "`.###aa####aa#.`", "`.####aa##aa##.`", "`.#####aaaa###.`", "`.######aa####.`", "`.#####aaaa###.`", "`.####aa##aa##.`", "`.###aa####aa#.`", "`.############.`", "`..............`", "````````````````", "````````````````" ] left_arrow_xpm = [ " 16 16 8 1", "` c #008080", ". c #555555", "# c #000000", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "````````````````", "````````````````", "````````.```````", "```````..```````", "``````...```````", "`````....```````", "````.....```````", "`````....```````", "``````...```````", "```````..```````", "````````.```````", "````````````````", "````````````````", "````````````````", "````````````````" ] x_button_hilite_xpm = [ " 16 16 8 1", "` c #008080", ". c #4766e0", "# c #c9dafb", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "`..............`", "`.############.`", "`.############.`", "`.##aa####aa##.`", "`.###aa##aa###.`", "`.####aaaa####.`", "`.#####aa#####.`", "`.####aaaa####.`", "`.###aa##aa###.`", "`.##aa####aa##.`", "`.############.`", "`.############.`", "`..............`", "````````````````", "````````````````" ] x_button_xpm = [ " 16 16 8 1", "` c #008080", ". c #555555", "# c #000000", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "````````````````", "````````````````", "````````````````", "````..````..````", "`````..``..`````", "``````....``````", "```````..```````", "``````....``````", "`````..``..`````", "````..````..````", "````````````````", "````````````````", "````````````````", "````````````````", "````````````````" ] left_arrow_pressed_xpm = [ " 16 16 8 1", "` c #008080", ". c #4766e0", "# c #9e9ede", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "`..............`", "`.############.`", "`.############.`", "`.#######a####.`", "`.######aa####.`", "`.#####aaa####.`", "`.####aaaa####.`", "`.###aaaaa####.`", "`.####aaaa####.`", "`.#####aaa####.`", "`.######aa####.`", "`.#######a####.`", "`..............`", "````````````````", "````````````````" ] left_arrow_hilite_xpm = [ " 16 16 8 1", "` c #008080", ". c #4766e0", "# c #c9dafb", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "`..............`", "`.############.`", "`.######a#####.`", "`.#####aa#####.`", "`.####aaa#####.`", "`.###aaaa#####.`", "`.##aaaaa#####.`", "`.###aaaa#####.`", "`.####aaa#####.`", "`.#####aa#####.`", "`.######a#####.`", "`.############.`", "`..............`", "````````````````", "````````````````" ] right_arrow_disabled_xpm = [ " 16 16 8 1", "` c #008080", ". c #555555", "# c #000000", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "````````````````", "````````````````", "```````.````````", "```````..```````", "```````.`.``````", "```````.``.`````", "```````.```.````", "```````.``.`````", "```````.`.``````", "```````..```````", "```````.````````", "````````````````", "````````````````", "````````````````", "````````````````" ] right_arrow_hilite_xpm = [ " 16 16 8 1", "` c #008080", ". c #4766e0", "# c #c9dafb", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "`..............`", "`.############.`", "`.####a#######.`", "`.####aa######.`", "`.####aaa#####.`", "`.####aaaa####.`", "`.####aaaaa###.`", "`.####aaaa####.`", "`.####aaa#####.`", "`.####aa######.`", "`.####a#######.`", "`.############.`", "`..............`", "````````````````", "````````````````" ] right_arrow_pressed_xpm = [ " 16 16 8 1", "` c #008080", ". c #4766e0", "# c #9e9ede", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "`..............`", "`.############.`", "`.############.`", "`.#####a######.`", "`.#####aa#####.`", "`.#####aaa####.`", "`.#####aaaa###.`", "`.#####aaaaa##.`", "`.#####aaaa###.`", "`.#####aaa####.`", "`.#####aa#####.`", "`.#####a######.`", "`..............`", "````````````````", "````````````````" ] right_arrow_xpm = [ " 16 16 8 1", "` c #008080", ". c #555555", "# c #000000", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "````````````````", "````````````````", "```````.````````", "```````..```````", "```````...``````", "```````....`````", "```````.....````", "```````....`````", "```````...``````", "```````..```````", "```````.````````", "````````````````", "````````````````", "````````````````", "````````````````" ] down_arrow_hilite_xpm = [ " 16 16 8 1", "` c #008080", ". c #4766e0", "# c #c9dafb", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "``.............`", "``.###########.`", "``.###########.`", "``.###########.`", "``.#aaaaaaaaa#.`", "``.##aaaaaaa##.`", "``.###aaaaa###.`", "``.####aaa####.`", "``.#####a#####.`", "``.###########.`", "``.###########.`", "``.###########.`", "``.............`", "````````````````", "````````````````" ] down_arrow_pressed_xpm = [ " 16 16 8 1", "` c #008080", ". c #4766e0", "# c #9e9ede", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "``.............`", "``.###########.`", "``.###########.`", "``.###########.`", "``.###########.`", "``.###########.`", "``.#aaaaaaaaa#.`", "``.##aaaaaaa##.`", "``.###aaaaa###.`", "``.####aaa####.`", "``.#####a#####.`", "``.###########.`", "``.............`", "````````````````", "````````````````" ] down_arrow_xpm = [ " 16 16 8 1", "` c #008080", ". c #000000", "# c #000000", "a c #000000", "b c #000000", "c c #000000", "d c #000000", "e c #000000", "````````````````", "````````````````", "````````````````", "````````````````", "````````````````", "````````````````", "````.........```", "`````.......````", "``````.....`````", "```````...``````", "````````.```````", "````````````````", "````````````````", "````````````````", "````````````````", "````````````````" ] #---------------------------------------------------------------------- from wx.lib.embeddedimage import PyEmbeddedImage Mondrian = PyEmbeddedImage( "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAHFJ" "REFUWIXt1jsKgDAQRdF7xY25cpcWC60kioI6Fm/ahHBCMh+BRmGMnAgEWnvPpzK8dvrFCCCA" "coD8og4c5Lr6WB3Q3l1TBwLYPuF3YS1gn1HphgEEEABcKERrGy0E3B0HFJg7C1N/f/kTBBBA" "+Vi+AMkgFEvBPD17AAAAAElFTkSuQmCC") #---------------------------------------------------------------------- def LightColour(colour, percent): """ Brighten the input colour by a percentage. :param `colour`: a valid `wx.Colour` instance; :param `percent`: the percentage by which the input colour should be brightened. """ end_colour = wx.WHITE rd = end_colour.Red() - colour.Red() gd = end_colour.Green() - colour.Green() bd = end_colour.Blue() - colour.Blue() high = 100 # We take the percent way of the colour from colour -. white i = percent r = colour.Red() + ((i*rd*100)/high)/100 g = colour.Green() + ((i*gd*100)/high)/100 b = colour.Blue() + ((i*bd*100)/high)/100 return wx.Colour(r, g, b) def RandomColour(): """ Creates a random colour. """ r = random.randint(0, 255) # Random value betweem 0-255 g = random.randint(0, 255) # Random value betweem 0-255 b = random.randint(0, 255) # Random value betweem 0-255 return wx.Colour(r, g, b) def PaintStraightGradientBox(dc, rect, startColour, endColour, vertical=True): """ Draws a gradient coloured box from `startColour` to `endColour`. :param `dc`: an instance of `wx.DC`; :param `rect`: the rectangle to fill with the gradient shading; :param `startColour`: the first colour in the gradient shading; :param `endColour`: the last colour in the gradient shading; :param `vertical`: ``True`` if the gradient shading is north to south, ``False`` if it is east to west. """ rd = endColour.Red() - startColour.Red() gd = endColour.Green() - startColour.Green() bd = endColour.Blue() - startColour.Blue() # Save the current pen and brush savedPen = dc.GetPen() savedBrush = dc.GetBrush() if vertical: high = rect.GetHeight()-1 else: high = rect.GetWidth()-1 if high < 1: return for i in xrange(high+1): r = startColour.Red() + ((i*rd*100)/high)/100 g = startColour.Green() + ((i*gd*100)/high)/100 b = startColour.Blue() + ((i*bd*100)/high)/100 p = wx.Pen(wx.Colour(r, g, b)) dc.SetPen(p) if vertical: dc.DrawLine(rect.x, rect.y+i, rect.x+rect.width, rect.y+i) else: dc.DrawLine(rect.x+i, rect.y, rect.x+i, rect.y+rect.height) # Restore the pen and brush dc.SetPen(savedPen) dc.SetBrush(savedBrush) # ----------------------------------------------------------------------------- # Util functions # ----------------------------------------------------------------------------- def DrawButton(dc, rect, focus, upperTabs): """ Draws a L{FlatNotebook} tab. :param `dc`: an instance of `wx.DC`; :param `rect`: the tab's client rectangle; :param `focus`: ``True`` if the tab has focus, ``False`` otherwise; :param `upperTabs`: ``True`` if the tabs are at the top, ``False`` if they are at the bottom. """ # Define the rounded rectangle base on the given rect # we need an array of 9 points for it regPts = [wx.Point() for indx in xrange(9)] if focus: if upperTabs: leftPt = wx.Point(rect.x, rect.y + (rect.height / 10)*8) rightPt = wx.Point(rect.x + rect.width - 2, rect.y + (rect.height / 10)*8) else: leftPt = wx.Point(rect.x, rect.y + (rect.height / 10)*5) rightPt = wx.Point(rect.x + rect.width - 2, rect.y + (rect.height / 10)*5) else: leftPt = wx.Point(rect.x, rect.y + (rect.height / 2)) rightPt = wx.Point(rect.x + rect.width - 2, rect.y + (rect.height / 2)) # Define the top region top = wx.RectPP(rect.GetTopLeft(), rightPt) bottom = wx.RectPP(leftPt, rect.GetBottomRight()) topStartColour = wx.WHITE if not focus: topStartColour = LightColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE), 50) topEndColour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE) bottomStartColour = topEndColour bottomEndColour = topEndColour # Incase we use bottom tabs, switch the colours if upperTabs: if focus: PaintStraightGradientBox(dc, top, topStartColour, topEndColour) PaintStraightGradientBox(dc, bottom, bottomStartColour, bottomEndColour) else: PaintStraightGradientBox(dc, top, topEndColour , topStartColour) PaintStraightGradientBox(dc, bottom, bottomStartColour, bottomEndColour) else: if focus: PaintStraightGradientBox(dc, bottom, topEndColour, bottomEndColour) PaintStraightGradientBox(dc, top,topStartColour, topStartColour) else: PaintStraightGradientBox(dc, bottom, bottomStartColour, bottomEndColour) PaintStraightGradientBox(dc, top, topEndColour, topStartColour) dc.SetBrush(wx.TRANSPARENT_BRUSH) # ---------------------------------------------------------------------------- # # Class FNBDropSource # Gives Some Custom UI Feedback during the DnD Operations # ---------------------------------------------------------------------------- # class FNBDropSource(wx.DropSource): """ Give some custom UI feedback during the drag and drop operation in this function. It is called on each mouse move, so your implementation must not be too slow. """ def __init__(self, win): """ Default class constructor. Used internally. :param `win`: the source window for which we wish to provide UI feedback during drag and drop operations. """ wx.DropSource.__init__(self, win) self._win = win def GiveFeedback(self, effect): """ You may give some custom UI feedback during the drag and drop operation in this function. It is called on each mouse move, so your implementation must not be too slow. :param `effect`: the effect to implement. One of ``wx.DragCopy``, ``wx.DragMove``, ``wx.DragLink`` and ``wx.DragNone``. :return: Return ``False`` if you want default feedback, or ``True`` if you implement your own feedback. The return values is ignored under GTK. :note: To show your own custom drag and drop UI feedback, you must override this method. """ self._win.DrawDragHint() return False # ---------------------------------------------------------------------------- # # Class FNBDragInfo # Stores All The Information To Allow Drag And Drop Between Different # FlatNotebooks. # ---------------------------------------------------------------------------- # class FNBDragInfo(object): """ Stores all the information to allow drag and drop between different L{FlatNotebook} instances. """ _map = weakref.WeakValueDictionary() def __init__(self, container, pageindex): """ Default class constructor. :param `container`: the drag and drop container, a page in L{FlatNotebook}; :param `pageindex`: the index of the tab that is actually being dragged. """ self._id = id(container) FNBDragInfo._map[self._id] = container self._pageindex = pageindex def GetContainer(self): """ Returns the L{FlatNotebook} page (usually a panel). """ return FNBDragInfo._map.get(self._id, None) def GetPageIndex(self): """ Returns the page index associated with a page. """ return self._pageindex # ---------------------------------------------------------------------------- # # Class FNBDropTarget # Simply Used To Handle The OnDrop() Method When Dragging And Dropping Between # Different FlatNotebooks. # ---------------------------------------------------------------------------- # class FNBDropTarget(wx.DropTarget): """ Class used to handle the L{FlatNotebook.OnDropTarget} method when dragging and dropping between different L{FlatNotebook} instances. """ def __init__(self, parent): """ Default class constructor. :param `parent`: the window handling the drag and drop, an instance of L{FlatNotebook}. """ wx.DropTarget.__init__(self) self._parent = parent self._dataobject = wx.CustomDataObject(wx.CustomDataFormat("FlatNotebook")) self.SetDataObject(self._dataobject) def OnData(self, x, y, dragres): """ Called after `OnDrop` returns ``True``. By default this will usually call `GetData` and will return the suggested default value `dragres`. :param `x`: the current x position of the mouse while dragging and dropping; :param `y`: the current y position of the mouse while dragging and dropping; :param `dragres`: an optional default return value. """ if not self.GetData(): return wx.DragNone draginfo = self._dataobject.GetData() drginfo = cPickle.loads(draginfo) return self._parent.OnDropTarget(x, y, drginfo.GetPageIndex(), drginfo.GetContainer()) # ---------------------------------------------------------------------------- # # Class PageInfo # Contains parameters for every FlatNotebook page # ---------------------------------------------------------------------------- # class PageInfo(object): """ This class holds all the information (caption, image, etc...) belonging to a single tab in L{FlatNotebook}. """ def __init__(self, caption="", imageindex=-1, tabangle=0, enabled=True): """ Default Class Constructor. :param `caption`: the tab caption; :param `imageindex`: the tab image index based on the assigned (set) `wx.ImageList` (if any); :param `tabangle`: the tab angle (only on standard tabs, from 0 to 15 degrees); :param `enabled`: sets the tab as enabled or disabled. """ self._strCaption = caption self._TabAngle = tabangle self._ImageIndex = imageindex self._bEnabled = enabled self._pos = wx.Point(-1, -1) self._size = wx.Size(-1, -1) self._region = wx.Region() self._xRect = wx.Rect() self._colour = None self._hasFocus = False self._pageTextColour = None def SetCaption(self, value): """ Sets the tab caption. :param `value`: the new tab caption string. """ self._strCaption = value def GetCaption(self): """ Returns the tab caption. """ return self._strCaption def SetPosition(self, value): """ Sets the tab position. :param `value`: an instance of `wx.Point`. """ self._pos = value def GetPosition(self): """ Returns the tab position. """ return self._pos def SetSize(self, value): """ Sets the tab size. :param `value`: an instance of `wx.Size`. """ self._size = value def GetSize(self): """ Returns the tab size. """ return self._size def SetTabAngle(self, value): """ Sets the tab header angle. :param `value`: the tab header angle (0 <= value <= 15 degrees). """ self._TabAngle = min(45, value) def GetTabAngle(self): """ Returns the tab angle. """ return self._TabAngle def SetImageIndex(self, value): """ Sets the tab image index. :param `value`: an index within the L{FlatNotebook} image list specifying the image to use for this tab. """ self._ImageIndex = value def GetImageIndex(self): """ Returns the tab image index. """ return self._ImageIndex def GetPageTextColour(self): """ Returns the tab text colour if it has been set previously, or ``None`` otherwise. """ return self._pageTextColour def SetPageTextColour(self, colour): """ Sets the tab text colour for this tab. :param `colour`: an instance of `wx.Colour`. You can pass ``None`` or `wx.NullColour` to return to the default page text colour. """ if colour is None or not colour.IsOk(): self._pageTextColour = None else: self._pageTextColour = colour def GetEnabled(self): """ Returns whether the tab is enabled or not. """ return self._bEnabled def EnableTab(self, enabled): """ Sets the tab enabled or disabled. :param `enabled`: ``True`` to enable a tab, ``False`` to disable it. """ self._bEnabled = enabled def SetRegion(self, points=[]): """ Sets the tab region. :param `points`: a Python list of `wx.Points` """ self._region = wx.RegionFromPoints(points) def GetRegion(self): """ Returns the tab region. """ return self._region def SetXRect(self, xrect): """ Sets the button 'X' area rect. :param `xrect`: an instance of `wx.Rect`, specifying the client rectangle of the 'X' button. """ self._xRect = xrect def GetXRect(self): """ Returns the button 'X' area rect. """ return self._xRect def GetColour(self): """ Returns the tab colour. """ return self._colour def SetColour(self, colour): """ Sets the tab colour. :param `colour`: a valid `wx.Colour` object. """ self._colour = colour # ---------------------------------------------------------------------------- # # Class FlatNotebookEvent # ---------------------------------------------------------------------------- # class FlatNotebookEvent(wx.PyCommandEvent): """ This events will be sent when a ``EVT_FLATNOTEBOOK_PAGE_CHANGED``, ``EVT_FLATNOTEBOOK_PAGE_CHANGING``, ``EVT_FLATNOTEBOOK_PAGE_CLOSING``, ``EVT_FLATNOTEBOOK_PAGE_CLOSED`` and ``EVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU`` is mapped in the parent. """ def __init__(self, eventType, eventId=1, nSel=-1, nOldSel=-1): """ Default class constructor. :param `eventType`: the event type; :param `eventId`: the event identifier; :param `nSel`: the current selection; :param `nOldSel`: the old selection. """ wx.PyCommandEvent.__init__(self, eventType, eventId) self._eventType = eventType self.notify = wx.NotifyEvent(eventType, eventId) def GetNotifyEvent(self): """ Returns the actual `wx.NotifyEvent`. """ return self.notify def IsAllowed(self): """ Returns ``True`` if the change is allowed (L{Veto} hasn't been called) or ``False`` otherwise (if it was). """ return self.notify.IsAllowed() def Veto(self): """ Prevents the change announced by this event from happening. :note: It is in general a good idea to notify the user about the reasons for vetoing the change because otherwise the applications behaviour (which just refuses to do what the user wants) might be quite surprising. """ self.notify.Veto() def Allow(self): """ This is the opposite of L{Veto}: it explicitly allows the event to be processed. For most events it is not necessary to call this method as the events are allowed anyhow but some are forbidden by default (this will be mentioned in the corresponding event description). """ self.notify.Allow() def SetSelection(self, nSel): """ Sets the event selection. :param `nSel`: an integer specifying the new selection. """ self._selection = nSel def SetOldSelection(self, nOldSel): """ Sets the id of the page selected before the change. :param `nOldSel`: an integer specifying the old selection. """ self._oldselection = nOldSel def GetSelection(self): """ Returns the currently selected page, or -1 if none was selected. """ return self._selection def GetOldSelection(self): """ Returns the page that was selected before the change, -1 if none was selected. """ return self._oldselection # ---------------------------------------------------------------------------- # # Class TabNavigatorWindow # ---------------------------------------------------------------------------- # class FlatNotebookDragEvent(FlatNotebookEvent): """ This event will be sent when a ``EVT_FLATNOTEBOOK_PAGE_DRAGGED_FOREIGN`` is mapped in the parent. """ def __init__(self, eventType, eventId=1, nSel=-1, nOldSel=-1): """ Default class constructor. :param `eventType`: the event type; :param `eventId`: the event identifier; :param `nSel`: the current selection; :param `nOldSel`: the old selection. """ wx.PyCommandEvent.__init__(self, eventType, eventId) self._eventType = eventType self.notify = wx.NotifyEvent(eventType, eventId) self._oldnotebook = -1 self._newnotebook = -1 def GetNotebook(self): """ Returns the new notebook. """ return self._newnotebook def GetOldNotebook(self): """ Returns the old notebook. """ return self._oldnotebook def SetNotebook(self, notebook): """ Sets the new notebook. :param `notebook`: an instance of L{FlatNotebook}. """ self._newnotebook = notebook def SetOldNotebook(self, old): """ Sets the old notebook. :param `notebook`: an instance of L{FlatNotebook}. """ self._oldnotebook = old # ---------------------------------------------------------------------------- # # Class TabNavigatorWindow # ---------------------------------------------------------------------------- # class TabNavigatorWindow(wx.Dialog): """ This class is used to create a modal dialog that enables `Smart Tabbing`, similar to what you would get by hitting ``Alt`` + ``Tab`` on Windows. """ def __init__(self, parent=None, icon=None): """ Default class constructor. Used internally. :param `parent`: the L{TabNavigatorWindow} parent window; :param `icon`: a valid `wx.Bitmap` object representing the icon to be displayed in the L{TabNavigatorWindow}. """ wx.Dialog.__init__(self, parent, wx.ID_ANY, "", style=0) self._selectedItem = -1 self._indexMap = [] if icon is None: self._bmp = Mondrian.GetBitmap() else: self._bmp = icon sz = wx.BoxSizer(wx.VERTICAL) self._listBox = wx.ListBox(self, wx.ID_ANY, wx.DefaultPosition, wx.Size(200, 150), [], wx.LB_SINGLE | wx.NO_BORDER) mem_dc = wx.MemoryDC() mem_dc.SelectObject(wx.EmptyBitmap(1,1)) font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) font.SetWeight(wx.BOLD) mem_dc.SetFont(font) panelHeight = mem_dc.GetCharHeight() panelHeight += 4 # Place a spacer of 2 pixels # Out signpost bitmap is 24 pixels if panelHeight < 24: panelHeight = 24 self._panel = wx.Panel(self, wx.ID_ANY, wx.DefaultPosition, wx.Size(200, panelHeight)) sz.Add(self._panel) sz.Add(self._listBox, 1, wx.EXPAND) self.SetSizer(sz) # Connect events to the list box self._listBox.Bind(wx.EVT_KEY_UP, self.OnKeyUp) self._listBox.Bind(wx.EVT_NAVIGATION_KEY, self.OnNavigationKey) self._listBox.Bind(wx.EVT_LISTBOX_DCLICK, self.OnItemSelected) # Connect paint event to the panel self._panel.Bind(wx.EVT_PAINT, self.OnPanelPaint) self._panel.Bind(wx.EVT_ERASE_BACKGROUND, self.OnPanelEraseBg) self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)) self._listBox.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)) self.PopulateListControl(parent) self.GetSizer().Fit(self) self.GetSizer().SetSizeHints(self) self.GetSizer().Layout() self.Centre() # Set focus on the list box to avoid having to click on it to change # the tab selection under GTK. self._listBox.SetFocus() def OnKeyUp(self, event): """ Handles the ``wx.EVT_KEY_UP`` for the L{TabNavigatorWindow}. :param `event`: a `wx.KeyEvent` event to be processed. """ if event.GetKeyCode() == wx.WXK_CONTROL: self.CloseDialog() def OnNavigationKey(self, event): """ Handles the ``wx.EVT_NAVIGATION_KEY`` for the L{TabNavigatorWindow}. :param `event`: a `wx.NavigationKeyEvent` event to be processed. """ selected = self._listBox.GetSelection() bk = self.GetParent() maxItems = bk.GetPageCount() if event.GetDirection(): # Select next page if selected == maxItems - 1: itemToSelect = 0 else: itemToSelect = selected + 1 else: # Previous page if selected == 0: itemToSelect = maxItems - 1 else: itemToSelect = selected - 1 self._listBox.SetSelection(itemToSelect) def PopulateListControl(self, book): """ Populates the L{TabNavigatorWindow} listbox with a list of tabs. :param `book`: an instance of L{FlatNotebook} containing the tabs to be displayed in the listbox. """ selection = book.GetSelection() count = book.GetPageCount() self._listBox.Append(book.GetPageText(selection)) self._indexMap.append(selection) prevSel = book.GetPreviousSelection() if prevSel != wx.NOT_FOUND: # Insert the previous selection as second entry self._listBox.Append(book.GetPageText(prevSel)) self._indexMap.append(prevSel) for c in xrange(count): # Skip selected page if c == selection: continue # Skip previous selected page as well if c == prevSel: continue self._listBox.Append(book.GetPageText(c)) self._indexMap.append(c) # Select the next entry after the current selection self._listBox.SetSelection(0) dummy = wx.NavigationKeyEvent() dummy.SetDirection(True) self.OnNavigationKey(dummy) def OnItemSelected(self, event): """ Handles the ``wx.EVT_LISTBOX_DCLICK`` for the L{TabNavigatorWindow}. :param `event`: a `wx.ListEvent` event to be processed. """ self.CloseDialog() def CloseDialog(self): """ Closes the L{TabNavigatorWindow} dialog, setting the new selection in L{FlatNotebook}. """ bk = self.GetParent() self._selectedItem = self._listBox.GetSelection() iter = self._indexMap[self._selectedItem] bk._pages.FireEvent(iter) self.EndModal(wx.ID_OK) def OnPanelPaint(self, event): """ Handles the ``wx.EVT_PAINT`` for the L{TabNavigatorWindow} top panel. :param `event`: a `wx.PaintEvent` event to be processed. """ dc = wx.PaintDC(self._panel) rect = self._panel.GetClientRect() bmp = wx.EmptyBitmap(rect.width, rect.height) mem_dc = wx.MemoryDC() mem_dc.SelectObject(bmp) endColour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW) startColour = LightColour(endColour, 50) PaintStraightGradientBox(mem_dc, rect, startColour, endColour) # Draw the caption title and place the bitmap # get the bitmap optimal position, and draw it bmpPt, txtPt = wx.Point(), wx.Point() bmpPt.y = (rect.height - self._bmp.GetHeight())/2 bmpPt.x = 3 mem_dc.DrawBitmap(self._bmp, bmpPt.x, bmpPt.y, True) # get the text position, and draw it font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) font.SetWeight(wx.BOLD) mem_dc.SetFont(font) fontHeight = mem_dc.GetCharHeight() txtPt.x = bmpPt.x + self._bmp.GetWidth() + 4 txtPt.y = (rect.height - fontHeight)/2 mem_dc.SetTextForeground(wx.WHITE) mem_dc.DrawText("Opened tabs:", txtPt.x, txtPt.y) mem_dc.SelectObject(wx.NullBitmap) dc.DrawBitmap(bmp, 0, 0) def OnPanelEraseBg(self, event): """ Handles the ``wx.EVT_ERASE_BACKGROUND`` for the L{TabNavigatorWindow} top panel. :param `event`: a `wx.EraseEvent` event to be processed. :note: This method is intentionally empty to reduce flicker. """ pass # ---------------------------------------------------------------------------- # # Class FNBRenderer # ---------------------------------------------------------------------------- # class FNBRenderer(object): """ Parent class for the 6 renderers defined: `Standard`, `VC71`, `Fancy`, `Firefox 2`, `VC8` and `Ribbon`. This class implements the common methods of all 6 renderers. """ def __init__(self): """ Default class constructor. """ self._tabHeight = None if wx.Platform == "__WXMAC__": # Get proper highlight colour for focus rectangle from the # current Mac theme. kThemeBrushFocusHighlight is # available on Mac OS 8.5 and higher if hasattr(wx, 'MacThemeColour'): c = wx.MacThemeColour(Carbon.Appearance.kThemeBrushFocusHighlight) else: brush = wx.Brush(wx.BLACK) brush.MacSetTheme(Carbon.Appearance.kThemeBrushFocusHighlight) c = brush.GetColour() self._focusPen = wx.Pen(c, 2, wx.SOLID) else: self._focusPen = wx.Pen(wx.BLACK, 1, wx.USER_DASH) self._focusPen.SetDashes([1, 1]) self._focusPen.SetCap(wx.CAP_BUTT) def GetLeftButtonPos(self, pageContainer): """ Returns the left button position in the navigation area. :param `pageContainer`: an instance of L{FlatNotebook}. """ pc = pageContainer agwStyle = pc.GetParent().GetAGWWindowStyleFlag() rect = pc.GetClientRect() clientWidth = rect.width if agwStyle & FNB_NO_X_BUTTON: return clientWidth - 38 else: return clientWidth - 54 def GetRightButtonPos(self, pageContainer): """ Returns the right button position in the navigation area. :param `pageContainer`: an instance of L{FlatNotebook}. """ pc = pageContainer agwStyle = pc.GetParent().GetAGWWindowStyleFlag() rect = pc.GetClientRect() clientWidth = rect.width if agwStyle & FNB_NO_X_BUTTON: return clientWidth - 22 else: return clientWidth - 38 def GetDropArrowButtonPos(self, pageContainer): """ Returns the drop down button position in the navigation area. :param `pageContainer`: an instance of L{FlatNotebook}. """ return self.GetRightButtonPos(pageContainer) def GetXPos(self, pageContainer): """ Returns the 'X' button position in the navigation area. :param `pageContainer`: an instance of L{FlatNotebook}. """ pc = pageContainer agwStyle = pc.GetParent().GetAGWWindowStyleFlag() rect = pc.GetClientRect() clientWidth = rect.width if agwStyle & FNB_NO_X_BUTTON: return clientWidth else: return clientWidth - 22 def GetButtonsAreaLength(self, pageContainer): """ Returns the navigation area width. :param `pageContainer`: an instance of L{FlatNotebook}. """ pc = pageContainer agwStyle = pc.GetParent().GetAGWWindowStyleFlag() # '' if agwStyle & FNB_NO_NAV_BUTTONS and agwStyle & FNB_NO_X_BUTTON and not agwStyle & FNB_DROPDOWN_TABS_LIST: return 0 # 'x' elif agwStyle & FNB_NO_NAV_BUTTONS and not agwStyle & FNB_NO_X_BUTTON and not agwStyle & FNB_DROPDOWN_TABS_LIST: return 22 # '<>' if not agwStyle & FNB_NO_NAV_BUTTONS and agwStyle & FNB_NO_X_BUTTON and not agwStyle & FNB_DROPDOWN_TABS_LIST: return 53 - 16 # 'vx' if agwStyle & FNB_DROPDOWN_TABS_LIST and not agwStyle & FNB_NO_X_BUTTON: return 22 + 16 # 'v' if agwStyle & FNB_DROPDOWN_TABS_LIST and agwStyle & FNB_NO_X_BUTTON: return 22 # '<>x' return 53 def DrawArrowAccordingToState(self, dc, pc, rect): """ Draws the left and right scrolling arrows. :param `dc`: an instance of `wx.DC`; :param `pc`: an instance of L{FlatNotebook}; :param `rect`: the client rectangle containing the scrolling arrows. """ lightFactor = (pc.HasAGWFlag(FNB_BACKGROUND_GRADIENT) and [70] or [0])[0] PaintStraightGradientBox(dc, rect, pc._tabAreaColour, LightColour(pc._tabAreaColour, lightFactor)) def DrawLeftArrow(self, pageContainer, dc): """ Draws the left navigation arrow. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`. """ pc = pageContainer agwStyle = pc.GetParent().GetAGWWindowStyleFlag() if agwStyle & FNB_NO_NAV_BUTTONS: return # Make sure that there are pages in the container if not pc._pagesInfoVec: return # Set the bitmap according to the button status if pc._nLeftButtonStatus == FNB_BTN_HOVER: arrowBmp = wx.BitmapFromXPMData(left_arrow_hilite_xpm) elif pc._nLeftButtonStatus == FNB_BTN_PRESSED: arrowBmp = wx.BitmapFromXPMData(left_arrow_pressed_xpm) else: arrowBmp = wx.BitmapFromXPMData(left_arrow_xpm) if pc._nFrom == 0: # Handle disabled arrow arrowBmp = wx.BitmapFromXPMData(left_arrow_disabled_xpm) arrowBmp.SetMask(wx.Mask(arrowBmp, MASK_COLOUR)) # Erase old bitmap posx = self.GetLeftButtonPos(pc) self.DrawArrowAccordingToState(dc, pc, wx.Rect(posx, 6, 16, 14)) # Draw the new bitmap dc.DrawBitmap(arrowBmp, posx, 6, True) def DrawRightArrow(self, pageContainer, dc): """ Draws the right navigation arrow. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`. """ pc = pageContainer agwStyle = pc.GetParent().GetAGWWindowStyleFlag() if agwStyle & FNB_NO_NAV_BUTTONS: return # Make sure that there are pages in the container if not pc._pagesInfoVec: return # Set the bitmap according to the button status if pc._nRightButtonStatus == FNB_BTN_HOVER: arrowBmp = wx.BitmapFromXPMData(right_arrow_hilite_xpm) elif pc._nRightButtonStatus == FNB_BTN_PRESSED: arrowBmp = wx.BitmapFromXPMData(right_arrow_pressed_xpm) else: arrowBmp = wx.BitmapFromXPMData(right_arrow_xpm) # Check if the right most tab is visible, if it is # don't rotate right anymore if pc._pagesInfoVec[-1].GetPosition() != wx.Point(-1, -1): arrowBmp = wx.BitmapFromXPMData(right_arrow_disabled_xpm) arrowBmp.SetMask(wx.Mask(arrowBmp, MASK_COLOUR)) # erase old bitmap posx = self.GetRightButtonPos(pc) self.DrawArrowAccordingToState(dc, pc, wx.Rect(posx, 6, 16, 14)) # Draw the new bitmap dc.DrawBitmap(arrowBmp, posx, 6, True) def DrawDropDownArrow(self, pageContainer, dc): """ Draws the drop-down arrow in the navigation area. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`. """ pc = pageContainer # Check if this style is enabled agwStyle = pc.GetParent().GetAGWWindowStyleFlag() if not agwStyle & FNB_DROPDOWN_TABS_LIST: return # Make sure that there are pages in the container if not pc._pagesInfoVec: return if pc._nArrowDownButtonStatus == FNB_BTN_HOVER: downBmp = wx.BitmapFromXPMData(down_arrow_hilite_xpm) elif pc._nArrowDownButtonStatus == FNB_BTN_PRESSED: downBmp = wx.BitmapFromXPMData(down_arrow_pressed_xpm) else: downBmp = wx.BitmapFromXPMData(down_arrow_xpm) downBmp.SetMask(wx.Mask(downBmp, MASK_COLOUR)) # erase old bitmap posx = self.GetDropArrowButtonPos(pc) self.DrawArrowAccordingToState(dc, pc, wx.Rect(posx, 6, 16, 14)) # Draw the new bitmap dc.DrawBitmap(downBmp, posx, 6, True) def DrawX(self, pageContainer, dc): """ Draw the 'X' navigation button in the navigation area. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`. """ pc = pageContainer # Check if this style is enabled agwStyle = pc.GetParent().GetAGWWindowStyleFlag() if agwStyle & FNB_NO_X_BUTTON: return # Make sure that there are pages in the container if not pc._pagesInfoVec: return # Set the bitmap according to the button status if pc._nXButtonStatus == FNB_BTN_HOVER: xbmp = wx.BitmapFromXPMData(x_button_hilite_xpm) elif pc._nXButtonStatus == FNB_BTN_PRESSED: xbmp = wx.BitmapFromXPMData(x_button_pressed_xpm) else: xbmp = wx.BitmapFromXPMData(x_button_xpm) xbmp.SetMask(wx.Mask(xbmp, MASK_COLOUR)) # erase old bitmap posx = self.GetXPos(pc) self.DrawArrowAccordingToState(dc, pc, wx.Rect(posx, 6, 16, 14)) # Draw the new bitmap dc.DrawBitmap(xbmp, posx, 6, True) def DrawTabX(self, pageContainer, dc, rect, tabIdx, btnStatus): """ Draws the 'X' in the selected tab. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`; :param `rect`: the current tab client rectangle; :param `tabIdx`: the index of the current tab; :param `btnStatus`: the status of the 'X' button in the current tab. """ pc = pageContainer if not pc.HasAGWFlag(FNB_X_ON_TAB): return # We draw the 'x' on the active tab only if tabIdx != pc.GetSelection() or tabIdx < 0: return # Set the bitmap according to the button status if btnStatus == FNB_BTN_HOVER: xBmp = wx.BitmapFromXPMData(x_button_hilite_xpm) elif btnStatus == FNB_BTN_PRESSED: xBmp = wx.BitmapFromXPMData(x_button_pressed_xpm) else: xBmp = wx.BitmapFromXPMData(x_button_xpm) # Set the masking xBmp.SetMask(wx.Mask(xBmp, MASK_COLOUR)) # Draw the new bitmap dc.DrawBitmap(xBmp, rect.x, rect.y, True) # Update the vector rr = wx.Rect(rect.x, rect.y, 14, 13) pc._pagesInfoVec[tabIdx].SetXRect(rr) def DrawTabsLine(self, pageContainer, dc, selTabX1=-1, selTabX2=-1): """ Draws a line over the tabs. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`; :param `selTabX1`: first x coordinate of the tab line; :param `selTabX2`: second x coordinate of the tab line. """ pc = pageContainer clntRect = pc.GetClientRect() clientRect3 = wx.Rect(0, 0, clntRect.width, clntRect.height) if pc.HasAGWFlag(FNB_FF2): if not pc.HasAGWFlag(FNB_BOTTOM): fillColour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE) else: fillColour = wx.WHITE dc.SetPen(wx.Pen(fillColour)) if pc.HasAGWFlag(FNB_BOTTOM): dc.DrawLine(1, 0, clntRect.width-1, 0) dc.DrawLine(1, 1, clntRect.width-1, 1) dc.SetPen(wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW))) dc.DrawLine(1, 2, clntRect.width-1, 2) dc.SetPen(wx.Pen(fillColour)) dc.DrawLine(selTabX1 + 2, 2, selTabX2 - 1, 2) else: dc.DrawLine(1, clntRect.height, clntRect.width-1, clntRect.height) dc.DrawLine(1, clntRect.height-1, clntRect.width-1, clntRect.height-1) dc.SetPen(wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW))) dc.DrawLine(1, clntRect.height-2, clntRect.width-1, clntRect.height-2) dc.SetPen(wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))) dc.DrawLine(selTabX1 + 2, clntRect.height-2, selTabX2-1, clntRect.height-2) else: if pc.HasAGWFlag(FNB_BOTTOM): clientRect = wx.Rect(0, 2, clntRect.width, clntRect.height - 2) clientRect2 = wx.Rect(0, 1, clntRect.width, clntRect.height - 1) else: clientRect = wx.Rect(0, 0, clntRect.width, clntRect.height - 2) clientRect2 = wx.Rect(0, 0, clntRect.width, clntRect.height - 1) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.SetPen(wx.Pen(pc.GetSingleLineBorderColour())) dc.DrawRectangleRect(clientRect2) dc.DrawRectangleRect(clientRect3) dc.SetPen(wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW))) dc.DrawRectangleRect(clientRect) if not pc.HasAGWFlag(FNB_TABS_BORDER_SIMPLE): dc.SetPen(wx.Pen((pc.HasAGWFlag(FNB_VC71) and [wx.Colour(247, 243, 233)] or [pc._tabAreaColour])[0])) dc.DrawLine(0, 0, 0, clientRect.height+1) if pc.HasAGWFlag(FNB_BOTTOM): dc.DrawLine(0, clientRect.height+1, clientRect.width, clientRect.height+1) else: dc.DrawLine(0, 0, clientRect.width, 0) dc.DrawLine(clientRect.width - 1, 0, clientRect.width - 1, clientRect.height+1) def CalcTabWidth(self, pageContainer, tabIdx, tabHeight): """ Calculates the width of the input tab. :param `pageContainer`: an instance of L{FlatNotebook}; :param `tabIdx`: the index of the input tab; :param `tabHeight`: the height of the tab. """ pc = pageContainer dc = wx.MemoryDC() dc.SelectObject(wx.EmptyBitmap(1,1)) boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) boldFont.SetWeight(wx.FONTWEIGHT_BOLD) if pc.IsDefaultTabs(): shapePoints = int(tabHeight*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi)) # Calculate the text length using the bold font, so when selecting a tab # its width will not change dc.SetFont(boldFont) width, pom = dc.GetTextExtent(pc.GetPageText(tabIdx)) # Set a minimum size to a tab if width < 20: width = 20 tabWidth = 2*pc._pParent.GetPadding() + width # Style to add a small 'x' button on the top right # of the tab if pc.HasAGWFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection(): # The xpm image that contains the 'x' button is 9 pixels spacer = 9 if pc.HasAGWFlag(FNB_VC8): spacer = 4 tabWidth += pc._pParent.GetPadding() + spacer if pc.IsDefaultTabs(): # Default style tabWidth += 2*shapePoints hasImage = pc._ImageList != None and pc._pagesInfoVec[tabIdx].GetImageIndex() != -1 # For VC71 style, we only add the icon size (16 pixels) if hasImage: if not pc.IsDefaultTabs(): tabWidth += 16 + pc._pParent.GetPadding() else: # Default style tabWidth += 16 + pc._pParent.GetPadding() + shapePoints/2 return tabWidth def CalcTabHeight(self, pageContainer): """ Calculates the height of the input tab. :param `pageContainer`: an instance of L{FlatNotebook}. """ if self._tabHeight: return self._tabHeight pc = pageContainer dc = wx.MemoryDC() dc.SelectObject(wx.EmptyBitmap(1,1)) # For GTK it seems that we must do this steps in order # for the tabs will get the proper height on initialization # on MSW, preforming these steps yields wierd results normalFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) boldFont = normalFont if "__WXGTK__" in wx.PlatformInfo: boldFont.SetWeight(wx.FONTWEIGHT_BOLD) dc.SetFont(boldFont) height = dc.GetCharHeight() tabHeight = height + FNB_HEIGHT_SPACER # We use 8 pixels as padding if "__WXGTK__" in wx.PlatformInfo: # On GTK the tabs are should be larger tabHeight += 6 self._tabHeight = tabHeight return tabHeight def DrawTabs(self, pageContainer, dc): """ Actually draws the tabs in L{FlatNotebook}. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`. """ pc = pageContainer if "__WXMAC__" in wx.PlatformInfo: # Works well on MSW & GTK, however this lines should be skipped on MAC if not pc._pagesInfoVec or pc._nFrom >= len(pc._pagesInfoVec): pc.Hide() return # Get the text hight tabHeight = self.CalcTabHeight(pageContainer) agwStyle = pc.GetParent().GetAGWWindowStyleFlag() # Calculate the number of rows required for drawing the tabs rect = pc.GetClientRect() clientWidth = rect.width # Set the maximum client size pc.SetSizeHints(self.GetButtonsAreaLength(pc), tabHeight) borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)) if agwStyle & FNB_VC71: backBrush = wx.Brush(wx.Colour(247, 243, 233)) else: backBrush = wx.Brush(pc._tabAreaColour) noselBrush = wx.Brush(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNFACE)) selBrush = wx.Brush(pc._activeTabColour) size = pc.GetSize() # Background dc.SetTextBackground((agwStyle & FNB_VC71 and [wx.Colour(247, 243, 233)] or [pc.GetBackgroundColour()])[0]) dc.SetTextForeground(pc._activeTextColour) dc.SetBrush(backBrush) # If border style is set, set the pen to be border pen if pc.HasAGWFlag(FNB_TABS_BORDER_SIMPLE): dc.SetPen(borderPen) else: colr = (pc.HasAGWFlag(FNB_VC71) and [wx.Colour(247, 243, 233)] or [pc.GetBackgroundColour()])[0] dc.SetPen(wx.Pen(colr)) if pc.HasAGWFlag(FNB_FF2): lightFactor = (pc.HasAGWFlag(FNB_BACKGROUND_GRADIENT) and [70] or [0])[0] PaintStraightGradientBox(dc, pc.GetClientRect(), pc._tabAreaColour, LightColour(pc._tabAreaColour, lightFactor)) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.DrawRectangle(0, 0, size.x, size.y) # We always draw the bottom/upper line of the tabs # regradless the style dc.SetPen(borderPen) if not pc.HasAGWFlag(FNB_FF2): self.DrawTabsLine(pc, dc) # Restore the pen dc.SetPen(borderPen) if pc.HasAGWFlag(FNB_VC71): greyLineYVal = (pc.HasAGWFlag(FNB_BOTTOM) and [0] or [size.y - 2])[0] whiteLineYVal = (pc.HasAGWFlag(FNB_BOTTOM) and [3] or [size.y - 3])[0] pen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE)) dc.SetPen(pen) # Draw thik grey line between the windows area and # the tab area for num in xrange(3): dc.DrawLine(0, greyLineYVal + num, size.x, greyLineYVal + num) wbPen = (pc.HasAGWFlag(FNB_BOTTOM) and [wx.BLACK_PEN] or [wx.WHITE_PEN])[0] dc.SetPen(wbPen) dc.DrawLine(1, whiteLineYVal, size.x - 1, whiteLineYVal) # Restore the pen dc.SetPen(borderPen) # Draw labels normalFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) boldFont.SetWeight(wx.FONTWEIGHT_BOLD) dc.SetFont(boldFont) posx = pc._pParent.GetPadding() # Update all the tabs from 0 to 'pc._nFrom' to be non visible for i in xrange(pc._nFrom): pc._pagesInfoVec[i].SetPosition(wx.Point(-1, -1)) pc._pagesInfoVec[i].GetRegion().Clear() count = pc._nFrom #---------------------------------------------------------- # Go over and draw the visible tabs #---------------------------------------------------------- x1 = x2 = -1 for i in xrange(pc._nFrom, len(pc._pagesInfoVec)): dc.SetPen(borderPen) if not pc.HasAGWFlag(FNB_FF2): dc.SetBrush((i==pc.GetSelection() and [selBrush] or [noselBrush])[0]) # Now set the font to the correct font dc.SetFont((i==pc.GetSelection() and [boldFont] or [normalFont])[0]) # Add the padding to the tab width # Tab width: # +-----------------------------------------------------------+ # | PADDING | IMG | IMG_PADDING | TEXT | PADDING | x |PADDING | # +-----------------------------------------------------------+ tabWidth = self.CalcTabWidth(pageContainer, i, tabHeight) # Check if we can draw more if posx + tabWidth + self.GetButtonsAreaLength(pc) >= clientWidth: break count = count + 1 # By default we clean the tab region pc._pagesInfoVec[i].GetRegion().Clear() # Clean the 'x' buttn on the tab. # A 'Clean' rectangle, is a rectangle with width or height # with values lower than or equal to 0 pc._pagesInfoVec[i].GetXRect().SetSize(wx.Size(-1, -1)) # Draw the tab (border, text, image & 'x' on tab) self.DrawTab(pc, dc, posx, i, tabWidth, tabHeight, pc._nTabXButtonStatus) if pc.GetSelection() == i: x1 = posx x2 = posx + tabWidth + 2 # Restore the text forground dc.SetTextForeground(pc._activeTextColour) # Update the tab position & size posy = (pc.HasAGWFlag(FNB_BOTTOM) and [0] or [VERTICAL_BORDER_PADDING])[0] pc._pagesInfoVec[i].SetPosition(wx.Point(posx, posy)) pc._pagesInfoVec[i].SetSize(wx.Size(tabWidth, tabHeight)) self.DrawFocusRectangle(dc, pc, pc._pagesInfoVec[i]) posx += tabWidth # Update all tabs that can not fit into the screen as non-visible for i in xrange(count, len(pc._pagesInfoVec)): pc._pagesInfoVec[i].SetPosition(wx.Point(-1, -1)) pc._pagesInfoVec[i].GetRegion().Clear() # Draw the left/right/close buttons # Left arrow self.DrawLeftArrow(pc, dc) self.DrawRightArrow(pc, dc) self.DrawX(pc, dc) self.DrawDropDownArrow(pc, dc) if pc.HasAGWFlag(FNB_FF2): self.DrawTabsLine(pc, dc, x1, x2) def DrawFocusRectangle(self, dc, pageContainer, page): """ Draws a focus rectangle like the native `wx.Notebooks`. :param `dc`: an instance of `wx.DC`; :param `pageContainer`: an instance of L{FlatNotebook}; :param `page`: an instance of L{PageInfo}, representing a page in the notebook. """ if not page._hasFocus: return tabPos = wx.Point(*page.GetPosition()) if pageContainer.GetParent().GetAGWWindowStyleFlag() & FNB_VC8: vc8ShapeLen = self.CalcTabHeight(pageContainer) - VERTICAL_BORDER_PADDING - 2 tabPos.x += vc8ShapeLen rect = wx.RectPS(tabPos, page.GetSize()) rect = wx.Rect(rect.x+2, rect.y+2, rect.width-4, rect.height-8) if wx.Platform == '__WXMAC__': rect.SetWidth(rect.GetWidth() + 1) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.SetPen(self._focusPen) dc.DrawRoundedRectangleRect(rect, 2) def DrawDragHint(self, pc, tabIdx): """ Draws tab drag hint, the default implementation is to do nothing. You can override this function to provide a nice feedback to user. :param `pc`: an instance of L{FlatNotebook}; :param `tabIdx`: the index of the tab we are dragging. :note: To show your own custom drag and drop UI feedback, you must override this method in your derived class. """ pass def NumberTabsCanFit(self, pageContainer, fr=-1): """ Calculates the number of tabs that can fit on the available space on screen. :param `pageContainer`: an instance of L{FlatNotebook}; :param `fr`: the current first visible tab. """ pc = pageContainer rect = pc.GetClientRect() clientWidth = rect.width vTabInfo = [] tabHeight = self.CalcTabHeight(pageContainer) # The drawing starts from posx posx = pc._pParent.GetPadding() if fr < 0: fr = pc._nFrom for i in xrange(fr, len(pc._pagesInfoVec)): tabWidth = self.CalcTabWidth(pageContainer, i, tabHeight) if posx + tabWidth + self.GetButtonsAreaLength(pc) >= clientWidth: break; # Add a result to the returned vector tabRect = wx.Rect(posx, VERTICAL_BORDER_PADDING, tabWidth , tabHeight) vTabInfo.append(tabRect) # Advance posx posx += tabWidth + FNB_HEIGHT_SPACER return vTabInfo # ---------------------------------------------------------------------------- # # Class FNBRendererMgr # A manager that handles all the renderers defined below and calls the # appropriate one when drawing is needed # ---------------------------------------------------------------------------- # class FNBRendererMgr(object): """ This class represents a manager that handles all the 6 renderers defined and calls the appropriate one when drawing is needed. """ def __init__(self): """ Default class constructor. """ # register renderers self._renderers = {} self._renderers.update({-1: FNBRendererDefault()}) self._renderers.update({FNB_VC71: FNBRendererVC71()}) self._renderers.update({FNB_FANCY_TABS: FNBRendererFancy()}) self._renderers.update({FNB_VC8: FNBRendererVC8()}) self._renderers.update({FNB_RIBBON_TABS: FNBRendererRibbonTabs()}) self._renderers.update({FNB_FF2: FNBRendererFirefox2()}) def GetRenderer(self, style): """ Returns the current renderer based on the style selected. :param `style`: represents one of the 6 implemented styles for L{FlatNotebook}, namely one of these bits: ===================== ========= ====================== Tabs style Hex Value Description ===================== ========= ====================== ``FNB_VC71`` 0x1 Use Visual Studio 2003 (VC7.1) style for tabs ``FNB_FANCY_TABS`` 0x2 Use fancy style - square tabs filled with gradient colouring ``FNB_VC8`` 0x100 Use Visual Studio 2005 (VC8) style for tabs ``FNB_FF2`` 0x20000 Use Firefox 2 style for tabs ``FNB_RIBBON_TABS`` 0x80000 Use the Ribbon Tabs style to render the tabs ===================== ========= ====================== """ if style & FNB_VC71: return self._renderers[FNB_VC71] if style & FNB_FANCY_TABS: return self._renderers[FNB_FANCY_TABS] if style & FNB_VC8: return self._renderers[FNB_VC8] if style & FNB_FF2: return self._renderers[FNB_FF2] if style & FNB_RIBBON_TABS: return self._renderers[FNB_RIBBON_TABS] # the default is to return the default renderer return self._renderers[-1] #------------------------------------------ # Default renderer #------------------------------------------ class FNBRendererDefault(FNBRenderer): """ This class handles the drawing of tabs using the standard renderer. """ def __init__(self): """ Default class constructor. """ FNBRenderer.__init__(self) def DrawTab(self, pageContainer, dc, posx, tabIdx, tabWidth, tabHeight, btnStatus): """ Draws a tab using the `Standard` style. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`; :param `posx`: the x position of the tab; :param `tabIdx`: the index of the tab; :param `tabWidth`: the tab's width; :param `tabHeight`: the tab's height; :param `btnStatus`: the status of the 'X' button inside this tab. """ # Default style borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)) pc = pageContainer tabPoints = [wx.Point() for ii in xrange(7)] tabPoints[0].x = posx tabPoints[0].y = (pc.HasAGWFlag(FNB_BOTTOM) and [2] or [tabHeight - 2])[0] tabPoints[1].x = int(posx+(tabHeight-2)*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi)) tabPoints[1].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - (VERTICAL_BORDER_PADDING+2)] or [(VERTICAL_BORDER_PADDING+2)])[0] tabPoints[2].x = tabPoints[1].x+2 tabPoints[2].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0] tabPoints[3].x = int(posx+tabWidth-(tabHeight-2)*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi))-2 tabPoints[3].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0] tabPoints[4].x = tabPoints[3].x+2 tabPoints[4].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - (VERTICAL_BORDER_PADDING+2)] or [(VERTICAL_BORDER_PADDING+2)])[0] tabPoints[5].x = int(tabPoints[4].x+(tabHeight-2)*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi)) tabPoints[5].y = (pc.HasAGWFlag(FNB_BOTTOM) and [2] or [tabHeight - 2])[0] tabPoints[6].x = tabPoints[0].x tabPoints[6].y = tabPoints[0].y if tabIdx == pc.GetSelection(): # Draw the tab as rounded rectangle dc.DrawPolygon(tabPoints) else: if tabIdx != pc.GetSelection() - 1: # Draw a vertical line to the right of the text pt1x = tabPoints[5].x pt1y = (pc.HasAGWFlag(FNB_BOTTOM) and [4] or [tabHeight - 6])[0] pt2x = tabPoints[5].x pt2y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - 4] or [4])[0] dc.DrawLine(pt1x, pt1y, pt2x, pt2y) if tabIdx == pc.GetSelection(): savePen = dc.GetPen() whitePen = wx.Pen(wx.WHITE) whitePen.SetWidth(1) dc.SetPen(whitePen) secPt = wx.Point(tabPoints[5].x + 1, tabPoints[5].y) dc.DrawLine(tabPoints[0].x, tabPoints[0].y, secPt.x, secPt.y) # Restore the pen dc.SetPen(savePen) # ----------------------------------- # Text and image drawing # ----------------------------------- # Text drawing offset from the left border of the # rectangle # The width of the images are 16 pixels padding = pc.GetParent().GetPadding() shapePoints = int(tabHeight*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi)) hasImage = pc._pagesInfoVec[tabIdx].GetImageIndex() != -1 imageYCoord = (pc.HasAGWFlag(FNB_BOTTOM) and [6] or [8])[0] if hasImage: textOffset = 2*pc._pParent._nPadding + 16 + shapePoints/2 else: textOffset = pc._pParent._nPadding + shapePoints/2 textOffset += 2 if tabIdx != pc.GetSelection(): # Set the text background to be like the vertical lines dc.SetTextForeground(pc._pParent.GetNonActiveTabTextColour()) if hasImage: imageXOffset = textOffset - 16 - padding pc._ImageList.Draw(pc._pagesInfoVec[tabIdx].GetImageIndex(), dc, posx + imageXOffset, imageYCoord, wx.IMAGELIST_DRAW_TRANSPARENT, True) pageTextColour = pc._pParent.GetPageTextColour(tabIdx) if pageTextColour is not None: dc.SetTextForeground(pageTextColour) dc.DrawText(pc.GetPageText(tabIdx), posx + textOffset, imageYCoord) # draw 'x' on tab (if enabled) if pc.HasAGWFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection(): textWidth, textHeight = dc.GetTextExtent(pc.GetPageText(tabIdx)) tabCloseButtonXCoord = posx + textOffset + textWidth + 1 # take a bitmap from the position of the 'x' button (the x on tab button) # this bitmap will be used later to delete old buttons tabCloseButtonYCoord = imageYCoord x_rect = wx.Rect(tabCloseButtonXCoord, tabCloseButtonYCoord, 16, 16) # Draw the tab self.DrawTabX(pc, dc, x_rect, tabIdx, btnStatus) #------------------------------------------ # Firefox2 renderer #------------------------------------------ class FNBRendererFirefox2(FNBRenderer): """ This class handles the drawing of tabs using the `Firefox 2` renderer. """ def __init__(self): """ Default class constructor. """ FNBRenderer.__init__(self) def DrawTab(self, pageContainer, dc, posx, tabIdx, tabWidth, tabHeight, btnStatus): """ Draws a tab using the `Firefox 2` style. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`; :param `posx`: the x position of the tab; :param `tabIdx`: the index of the tab; :param `tabWidth`: the tab's width; :param `tabHeight`: the tab's height; :param `btnStatus`: the status of the 'X' button inside this tab. """ borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)) pc = pageContainer tabPoints = [wx.Point() for indx in xrange(7)] tabPoints[0].x = posx + 2 tabPoints[0].y = (pc.HasAGWFlag(FNB_BOTTOM) and [2] or [tabHeight - 2])[0] tabPoints[1].x = tabPoints[0].x tabPoints[1].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - (VERTICAL_BORDER_PADDING+2)] or [(VERTICAL_BORDER_PADDING+2)])[0] tabPoints[2].x = tabPoints[1].x+2 tabPoints[2].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0] tabPoints[3].x = posx + tabWidth - 2 tabPoints[3].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0] tabPoints[4].x = tabPoints[3].x + 2 tabPoints[4].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - (VERTICAL_BORDER_PADDING+2)] or [(VERTICAL_BORDER_PADDING+2)])[0] tabPoints[5].x = tabPoints[4].x tabPoints[5].y = (pc.HasAGWFlag(FNB_BOTTOM) and [2] or [tabHeight - 2])[0] tabPoints[6].x = tabPoints[0].x tabPoints[6].y = tabPoints[0].y #------------------------------------ # Paint the tab with gradient #------------------------------------ rr = wx.RectPP(tabPoints[2], tabPoints[5]) DrawButton(dc, rr, pc.GetSelection() == tabIdx , not pc.HasAGWFlag(FNB_BOTTOM)) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.SetPen(borderPen) # Draw the tab as rounded rectangle dc.DrawPolygon(tabPoints) # ----------------------------------- # Text and image drawing # ----------------------------------- # The width of the images are 16 pixels padding = pc.GetParent().GetPadding() shapePoints = int(tabHeight*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi)) hasImage = pc._pagesInfoVec[tabIdx].GetImageIndex() != -1 imageYCoord = (pc.HasAGWFlag(FNB_BOTTOM) and [6] or [8])[0] if hasImage: textOffset = 2*padding + 16 + shapePoints/2 else: textOffset = padding + shapePoints/2 textOffset += 2 if tabIdx != pc.GetSelection(): # Set the text background to be like the vertical lines dc.SetTextForeground(pc._pParent.GetNonActiveTabTextColour()) if hasImage: imageXOffset = textOffset - 16 - padding pc._ImageList.Draw(pc._pagesInfoVec[tabIdx].GetImageIndex(), dc, posx + imageXOffset, imageYCoord, wx.IMAGELIST_DRAW_TRANSPARENT, True) pageTextColour = pc._pParent.GetPageTextColour(tabIdx) if pageTextColour is not None: dc.SetTextForeground(pageTextColour) dc.DrawText(pc.GetPageText(tabIdx), posx + textOffset, imageYCoord) # draw 'x' on tab (if enabled) if pc.HasAGWFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection(): textWidth, textHeight = dc.GetTextExtent(pc.GetPageText(tabIdx)) tabCloseButtonXCoord = posx + textOffset + textWidth + 1 # take a bitmap from the position of the 'x' button (the x on tab button) # this bitmap will be used later to delete old buttons tabCloseButtonYCoord = imageYCoord x_rect = wx.Rect(tabCloseButtonXCoord, tabCloseButtonYCoord, 16, 16) # Draw the tab self.DrawTabX(pc, dc, x_rect, tabIdx, btnStatus) #------------------------------------------------------------------ # Visual studio 7.1 #------------------------------------------------------------------ class FNBRendererVC71(FNBRenderer): """ This class handles the drawing of tabs using the `VC71` renderer. """ def __init__(self): """ Default class constructor. """ FNBRenderer.__init__(self) def DrawTab(self, pageContainer, dc, posx, tabIdx, tabWidth, tabHeight, btnStatus): """ Draws a tab using the `VC71` style. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`; :param `posx`: the x position of the tab; :param `tabIdx`: the index of the tab; :param `tabWidth`: the tab's width; :param `tabHeight`: the tab's height; :param `btnStatus`: the status of the 'X' button inside this tab. """ # Visual studio 7.1 style borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)) pc = pageContainer dc.SetPen((tabIdx == pc.GetSelection() and [wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))] or [borderPen])[0]) dc.SetBrush((tabIdx == pc.GetSelection() and [wx.Brush(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))] or [wx.Brush(wx.Colour(247, 243, 233))])[0]) if tabIdx == pc.GetSelection(): posy = (pc.HasAGWFlag(FNB_BOTTOM) and [0] or [VERTICAL_BORDER_PADDING])[0] tabH = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - 5] or [tabHeight - 3])[0] dc.DrawRectangle(posx, posy, tabWidth, tabH) # Draw a black line on the left side of the # rectangle dc.SetPen(wx.BLACK_PEN) blackLineY1 = VERTICAL_BORDER_PADDING blackLineY2 = tabH dc.DrawLine(posx + tabWidth, blackLineY1, posx + tabWidth, blackLineY2) # To give the tab more 3D look we do the following # Incase the tab is on top, # Draw a thik white line on topof the rectangle # Otherwise, draw a thin (1 pixel) black line at the bottom pen = wx.Pen((pc.HasAGWFlag(FNB_BOTTOM) and [wx.BLACK] or [wx.WHITE])[0]) dc.SetPen(pen) whiteLinePosY = (pc.HasAGWFlag(FNB_BOTTOM) and [blackLineY2] or [VERTICAL_BORDER_PADDING ])[0] dc.DrawLine(posx , whiteLinePosY, posx + tabWidth + 1, whiteLinePosY) # Draw a white vertical line to the left of the tab dc.SetPen(wx.WHITE_PEN) if not pc.HasAGWFlag(FNB_BOTTOM): blackLineY2 += 1 dc.DrawLine(posx, blackLineY1, posx, blackLineY2) else: # We dont draw a rectangle for non selected tabs, but only # vertical line on the left blackLineY1 = (pc.HasAGWFlag(FNB_BOTTOM) and [VERTICAL_BORDER_PADDING + 2] or [VERTICAL_BORDER_PADDING + 1])[0] blackLineY2 = pc.GetSize().y - 5 dc.DrawLine(posx + tabWidth, blackLineY1, posx + tabWidth, blackLineY2) # ----------------------------------- # Text and image drawing # ----------------------------------- # Text drawing offset from the left border of the # rectangle # The width of the images are 16 pixels padding = pc.GetParent().GetPadding() hasImage = pc._pagesInfoVec[tabIdx].GetImageIndex() != -1 imageYCoord = (pc.HasAGWFlag(FNB_BOTTOM) and [5] or [8])[0] if hasImage: textOffset = 2*pc._pParent._nPadding + 16 else: textOffset = pc._pParent._nPadding if tabIdx != pc.GetSelection(): # Set the text background to be like the vertical lines dc.SetTextForeground(pc._pParent.GetNonActiveTabTextColour()) if hasImage: imageXOffset = textOffset - 16 - padding pc._ImageList.Draw(pc._pagesInfoVec[tabIdx].GetImageIndex(), dc, posx + imageXOffset, imageYCoord, wx.IMAGELIST_DRAW_TRANSPARENT, True) pageTextColour = pc._pParent.GetPageTextColour(tabIdx) if pageTextColour is not None: dc.SetTextForeground(pageTextColour) dc.DrawText(pc.GetPageText(tabIdx), posx + textOffset, imageYCoord) # draw 'x' on tab (if enabled) if pc.HasAGWFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection(): textWidth, textHeight = dc.GetTextExtent(pc.GetPageText(tabIdx)) tabCloseButtonXCoord = posx + textOffset + textWidth + 1 # take a bitmap from the position of the 'x' button (the x on tab button) # this bitmap will be used later to delete old buttons tabCloseButtonYCoord = imageYCoord x_rect = wx.Rect(tabCloseButtonXCoord, tabCloseButtonYCoord, 16, 16) # Draw the tab self.DrawTabX(pc, dc, x_rect, tabIdx, btnStatus) #------------------------------------------------------------------ # Fancy style #------------------------------------------------------------------ class FNBRendererFancy(FNBRenderer): """ This class handles the drawing of tabs using the `Fancy` renderer. """ def __init__(self): """ Default class constructor. """ FNBRenderer.__init__(self) def DrawTab(self, pageContainer, dc, posx, tabIdx, tabWidth, tabHeight, btnStatus): """ Draws a tab using the `Fancy` style, similar to the `VC71` one but with gradients. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`; :param `posx`: the x position of the tab; :param `tabIdx`: the index of the tab; :param `tabWidth`: the tab's width; :param `tabHeight`: the tab's height; :param `btnStatus`: the status of the 'X' button inside this tab. """ # Fancy tabs - like with VC71 but with the following differences: # - The Selected tab is coloured with gradient colour borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)) pc = pageContainer pen = (tabIdx == pc.GetSelection() and [wx.Pen(pc._pParent.GetBorderColour())] or [wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))])[0] if tabIdx == pc.GetSelection(): posy = (pc.HasAGWFlag(FNB_BOTTOM) and [2] or [VERTICAL_BORDER_PADDING])[0] th = tabHeight - 5 rect = wx.Rect(posx, posy, tabWidth, th) col2 = (pc.HasAGWFlag(FNB_BOTTOM) and [pc._pParent.GetGradientColourTo()] or [pc._pParent.GetGradientColourFrom()])[0] col1 = (pc.HasAGWFlag(FNB_BOTTOM) and [pc._pParent.GetGradientColourFrom()] or [pc._pParent.GetGradientColourTo()])[0] PaintStraightGradientBox(dc, rect, col1, col2) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.SetPen(pen) dc.DrawRectangleRect(rect) # erase the bottom/top line of the rectangle dc.SetPen(wx.Pen(pc._pParent.GetGradientColourFrom())) if pc.HasAGWFlag(FNB_BOTTOM): dc.DrawLine(rect.x, 2, rect.x + rect.width, 2) else: dc.DrawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width, rect.y + rect.height - 1) else: # We dont draw a rectangle for non selected tabs, but only # vertical line on the left dc.SetPen(borderPen) dc.DrawLine(posx + tabWidth, VERTICAL_BORDER_PADDING + 3, posx + tabWidth, tabHeight - 4) # ----------------------------------- # Text and image drawing # ----------------------------------- # Text drawing offset from the left border of the # rectangle # The width of the images are 16 pixels padding = pc.GetParent().GetPadding() hasImage = pc._pagesInfoVec[tabIdx].GetImageIndex() != -1 imageYCoord = (pc.HasAGWFlag(FNB_BOTTOM) and [6] or [8])[0] if hasImage: textOffset = 2*pc._pParent._nPadding + 16 else: textOffset = pc._pParent._nPadding textOffset += 2 if tabIdx != pc.GetSelection(): # Set the text background to be like the vertical lines dc.SetTextForeground(pc._pParent.GetNonActiveTabTextColour()) if hasImage: imageXOffset = textOffset - 16 - padding pc._ImageList.Draw(pc._pagesInfoVec[tabIdx].GetImageIndex(), dc, posx + imageXOffset, imageYCoord, wx.IMAGELIST_DRAW_TRANSPARENT, True) pageTextColour = pc._pParent.GetPageTextColour(tabIdx) if pageTextColour is not None: dc.SetTextForeground(pageTextColour) dc.DrawText(pc.GetPageText(tabIdx), posx + textOffset, imageYCoord) # draw 'x' on tab (if enabled) if pc.HasAGWFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection(): textWidth, textHeight = dc.GetTextExtent(pc.GetPageText(tabIdx)) tabCloseButtonXCoord = posx + textOffset + textWidth + 1 # take a bitmap from the position of the 'x' button (the x on tab button) # this bitmap will be used later to delete old buttons tabCloseButtonYCoord = imageYCoord x_rect = wx.Rect(tabCloseButtonXCoord, tabCloseButtonYCoord, 16, 16) # Draw the tab self.DrawTabX(pc, dc, x_rect, tabIdx, btnStatus) #------------------------------------------------------------------ # Visual studio 2005 (VS8) #------------------------------------------------------------------ class FNBRendererVC8(FNBRenderer): """ This class handles the drawing of tabs using the `VC8` renderer. """ def __init__(self): """ Default class constructor. """ FNBRenderer.__init__(self) self._first = True self._factor = 1 def DrawTabs(self, pageContainer, dc): """ Draws all the tabs using `VC8` style. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`. """ pc = pageContainer if "__WXMAC__" in wx.PlatformInfo: # Works well on MSW & GTK, however this lines should be skipped on MAC if not pc._pagesInfoVec or pc._nFrom >= len(pc._pagesInfoVec): pc.Hide() return # Get the text hight tabHeight = self.CalcTabHeight(pageContainer) # Set the font for measuring the tab height normalFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) boldFont.SetWeight(wx.FONTWEIGHT_BOLD) # Calculate the number of rows required for drawing the tabs rect = pc.GetClientRect() # Set the maximum client size pc.SetSizeHints(self.GetButtonsAreaLength(pc), tabHeight) borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)) # Create brushes backBrush = wx.Brush(pc._tabAreaColour) noselBrush = wx.Brush(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNFACE)) selBrush = wx.Brush(pc._activeTabColour) size = pc.GetSize() # Background dc.SetTextBackground(pc.GetBackgroundColour()) dc.SetTextForeground(pc._activeTextColour) # If border style is set, set the pen to be border pen if pc.HasAGWFlag(FNB_TABS_BORDER_SIMPLE): dc.SetPen(borderPen) else: dc.SetPen(wx.TRANSPARENT_PEN) lightFactor = (pc.HasAGWFlag(FNB_BACKGROUND_GRADIENT) and [70] or [0])[0] # For VC8 style, we colour the tab area in gradient colouring lightcolour = LightColour(pc._tabAreaColour, lightFactor) PaintStraightGradientBox(dc, pc.GetClientRect(), pc._tabAreaColour, lightcolour) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.DrawRectangle(0, 0, size.x, size.y) # We always draw the bottom/upper line of the tabs # regradless the style dc.SetPen(borderPen) self.DrawTabsLine(pc, dc) # Restore the pen dc.SetPen(borderPen) # Draw labels dc.SetFont(boldFont) # Update all the tabs from 0 to 'pc.self._nFrom' to be non visible for i in xrange(pc._nFrom): pc._pagesInfoVec[i].SetPosition(wx.Point(-1, -1)) pc._pagesInfoVec[i].GetRegion().Clear() # Draw the visible tabs, in VC8 style, we draw them from right to left vTabsInfo = self.NumberTabsCanFit(pc) activeTabPosx = 0 activeTabWidth = 0 activeTabHeight = 0 for cur in xrange(len(vTabsInfo)-1, -1, -1): # 'i' points to the index of the currently drawn tab # in pc.GetPageInfoVector() vector i = pc._nFrom + cur dc.SetPen(borderPen) dc.SetBrush((i==pc.GetSelection() and [selBrush] or [noselBrush])[0]) # Now set the font to the correct font dc.SetFont((i==pc.GetSelection() and [boldFont] or [normalFont])[0]) # Add the padding to the tab width # Tab width: # +-----------------------------------------------------------+ # | PADDING | IMG | IMG_PADDING | TEXT | PADDING | x |PADDING | # +-----------------------------------------------------------+ tabWidth = self.CalcTabWidth(pageContainer, i, tabHeight) posx = vTabsInfo[cur].x # By default we clean the tab region # incase we use the VC8 style which requires # the region, it will be filled by the function # drawVc8Tab pc._pagesInfoVec[i].GetRegion().Clear() # Clean the 'x' buttn on the tab # 'Clean' rectanlge is a rectangle with width or height # with values lower than or equal to 0 pc._pagesInfoVec[i].GetXRect().SetSize(wx.Size(-1, -1)) # Draw the tab # Incase we are drawing the active tab # we need to redraw so it will appear on top # of all other tabs # when using the vc8 style, we keep the position of the active tab so we will draw it again later if i == pc.GetSelection() and pc.HasAGWFlag(FNB_VC8): activeTabPosx = posx activeTabWidth = tabWidth activeTabHeight = tabHeight else: self.DrawTab(pc, dc, posx, i, tabWidth, tabHeight, pc._nTabXButtonStatus) # Restore the text forground dc.SetTextForeground(pc._activeTextColour) # Update the tab position & size pc._pagesInfoVec[i].SetPosition(wx.Point(posx, VERTICAL_BORDER_PADDING)) pc._pagesInfoVec[i].SetSize(wx.Size(tabWidth, tabHeight)) # Incase we are in VC8 style, redraw the active tab (incase it is visible) if pc.GetSelection() >= pc._nFrom and pc.GetSelection() < pc._nFrom + len(vTabsInfo): self.DrawTab(pc, dc, activeTabPosx, pc.GetSelection(), activeTabWidth, activeTabHeight, pc._nTabXButtonStatus) # Update all tabs that can not fit into the screen as non-visible for xx in xrange(pc._nFrom + len(vTabsInfo), len(pc._pagesInfoVec)): pc._pagesInfoVec[xx].SetPosition(wx.Point(-1, -1)) pc._pagesInfoVec[xx].GetRegion().Clear() # Draw the left/right/close buttons # Left arrow self.DrawLeftArrow(pc, dc) self.DrawRightArrow(pc, dc) self.DrawX(pc, dc) self.DrawDropDownArrow(pc, dc) def DrawTab(self, pageContainer, dc, posx, tabIdx, tabWidth, tabHeight, btnStatus): """ Draws a tab using the `VC8` style. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`; :param `posx`: the x position of the tab; :param `tabIdx`: the index of the tab; :param `tabWidth`: the tab's width; :param `tabHeight`: the tab's height; :param `btnStatus`: the status of the 'X' button inside this tab. """ pc = pageContainer borderPen = wx.Pen(pc._pParent.GetBorderColour()) tabPoints = [wx.Point() for ii in xrange(8)] # If we draw the first tab or the active tab, # we draw a full tab, else we draw a truncated tab # # X(2) X(3) # X(1) X(4) # # X(5) # # X(0),(7) X(6) # # tabPoints[0].x = (pc.HasAGWFlag(FNB_BOTTOM) and [posx] or [posx+self._factor])[0] tabPoints[0].y = (pc.HasAGWFlag(FNB_BOTTOM) and [2] or [tabHeight - 3])[0] tabPoints[1].x = tabPoints[0].x + tabHeight - VERTICAL_BORDER_PADDING - 3 - self._factor tabPoints[1].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - (VERTICAL_BORDER_PADDING+2)] or [(VERTICAL_BORDER_PADDING+2)])[0] tabPoints[2].x = tabPoints[1].x + 4 tabPoints[2].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0] tabPoints[3].x = tabPoints[2].x + tabWidth - 2 tabPoints[3].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabHeight - VERTICAL_BORDER_PADDING] or [VERTICAL_BORDER_PADDING])[0] tabPoints[4].x = tabPoints[3].x + 1 tabPoints[4].y = (pc.HasAGWFlag(FNB_BOTTOM) and [tabPoints[3].y - 1] or [tabPoints[3].y + 1])[0] tabPoints[5].x = tabPoints[4].x + 1 tabPoints[5].y = (pc.HasAGWFlag(FNB_BOTTOM) and [(tabPoints[4].y - 1)] or [tabPoints[4].y + 1])[0] tabPoints[6].x = tabPoints[2].x + tabWidth tabPoints[6].y = tabPoints[0].y tabPoints[7].x = tabPoints[0].x tabPoints[7].y = tabPoints[0].y pc._pagesInfoVec[tabIdx].SetRegion(tabPoints) # Draw the polygon br = dc.GetBrush() dc.SetBrush(wx.Brush((tabIdx == pc.GetSelection() and [pc._activeTabColour] or [pc._colourTo])[0])) dc.SetPen(wx.Pen((tabIdx == pc.GetSelection() and [wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)] or [pc._colourBorder])[0])) dc.DrawPolygon(tabPoints) # Restore the brush dc.SetBrush(br) rect = pc.GetClientRect() if tabIdx != pc.GetSelection() and not pc.HasAGWFlag(FNB_BOTTOM): # Top default tabs dc.SetPen(wx.Pen(pc._pParent.GetBorderColour())) lineY = rect.height curPen = dc.GetPen() curPen.SetWidth(1) dc.SetPen(curPen) dc.DrawLine(posx, lineY, posx+rect.width, lineY) # Incase we are drawing the selected tab, we draw the border of it as well # but without the bottom (upper line incase of wxBOTTOM) if tabIdx == pc.GetSelection(): borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)) dc.SetPen(borderPen) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.DrawPolygon(tabPoints) # Delete the bottom line (or the upper one, incase we use wxBOTTOM) dc.SetPen(wx.WHITE_PEN) dc.DrawLine(tabPoints[0].x, tabPoints[0].y, tabPoints[6].x, tabPoints[6].y) self.FillVC8GradientColour(pc, dc, tabPoints, tabIdx == pc.GetSelection(), tabIdx) # Draw a thin line to the right of the non-selected tab if tabIdx != pc.GetSelection(): dc.SetPen(wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))) dc.DrawLine(tabPoints[4].x-1, tabPoints[4].y, tabPoints[5].x-1, tabPoints[5].y) dc.DrawLine(tabPoints[5].x-1, tabPoints[5].y, tabPoints[6].x-1, tabPoints[6].y) # Text drawing offset from the left border of the # rectangle # The width of the images are 16 pixels vc8ShapeLen = tabHeight - VERTICAL_BORDER_PADDING - 2 if pc.TabHasImage(tabIdx): textOffset = 2*pc._pParent.GetPadding() + 16 + vc8ShapeLen else: textOffset = pc._pParent.GetPadding() + vc8ShapeLen # Draw the image for the tab if any imageYCoord = (pc.HasAGWFlag(FNB_BOTTOM) and [6] or [8])[0] if pc.TabHasImage(tabIdx): imageXOffset = textOffset - 16 - pc._pParent.GetPadding() pc._ImageList.Draw(pc._pagesInfoVec[tabIdx].GetImageIndex(), dc, posx + imageXOffset, imageYCoord, wx.IMAGELIST_DRAW_TRANSPARENT, True) boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) # if selected tab, draw text in bold if tabIdx == pc.GetSelection(): boldFont.SetWeight(wx.FONTWEIGHT_BOLD) dc.SetFont(boldFont) pageTextColour = pc._pParent.GetPageTextColour(tabIdx) if pageTextColour is not None: dc.SetTextForeground(pageTextColour) dc.DrawText(pc.GetPageText(tabIdx), posx + textOffset, imageYCoord) # draw 'x' on tab (if enabled) if pc.HasAGWFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection(): textWidth, textHeight = dc.GetTextExtent(pc.GetPageText(tabIdx)) tabCloseButtonXCoord = posx + textOffset + textWidth + 1 # take a bitmap from the position of the 'x' button (the x on tab button) # this bitmap will be used later to delete old buttons tabCloseButtonYCoord = imageYCoord x_rect = wx.Rect(tabCloseButtonXCoord, tabCloseButtonYCoord, 16, 16) # Draw the tab self.DrawTabX(pc, dc, x_rect, tabIdx, btnStatus) self.DrawFocusRectangle(dc, pc, pc._pagesInfoVec[tabIdx]) def FillVC8GradientColour(self, pageContainer, dc, tabPoints, bSelectedTab, tabIdx): """ Fills a tab with a gradient shading. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`; :param `tabPoints`: a Python list of `wx.Points` representing the tab outline; :param `bSelectedTab`: ``True`` if the tab is selected, ``False`` otherwise; :param `tabIdx`: the index of the tab; """ # calculate gradient coefficients pc = pageContainer if self._first: self._first = False pc._colourTo = LightColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE), 0) pc._colourFrom = LightColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE), 60) col2 = pc._pParent.GetGradientColourTo() col1 = pc._pParent.GetGradientColourFrom() # If colourful tabs style is set, override the tab colour if pc.HasAGWFlag(FNB_COLOURFUL_TABS): if not pc._pagesInfoVec[tabIdx].GetColour(): # First time, generate colour, and keep it in the vector tabColour = RandomColour() pc._pagesInfoVec[tabIdx].SetColour(tabColour) if pc.HasAGWFlag(FNB_BOTTOM): col2 = LightColour(pc._pagesInfoVec[tabIdx].GetColour(), 50) col1 = LightColour(pc._pagesInfoVec[tabIdx].GetColour(), 80) else: col1 = LightColour(pc._pagesInfoVec[tabIdx].GetColour(), 50) col2 = LightColour(pc._pagesInfoVec[tabIdx].GetColour(), 80) size = abs(tabPoints[2].y - tabPoints[0].y) - 1 rf, gf, bf = 0, 0, 0 rstep = float(col2.Red() - col1.Red())/float(size) gstep = float(col2.Green() - col1.Green())/float(size) bstep = float(col2.Blue() - col1.Blue())/float(size) y = tabPoints[0].y # If we are drawing the selected tab, we need also to draw a line # from 0.tabPoints[0].x and tabPoints[6].x . end, we achieve this # by drawing the rectangle with transparent brush # the line under the selected tab will be deleted by the drwaing loop if bSelectedTab: self.DrawTabsLine(pc, dc) while 1: if pc.HasAGWFlag(FNB_BOTTOM): if y > tabPoints[0].y + size: break else: if y < tabPoints[0].y - size: break currCol = wx.Colour(col1.Red() + rf, col1.Green() + gf, col1.Blue() + bf) dc.SetPen((bSelectedTab and [wx.Pen(pc._activeTabColour)] or [wx.Pen(currCol)])[0]) startX = self.GetStartX(tabPoints, y, pc.GetParent().GetAGWWindowStyleFlag()) endX = self.GetEndX(tabPoints, y, pc.GetParent().GetAGWWindowStyleFlag()) dc.DrawLine(startX, y, endX, y) # Draw the border using the 'edge' point dc.SetPen(wx.Pen((bSelectedTab and [wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)] or [pc._colourBorder])[0])) dc.DrawPoint(startX, y) dc.DrawPoint(endX, y) # Progress the colour rf += rstep gf += gstep bf += bstep if pc.HasAGWFlag(FNB_BOTTOM): y = y + 1 else: y = y - 1 def GetStartX(self, tabPoints, y, style): """ Returns the `x` start position of a tab. :param `tabPoints`: a Python list of `wx.Points` representing the tab outline; :param `y`: the y start position of the tab; :param `style`: can be ``FNB_BOTTOM`` or the default (tabs at top). """ x1, x2, y1, y2 = 0.0, 0.0, 0.0, 0.0 # We check the 3 points to the left bBottomStyle = (style & FNB_BOTTOM and [True] or [False])[0] match = False if bBottomStyle: for i in xrange(3): if y >= tabPoints[i].y and y < tabPoints[i+1].y: x1 = tabPoints[i].x x2 = tabPoints[i+1].x y1 = tabPoints[i].y y2 = tabPoints[i+1].y match = True break else: for i in xrange(3): if y <= tabPoints[i].y and y > tabPoints[i+1].y: x1 = tabPoints[i].x x2 = tabPoints[i+1].x y1 = tabPoints[i].y y2 = tabPoints[i+1].y match = True break if not match: return tabPoints[2].x # According to the equation y = ax + b => x = (y-b)/a # We know the first 2 points x1, x2, y1, y2 = map(float, (x1, x2, y1, y2)) if abs(x2 - x1) < 1e-6: return x2 else: a = (y2 - y1)/(x2 - x1) b = y1 - ((y2 - y1)/(x2 - x1))*x1 if a == 0: return int(x1) x = (y - b)/a return int(x) def GetEndX(self, tabPoints, y, style): """ Returns the `x` end position of a tab. :param `tabPoints`: a Python list of `wx.Points` representing the tab outline; :param `y`: the y end position of the tab; :param `style`: can be ``FNB_BOTTOM`` or the default (tabs at top). """ x1, x2, y1, y2 = 0.0, 0.0, 0.0, 0.0 # We check the 3 points to the left bBottomStyle = (style & FNB_BOTTOM and [True] or [False])[0] match = False if bBottomStyle: for i in xrange(7, 3, -1): if y >= tabPoints[i].y and y < tabPoints[i-1].y: x1 = tabPoints[i].x x2 = tabPoints[i-1].x y1 = tabPoints[i].y y2 = tabPoints[i-1].y match = True break else: for i in xrange(7, 3, -1): if y <= tabPoints[i].y and y > tabPoints[i-1].y: x1 = tabPoints[i].x x2 = tabPoints[i-1].x y1 = tabPoints[i].y y2 = tabPoints[i-1].y match = True break if not match: return tabPoints[3].x # According to the equation y = ax + b => x = (y-b)/a # We know the first 2 points # Vertical line if x1 == x2: return int(x1) a = (y2 - y1)/(x2 - x1) b = y1 - ((y2 - y1)/(x2 - x1))*x1 if a == 0: return int(x1) x = (y - b)/a return int(x) def NumberTabsCanFit(self, pageContainer, fr=-1): """ Calculates the number of tabs that can fit on the available space on screen. :param `pageContainer`: an instance of L{FlatNotebook}; :param `fr`: the current first visible tab. """ pc = pageContainer rect = pc.GetClientRect() clientWidth = rect.width # Empty results vTabInfo = [] tabHeight = self.CalcTabHeight(pageContainer) # The drawing starts from posx posx = pc._pParent.GetPadding() if fr < 0: fr = pc._nFrom for i in xrange(fr, len(pc._pagesInfoVec)): vc8glitch = tabHeight + FNB_HEIGHT_SPACER tabWidth = self.CalcTabWidth(pageContainer, i, tabHeight) if posx + tabWidth + vc8glitch + self.GetButtonsAreaLength(pc) >= clientWidth: break # Add a result to the returned vector tabRect = wx.Rect(posx, VERTICAL_BORDER_PADDING, tabWidth, tabHeight) vTabInfo.append(tabRect) # Advance posx posx += tabWidth + FNB_HEIGHT_SPACER return vTabInfo #------------------------------------------------------------------ # Ribbon Tabs style #------------------------------------------------------------------ class FNBRendererRibbonTabs(FNBRenderer): """ This class handles the drawing of tabs using the `Ribbon Tabs` renderer. """ def __init__(self): """ Default class constructor. """ FNBRenderer.__init__(self) self._first = True self._factor = 1 # definte this because we don't want to use the bold font def CalcTabWidth(self, pageContainer, tabIdx, tabHeight): """ Calculates the width of the input tab. :param `pageContainer`: an instance of L{FlatNotebook}; :param `tabIdx`: the index of the input tab; :param `tabHeight`: the height of the tab. """ pc = pageContainer dc = wx.MemoryDC() dc.SelectObject(wx.EmptyBitmap(1,1)) font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) if pc.IsDefaultTabs(): shapePoints = int(tabHeight*math.tan(float(pc._pagesInfoVec[tabIdx].GetTabAngle())/180.0*math.pi)) dc.SetFont(font) width, pom = dc.GetTextExtent(pc.GetPageText(tabIdx)) # Set a minimum size to a tab if width < 20: width = 20 tabWidth = 2*pc._pParent.GetPadding() + width # Style to add a small 'x' button on the top right # of the tab if pc.HasAGWFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection(): # The xpm image that contains the 'x' button is 9 pixels spacer = 9 if pc.HasAGWFlag(FNB_VC8): spacer = 4 tabWidth += pc._pParent.GetPadding() + spacer if pc.IsDefaultTabs(): # Default style tabWidth += 2*shapePoints hasImage = pc._ImageList != None and pc._pagesInfoVec[tabIdx].GetImageIndex() != -1 # For VC71 style, we only add the icon size (16 pixels) if hasImage: if not pc.IsDefaultTabs(): tabWidth += 16 + pc._pParent.GetPadding() else: # Default style tabWidth += 16 + pc._pParent.GetPadding() + shapePoints/2 return tabWidth def DrawTab(self, pageContainer, dc, posx, tabIdx, tabWidth, tabHeight, btnStatus): """ Draws a tab using the `Ribbon Tabs` style. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`; :param `posx`: the x position of the tab; :param `tabIdx`: the index of the tab; :param `tabWidth`: the tab's width; :param `tabHeight`: the tab's height; :param `btnStatus`: the status of the 'X' button inside this tab. """ pc = pageContainer gc = wx.GraphicsContext.Create(dc) gc.SetPen(dc.GetPen()) gc.SetBrush(dc.GetBrush()) spacer = math.ceil(float(FNB_HEIGHT_SPACER)/2/2) gc.DrawRoundedRectangle(posx+1,spacer,tabWidth-1,tabHeight-spacer*2,5) if tabIdx == pc.GetSelection(): pass else: if tabIdx != pc.GetSelection() - 1: pass # ----------------------------------- # Text and image drawing # ----------------------------------- # Text drawing offset from the left border of the # rectangle # The width of the images are 16 pixels padding = pc.GetParent().GetPadding() hasImage = pc._pagesInfoVec[tabIdx].GetImageIndex() != -1 imageYCoord = FNB_HEIGHT_SPACER/2 if hasImage: textOffset = 2*pc._pParent._nPadding + 16 else: textOffset = pc._pParent._nPadding textOffset += 2 if tabIdx != pc.GetSelection(): # Set the text background to be like the vertical lines dc.SetTextForeground(pc._pParent.GetNonActiveTabTextColour()) if hasImage: imageXOffset = textOffset - 16 - padding pc._ImageList.Draw(pc._pagesInfoVec[tabIdx].GetImageIndex(), dc, posx + imageXOffset, imageYCoord, wx.IMAGELIST_DRAW_TRANSPARENT, True) pageTextColour = pc._pParent.GetPageTextColour(tabIdx) if pageTextColour is not None: dc.SetTextForeground(pageTextColour) dc.DrawText(pc.GetPageText(tabIdx), posx + textOffset, imageYCoord) # draw 'x' on tab (if enabled) if pc.HasAGWFlag(FNB_X_ON_TAB) and tabIdx == pc.GetSelection(): textWidth, textHeight = dc.GetTextExtent(pc.GetPageText(tabIdx)) tabCloseButtonXCoord = posx + textOffset + textWidth + 1 # take a bitmap from the position of the 'x' button (the x on tab button) # this bitmap will be used later to delete old buttons tabCloseButtonYCoord = imageYCoord x_rect = wx.Rect(tabCloseButtonXCoord, tabCloseButtonYCoord, 16, 16) # Draw the tab self.DrawTabX(pc, dc, x_rect, tabIdx, btnStatus) def DrawTabs(self, pageContainer, dc): """ Actually draws the tabs in L{FlatNotebook}. :param `pageContainer`: an instance of L{FlatNotebook}; :param `dc`: an instance of `wx.DC`. """ pc = pageContainer #style = pc.GetParent().GetWindowStyleFlag() if "__WXMAC__" in wx.PlatformInfo: # Works well on MSW & GTK, however this lines should be skipped on MAC if not pc._pagesInfoVec or pc._nFrom >= len(pc._pagesInfoVec): pc.Hide() return # Get the text height tabHeight = self.CalcTabHeight(pageContainer) # Calculate the number of rows required for drawing the tabs rect = pc.GetClientRect() clientWidth = rect.width # Set the maximum client size pc.SetSizeHints(self.GetButtonsAreaLength(pc), tabHeight) size = pc.GetSize() # Background dc.SetTextBackground(pc.GetBackgroundColour()) dc.SetTextForeground(pc._activeTextColour) borderPen = wx.Pen(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW)) backBrush = wx.Brush(pc._tabAreaColour) # If border style is set, set the pen to be border pen if pc.HasAGWFlag(FNB_TABS_BORDER_SIMPLE): dc.SetPen(borderPen) else: dc.SetPen(wx.Pen(pc._tabAreaColour)) dc.SetBrush(backBrush) dc.DrawRectangle(0, 0, size.x, size.y) # Draw labels font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) dc.SetFont(font) posx = pc._pParent.GetPadding() # Update all the tabs from 0 to 'pc._nFrom' to be non visible for i in xrange(pc._nFrom): pc._pagesInfoVec[i].SetPosition(wx.Point(-1, -1)) count = pc._nFrom #---------------------------------------------------------- # Go over and draw the visible tabs #---------------------------------------------------------- selPen = wx.Pen(colourutils.AdjustColour(pc._tabAreaColour,-20)) noselPen = wx.Pen(pc._tabAreaColour) noselBrush = wx.Brush(pc._tabAreaColour) selBrush = wx.Brush(LightColour(pc._tabAreaColour,60)) for i in xrange(pc._nFrom, len(pc._pagesInfoVec)): # This style highlights the selected tab and the tab the mouse is over highlight = (i==pc.GetSelection()) or pc.IsMouseHovering(i) dc.SetPen((highlight and [selPen] or [noselPen])[0]) dc.SetBrush((highlight and [selBrush] or [noselBrush])[0]) # Add the padding to the tab width # Tab width: # +-----------------------------------------------------------+ # | PADDING | IMG | IMG_PADDING | TEXT | PADDING | x |PADDING | # +-----------------------------------------------------------+ tabWidth = self.CalcTabWidth(pageContainer, i, tabHeight) # Check if we can draw more if posx + tabWidth + self.GetButtonsAreaLength(pc) >= clientWidth: break count = count + 1 # By default we clean the tab region #pc._pagesInfoVec[i].GetRegion().Clear() # Clean the 'x' buttn on the tab. # A 'Clean' rectangle, is a rectangle with width or height # with values lower than or equal to 0 pc._pagesInfoVec[i].GetXRect().SetSize(wx.Size(-1, -1)) # Draw the tab (border, text, image & 'x' on tab) self.DrawTab(pc, dc, posx, i, tabWidth, tabHeight, pc._nTabXButtonStatus) # Restore the text forground dc.SetTextForeground(pc._activeTextColour) # Update the tab position & size posy = (pc.HasAGWFlag(FNB_BOTTOM) and [0] or [VERTICAL_BORDER_PADDING])[0] pc._pagesInfoVec[i].SetPosition(wx.Point(posx, posy)) pc._pagesInfoVec[i].SetSize(wx.Size(tabWidth, tabHeight)) posx += tabWidth # Update all tabs that can not fit into the screen as non-visible for i in xrange(count, len(pc._pagesInfoVec)): pc._pagesInfoVec[i].SetPosition(wx.Point(-1, -1)) pc._pagesInfoVec[i].GetRegion().Clear() # Draw the left/right/close buttons # Left arrow self.DrawLeftArrow(pc, dc) self.DrawRightArrow(pc, dc) self.DrawX(pc, dc) self.DrawDropDownArrow(pc, dc) # ---------------------------------------------------------------------------- # # Class FlatNotebook # ---------------------------------------------------------------------------- # class FlatNotebook(wx.PyPanel): """ The L{FlatNotebook} is a full implementation of the `wx.Notebook`, and designed to be a drop-in replacement for `wx.Notebook`. The API functions are similar so one can expect the function to behave in the same way. """ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, agwStyle=0, name="FlatNotebook"): """ Default class constructor. :param `parent`: the L{FlatNotebook} parent; :param `id`: an identifier for the control: a value of -1 is taken to mean a default; :param `pos`: the control position. A value of (-1, -1) indicates a default position, chosen by either the windowing system or wxPython, depending on platform; :param `size`: the control size. A value of (-1, -1) indicates a default size, chosen by either the windowing system or wxPython, depending on platform; :param `style`: the underlying `wx.PyPanel` window style; :param `agwStyle`: the AGW-specific window style. This can be a combination of the following bits: ================================ =========== ================================================== Window Styles Hex Value Description ================================ =========== ================================================== ``FNB_VC71`` 0x1 Use Visual Studio 2003 (VC7.1) style for tabs. ``FNB_FANCY_TABS`` 0x2 Use fancy style - square tabs filled with gradient colouring. ``FNB_TABS_BORDER_SIMPLE`` 0x4 Draw thin border around the page. ``FNB_NO_X_BUTTON`` 0x8 Do not display the 'X' button. ``FNB_NO_NAV_BUTTONS`` 0x10 Do not display the right/left arrows. ``FNB_MOUSE_MIDDLE_CLOSES_TABS`` 0x20 Use the mouse middle button for cloing tabs. ``FNB_BOTTOM`` 0x40 Place tabs at bottom - the default is to place them at top. ``FNB_NODRAG`` 0x80 Disable dragging of tabs. ``FNB_VC8`` 0x100 Use Visual Studio 2005 (VC8) style for tabs. ``FNB_X_ON_TAB`` 0x200 Place 'X' close button on the active tab. ``FNB_BACKGROUND_GRADIENT`` 0x400 Use gradients to paint the tabs background. ``FNB_COLOURFUL_TABS`` 0x800 Use colourful tabs (VC8 style only). ``FNB_DCLICK_CLOSES_TABS`` 0x1000 Style to close tab using double click. ``FNB_SMART_TABS`` 0x2000 Use `Smart Tabbing`, like ``Alt`` + ``Tab`` on Windows. ``FNB_DROPDOWN_TABS_LIST`` 0x4000 Use a dropdown menu on the left in place of the arrows. ``FNB_ALLOW_FOREIGN_DND`` 0x8000 Allows drag 'n' drop operations between different FlatNotebooks. ``FNB_HIDE_ON_SINGLE_TAB`` 0x10000 Hides the Page Container when there is one or fewer tabs. ``FNB_DEFAULT_STYLE`` 0x10020 FlatNotebook default style. ``FNB_FF2`` 0x20000 Use Firefox 2 style for tabs. ``FNB_NO_TAB_FOCUS`` 0x40000 Does not allow tabs to have focus. ``FNB_RIBBON_TABS`` 0x80000 Use the Ribbon Tabs style. ================================ =========== ================================================== :param `name`: the window name. """ self._bForceSelection = False self._nPadding = 6 self._nFrom = 0 style |= wx.TAB_TRAVERSAL self._pages = None self._windows = [] self._popupWin = None self._naviIcon = None self._agwStyle = agwStyle wx.PyPanel.__init__(self, parent, id, pos, size, style) self._pages = PageContainer(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, style) self.Bind(wx.EVT_NAVIGATION_KEY, self.OnNavigationKey) self.Init() def Init(self): """ Initializes all the class attributes. """ self._pages._colourBorder = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNSHADOW) self._mainSizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self._mainSizer) # The child panels will inherit this bg colour, so leave it at the default value #self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_APPWORKSPACE)) # Set default page height dc = wx.ClientDC(self) if "__WXGTK__" in wx.PlatformInfo: # For GTK it seems that we must do this steps in order # for the tabs will get the proper height on initialization # on MSW, preforming these steps yields wierd results boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) boldFont.SetWeight(wx.FONTWEIGHT_BOLD) dc.SetFont(boldFont) height = dc.GetCharHeight() tabHeight = height + FNB_HEIGHT_SPACER # We use 8 pixels as padding if "__WXGTK__" in wx.PlatformInfo: tabHeight += 6 self._pages.SetSizeHints(-1, tabHeight) # Add the tab container to the sizer self._mainSizer.Insert(0, self._pages, 0, wx.EXPAND) self._mainSizer.Layout() self._pages._nFrom = self._nFrom self._pDropTarget = FNBDropTarget(self) self.SetDropTarget(self._pDropTarget) def DoGetBestSize(self): """ Gets the size which best suits the window: for a control, it would be the minimal size which doesn't truncate the control, for a panel - the same size as it would have after a call to `Fit()`. :note: Overridden from `wx.PyPanel`. """ if not self._windows: # Something is better than nothing... no pages! return wx.Size(20, 20) maxWidth = maxHeight = 0 tabHeight = self.GetPageBestSize().height for win in self._windows: # Loop over all the windows to get their best size width, height = win.GetBestSize() maxWidth, maxHeight = max(maxWidth, width), max(maxHeight, height) return wx.Size(maxWidth, maxHeight+tabHeight) def SetActiveTabTextColour(self, textColour): """ Sets the text colour for the active tab. :param `textColour`: a valid `wx.Colour` object. """ self._pages._activeTextColour = textColour def OnDropTarget(self, x, y, nTabPage, wnd_oldContainer): """ Handles the drop action from a drag and drop operation. :param `x`: the x position of the drop action; :param `y`: the y position of the drop action; :param `nTabPage`: the index of the tab being dropped; :param `wnd_oldContainer`: the L{FlatNotebook} to which the dropped tab previously belonged to. """ return self._pages.OnDropTarget(x, y, nTabPage, wnd_oldContainer) def GetPreviousSelection(self): """ Returns the previous selection. """ return self._pages._iPreviousActivePage def AddPage(self, page, text, select=False, imageId=-1): """ Adds a page to the L{FlatNotebook}. :param `page`: specifies the new page; :param `text`: specifies the text for the new page; :param `select`: specifies whether the page should be selected; :param `imageId`: specifies the optional image index for the new page. :return: ``True`` if successful, ``False`` otherwise. """ # sanity check if not page: return False # reparent the window to us page.Reparent(self) # Add tab bSelected = select or len(self._windows) == 0 if bSelected: bSelected = False # Check for selection and send events oldSelection = self._pages._iActivePage tabIdx = len(self._windows) event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CHANGING, self.GetId()) event.SetSelection(tabIdx) event.SetOldSelection(oldSelection) event.SetEventObject(self) if not self.GetEventHandler().ProcessEvent(event) or event.IsAllowed() or len(self._windows) == 0: bSelected = True curSel = self._pages.GetSelection() if not self._pages.IsShown(): self._pages.Show() self._pages.AddPage(text, bSelected, imageId) self._windows.append(page) self.Freeze() # Check if a new selection was made if bSelected: if curSel >= 0: # Remove the window from the main sizer self._mainSizer.Detach(self._windows[curSel]) self._windows[curSel].Hide() if self.GetAGWWindowStyleFlag() & FNB_BOTTOM: self._mainSizer.Insert(0, page, 1, wx.EXPAND) else: # We leave a space of 1 pixel around the window self._mainSizer.Add(page, 1, wx.EXPAND) # Fire a wxEVT_FLATNOTEBOOK_PAGE_CHANGED event event.SetEventType(wxEVT_FLATNOTEBOOK_PAGE_CHANGED) event.SetOldSelection(oldSelection) self.GetEventHandler().ProcessEvent(event) else: # Hide the page page.Hide() self.Thaw() self._mainSizer.Layout() self.Refresh() return True def SetImageList(self, imageList): """ Sets the image list for the page control. :param `imageList`: an instance of `wx.ImageList`. """ self._pages.SetImageList(imageList) def AssignImageList(self, imageList): """ Assigns the image list for the page control. :param `imageList`: an instance of `wx.ImageList`. """ self._pages.AssignImageList(imageList) def GetImageList(self): """ Returns the associated image list. """ return self._pages.GetImageList() def InsertPage(self, indx, page, text, select=True, imageId=-1): """ Inserts a new page at the specified position. :param `indx`: specifies the position of the new page; :param `page`: specifies the new page; :param `text`: specifies the text for the new page; :param `select`: specifies whether the page should be selected; :param `imageId`: specifies the optional image index for the new page. :return: ``True`` if successful, ``False`` otherwise. """ # sanity check if not page: return False # reparent the window to us page.Reparent(self) if not self._windows: self.AddPage(page, text, select, imageId) return True # Insert tab bSelected = select or not self._windows curSel = self._pages.GetSelection() indx = max(0, min(indx, len(self._windows))) if indx <= len(self._windows): self._windows.insert(indx, page) else: self._windows.append(page) if bSelected: bSelected = False # Check for selection and send events oldSelection = self._pages._iActivePage event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CHANGING, self.GetId()) event.SetSelection(indx) event.SetOldSelection(oldSelection) event.SetEventObject(self) if not self.GetEventHandler().ProcessEvent(event) or event.IsAllowed() or len(self._windows) == 0: bSelected = True self._pages.InsertPage(indx, text, bSelected, imageId) if indx <= curSel: curSel = curSel + 1 self.Freeze() # Check if a new selection was made if bSelected: if curSel >= 0: # Remove the window from the main sizer self._mainSizer.Detach(self._windows[curSel]) self._windows[curSel].Hide() self._pages.SetSelection(indx) # Fire a wxEVT_FLATNOTEBOOK_PAGE_CHANGED event event.SetEventType(wxEVT_FLATNOTEBOOK_PAGE_CHANGED) event.SetOldSelection(oldSelection) self.GetEventHandler().ProcessEvent(event) else: # Hide the page page.Hide() self.Thaw() self._mainSizer.Layout() self.Refresh() return True def SetSelection(self, page): """ Sets the selection for the given page. :param `page`: an integer specifying the new selected page. :note: The call to this function **does not** generate the page changing events. """ if page >= len(self._windows) or not self._windows: return # Support for disabed tabs if not self._pages.GetEnabled(page) and len(self._windows) > 1 and not self._bForceSelection: return curSel = self._pages.GetSelection() # program allows the page change self.Freeze() if curSel >= 0: # Remove the window from the main sizer self._mainSizer.Detach(self._windows[curSel]) self._windows[curSel].Hide() if self.GetAGWWindowStyleFlag() & FNB_BOTTOM: self._mainSizer.Insert(0, self._windows[page], 1, wx.EXPAND) else: # We leave a space of 1 pixel around the window self._mainSizer.Add(self._windows[page], 1, wx.EXPAND) self._windows[page].Show() self.Thaw() self._mainSizer.Layout() if page != self._pages._iActivePage: # there is a real page changing self._pages._iPreviousActivePage = self._pages._iActivePage self._pages._iActivePage = page self._pages.DoSetSelection(page) def DeletePage(self, page): """ Deletes the specified page, and the associated window. :param `page`: an integer specifying the new selected page. :note: The call to this function generates the page changing events. """ if page >= len(self._windows) or page < 0: return # Fire a closing event event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CLOSING, self.GetId()) event.SetSelection(page) event.SetEventObject(self) self.GetEventHandler().ProcessEvent(event) # The event handler allows it? if not event.IsAllowed(): return self.Freeze() # Delete the requested page pageRemoved = self._windows[page] # If the page is the current window, remove it from the sizer # as well if page == self._pages.GetSelection(): self._mainSizer.Detach(pageRemoved) # Remove it from the array as well self._windows.pop(page) # Now we can destroy it in wxWidgets use Destroy instead of delete pageRemoved.Destroy() self.Thaw() self._pages.DoDeletePage(page) self.Refresh() self.Update() # Fire a closed event closedEvent = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CLOSED, self.GetId()) closedEvent.SetSelection(page) closedEvent.SetEventObject(self) self.GetEventHandler().ProcessEvent(closedEvent) def DeleteAllPages(self): """ Deletes all the pages in the L{FlatNotebook}. """ if not self._windows: return False self.Freeze() for page in self._windows: page.Destroy() self._windows = [] self.Thaw() # Clear the container of the tabs as well self._pages.DeleteAllPages() return True def GetCurrentPage(self): """ Returns the currently selected notebook page or ``None`` if none is selected. """ sel = self._pages.GetSelection() if sel < 0 or sel >= len(self._windows): return None return self._windows[sel] def GetPage(self, page): """ Returns the window at the given page position, or ``None``. """ if page >= len(self._windows): return None return self._windows[page] def GetPageIndex(self, win): """ Returns the index at which the window is found. :param `win`: an instance of `wx.Window`. """ try: return self._windows.index(win) except: return -1 def GetSelection(self): """ Returns the currently selected page, or -1 if none was selected. """ return self._pages.GetSelection() def AdvanceSelection(self, forward=True): """ Cycles through the tabs. :param `forward`: if ``True``, the selection is advanced in ascending order (to the right), otherwise the selection is advanced in descending order. :note: The call to this function generates the page changing events. """ self._pages.AdvanceSelection(forward) def GetPageCount(self): """ Returns the number of pages in the L{FlatNotebook} control. """ return self._pages.GetPageCount() def SetNavigatorIcon(self, bmp): """ Set the icon used by the L{TabNavigatorWindow}. :param `bmp`: a valid `wx.Bitmap` object. """ if isinstance(bmp, wx.Bitmap) and bmp.IsOk(): # Make sure image is proper size if bmp.GetSize() != (16, 16): img = bmp.ConvertToImage() img.Rescale(16, 16, wx.IMAGE_QUALITY_HIGH) bmp = wx.BitmapFromImage(img) self._naviIcon = bmp else: raise TypeError("SetNavigatorIcon requires a valid bitmap") def OnNavigationKey(self, event): """ Handles the ``wx.EVT_NAVIGATION_KEY`` event for L{FlatNotebook}. :param `event`: a `wx.NavigationKeyEvent` event to be processed. """ if event.IsWindowChange(): if len(self._windows) == 0: return # change pages if self.HasAGWFlag(FNB_SMART_TABS): if not self._popupWin: self._popupWin = TabNavigatorWindow(self, self._naviIcon) self._popupWin.SetReturnCode(wx.ID_OK) self._popupWin.ShowModal() self._popupWin.Destroy() self._popupWin = None else: # a dialog is already opened self._popupWin.OnNavigationKey(event) return else: # change pages self.AdvanceSelection(event.GetDirection()) else: event.Skip() def GetPageShapeAngle(self, page_index): """ Returns the angle associated to a tab. :param `page_index`: the index of the tab for which we wish to get the shape angle. """ if page_index < 0 or page_index >= len(self._pages._pagesInfoVec): return None, False result = self._pages._pagesInfoVec[page_index].GetTabAngle() return result, True def SetPageShapeAngle(self, page_index, angle): """ Sets the angle associated to a tab. :param `page_index`: the index of the tab for which we wish to get the shape angle; :param `angle`: the new shape angle for the tab (must be less than 15 degrees). """ if page_index < 0 or page_index >= len(self._pages._pagesInfoVec): return if angle > 15: return self._pages._pagesInfoVec[page_index].SetTabAngle(angle) def SetAllPagesShapeAngle(self, angle): """ Sets the angle associated to all the tab. :param `angle`: the new shape angle for the tab (must be less than 15 degrees). """ if angle > 15: return for ii in xrange(len(self._pages._pagesInfoVec)): self._pages._pagesInfoVec[ii].SetTabAngle(angle) self.Refresh() def GetPageBestSize(self): """ Return the page best size. """ return self._pages.GetClientSize() def SetPageText(self, page, text): """ Sets the text for the given page. :param `page`: an integer specifying the page index; :param `text`: the new tab label. """ bVal = self._pages.SetPageText(page, text) self._pages.Refresh() return bVal def SetPadding(self, padding): """ Sets the amount of space around each page's icon and label, in pixels. :param `padding`: the amount of space around each page's icon and label, in pixels. :note: Only the horizontal padding is considered. """ self._nPadding = padding.GetWidth() def GetTabArea(self): """ Returns the associated page. """ return self._pages def GetPadding(self): """ Returns the amount of space around each page's icon and label, in pixels. """ return self._nPadding def SetAGWWindowStyleFlag(self, agwStyle): """ Sets the L{FlatNotebook} window style flags. :param `agwStyle`: the AGW-specific window style. This can be a combination of the following bits: ================================ =========== ================================================== Window Styles Hex Value Description ================================ =========== ================================================== ``FNB_VC71`` 0x1 Use Visual Studio 2003 (VC7.1) style for tabs. ``FNB_FANCY_TABS`` 0x2 Use fancy style - square tabs filled with gradient colouring. ``FNB_TABS_BORDER_SIMPLE`` 0x4 Draw thin border around the page. ``FNB_NO_X_BUTTON`` 0x8 Do not display the 'X' button. ``FNB_NO_NAV_BUTTONS`` 0x10 Do not display the right/left arrows. ``FNB_MOUSE_MIDDLE_CLOSES_TABS`` 0x20 Use the mouse middle button for cloing tabs. ``FNB_BOTTOM`` 0x40 Place tabs at bottom - the default is to place them at top. ``FNB_NODRAG`` 0x80 Disable dragging of tabs. ``FNB_VC8`` 0x100 Use Visual Studio 2005 (VC8) style for tabs. ``FNB_X_ON_TAB`` 0x200 Place 'X' close button on the active tab. ``FNB_BACKGROUND_GRADIENT`` 0x400 Use gradients to paint the tabs background. ``FNB_COLOURFUL_TABS`` 0x800 Use colourful tabs (VC8 style only). ``FNB_DCLICK_CLOSES_TABS`` 0x1000 Style to close tab using double click. ``FNB_SMART_TABS`` 0x2000 Use `Smart Tabbing`, like ``Alt`` + ``Tab`` on Windows. ``FNB_DROPDOWN_TABS_LIST`` 0x4000 Use a dropdown menu on the left in place of the arrows. ``FNB_ALLOW_FOREIGN_DND`` 0x8000 Allows drag 'n' drop operations between different FlatNotebooks. ``FNB_HIDE_ON_SINGLE_TAB`` 0x10000 Hides the Page Container when there is one or fewer tabs. ``FNB_DEFAULT_STYLE`` 0x10020 FlatNotebook default style. ``FNB_FF2`` 0x20000 Use Firefox 2 style for tabs. ``FNB_NO_TAB_FOCUS`` 0x40000 Does not allow tabs to have focus. ``FNB_RIBBON_TABS`` 0x80000 Use the Ribbon Tabs style. ================================ =========== ================================================== """ self._agwStyle = agwStyle renderer = self._pages._mgr.GetRenderer(agwStyle) renderer._tabHeight = None if self._pages: # For changing the tab position (i.e. placing them top/bottom) # refreshing the tab container is not enough self.SetSelection(self._pages._iActivePage) if not self._pages.HasAGWFlag(FNB_HIDE_ON_SINGLE_TAB): #For Redrawing the Tabs once you remove the Hide tyle self._pages._ReShow() def GetAGWWindowStyleFlag(self): """ Returns the L{FlatNotebook} window style. :see: L{SetAGWWindowStyleFlag} for a list of valid window styles. """ return self._agwStyle def HasAGWFlag(self, flag): """ Returns whether a flag is present in the L{FlatNotebook} style. :param `flag`: one of the possible L{FlatNotebook} window styles. :see: L{SetAGWWindowStyleFlag} for a list of possible window style flags. """ agwStyle = self.GetAGWWindowStyleFlag() res = (agwStyle & flag and [True] or [False])[0] return res def RemovePage(self, page): """ Deletes the specified page, without deleting the associated window. :param `page`: an integer specifying the page index. """ if page >= len(self._windows): return False # Fire a closing event event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CLOSING, self.GetId()) event.SetSelection(page) event.SetEventObject(self) self.GetEventHandler().ProcessEvent(event) # The event handler allows it? if not event.IsAllowed(): return False self.Freeze() # Remove the requested page pageRemoved = self._windows[page] # If the page is the current window, remove it from the sizer # as well if page == self._pages.GetSelection(): self._mainSizer.Detach(pageRemoved) # Remove it from the array as well self._windows.pop(page) self.Thaw() self._pages.DoDeletePage(page) return True def SetRightClickMenu(self, menu): """ Sets the popup menu associated to a right click on a tab. :param `menu`: an instance of `wx.Menu`. """ self._pages._pRightClickMenu = menu def GetPageText(self, page): """ Returns the string for the given page. :param `page`: an integer specifying the page index. """ return self._pages.GetPageText(page) def SetGradientColours(self, fr, to, border): """ Sets the gradient colours for the tab. :param `fr`: the first gradient colour, an instance of `wx.Colour`; :param `to`: the second gradient colour, an instance of `wx.Colour`; :param `border`: the border colour, an instance of `wx.Colour`. """ self._pages._colourFrom = fr self._pages._colourTo = to self._pages._colourBorder = border def SetGradientColourFrom(self, fr): """ Sets the starting colour for the gradient. :param `fr`: the first gradient colour, an instance of `wx.Colour`. """ self._pages._colourFrom = fr def SetGradientColourTo(self, to): """ Sets the ending colour for the gradient. :param `to`: the second gradient colour, an instance of `wx.Colour`; """ self._pages._colourTo = to def SetGradientColourBorder(self, border): """ Sets the tab border colour. :param `border`: the border colour, an instance of `wx.Colour`. """ self._pages._colourBorder = border def GetGradientColourFrom(self): """ Gets first gradient colour. """ return self._pages._colourFrom def GetGradientColourTo(self): """ Gets second gradient colour. """ return self._pages._colourTo def GetGradientColourBorder(self): """ Gets the tab border colour. """ return self._pages._colourBorder def GetBorderColour(self): """ Returns the border colour. """ return self._pages._colourBorder def GetActiveTabTextColour(self): """ Get the active tab text colour. """ return self._pages._activeTextColour def SetPageImage(self, page, image): """ Sets the image index for the given page. :param `page`: an integer specifying the page index; :param `image`: an index into the image list which was set with L{SetImageList}. """ self._pages.SetPageImage(page, image) def GetPageImage(self, page): """ Returns the image index for the given page. :param `page`: an integer specifying the page index. """ return self._pages.GetPageImage(page) def GetEnabled(self, page): """ Returns whether a tab is enabled or not. :param `page`: an integer specifying the page index. """ return self._pages.GetEnabled(page) def EnableTab(self, page, enabled=True): """ Enables or disables a tab. :param `page`: an integer specifying the page index; :param `enabled`: ``True`` to enable a tab, ``False`` to disable it. """ if page >= len(self._windows): return self._windows[page].Enable(enabled) self._pages.EnableTab(page, enabled) def GetNonActiveTabTextColour(self): """ Returns the non active tabs text colour. """ return self._pages._nonActiveTextColour def SetNonActiveTabTextColour(self, colour): """ Sets the non active tabs text colour. :param `colour`: a valid instance of `wx.Colour`. """ self._pages._nonActiveTextColour = colour def GetPageTextColour(self, page): """ Returns the tab text colour if it has been set previously, or ``None`` otherwise. :param `page`: an integer specifying the page index. """ return self._pages.GetPageTextColour(page) def SetPageTextColour(self, page, colour): """ Sets the tab text colour individually. :param `page`: an integer specifying the page index; :param `colour`: an instance of `wx.Colour`. You can pass ``None`` or `wx.NullColour` to return to the default page text colour. """ self._pages.SetPageTextColour(page, colour) def SetTabAreaColour(self, colour): """ Sets the area behind the tabs colour. :param `colour`: a valid instance of `wx.Colour`. """ self._pages._tabAreaColour = colour def GetTabAreaColour(self): """ Returns the area behind the tabs colour. """ return self._pages._tabAreaColour def SetActiveTabColour(self, colour): """ Sets the active tab colour. :param `colour`: a valid instance of `wx.Colour`. """ self._pages._activeTabColour = colour def GetActiveTabColour(self): """ Returns the active tab colour. """ return self._pages._activeTabColour def EnsureVisible(self, page): """ Ensures that a tab is visible. :param `page`: an integer specifying the page index. """ self._pages.DoSetSelection(page) # ---------------------------------------------------------------------------- # # Class PageContainer # Acts as a container for the pages you add to FlatNotebook # ---------------------------------------------------------------------------- # class PageContainer(wx.Panel): """ This class acts as a container for the pages you add to L{FlatNotebook}. """ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0): """ Default class constructor. Used internally, do not call it in your code! :param `parent`: the L{PageContainer} parent; :param `id`: an identifier for the control: a value of -1 is taken to mean a default; :param `pos`: the control position. A value of (-1, -1) indicates a default position, chosen by either the windowing system or wxPython, depending on platform; :param `size`: the control size. A value of (-1, -1) indicates a default size, chosen by either the windowing system or wxPython, depending on platform; :param `style`: the window style. """ self._ImageList = None self._iActivePage = -1 self._pDropTarget = None self._nLeftClickZone = FNB_NOWHERE self._iPreviousActivePage = -1 self._pRightClickMenu = None self._nXButtonStatus = FNB_BTN_NONE self._nArrowDownButtonStatus = FNB_BTN_NONE self._pParent = parent self._nRightButtonStatus = FNB_BTN_NONE self._nLeftButtonStatus = FNB_BTN_NONE self._nTabXButtonStatus = FNB_BTN_NONE self._nHoveringOverTabIndex = -1 self._nHoveringOverLastTabIndex = -1 self._setCursor = False self._pagesInfoVec = [] self._colourTo = wx.SystemSettings_GetColour(wx.SYS_COLOUR_ACTIVECAPTION) self._colourFrom = wx.WHITE self._activeTabColour = wx.WHITE self._activeTextColour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNTEXT) self._nonActiveTextColour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNTEXT) self._tabAreaColour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_BTNFACE) self._nFrom = 0 self._isdragging = False # Set default page height, this is done according to the system font memDc = wx.MemoryDC() memDc.SelectObject(wx.EmptyBitmap(1,1)) if "__WXGTK__" in wx.PlatformInfo: boldFont = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) boldFont.SetWeight(wx.BOLD) memDc.SetFont(boldFont) height = memDc.GetCharHeight() tabHeight = height + FNB_HEIGHT_SPACER # We use 10 pixels as padding wx.Panel.__init__(self, parent, id, pos, wx.Size(size.x, tabHeight), style|wx.NO_BORDER|wx.NO_FULL_REPAINT_ON_RESIZE|wx.WANTS_CHARS) self._pDropTarget = FNBDropTarget(self) self.SetDropTarget(self._pDropTarget) self._mgr = FNBRendererMgr() self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_MIDDLE_DOWN, self.OnMiddleDown) self.Bind(wx.EVT_MOTION, self.OnMouseMove) self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel) self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave) self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnterWindow) self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick) self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) def OnEraseBackground(self, event): """ Handles the ``wx.EVT_ERASE_BACKGROUND`` event for L{PageContainer}. :param `event`: a `wx.EraseEvent` event to be processed. :note: This method is intentionally empty to reduce flicker. """ pass def _ReShow(self): """ Handles the redraw of the tabs when the ``FNB_HIDE_ON_SINGLE_TAB`` has been removed. """ self.Show() self.GetParent()._mainSizer.Layout() self.Refresh() def OnPaint(self, event): """ Handles the ``wx.EVT_PAINT`` event for L{PageContainer}. :param `event`: a `wx.PaintEvent` event to be processed. """ dc = wx.BufferedPaintDC(self) renderer = self._mgr.GetRenderer(self.GetParent().GetAGWWindowStyleFlag()) renderer.DrawTabs(self, dc) if self.HasAGWFlag(FNB_HIDE_ON_SINGLE_TAB) and len(self._pagesInfoVec) <= 1: self.Hide() self.GetParent()._mainSizer.Layout() self.Refresh() def AddPage(self, caption, selected=False, imgindex=-1): """ Adds a page to the L{PageContainer}. :param `page`: specifies the new page; :param `text`: specifies the text for the new page; :param `select`: specifies whether the page should be selected; :param `imageId`: specifies the optional image index for the new page. """ if selected: self._iPreviousActivePage = self._iActivePage self._iActivePage = len(self._pagesInfoVec) # Create page info and add it to the vector pageInfo = PageInfo(caption, imgindex) self._pagesInfoVec.append(pageInfo) self.Refresh() def InsertPage(self, indx, text, selected=True, imgindex=-1): """ Inserts a new page at the specified position. :param `indx`: specifies the position of the new page; :param `page`: specifies the new page; :param `text`: specifies the text for the new page; :param `select`: specifies whether the page should be selected; :param `imageId`: specifies the optional image index for the new page. """ if selected: self._iPreviousActivePage = self._iActivePage self._iActivePage = len(self._pagesInfoVec) self._pagesInfoVec.insert(indx, PageInfo(text, imgindex)) self.Refresh() return True def OnSize(self, event): """ Handles the ``wx.EVT_SIZE`` event for L{PageContainer}. :param `event`: a `wx.SizeEvent` event to be processed. """ # When resizing the control, try to fit to screen as many tabs as we can agwStyle = self.GetParent().GetAGWWindowStyleFlag() renderer = self._mgr.GetRenderer(agwStyle) fr = 0 page = self.GetSelection() for fr in xrange(self._nFrom): vTabInfo = renderer.NumberTabsCanFit(self, fr) if page - fr >= len(vTabInfo): continue break self._nFrom = fr self.Refresh() # Call on paint event.Skip() def OnMiddleDown(self, event): """ Handles the ``wx.EVT_MIDDLE_DOWN`` event for L{PageContainer}. :param `event`: a `wx.MouseEvent` event to be processed. """ # Test if this style is enabled agwStyle = self.GetParent().GetAGWWindowStyleFlag() if not agwStyle & FNB_MOUSE_MIDDLE_CLOSES_TABS: return where, tabIdx = self.HitTest(event.GetPosition()) if where == FNB_TAB: #self.DeletePage(tabIdx) # hack specific to Whyteboard, here. self.Parent.Parent.current_tab = tabIdx self.Parent.Parent.canvas = self.Parent.GetPage(tabIdx) self.Parent.Parent.on_close_tab() event.Skip() def OnMouseWheel(self, event): """ Handles the ``wx.EVT_MOUSEWHEEL`` event for L{PageContainer}. :param `event`: a `wx.MouseEvent` event to be processed. """ rotation = event.GetWheelRotation() delta = event.GetWheelDelta() steps = rotation/delta for tab in xrange(abs(steps)): if steps > 0: before = self._nLeftButtonStatus self._nLeftButtonStatus = FNB_BTN_PRESSED self.RotateLeft() self._nLeftButtonStatus = before else: before = self._nRightButtonStatus self._nRightButtonStatus = FNB_BTN_PRESSED self.RotateRight() self._nRightButtonStatus = before event.Skip() def OnRightDown(self, event): """ Handles the ``wx.EVT_RIGHT_DOWN`` event for L{PageContainer}. :param `event`: a `wx.MouseEvent` event to be processed. """ where, tabIdx = self.HitTest(event.GetPosition()) if where in [FNB_TAB, FNB_TAB_X]: if self._pagesInfoVec[tabIdx].GetEnabled(): # This shouldn't really change the selection, so it's commented out # Fire events and eventually (if allowed) change selection # self.FireEvent(tabIdx) # send a message to popup a custom menu event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU, self.GetParent().GetId()) event.SetSelection(tabIdx) event.SetOldSelection(self._iActivePage) event.SetEventObject(self.GetParent()) self.GetParent().GetEventHandler().ProcessEvent(event) if self._pRightClickMenu: self.PopupMenu(self._pRightClickMenu) event.Skip() def OnLeftDown(self, event): """ Handles the ``wx.EVT_LEFT_DOWN`` event for L{PageContainer}. :param `event`: a `wx.MouseEvent` event to be processed. """ # Reset buttons status self._nXButtonStatus = FNB_BTN_NONE self._nLeftButtonStatus = FNB_BTN_NONE self._nRightButtonStatus = FNB_BTN_NONE self._nTabXButtonStatus = FNB_BTN_NONE self._nArrowDownButtonStatus = FNB_BTN_NONE self._nLeftClickZone, tabIdx = self.HitTest(event.GetPosition()) if self._nLeftClickZone == FNB_DROP_DOWN_ARROW: self._nArrowDownButtonStatus = FNB_BTN_PRESSED self.Refresh() elif self._nLeftClickZone == FNB_LEFT_ARROW: self._nLeftButtonStatus = FNB_BTN_PRESSED self.Refresh() elif self._nLeftClickZone == FNB_RIGHT_ARROW: self._nRightButtonStatus = FNB_BTN_PRESSED self.Refresh() elif self._nLeftClickZone == FNB_X: self._nXButtonStatus = FNB_BTN_PRESSED self.Refresh() elif self._nLeftClickZone == FNB_TAB_X: self._nTabXButtonStatus = FNB_BTN_PRESSED self.Refresh() elif self._nLeftClickZone == FNB_TAB: if self._iActivePage != tabIdx: # In case the tab is disabled, we dont allow to choose it if len(self._pagesInfoVec) > tabIdx and \ self._pagesInfoVec[tabIdx].GetEnabled(): self.FireEvent(tabIdx) def RotateLeft(self): """ Scrolls tabs to the left by bulk of 5 tabs. """ if self._nFrom == 0: return # Make sure that the button was pressed before if self._nLeftButtonStatus != FNB_BTN_PRESSED: return self._nLeftButtonStatus = FNB_BTN_HOVER # We scroll left with bulks of 5 scrollLeft = self.GetNumTabsCanScrollLeft() self._nFrom -= scrollLeft if self._nFrom < 0: self._nFrom = 0 self.Refresh() def RotateRight(self): """ Scrolls tabs to the right by bulk of 5 tabs. """ if self._nFrom >= len(self._pagesInfoVec) - 1: return # Make sure that the button was pressed before if self._nRightButtonStatus != FNB_BTN_PRESSED: return self._nRightButtonStatus = FNB_BTN_HOVER # Check if the right most tab is visible, if it is # don't rotate right anymore if self._pagesInfoVec[len(self._pagesInfoVec)-1].GetPosition() != wx.Point(-1, -1): return self._nFrom += 1 self.Refresh() def OnLeftUp(self, event): """ Handles the ``wx.EVT_LEFT_UP`` event for L{PageContainer}. :param `event`: a `wx.MouseEvent` event to be processed. """ # forget the zone that was initially clicked self._nLeftClickZone = FNB_NOWHERE where, tabIdx = self.HitTest(event.GetPosition()) if not self.HasAGWFlag(FNB_NO_TAB_FOCUS): # Make sure selected tab has focus self.SetFocus() if where == FNB_LEFT_ARROW: self.RotateLeft() elif where == FNB_RIGHT_ARROW: self.RotateRight() elif where == FNB_X: # Make sure that the button was pressed before if self._nXButtonStatus != FNB_BTN_PRESSED: return self._nXButtonStatus = FNB_BTN_HOVER self.DeletePage(self._iActivePage) elif where == FNB_TAB_X: # Make sure that the button was pressed before if self._nTabXButtonStatus != FNB_BTN_PRESSED: return self._nTabXButtonStatus = FNB_BTN_HOVER #self.DeletePage(self._iActivePage) # hack specific to Whyteboard, here. self.Parent.Parent.current_tab = tabIdx self.Parent.Parent.canvas = self.Parent.GetPage(tabIdx) self.Parent.Parent.on_close_tab() elif where == FNB_DROP_DOWN_ARROW: # Make sure that the button was pressed before if self._nArrowDownButtonStatus != FNB_BTN_PRESSED: return self._nArrowDownButtonStatus = FNB_BTN_NONE # Refresh the button status renderer = self._mgr.GetRenderer(self.GetParent().GetAGWWindowStyleFlag()) dc = wx.ClientDC(self) renderer.DrawDropDownArrow(self, dc) self.PopupTabsMenu() self._pParent.Parent.SetFocus() event.Skip() def HitTest(self, pt): """ HitTest method for L{PageContainer}. :param `pt`: an instance of `wx.Point`, to test for hits. :return: The hit test flag (if any) and the hit page index (if any). The return value can be one of the following bits: ========================= ======= ================================= HitTest Flag Value Description ========================= ======= ================================= ``FNB_NOWHERE`` 0 Indicates mouse coordinates not on any tab of the notebook ``FNB_TAB`` 1 Indicates mouse coordinates inside a tab ``FNB_X`` 2 Indicates mouse coordinates inside the 'X' button region ``FNB_TAB_X`` 3 Indicates mouse coordinates inside the 'X' region in a tab ``FNB_LEFT_ARROW`` 4 Indicates mouse coordinates inside the left arrow region ``FNB_RIGHT_ARROW`` 5 Indicates mouse coordinates inside the right arrow region ``FNB_DROP_DOWN_ARROW`` 6 Indicates mouse coordinates inside the drop down arrow region ========================= ======= ================================= """ agwStyle = self.GetParent().GetAGWWindowStyleFlag() render = self._mgr.GetRenderer(agwStyle) fullrect = self.GetClientRect() btnLeftPos = render.GetLeftButtonPos(self) btnRightPos = render.GetRightButtonPos(self) btnXPos = render.GetXPos(self) tabIdx = -1 if len(self._pagesInfoVec) == 0: return FNB_NOWHERE, tabIdx rect = wx.Rect(btnXPos, 8, 16, 16) if rect.Contains(pt): return (agwStyle & FNB_NO_X_BUTTON and [FNB_NOWHERE] or [FNB_X])[0], tabIdx rect = wx.Rect(btnRightPos, 8, 16, 16) if agwStyle & FNB_DROPDOWN_TABS_LIST: rect = wx.Rect(render.GetDropArrowButtonPos(self), 8, 16, 16) if rect.Contains(pt): return FNB_DROP_DOWN_ARROW, tabIdx if rect.Contains(pt): return (agwStyle & FNB_NO_NAV_BUTTONS and [FNB_NOWHERE] or [FNB_RIGHT_ARROW])[0], tabIdx rect = wx.Rect(btnLeftPos, 8, 16, 16) if rect.Contains(pt): return (agwStyle & FNB_NO_NAV_BUTTONS and [FNB_NOWHERE] or [FNB_LEFT_ARROW])[0], tabIdx # Test whether a left click was made on a tab bFoundMatch = False for cur in xrange(self._nFrom, len(self._pagesInfoVec)): pgInfo = self._pagesInfoVec[cur] if pgInfo.GetPosition() == wx.Point(-1, -1): continue if agwStyle & FNB_X_ON_TAB and cur == self.GetSelection(): # 'x' button exists on a tab if self._pagesInfoVec[cur].GetXRect().Contains(pt): return FNB_TAB_X, cur if agwStyle & FNB_VC8: if self._pagesInfoVec[cur].GetRegion().Contains(pt.x, pt.y): if bFoundMatch or cur == self.GetSelection(): return FNB_TAB, cur tabIdx = cur bFoundMatch = True else: tabRect = wx.Rect(pgInfo.GetPosition().x, pgInfo.GetPosition().y, pgInfo.GetSize().x, pgInfo.GetSize().y) if tabRect.Contains(pt): # We have a match return FNB_TAB, cur if bFoundMatch: return FNB_TAB, tabIdx if self._isdragging: # We are doing DND, so check also the region outside the tabs # try before the first tab pgInfo = self._pagesInfoVec[0] tabRect = wx.Rect(0, pgInfo.GetPosition().y, pgInfo.GetPosition().x, self.GetParent().GetSize().y) if tabRect.Contains(pt): return FNB_TAB, 0 # try after the last tab pgInfo = self._pagesInfoVec[-1] startpos = pgInfo.GetPosition().x+pgInfo.GetSize().x tabRect = wx.Rect(startpos, pgInfo.GetPosition().y, fullrect.width-startpos, self.GetParent().GetSize().y) if tabRect.Contains(pt): return FNB_TAB, len(self._pagesInfoVec) # Default return FNB_NOWHERE, -1 def SetSelection(self, page): """ Sets the selected page. :param `page`: an integer specifying the page index. """ book = self.GetParent() book.SetSelection(page) self.DoSetSelection(page) def DoSetSelection(self, page): """ Does the actual selection of a page. :param `page`: an integer specifying the page index. """ if page < len(self._pagesInfoVec): #! fix for tabfocus da_page = self._pParent.GetPage(page) if da_page != None: da_page.SetFocus() if not self.IsTabVisible(page): # Try to remove one tab from start and try again if not self.CanFitToScreen(page): if self._nFrom > page: self._nFrom = page else: while self._nFrom < page: self._nFrom += 1 if self.CanFitToScreen(page): break self.Refresh() def DeletePage(self, page): """ Delete the specified page from L{PageContainer}. :param `page`: an integer specifying the page index. """ book = self.GetParent() book.DeletePage(page) book.Refresh() def IsMouseHovering(self, page): """ Returns whether or not the mouse is hovering over this page's tab :param `page`: an integer specifying the page index. """ return page == self._nHoveringOverTabIndex def IsTabVisible(self, page): """ Returns whether a tab is visible or not. :param `page`: an integer specifying the page index. """ iLastVisiblePage = self.GetLastVisibleTab() return page <= iLastVisiblePage and page >= self._nFrom def DoDeletePage(self, page): """ Does the actual page deletion. :param `page`: an integer specifying the page index. """ # Remove the page from the vector book = self.GetParent() self._pagesInfoVec.pop(page) # Thanks to Yiaanis AKA Mandrav if self._iActivePage >= page: self._iActivePage = self._iActivePage - 1 self._iPreviousActivePage = -1 # The delete page was the last first on the array, # but the book still has more pages, so we set the # active page to be the first one (0) if self._iActivePage < 0 and len(self._pagesInfoVec) > 0: self._iActivePage = 0 self._iPreviousActivePage = -1 # Refresh the tabs if self._iActivePage >= 0: book._bForceSelection = True # Check for selection and send event event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CHANGING, self.GetParent().GetId()) event.SetSelection(self._iActivePage) event.SetOldSelection(self._iPreviousActivePage) event.SetEventObject(self.GetParent()) self.GetParent().GetEventHandler().ProcessEvent(event) book.SetSelection(self._iActivePage) book._bForceSelection = False # Fire a wxEVT_FLATNOTEBOOK_PAGE_CHANGED event event.SetEventType(wxEVT_FLATNOTEBOOK_PAGE_CHANGED) event.SetOldSelection(self._iPreviousActivePage) self.GetParent().GetEventHandler().ProcessEvent(event) #if not self._pagesInfoVec: # # Erase the page container drawings # dc = wx.ClientDC(self) # dc.Clear() def DeleteAllPages(self): """ Deletes all the pages in the L{PageContainer}. """ self._iActivePage = -1 self._iPreviousActivePage = -1 self._nFrom = 0 self._pagesInfoVec = [] # Erase the page container drawings dc = wx.ClientDC(self) dc.Clear() def OnMouseMove(self, event): """ Handles the ``wx.EVT_MOTION`` event for L{PageContainer}. :param `event`: a `wx.MouseEvent` event to be processed. """ if self._pagesInfoVec and self.IsShown(): xButtonStatus = self._nXButtonStatus xTabButtonStatus = self._nTabXButtonStatus rightButtonStatus = self._nRightButtonStatus leftButtonStatus = self._nLeftButtonStatus dropDownButtonStatus = self._nArrowDownButtonStatus agwStyle = self.GetParent().GetAGWWindowStyleFlag() self._nXButtonStatus = FNB_BTN_NONE self._nRightButtonStatus = FNB_BTN_NONE self._nLeftButtonStatus = FNB_BTN_NONE self._nTabXButtonStatus = FNB_BTN_NONE self._nArrowDownButtonStatus = FNB_BTN_NONE bRedrawTabs = False self._nHoveringOverTabIndex = -1 where, tabIdx = self.HitTest(event.GetPosition()) if where == FNB_X: if event.LeftIsDown(): self._nXButtonStatus = (self._nLeftClickZone==FNB_X and [FNB_BTN_PRESSED] or [FNB_BTN_NONE])[0] else: self._nXButtonStatus = FNB_BTN_HOVER elif where == FNB_DROP_DOWN_ARROW: if event.LeftIsDown(): self._nArrowDownButtonStatus = (self._nLeftClickZone==FNB_DROP_DOWN_ARROW and [FNB_BTN_PRESSED] or [FNB_BTN_NONE])[0] else: self._nArrowDownButtonStatus = FNB_BTN_HOVER elif where == FNB_TAB_X: if event.LeftIsDown(): self._nTabXButtonStatus = (self._nLeftClickZone==FNB_TAB_X and [FNB_BTN_PRESSED] or [FNB_BTN_NONE])[0] else: self._nTabXButtonStatus = FNB_BTN_HOVER elif where == FNB_RIGHT_ARROW: if event.LeftIsDown(): self._nRightButtonStatus = (self._nLeftClickZone==FNB_RIGHT_ARROW and [FNB_BTN_PRESSED] or [FNB_BTN_NONE])[0] else: self._nRightButtonStatus = FNB_BTN_HOVER elif where == FNB_LEFT_ARROW: if event.LeftIsDown(): self._nLeftButtonStatus = (self._nLeftClickZone==FNB_LEFT_ARROW and [FNB_BTN_PRESSED] or [FNB_BTN_NONE])[0] else: self._nLeftButtonStatus = FNB_BTN_HOVER elif where == FNB_TAB: # Call virtual method for showing tooltip self.ShowTabTooltip(tabIdx) if not self.GetEnabled(tabIdx): # Set the cursor to be 'No-entry' wx.SetCursor(wx.StockCursor(wx.CURSOR_NO_ENTRY)) self._setCursor = True else: self._nHoveringOverTabIndex = tabIdx if self._setCursor: wx.SetCursor(wx.StockCursor(wx.CURSOR_ARROW)) self._setCursor = False # Support for drag and drop if event.Dragging() and not (agwStyle & FNB_NODRAG): self._isdragging = True draginfo = FNBDragInfo(self, tabIdx) drginfo = cPickle.dumps(draginfo) dataobject = wx.CustomDataObject(wx.CustomDataFormat("FlatNotebook")) dataobject.SetData(drginfo) dragSource = FNBDropSource(self) dragSource.SetData(dataobject) dragSource.DoDragDrop(wx.Drag_DefaultMove) if self._nHoveringOverTabIndex != self._nHoveringOverLastTabIndex: self._nHoveringOverLastTabIndex = self._nHoveringOverTabIndex if self._nHoveringOverTabIndex >= 0: bRedrawTabs = True bRedrawX = self._nXButtonStatus != xButtonStatus bRedrawRight = self._nRightButtonStatus != rightButtonStatus bRedrawLeft = self._nLeftButtonStatus != leftButtonStatus bRedrawTabX = self._nTabXButtonStatus != xTabButtonStatus bRedrawDropArrow = self._nArrowDownButtonStatus != dropDownButtonStatus render = self._mgr.GetRenderer(agwStyle) if (bRedrawX or bRedrawRight or bRedrawLeft or bRedrawTabX or bRedrawDropArrow or bRedrawTabs): dc = wx.ClientDC(self) if bRedrawX: render.DrawX(self, dc) if bRedrawLeft: render.DrawLeftArrow(self, dc) if bRedrawRight: render.DrawRightArrow(self, dc) if bRedrawTabX or bRedrawTabs: self.Refresh() if bRedrawDropArrow: render.DrawDropDownArrow(self, dc) event.Skip() def GetLastVisibleTab(self): """ Returns the last visible tab in the tab area. """ if self._nFrom < 0: return -1 ii = 0 for ii in xrange(self._nFrom, len(self._pagesInfoVec)): if self._pagesInfoVec[ii].GetPosition() == wx.Point(-1, -1): break return ii-1 def GetNumTabsCanScrollLeft(self): """ Returns the number of tabs than can be scrolled left. """ if self._nFrom - 1 >= 0: return 1 return 0 def IsDefaultTabs(self): """ Returns whether a tab has a default style. """ agwStyle = self.GetParent().GetAGWWindowStyleFlag() res = (agwStyle & FNB_VC71) or (agwStyle & FNB_FANCY_TABS) or (agwStyle & FNB_VC8) return not res def AdvanceSelection(self, forward=True): """ Cycles through the tabs. :param `forward`: if ``True``, the selection is advanced in ascending order (to the right), otherwise the selection is advanced in descending order. :note: The call to this function generates the page changing events. """ nSel = self.GetSelection() if nSel < 0: return nMax = self.GetPageCount() - 1 if forward: newSelection = (nSel == nMax and [0] or [nSel + 1])[0] else: newSelection = (nSel == 0 and [nMax] or [nSel - 1])[0] if not self._pagesInfoVec[newSelection].GetEnabled(): return self.FireEvent(newSelection) def OnMouseLeave(self, event): """ Handles the ``wx.EVT_LEAVE_WINDOW`` event for L{PageContainer}. :param `event`: a `wx.MouseEvent` event to be processed. """ self._nLeftButtonStatus = FNB_BTN_NONE self._nXButtonStatus = FNB_BTN_NONE self._nRightButtonStatus = FNB_BTN_NONE self._nTabXButtonStatus = FNB_BTN_NONE self._nArrowDownButtonStatus = FNB_BTN_NONE self._nHoveringOverTabIndex = -1 self._nHoveringOverLastTabIndex = -1 self.Refresh() selection = self.GetSelection() if selection == -1: event.Skip() return if not self.IsTabVisible(selection): if selection == len(self._pagesInfoVec) - 1: if not self.CanFitToScreen(selection): event.Skip() return else: event.Skip() return agwStyle = self.GetParent().GetAGWWindowStyleFlag() render = self._mgr.GetRenderer(agwStyle) dc = wx.ClientDC(self) render.DrawTabX(self, dc, self._pagesInfoVec[selection].GetXRect(), selection, self._nTabXButtonStatus) if not agwStyle & FNB_RIBBON_TABS: render.DrawFocusRectangle(dc, self, self._pagesInfoVec[selection]) event.Skip() def OnMouseEnterWindow(self, event): """ Handles the ``wx.EVT_ENTER_WINDOW`` event for L{PageContainer}. :param `event`: a `wx.MouseEvent` event to be processed. """ self._nLeftButtonStatus = FNB_BTN_NONE self._nXButtonStatus = FNB_BTN_NONE self._nRightButtonStatus = FNB_BTN_NONE self._nLeftClickZone = FNB_BTN_NONE self._nArrowDownButtonStatus = FNB_BTN_NONE event.Skip() def ShowTabTooltip(self, tabIdx): """ Shows a tab tooltip. :param `tabIdx`: an integer specifying the page index. """ pWindow = self._pParent.GetPage(tabIdx) if pWindow: pToolTip = pWindow.GetToolTip() if pToolTip and pToolTip.GetWindow() == pWindow: self.SetToolTipString(pToolTip.GetTip()) def SetPageImage(self, page, image): """ Sets the image index for the given page. :param `page`: an integer specifying the page index; :param `image`: an index into the image list which was set with L{SetImageList}. """ if page < len(self._pagesInfoVec): self._pagesInfoVec[page].SetImageIndex(image) self.Refresh() def GetPageImage(self, page): """ Returns the image index associated to a page. :param `page`: an integer specifying the page index. """ if page < len(self._pagesInfoVec): return self._pagesInfoVec[page].GetImageIndex() return -1 def GetPageTextColour(self, page): """ Returns the tab text colour if it has been set previously, or ``None`` otherwise. :param `page`: an integer specifying the page index. """ if page < len(self._pagesInfoVec): return self._pagesInfoVec[page].GetPageTextColour() return None def SetPageTextColour(self, page, colour): """ Sets the tab text colour individually. :param `page`: an integer specifying the page index; :param `colour`: an instance of `wx.Colour`. You can pass ``None`` or `wx.NullColour` to return to the default page text colour. """ if page < len(self._pagesInfoVec): self._pagesInfoVec[page].SetPageTextColour(colour) self.Refresh() def OnDropTarget(self, x, y, nTabPage, wnd_oldContainer): """ Handles the drop action from a drag and drop operation. :param `x`: the x position of the drop action; :param `y`: the y position of the drop action; :param `nTabPage`: the index of the tab being dropped; :param `wnd_oldContainer`: the L{FlatNotebook} to which the dropped tab previously belonged to. """ # Disable drag'n'drop for disabled tab if len(wnd_oldContainer._pagesInfoVec) > nTabPage and \ not wnd_oldContainer._pagesInfoVec[nTabPage].GetEnabled(): return wx.DragCancel self._isdragging = True oldContainer = wnd_oldContainer nIndex = -1 where, nIndex = self.HitTest(wx.Point(x, y)) oldNotebook = oldContainer.GetParent() newNotebook = self.GetParent() if oldNotebook == newNotebook: if nTabPage >= 0: if where == FNB_TAB: self.MoveTabPage(nTabPage, nIndex) event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_DROPPED, self.GetParent().GetId()) event.SetSelection(nIndex) event.SetOldSelection(nTabPage) event.SetEventObject(self.GetParent()) self.GetParent().GetEventHandler().ProcessEvent(event) elif self.GetParent().GetAGWWindowStyleFlag() & FNB_ALLOW_FOREIGN_DND: if wx.Platform in ["__WXMSW__", "__WXGTK__", "__WXMAC__"]: if nTabPage >= 0: window = oldNotebook.GetPage(nTabPage) if window: where, nIndex = newNotebook._pages.HitTest(wx.Point(x, y)) caption = oldContainer.GetPageText(nTabPage) imageindex = oldContainer.GetPageImage(nTabPage) oldNotebook.RemovePage(nTabPage) window.Reparent(newNotebook) if imageindex >= 0: bmp = oldNotebook.GetImageList().GetBitmap(imageindex) newImageList = newNotebook.GetImageList() if not newImageList: xbmp, ybmp = bmp.GetWidth(), bmp.GetHeight() newImageList = wx.ImageList(xbmp, ybmp) imageindex = 0 else: imageindex = newImageList.GetImageCount() newImageList.Add(bmp) newNotebook.SetImageList(newImageList) newNotebook.InsertPage(nIndex, window, caption, True, imageindex) event = FlatNotebookDragEvent(wxEVT_FLATNOTEBOOK_PAGE_DROPPED_FOREIGN, self.GetParent().GetId()) event.SetSelection(nIndex) event.SetOldSelection(nTabPage) event.SetNotebook(newNotebook) event.SetOldNotebook(oldNotebook) event.SetEventObject(self.GetParent()) self.GetParent().GetEventHandler().ProcessEvent(event) self._isdragging = False return wx.DragMove def MoveTabPage(self, nMove, nMoveTo): """ Moves a tab inside the same L{FlatNotebook}. :param `nMove`: the start index of the moved tab; :param `nMoveTo`: the destination index of the moved tab. """ if nMove == nMoveTo: return elif nMoveTo < len(self._pParent._windows): nMoveTo = nMoveTo + 1 self._pParent.Freeze() # Remove the window from the main sizer nCurSel = self._pParent._pages.GetSelection() self._pParent._mainSizer.Detach(self._pParent._windows[nCurSel]) self._pParent._windows[nCurSel].Hide() pWindow = self._pParent._windows[nMove] self._pParent._windows.pop(nMove) self._pParent._windows.insert(nMoveTo-1, pWindow) pgInfo = self._pagesInfoVec[nMove] self._pagesInfoVec.pop(nMove) self._pagesInfoVec.insert(nMoveTo - 1, pgInfo) # Add the page according to the style pSizer = self._pParent._mainSizer agwStyle = self.GetParent().GetAGWWindowStyleFlag() if agwStyle & FNB_BOTTOM: pSizer.Insert(0, pWindow, 1, wx.EXPAND) else: # We leave a space of 1 pixel around the window pSizer.Add(pWindow, 1, wx.EXPAND) pWindow.Show() pSizer.Layout() self._iActivePage = nMoveTo - 1 self._iPreviousActivePage = -1 self.DoSetSelection(self._iActivePage) self.Refresh() self._pParent.Thaw() def CanFitToScreen(self, page): """ Returns wheter a tab can fit in the left space in the screen or not. :param `page`: an integer specifying the page index. """ # Incase the from is greater than page, # we need to reset the self._nFrom, so in order # to force the caller to do so, we return false if self._nFrom > page: return False agwStyle = self.GetParent().GetAGWWindowStyleFlag() render = self._mgr.GetRenderer(agwStyle) vTabInfo = render.NumberTabsCanFit(self) if page - self._nFrom >= len(vTabInfo): return False return True def GetNumOfVisibleTabs(self): """ Returns the number of visible tabs. """ count = 0 for ii in xrange(self._nFrom, len(self._pagesInfoVec)): if self._pagesInfoVec[ii].GetPosition() == wx.Point(-1, -1): break count = count + 1 return count def GetEnabled(self, page): """ Returns whether a tab is enabled or not. :param `page`: an integer specifying the page index. """ if page >= len(self._pagesInfoVec): return True # Seems strange, but this is the default return self._pagesInfoVec[page].GetEnabled() def EnableTab(self, page, enabled=True): """ Enables or disables a tab. :param `page`: an integer specifying the page index; :param `enabled`: ``True`` to enable a tab, ``False`` to disable it. """ if page >= len(self._pagesInfoVec): return self._pagesInfoVec[page].EnableTab(enabled) def GetSingleLineBorderColour(self): """ Returns the colour for the single line border. """ if self.HasAGWFlag(FNB_FANCY_TABS): return self._colourFrom return wx.WHITE def HasAGWFlag(self, flag): """ Returns whether a flag is present in the L{FlatNotebook} style. :param `flag`: one of the possible L{FlatNotebook} window styles. :see: L{FlatNotebook.SetAGWWindowStyleFlag} for a list of possible window style flags. """ agwStyle = self.GetParent().GetAGWWindowStyleFlag() res = (agwStyle & flag and [True] or [False])[0] return res def ClearAGWFlag(self, flag): """ Deletes a flag from the L{FlatNotebook} style. :param `flag`: one of the possible L{FlatNotebook} window styles. :see: L{FlatNotebook.SetAGWWindowStyleFlag} for a list of possible window style flags. """ parent = self.GetParent() agwStyle = parent.GetAGWWindowStyleFlag() agwStyle &= ~flag parent.SetAGWWindowStyleFlag(agwStyle) def SetAGWWindowStyleFlag(self, agwStyle): """ Sets the L{FlatNotebook} window style. :param `agwStyle`: the new L{FlatNotebook} window style. :see: The L{FlatNotebook.__init__} method for the `agwStyle` parameter description. """ self.GetParent().SetAGWWindowStyleFlag(agwStyle) def GetAGWWindowStyleFlag(self): """ Returns the L{FlatNotebook} window style. :see: The L{FlatNotebook.__init__} method for the `agwStyle` parameter description. """ return self.GetParent().GetAGWWindowStyleFlag() def TabHasImage(self, tabIdx): """ Returns whether a tab has an associated image index or not. :param `tabIdx`: an integer specifying the page index. """ if self._ImageList: return self._pagesInfoVec[tabIdx].GetImageIndex() != -1 return False def OnLeftDClick(self, event): """ Handles the ``wx.EVT_LEFT_DCLICK`` event for L{PageContainer}. :param `event`: a `wx.MouseEvent` event to be processed. """ where, tabIdx = self.HitTest(event.GetPosition()) if where == FNB_RIGHT_ARROW: self._nRightButtonStatus = FNB_BTN_PRESSED self.RotateRight() elif where == FNB_LEFT_ARROW: self._nLeftButtonStatus = FNB_BTN_PRESSED self.RotateLeft() elif self.HasAGWFlag(FNB_DCLICK_CLOSES_TABS): if where == FNB_TAB: self.DeletePage(tabIdx) else: event.Skip() def OnSetFocus(self, event): """ Handles the ``wx.EVT_SET_FOCUS`` event for L{PageContainer}. :param `event`: a `wx.FocusEvent` event to be processed. """ if self._iActivePage < 0: event.Skip() return self.SetFocusedPage(self._iActivePage) def OnKillFocus(self, event): """ Handles the ``wx.EVT_KILL_FOCUS`` event for L{PageContainer}. :param `event`: a `wx.FocusEvent` event to be processed. """ self.SetFocusedPage() def OnKeyDown(self, event): """ Handles the ``wx.EVT_KEY_DOWN`` event for L{PageContainer}. :param `event`: a `wx.KeyEvent` event to be processed. :note: When the L{PageContainer} has the focus tabs can be changed with the left/right arrow keys. """ key = event.GetKeyCode() if key == wx.WXK_LEFT: self.GetParent().AdvanceSelection(False) self.SetFocus() elif key == wx.WXK_RIGHT: self.GetParent().AdvanceSelection(True) self.SetFocus() elif key == wx.WXK_TAB and not event.ControlDown(): flags = 0 if not event.ShiftDown(): flags |= wx.NavigationKeyEvent.IsForward if event.CmdDown(): flags |= wx.NavigationKeyEvent.WinChange self.Navigate(flags) else: event.Skip() def SetFocusedPage(self, pageIndex=-1): """ Sets/Unsets the focus on the appropriate page. :param `pageIndex`: an integer specifying the page index. If `pageIndex` is defaulted to -1, we have lost focus and no focus indicator is drawn. """ for indx, page in enumerate(self._pagesInfoVec): if indx == pageIndex: page._hasFocus = True else: page._hasFocus = False self.Refresh() def PopupTabsMenu(self): """ Pops up the menu activated with the drop down arrow in the navigation area. """ popupMenu = wx.Menu() for i in xrange(len(self._pagesInfoVec)): pi = self._pagesInfoVec[i] item = wx.MenuItem(popupMenu, i+1, pi.GetCaption(), pi.GetCaption(), wx.ITEM_NORMAL) self.Bind(wx.EVT_MENU, self.OnTabMenuSelection, item) # There is an alignment problem with wx2.6.3 & Menus so only use # images for versions above 2.6.3 if wx.VERSION > (2, 6, 3, 0) and self.TabHasImage(i): item.SetBitmap(self.GetImageList().GetBitmap(pi.GetImageIndex())) popupMenu.AppendItem(item) item.Enable(pi.GetEnabled()) self.PopupMenu(popupMenu) def OnTabMenuSelection(self, event): """ Handles the ``wx.EVT_MENU`` event for L{PageContainer}. :param `event`: a `wx.MenuEvent` event to be processed. """ selection = event.GetId() - 1 self.FireEvent(selection) def FireEvent(self, selection): """ Fires the ``EVT_FLATNOTEBOOK_PAGE_CHANGING`` and ``EVT_FLATNOTEBOOK_PAGE_CHANGED`` events called from other methods (from menu selection or `Smart Tabbing`). This is an utility function. :param `selection`: the new selection inside L{FlatNotebook}. """ if selection == self._iActivePage: # No events for the same selection return oldSelection = self._iActivePage event = FlatNotebookEvent(wxEVT_FLATNOTEBOOK_PAGE_CHANGING, self.GetParent().GetId()) event.SetSelection(selection) event.SetOldSelection(oldSelection) event.SetEventObject(self.GetParent()) if not self.GetParent().GetEventHandler().ProcessEvent(event) or event.IsAllowed(): self.SetSelection(selection) # Fire a wxEVT_FLATNOTEBOOK_PAGE_CHANGED event event.SetEventType(wxEVT_FLATNOTEBOOK_PAGE_CHANGED) event.SetOldSelection(oldSelection) self.GetParent().GetEventHandler().ProcessEvent(event) if not self.HasAGWFlag(FNB_NO_TAB_FOCUS): self.SetFocus() def SetImageList(self, imglist): """ Sets the image list for the L{PageContainer}. :param `imageList`: an instance of `wx.ImageList`. """ self._ImageList = imglist def AssignImageList(self, imglist): """ Assigns the image list for the L{PageContainer}. :param `imageList`: an instance of `wx.ImageList`. """ self._ImageList = imglist def GetImageList(self): """ Returns the image list for the page control. """ return self._ImageList def GetSelection(self): """ Returns the current selected page. """ return self._iActivePage def GetPageCount(self): """ Returns the number of tabs in the L{FlatNotebook} control. """ return len(self._pagesInfoVec) def GetPageText(self, page): """ Returns the tab caption of the page. :param `page`: an integer specifying the page index. """ if page < len(self._pagesInfoVec): return self._pagesInfoVec[page].GetCaption() else: return u'' def SetPageText(self, page, text): """ Sets the tab caption of the page. :param `page`: an integer specifying the page index; :param `text`: the new tab label. """ if page < len(self._pagesInfoVec): self._pagesInfoVec[page].SetCaption(text) return True else: return False def DrawDragHint(self): """ Draws small arrow at the place that the tab will be placed. """ # get the index of tab that will be replaced with the dragged tab pt = wx.GetMousePosition() client_pt = self.ScreenToClient(pt) where, tabIdx = self.HitTest(client_pt) self._mgr.GetRenderer(self.GetParent().GetAGWWindowStyleFlag()).DrawDragHint(self, tabIdx) # ---------------------------------------------------------------------------- # # Class FlatNotebookCompatible # This class is more compatible with the wx.Notebook API. # ---------------------------------------------------------------------------- # class FlatNotebookCompatible(FlatNotebook): """ This class is more compatible with the `wx.Notebook` API, especially regarding page changing events. Use the L{FlatNotebookCompatible.SetSelection} method if you wish to send page changing events, or L{FlatNotebookCompatible.ChangeSelection} otherwise. """ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, agwStyle=0, name="FlatNotebook"): """ Default class constructor. :param `parent`: the L{FlatNotebook} parent; :param `id`: an identifier for the control: a value of -1 is taken to mean a default; :param `pos`: the control position. A value of (-1, -1) indicates a default position, chosen by either the windowing system or wxPython, depending on platform; :param `size`: the control size. A value of (-1, -1) indicates a default size, chosen by either the windowing system or wxPython, depending on platform; :param `style`: the underlying `wx.PyPanel` window style; :param `agwStyle`: the AGW-specific window style. This can be a combination of the following bits: ================================ =========== ================================================== Window Styles Hex Value Description ================================ =========== ================================================== ``FNB_VC71`` 0x1 Use Visual Studio 2003 (VC7.1) style for tabs. ``FNB_FANCY_TABS`` 0x2 Use fancy style - square tabs filled with gradient colouring. ``FNB_TABS_BORDER_SIMPLE`` 0x4 Draw thin border around the page. ``FNB_NO_X_BUTTON`` 0x8 Do not display the 'X' button. ``FNB_NO_NAV_BUTTONS`` 0x10 Do not display the right/left arrows. ``FNB_MOUSE_MIDDLE_CLOSES_TABS`` 0x20 Use the mouse middle button for cloing tabs. ``FNB_BOTTOM`` 0x40 Place tabs at bottom - the default is to place them at top. ``FNB_NODRAG`` 0x80 Disable dragging of tabs. ``FNB_VC8`` 0x100 Use Visual Studio 2005 (VC8) style for tabs. ``FNB_X_ON_TAB`` 0x200 Place 'X' close button on the active tab. ``FNB_BACKGROUND_GRADIENT`` 0x400 Use gradients to paint the tabs background. ``FNB_COLOURFUL_TABS`` 0x800 Use colourful tabs (VC8 style only). ``FNB_DCLICK_CLOSES_TABS`` 0x1000 Style to close tab using double click. ``FNB_SMART_TABS`` 0x2000 Use `Smart Tabbing`, like ``Alt`` + ``Tab`` on Windows. ``FNB_DROPDOWN_TABS_LIST`` 0x4000 Use a dropdown menu on the left in place of the arrows. ``FNB_ALLOW_FOREIGN_DND`` 0x8000 Allows drag 'n' drop operations between different FlatNotebooks. ``FNB_HIDE_ON_SINGLE_TAB`` 0x10000 Hides the Page Container when there is one or fewer tabs. ``FNB_DEFAULT_STYLE`` 0x10020 FlatNotebook default style. ``FNB_FF2`` 0x20000 Use Firefox 2 style for tabs. ``FNB_NO_TAB_FOCUS`` 0x40000 Does not allow tabs to have focus. ``FNB_RIBBON_TABS`` 0x80000 Use the Ribbon Tabs style. ================================ =========== ================================================== :param `name`: the window name. """ FlatNotebook.__init__(self, parent, id, pos, size, style, agwStyle, name) def SetSelection(self, page): """ Sets the selection for the given page. :param `page`: an integer specifying the new selected page. :note: The call to this function **generates** the page changing events. """ if page >= len(self._windows) or not self._windows: return # Support for disabed tabs if not self._pages.GetEnabled(page) and len(self._windows) > 1 and not self._bForceSelection: return self.FireEvent(page) def ChangeSelection(self, page): """ Sets the selection for the given page. :param `page`: an integer specifying the new selected page. :note: The call to this function **does not** generate the page changing events. """ FlatNotebook.SetSelection(self, page) whyteboard-0.41.1/whyteboard/lib/validate.py0000777000175000017500000013213711443222121020072 0ustar stevesteve# validate.py # A Validator object # Copyright (C) 2005 Michael Foord, Mark Andrews, Nicola Larosa # E-mail: fuzzyman AT voidspace DOT org DOT uk # mark AT la-la DOT com # nico AT tekNico DOT net # This software is licensed under the terms of the BSD license. # http://www.voidspace.org.uk/python/license.shtml # Basically you're free to copy, modify, distribute and relicense it, # So long as you keep a copy of the license with it. # Scripts maintained at http://www.voidspace.org.uk/python/index.shtml # For information about bugfixes, updates and support, please join the # ConfigObj mailing list: # http://lists.sourceforge.net/lists/listinfo/configobj-develop # Comments, suggestions and bug reports welcome. """ The Validator object is used to check that supplied values conform to a specification. The value can be supplied as a string - e.g. from a config file. In this case the check will also *convert* the value to the required type. This allows you to add validation as a transparent layer to access data stored as strings. The validation checks that the data is correct *and* converts it to the expected type. Some standard checks are provided for basic data types. Additional checks are easy to write. They can be provided when the ``Validator`` is instantiated or added afterwards. The standard functions work with the following basic data types : * integers * floats * booleans * strings * ip_addr plus lists of these datatypes Adding additional checks is done through coding simple functions. The full set of standard checks are : * 'integer': matches integer values (including negative) Takes optional 'min' and 'max' arguments : :: integer() integer(3, 9) # any value from 3 to 9 integer(min=0) # any positive value integer(max=9) * 'float': matches float values Has the same parameters as the integer check. * 'boolean': matches boolean values - ``True`` or ``False`` Acceptable string values for True are : true, on, yes, 1 Acceptable string values for False are : false, off, no, 0 Any other value raises an error. * 'ip_addr': matches an Internet Protocol address, v.4, represented by a dotted-quad string, i.e. '1.2.3.4'. * 'string': matches any string. Takes optional keyword args 'min' and 'max' to specify min and max lengths of the string. * 'list': matches any list. Takes optional keyword args 'min', and 'max' to specify min and max sizes of the list. (Always returns a list.) * 'tuple': matches any tuple. Takes optional keyword args 'min', and 'max' to specify min and max sizes of the tuple. (Always returns a tuple.) * 'int_list': Matches a list of integers. Takes the same arguments as list. * 'float_list': Matches a list of floats. Takes the same arguments as list. * 'bool_list': Matches a list of boolean values. Takes the same arguments as list. * 'ip_addr_list': Matches a list of IP addresses. Takes the same arguments as list. * 'string_list': Matches a list of strings. Takes the same arguments as list. * 'mixed_list': Matches a list with different types in specific positions. List size must match the number of arguments. Each position can be one of : 'integer', 'float', 'ip_addr', 'string', 'boolean' So to specify a list with two strings followed by two integers, you write the check as : :: mixed_list('string', 'string', 'integer', 'integer') * 'pass': This check matches everything ! It never fails and the value is unchanged. It is also the default if no check is specified. * 'option': This check matches any from a list of options. You specify this check with : :: option('option 1', 'option 2', 'option 3') You can supply a default value (returned if no value is supplied) using the default keyword argument. You specify a list argument for default using a list constructor syntax in the check : :: checkname(arg1, arg2, default=list('val 1', 'val 2', 'val 3')) A badly formatted set of arguments will raise a ``VdtParamError``. """ __docformat__ = "restructuredtext en" __version__ = '1.0.0' __revision__ = '$Id: validate.py 123 2005-09-08 08:54:28Z fuzzyman $' __all__ = ( '__version__', 'dottedQuadToNum', 'numToDottedQuad', 'ValidateError', 'VdtUnknownCheckError', 'VdtParamError', 'VdtTypeError', 'VdtValueError', 'VdtValueTooSmallError', 'VdtValueTooBigError', 'VdtValueTooShortError', 'VdtValueTooLongError', 'VdtMissingValue', 'Validator', 'is_integer', 'is_float', 'is_boolean', 'is_list', 'is_tuple', 'is_ip_addr', 'is_string', 'is_int_list', 'is_bool_list', 'is_float_list', 'is_string_list', 'is_ip_addr_list', 'is_mixed_list', 'is_option', '__docformat__', ) import re _list_arg = re.compile(r''' (?: ([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*list\( ( (?: \s* (?: (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\s\)][^,\)]*?) # unquoted ) \s*,\s* )* (?: (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\s\)][^,\)]*?) # unquoted )? # last one ) \) ) ''', re.VERBOSE | re.DOTALL) # two groups _list_members = re.compile(r''' ( (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\s=][^,=]*?) # unquoted ) (?: (?:\s*,\s*)|(?:\s*$) # comma ) ''', re.VERBOSE | re.DOTALL) # one group _paramstring = r''' (?: ( (?: [a-zA-Z_][a-zA-Z0-9_]*\s*=\s*list\( (?: \s* (?: (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\s\)][^,\)]*?) # unquoted ) \s*,\s* )* (?: (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\s\)][^,\)]*?) # unquoted )? # last one \) )| (?: (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\s=][^,=]*?)| # unquoted (?: # keyword argument [a-zA-Z_][a-zA-Z0-9_]*\s*=\s* (?: (?:".*?")| # double quotes (?:'.*?')| # single quotes (?:[^'",\s=][^,=]*?) # unquoted ) ) ) ) (?: (?:\s*,\s*)|(?:\s*$) # comma ) ) ''' _matchstring = '^%s*' % _paramstring # Python pre 2.2.1 doesn't have bool try: bool except NameError: def bool(val): """Simple boolean equivalent function. """ if val: return 1 else: return 0 def dottedQuadToNum(ip): """ Convert decimal dotted quad string to long integer >>> int(dottedQuadToNum('1 ')) 1 >>> int(dottedQuadToNum(' 1.2')) 16777218 >>> int(dottedQuadToNum(' 1.2.3 ')) 16908291 >>> int(dottedQuadToNum('1.2.3.4')) 16909060 >>> dottedQuadToNum('1.2.3. 4') 16909060 >>> dottedQuadToNum('255.255.255.255') 4294967295L >>> dottedQuadToNum('255.255.255.256') Traceback (most recent call last): ValueError: Not a good dotted-quad IP: 255.255.255.256 """ # import here to avoid it when ip_addr values are not used import socket, struct try: return struct.unpack('!L', socket.inet_aton(ip.strip()))[0] except socket.error: # bug in inet_aton, corrected in Python 2.3 if ip.strip() == '255.255.255.255': return 0xFFFFFFFFL else: raise ValueError('Not a good dotted-quad IP: %s' % ip) return def numToDottedQuad(num): """ Convert long int to dotted quad string >>> numToDottedQuad(-1L) Traceback (most recent call last): ValueError: Not a good numeric IP: -1 >>> numToDottedQuad(1L) '0.0.0.1' >>> numToDottedQuad(16777218L) '1.0.0.2' >>> numToDottedQuad(16908291L) '1.2.0.3' >>> numToDottedQuad(16909060L) '1.2.3.4' >>> numToDottedQuad(4294967295L) '255.255.255.255' >>> numToDottedQuad(4294967296L) Traceback (most recent call last): ValueError: Not a good numeric IP: 4294967296 """ # import here to avoid it when ip_addr values are not used import socket, struct # no need to intercept here, 4294967295L is fine if num > 4294967295L or num < 0: raise ValueError('Not a good numeric IP: %s' % num) try: return socket.inet_ntoa( struct.pack('!L', long(num))) except (socket.error, struct.error, OverflowError): raise ValueError('Not a good numeric IP: %s' % num) class ValidateError(Exception): """ This error indicates that the check failed. It can be the base class for more specific errors. Any check function that fails ought to raise this error. (or a subclass) >>> raise ValidateError Traceback (most recent call last): ValidateError """ class VdtMissingValue(ValidateError): """No value was supplied to a check that needed one.""" class VdtUnknownCheckError(ValidateError): """An unknown check function was requested""" def __init__(self, value): """ >>> raise VdtUnknownCheckError('yoda') Traceback (most recent call last): VdtUnknownCheckError: the check "yoda" is unknown. """ ValidateError.__init__(self, 'the check "%s" is unknown.' % (value,)) class VdtParamError(SyntaxError): """An incorrect parameter was passed""" def __init__(self, name, value): """ >>> raise VdtParamError('yoda', 'jedi') Traceback (most recent call last): VdtParamError: passed an incorrect value "jedi" for parameter "yoda". """ SyntaxError.__init__(self, 'passed an incorrect value "%s" for parameter "%s".' % (value, name)) class VdtTypeError(ValidateError): """The value supplied was of the wrong type""" def __init__(self, value): """ >>> raise VdtTypeError('jedi') Traceback (most recent call last): VdtTypeError: the value "jedi" is of the wrong type. """ ValidateError.__init__(self, 'the value "%s" is of the wrong type.' % (value,)) class VdtValueError(ValidateError): """The value supplied was of the correct type, but was not an allowed value.""" def __init__(self, value): """ >>> raise VdtValueError('jedi') Traceback (most recent call last): VdtValueError: the value "jedi" is unacceptable. """ ValidateError.__init__(self, 'the value "%s" is unacceptable.' % (value,)) class VdtValueTooSmallError(VdtValueError): """The value supplied was of the correct type, but was too small.""" def __init__(self, value): """ >>> raise VdtValueTooSmallError('0') Traceback (most recent call last): VdtValueTooSmallError: the value "0" is too small. """ ValidateError.__init__(self, 'the value "%s" is too small.' % (value,)) class VdtValueTooBigError(VdtValueError): """The value supplied was of the correct type, but was too big.""" def __init__(self, value): """ >>> raise VdtValueTooBigError('1') Traceback (most recent call last): VdtValueTooBigError: the value "1" is too big. """ ValidateError.__init__(self, 'the value "%s" is too big.' % (value,)) class VdtValueTooShortError(VdtValueError): """The value supplied was of the correct type, but was too short.""" def __init__(self, value): """ >>> raise VdtValueTooShortError('jed') Traceback (most recent call last): VdtValueTooShortError: the value "jed" is too short. """ ValidateError.__init__( self, 'the value "%s" is too short.' % (value,)) class VdtValueTooLongError(VdtValueError): """The value supplied was of the correct type, but was too long.""" def __init__(self, value): """ >>> raise VdtValueTooLongError('jedie') Traceback (most recent call last): VdtValueTooLongError: the value "jedie" is too long. """ ValidateError.__init__(self, 'the value "%s" is too long.' % (value,)) class Validator(object): """ Validator is an object that allows you to register a set of 'checks'. These checks take input and test that it conforms to the check. This can also involve converting the value from a string into the correct datatype. The ``check`` method takes an input string which configures which check is to be used and applies that check to a supplied value. An example input string would be: 'int_range(param1, param2)' You would then provide something like: >>> def int_range_check(value, min, max): ... # turn min and max from strings to integers ... min = int(min) ... max = int(max) ... # check that value is of the correct type. ... # possible valid inputs are integers or strings ... # that represent integers ... if not isinstance(value, (int, long, basestring)): ... raise VdtTypeError(value) ... elif isinstance(value, basestring): ... # if we are given a string ... # attempt to convert to an integer ... try: ... value = int(value) ... except ValueError: ... raise VdtValueError(value) ... # check the value is between our constraints ... if not min <= value: ... raise VdtValueTooSmallError(value) ... if not value <= max: ... raise VdtValueTooBigError(value) ... return value >>> fdict = {'int_range': int_range_check} >>> vtr1 = Validator(fdict) >>> vtr1.check('int_range(20, 40)', '30') 30 >>> vtr1.check('int_range(20, 40)', '60') Traceback (most recent call last): VdtValueTooBigError: the value "60" is too big. New functions can be added with : :: >>> vtr2 = Validator() >>> vtr2.functions['int_range'] = int_range_check Or by passing in a dictionary of functions when Validator is instantiated. Your functions *can* use keyword arguments, but the first argument should always be 'value'. If the function doesn't take additional arguments, the parentheses are optional in the check. It can be written with either of : :: keyword = function_name keyword = function_name() The first program to utilise Validator() was Michael Foord's ConfigObj, an alternative to ConfigParser which supports lists and can validate a config file using a config schema. For more details on using Validator with ConfigObj see: http://www.voidspace.org.uk/python/configobj.html """ # this regex does the initial parsing of the checks _func_re = re.compile(r'(.+?)\((.*)\)', re.DOTALL) # this regex takes apart keyword arguments _key_arg = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$', re.DOTALL) # this regex finds keyword=list(....) type values _list_arg = _list_arg # this regex takes individual values out of lists - in one pass _list_members = _list_members # These regexes check a set of arguments for validity # and then pull the members out _paramfinder = re.compile(_paramstring, re.VERBOSE | re.DOTALL) _matchfinder = re.compile(_matchstring, re.VERBOSE | re.DOTALL) def __init__(self, functions=None): """ >>> vtri = Validator() """ self.functions = { '': self._pass, 'integer': is_integer, 'float': is_float, 'boolean': is_boolean, 'ip_addr': is_ip_addr, 'string': is_string, 'list': is_list, 'tuple': is_tuple, 'int_list': is_int_list, 'float_list': is_float_list, 'bool_list': is_bool_list, 'ip_addr_list': is_ip_addr_list, 'string_list': is_string_list, 'mixed_list': is_mixed_list, 'pass': self._pass, 'option': is_option, 'force_list': force_list, } if functions is not None: self.functions.update(functions) # tekNico: for use by ConfigObj self.baseErrorClass = ValidateError self._cache = {} def check(self, check, value, missing=False): """ Usage: check(check, value) Arguments: check: string representing check to apply (including arguments) value: object to be checked Returns value, converted to correct type if necessary If the check fails, raises a ``ValidateError`` subclass. >>> vtor.check('yoda', '') Traceback (most recent call last): VdtUnknownCheckError: the check "yoda" is unknown. >>> vtor.check('yoda()', '') Traceback (most recent call last): VdtUnknownCheckError: the check "yoda" is unknown. >>> vtor.check('string(default="")', '', missing=True) '' """ fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check) if missing: if default is None: # no information needed here - to be handled by caller raise VdtMissingValue() value = self._handle_none(default) if value is None: return None return self._check_value(value, fun_name, fun_args, fun_kwargs) def _handle_none(self, value): if value == 'None': value = None elif value in ("'None'", '"None"'): # Special case a quoted None value = self._unquote(value) return value def _parse_with_caching(self, check): if check in self._cache: fun_name, fun_args, fun_kwargs, default = self._cache[check] # We call list and dict below to work with *copies* of the data # rather than the original (which are mutable of course) fun_args = list(fun_args) fun_kwargs = dict(fun_kwargs) else: fun_name, fun_args, fun_kwargs, default = self._parse_check(check) fun_kwargs = dict((str(key), value) for (key, value) in fun_kwargs.items()) self._cache[check] = fun_name, list(fun_args), dict(fun_kwargs), default return fun_name, fun_args, fun_kwargs, default def _check_value(self, value, fun_name, fun_args, fun_kwargs): try: fun = self.functions[fun_name] except KeyError: raise VdtUnknownCheckError(fun_name) else: return fun(value, *fun_args, **fun_kwargs) def _parse_check(self, check): fun_match = self._func_re.match(check) if fun_match: fun_name = fun_match.group(1) arg_string = fun_match.group(2) arg_match = self._matchfinder.match(arg_string) if arg_match is None: # Bad syntax raise VdtParamError('Bad syntax in check "%s".' % check) fun_args = [] fun_kwargs = {} # pull out args of group 2 for arg in self._paramfinder.findall(arg_string): # args may need whitespace removing (before removing quotes) arg = arg.strip() listmatch = self._list_arg.match(arg) if listmatch: key, val = self._list_handle(listmatch) fun_kwargs[key] = val continue keymatch = self._key_arg.match(arg) if keymatch: val = keymatch.group(2) if not val in ("'None'", '"None"'): # Special case a quoted None val = self._unquote(val) fun_kwargs[keymatch.group(1)] = val continue fun_args.append(self._unquote(arg)) else: # allows for function names without (args) return check, (), {}, None # Default must be deleted if the value is specified too, # otherwise the check function will get a spurious "default" keyword arg try: default = fun_kwargs.pop('default', None) except AttributeError: # Python 2.2 compatibility default = None try: default = fun_kwargs['default'] del fun_kwargs['default'] except KeyError: pass return fun_name, fun_args, fun_kwargs, default def _unquote(self, val): """Unquote a value if necessary.""" if (len(val) >= 2) and (val[0] in ("'", '"')) and (val[0] == val[-1]): val = val[1:-1] return val def _list_handle(self, listmatch): """Take apart a ``keyword=list('val, 'val')`` type string.""" out = [] name = listmatch.group(1) args = listmatch.group(2) for arg in self._list_members.findall(args): out.append(self._unquote(arg)) return name, out def _pass(self, value): """ Dummy check that always passes >>> vtor.check('', 0) 0 >>> vtor.check('', '0') '0' """ return value def get_default_value(self, check): """ Given a check, return the default value for the check (converted to the right type). If the check doesn't specify a default value then a ``KeyError`` will be raised. """ fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check) if default is None: raise KeyError('Check "%s" has no default value.' % check) value = self._handle_none(default) if value is None: return value return self._check_value(value, fun_name, fun_args, fun_kwargs) def _is_num_param(names, values, to_float=False): """ Return numbers from inputs or raise VdtParamError. Lets ``None`` pass through. Pass in keyword argument ``to_float=True`` to use float for the conversion rather than int. >>> _is_num_param(('', ''), (0, 1.0)) [0, 1] >>> _is_num_param(('', ''), (0, 1.0), to_float=True) [0.0, 1.0] >>> _is_num_param(('a'), ('a')) Traceback (most recent call last): VdtParamError: passed an incorrect value "a" for parameter "a". """ fun = to_float and float or int out_params = [] for (name, val) in zip(names, values): if val is None: out_params.append(val) elif isinstance(val, (int, long, float, basestring)): try: out_params.append(fun(val)) except ValueError, e: raise VdtParamError(name, val) else: raise VdtParamError(name, val) return out_params # built in checks # you can override these by setting the appropriate name # in Validator.functions # note: if the params are specified wrongly in your input string, # you will also raise errors. def is_integer(value, min=None, max=None): """ A check that tests that a given value is an integer (int, or long) and optionally, between bounds. A negative value is accepted, while a float will fail. If the value is a string, then the conversion is done - if possible. Otherwise a VdtError is raised. >>> vtor.check('integer', '-1') -1 >>> vtor.check('integer', '0') 0 >>> vtor.check('integer', 9) 9 >>> vtor.check('integer', 'a') Traceback (most recent call last): VdtTypeError: the value "a" is of the wrong type. >>> vtor.check('integer', '2.2') Traceback (most recent call last): VdtTypeError: the value "2.2" is of the wrong type. >>> vtor.check('integer(10)', '20') 20 >>> vtor.check('integer(max=20)', '15') 15 >>> vtor.check('integer(10)', '9') Traceback (most recent call last): VdtValueTooSmallError: the value "9" is too small. >>> vtor.check('integer(10)', 9) Traceback (most recent call last): VdtValueTooSmallError: the value "9" is too small. >>> vtor.check('integer(max=20)', '35') Traceback (most recent call last): VdtValueTooBigError: the value "35" is too big. >>> vtor.check('integer(max=20)', 35) Traceback (most recent call last): VdtValueTooBigError: the value "35" is too big. >>> vtor.check('integer(0, 9)', False) 0 """ (min_val, max_val) = _is_num_param(('min', 'max'), (min, max)) if not isinstance(value, (int, long, basestring)): raise VdtTypeError(value) if isinstance(value, basestring): # if it's a string - does it represent an integer ? try: value = int(value) except ValueError: raise VdtTypeError(value) if (min_val is not None) and (value < min_val): raise VdtValueTooSmallError(value) if (max_val is not None) and (value > max_val): raise VdtValueTooBigError(value) return value def is_float(value, min=None, max=None): """ A check that tests that a given value is a float (an integer will be accepted), and optionally - that it is between bounds. If the value is a string, then the conversion is done - if possible. Otherwise a VdtError is raised. This can accept negative values. >>> vtor.check('float', '2') 2.0 From now on we multiply the value to avoid comparing decimals >>> vtor.check('float', '-6.8') * 10 -68.0 >>> vtor.check('float', '12.2') * 10 122.0 >>> vtor.check('float', 8.4) * 10 84.0 >>> vtor.check('float', 'a') Traceback (most recent call last): VdtTypeError: the value "a" is of the wrong type. >>> vtor.check('float(10.1)', '10.2') * 10 102.0 >>> vtor.check('float(max=20.2)', '15.1') * 10 151.0 >>> vtor.check('float(10.0)', '9.0') Traceback (most recent call last): VdtValueTooSmallError: the value "9.0" is too small. >>> vtor.check('float(max=20.0)', '35.0') Traceback (most recent call last): VdtValueTooBigError: the value "35.0" is too big. """ (min_val, max_val) = _is_num_param( ('min', 'max'), (min, max), to_float=True) if not isinstance(value, (int, long, float, basestring)): raise VdtTypeError(value) if not isinstance(value, float): # if it's a string - does it represent a float ? try: value = float(value) except ValueError: raise VdtTypeError(value) if (min_val is not None) and (value < min_val): raise VdtValueTooSmallError(value) if (max_val is not None) and (value > max_val): raise VdtValueTooBigError(value) return value bool_dict = { True: True, 'on': True, '1': True, 'true': True, 'yes': True, False: False, 'off': False, '0': False, 'false': False, 'no': False, } def is_boolean(value): """ Check if the value represents a boolean. >>> vtor.check('boolean', 0) 0 >>> vtor.check('boolean', False) 0 >>> vtor.check('boolean', '0') 0 >>> vtor.check('boolean', 'off') 0 >>> vtor.check('boolean', 'false') 0 >>> vtor.check('boolean', 'no') 0 >>> vtor.check('boolean', 'nO') 0 >>> vtor.check('boolean', 'NO') 0 >>> vtor.check('boolean', 1) 1 >>> vtor.check('boolean', True) 1 >>> vtor.check('boolean', '1') 1 >>> vtor.check('boolean', 'on') 1 >>> vtor.check('boolean', 'true') 1 >>> vtor.check('boolean', 'yes') 1 >>> vtor.check('boolean', 'Yes') 1 >>> vtor.check('boolean', 'YES') 1 >>> vtor.check('boolean', '') Traceback (most recent call last): VdtTypeError: the value "" is of the wrong type. >>> vtor.check('boolean', 'up') Traceback (most recent call last): VdtTypeError: the value "up" is of the wrong type. """ if isinstance(value, basestring): try: return bool_dict[value.lower()] except KeyError: raise VdtTypeError(value) # we do an equality test rather than an identity test # this ensures Python 2.2 compatibilty # and allows 0 and 1 to represent True and False if value == False: return False elif value == True: return True else: raise VdtTypeError(value) def is_ip_addr(value): """ Check that the supplied value is an Internet Protocol address, v.4, represented by a dotted-quad string, i.e. '1.2.3.4'. >>> vtor.check('ip_addr', '1 ') '1' >>> vtor.check('ip_addr', ' 1.2') '1.2' >>> vtor.check('ip_addr', ' 1.2.3 ') '1.2.3' >>> vtor.check('ip_addr', '1.2.3.4') '1.2.3.4' >>> vtor.check('ip_addr', '0.0.0.0') '0.0.0.0' >>> vtor.check('ip_addr', '255.255.255.255') '255.255.255.255' >>> vtor.check('ip_addr', '255.255.255.256') Traceback (most recent call last): VdtValueError: the value "255.255.255.256" is unacceptable. >>> vtor.check('ip_addr', '1.2.3.4.5') Traceback (most recent call last): VdtValueError: the value "1.2.3.4.5" is unacceptable. >>> vtor.check('ip_addr', 0) Traceback (most recent call last): VdtTypeError: the value "0" is of the wrong type. """ if not isinstance(value, basestring): raise VdtTypeError(value) value = value.strip() try: dottedQuadToNum(value) except ValueError: raise VdtValueError(value) return value def is_list(value, min=None, max=None): """ Check that the value is a list of values. You can optionally specify the minimum and maximum number of members. It does no check on list members. >>> vtor.check('list', ()) [] >>> vtor.check('list', []) [] >>> vtor.check('list', (1, 2)) [1, 2] >>> vtor.check('list', [1, 2]) [1, 2] >>> vtor.check('list(3)', (1, 2)) Traceback (most recent call last): VdtValueTooShortError: the value "(1, 2)" is too short. >>> vtor.check('list(max=5)', (1, 2, 3, 4, 5, 6)) Traceback (most recent call last): VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long. >>> vtor.check('list(min=3, max=5)', (1, 2, 3, 4)) [1, 2, 3, 4] >>> vtor.check('list', 0) Traceback (most recent call last): VdtTypeError: the value "0" is of the wrong type. >>> vtor.check('list', '12') Traceback (most recent call last): VdtTypeError: the value "12" is of the wrong type. """ (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) if isinstance(value, basestring): raise VdtTypeError(value) try: num_members = len(value) except TypeError: raise VdtTypeError(value) if min_len is not None and num_members < min_len: raise VdtValueTooShortError(value) if max_len is not None and num_members > max_len: raise VdtValueTooLongError(value) return list(value) def is_tuple(value, min=None, max=None): """ Check that the value is a tuple of values. You can optionally specify the minimum and maximum number of members. It does no check on members. >>> vtor.check('tuple', ()) () >>> vtor.check('tuple', []) () >>> vtor.check('tuple', (1, 2)) (1, 2) >>> vtor.check('tuple', [1, 2]) (1, 2) >>> vtor.check('tuple(3)', (1, 2)) Traceback (most recent call last): VdtValueTooShortError: the value "(1, 2)" is too short. >>> vtor.check('tuple(max=5)', (1, 2, 3, 4, 5, 6)) Traceback (most recent call last): VdtValueTooLongError: the value "(1, 2, 3, 4, 5, 6)" is too long. >>> vtor.check('tuple(min=3, max=5)', (1, 2, 3, 4)) (1, 2, 3, 4) >>> vtor.check('tuple', 0) Traceback (most recent call last): VdtTypeError: the value "0" is of the wrong type. >>> vtor.check('tuple', '12') Traceback (most recent call last): VdtTypeError: the value "12" is of the wrong type. """ return tuple(is_list(value, min, max)) def is_string(value, min=None, max=None): """ Check that the supplied value is a string. You can optionally specify the minimum and maximum number of members. >>> vtor.check('string', '0') '0' >>> vtor.check('string', 0) Traceback (most recent call last): VdtTypeError: the value "0" is of the wrong type. >>> vtor.check('string(2)', '12') '12' >>> vtor.check('string(2)', '1') Traceback (most recent call last): VdtValueTooShortError: the value "1" is too short. >>> vtor.check('string(min=2, max=3)', '123') '123' >>> vtor.check('string(min=2, max=3)', '1234') Traceback (most recent call last): VdtValueTooLongError: the value "1234" is too long. """ if not isinstance(value, basestring): raise VdtTypeError(value) (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) try: num_members = len(value) except TypeError: raise VdtTypeError(value) if min_len is not None and num_members < min_len: raise VdtValueTooShortError(value) if max_len is not None and num_members > max_len: raise VdtValueTooLongError(value) return value def is_int_list(value, min=None, max=None): """ Check that the value is a list of integers. You can optionally specify the minimum and maximum number of members. Each list member is checked that it is an integer. >>> vtor.check('int_list', ()) [] >>> vtor.check('int_list', []) [] >>> vtor.check('int_list', (1, 2)) [1, 2] >>> vtor.check('int_list', [1, 2]) [1, 2] >>> vtor.check('int_list', [1, 'a']) Traceback (most recent call last): VdtTypeError: the value "a" is of the wrong type. """ return [is_integer(mem) for mem in is_list(value, min, max)] def is_bool_list(value, min=None, max=None): """ Check that the value is a list of booleans. You can optionally specify the minimum and maximum number of members. Each list member is checked that it is a boolean. >>> vtor.check('bool_list', ()) [] >>> vtor.check('bool_list', []) [] >>> check_res = vtor.check('bool_list', (True, False)) >>> check_res == [True, False] 1 >>> check_res = vtor.check('bool_list', [True, False]) >>> check_res == [True, False] 1 >>> vtor.check('bool_list', [True, 'a']) Traceback (most recent call last): VdtTypeError: the value "a" is of the wrong type. """ return [is_boolean(mem) for mem in is_list(value, min, max)] def is_float_list(value, min=None, max=None): """ Check that the value is a list of floats. You can optionally specify the minimum and maximum number of members. Each list member is checked that it is a float. >>> vtor.check('float_list', ()) [] >>> vtor.check('float_list', []) [] >>> vtor.check('float_list', (1, 2.0)) [1.0, 2.0] >>> vtor.check('float_list', [1, 2.0]) [1.0, 2.0] >>> vtor.check('float_list', [1, 'a']) Traceback (most recent call last): VdtTypeError: the value "a" is of the wrong type. """ return [is_float(mem) for mem in is_list(value, min, max)] def is_string_list(value, min=None, max=None): """ Check that the value is a list of strings. You can optionally specify the minimum and maximum number of members. Each list member is checked that it is a string. >>> vtor.check('string_list', ()) [] >>> vtor.check('string_list', []) [] >>> vtor.check('string_list', ('a', 'b')) ['a', 'b'] >>> vtor.check('string_list', ['a', 1]) Traceback (most recent call last): VdtTypeError: the value "1" is of the wrong type. >>> vtor.check('string_list', 'hello') Traceback (most recent call last): VdtTypeError: the value "hello" is of the wrong type. """ if isinstance(value, basestring): raise VdtTypeError(value) return [is_string(mem) for mem in is_list(value, min, max)] def is_ip_addr_list(value, min=None, max=None): """ Check that the value is a list of IP addresses. You can optionally specify the minimum and maximum number of members. Each list member is checked that it is an IP address. >>> vtor.check('ip_addr_list', ()) [] >>> vtor.check('ip_addr_list', []) [] >>> vtor.check('ip_addr_list', ('1.2.3.4', '5.6.7.8')) ['1.2.3.4', '5.6.7.8'] >>> vtor.check('ip_addr_list', ['a']) Traceback (most recent call last): VdtValueError: the value "a" is unacceptable. """ return [is_ip_addr(mem) for mem in is_list(value, min, max)] def force_list(value, min=None, max=None): """ Check that a value is a list, coercing strings into a list with one member. Useful where users forget the trailing comma that turns a single value into a list. You can optionally specify the minimum and maximum number of members. A minumum of greater than one will fail if the user only supplies a string. >>> vtor.check('force_list', ()) [] >>> vtor.check('force_list', []) [] >>> vtor.check('force_list', 'hello') ['hello'] """ if not isinstance(value, (list, tuple)): value = [value] return is_list(value, min, max) fun_dict = { 'integer': is_integer, 'float': is_float, 'ip_addr': is_ip_addr, 'string': is_string, 'boolean': is_boolean, } def is_mixed_list(value, *args): """ Check that the value is a list. Allow specifying the type of each member. Work on lists of specific lengths. You specify each member as a positional argument specifying type Each type should be one of the following strings : 'integer', 'float', 'ip_addr', 'string', 'boolean' So you can specify a list of two strings, followed by two integers as : mixed_list('string', 'string', 'integer', 'integer') The length of the list must match the number of positional arguments you supply. >>> mix_str = "mixed_list('integer', 'float', 'ip_addr', 'string', 'boolean')" >>> check_res = vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', True)) >>> check_res == [1, 2.0, '1.2.3.4', 'a', True] 1 >>> check_res = vtor.check(mix_str, ('1', '2.0', '1.2.3.4', 'a', 'True')) >>> check_res == [1, 2.0, '1.2.3.4', 'a', True] 1 >>> vtor.check(mix_str, ('b', 2.0, '1.2.3.4', 'a', True)) Traceback (most recent call last): VdtTypeError: the value "b" is of the wrong type. >>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a')) Traceback (most recent call last): VdtValueTooShortError: the value "(1, 2.0, '1.2.3.4', 'a')" is too short. >>> vtor.check(mix_str, (1, 2.0, '1.2.3.4', 'a', 1, 'b')) Traceback (most recent call last): VdtValueTooLongError: the value "(1, 2.0, '1.2.3.4', 'a', 1, 'b')" is too long. >>> vtor.check(mix_str, 0) Traceback (most recent call last): VdtTypeError: the value "0" is of the wrong type. This test requires an elaborate setup, because of a change in error string output from the interpreter between Python 2.2 and 2.3 . >>> res_seq = ( ... 'passed an incorrect value "', ... 'yoda', ... '" for parameter "mixed_list".', ... ) >>> res_str = "'".join(res_seq) >>> try: ... vtor.check('mixed_list("yoda")', ('a')) ... except VdtParamError, err: ... str(err) == res_str 1 """ try: length = len(value) except TypeError: raise VdtTypeError(value) if length < len(args): raise VdtValueTooShortError(value) elif length > len(args): raise VdtValueTooLongError(value) try: return [fun_dict[arg](val) for arg, val in zip(args, value)] except KeyError, e: raise VdtParamError('mixed_list', e) def is_option(value, *options): """ This check matches the value to any of a set of options. >>> vtor.check('option("yoda", "jedi")', 'yoda') 'yoda' >>> vtor.check('option("yoda", "jedi")', 'jed') Traceback (most recent call last): VdtValueError: the value "jed" is unacceptable. >>> vtor.check('option("yoda", "jedi")', 0) Traceback (most recent call last): VdtTypeError: the value "0" is of the wrong type. """ if not isinstance(value, basestring): raise VdtTypeError(value) if not value in options: raise VdtValueError(value) return value def _test(value, *args, **keywargs): """ A function that exists for test purposes. >>> checks = [ ... '3, 6, min=1, max=3, test=list(a, b, c)', ... '3', ... '3, 6', ... '3,', ... 'min=1, test="a b c"', ... 'min=5, test="a, b, c"', ... 'min=1, max=3, test="a, b, c"', ... 'min=-100, test=-99', ... 'min=1, max=3', ... '3, 6, test="36"', ... '3, 6, test="a, b, c"', ... '3, max=3, test=list("a", "b", "c")', ... '''3, max=3, test=list("'a'", 'b', "x=(c)")''', ... "test='x=fish(3)'", ... ] >>> v = Validator({'test': _test}) >>> for entry in checks: ... print v.check(('test(%s)' % entry), 3) (3, ('3', '6'), {'test': ['a', 'b', 'c'], 'max': '3', 'min': '1'}) (3, ('3',), {}) (3, ('3', '6'), {}) (3, ('3',), {}) (3, (), {'test': 'a b c', 'min': '1'}) (3, (), {'test': 'a, b, c', 'min': '5'}) (3, (), {'test': 'a, b, c', 'max': '3', 'min': '1'}) (3, (), {'test': '-99', 'min': '-100'}) (3, (), {'max': '3', 'min': '1'}) (3, ('3', '6'), {'test': '36'}) (3, ('3', '6'), {'test': 'a, b, c'}) (3, ('3',), {'test': ['a', 'b', 'c'], 'max': '3'}) (3, ('3',), {'test': ["'a'", 'b', 'x=(c)'], 'max': '3'}) (3, (), {'test': 'x=fish(3)'}) >>> v = Validator() >>> v.check('integer(default=6)', '3') 3 >>> v.check('integer(default=6)', None, True) 6 >>> v.get_default_value('integer(default=6)') 6 >>> v.get_default_value('float(default=6)') 6.0 >>> v.get_default_value('pass(default=None)') >>> v.get_default_value("string(default='None')") 'None' >>> v.get_default_value('pass') Traceback (most recent call last): KeyError: 'Check "pass" has no default value.' >>> v.get_default_value('pass(default=list(1, 2, 3, 4))') ['1', '2', '3', '4'] >>> v = Validator() >>> v.check("pass(default=None)", None, True) >>> v.check("pass(default='None')", None, True) 'None' >>> v.check('pass(default="None")', None, True) 'None' >>> v.check('pass(default=list(1, 2, 3, 4))', None, True) ['1', '2', '3', '4'] Bug test for unicode arguments >>> v = Validator() >>> v.check(u'string(min=4)', u'test') u'test' >>> v = Validator() >>> v.get_default_value(u'string(min=4, default="1234")') u'1234' >>> v.check(u'string(min=4, default="1234")', u'test') u'test' >>> v = Validator() >>> default = v.get_default_value('string(default=None)') >>> default == None 1 """ return (value, args, keywargs) def _test2(): """ >>> >>> v = Validator() >>> v.get_default_value('string(default="#ff00dd")') '#ff00dd' >>> v.get_default_value('integer(default=3) # comment') 3 """ def _test3(): r""" >>> vtor.check('string(default="")', '', missing=True) '' >>> vtor.check('string(default="\n")', '', missing=True) '\n' >>> print vtor.check('string(default="\n")', '', missing=True), >>> vtor.check('string()', '\n') '\n' >>> vtor.check('string(default="\n\n\n")', '', missing=True) '\n\n\n' >>> vtor.check('string()', 'random \n text goes here\n\n') 'random \n text goes here\n\n' >>> vtor.check('string(default=" \nrandom text\ngoes \n here\n\n ")', ... '', missing=True) ' \nrandom text\ngoes \n here\n\n ' >>> vtor.check("string(default='\n\n\n')", '', missing=True) '\n\n\n' >>> vtor.check("option('\n','a','b',default='\n')", '', missing=True) '\n' >>> vtor.check("string_list()", ['foo', '\n', 'bar']) ['foo', '\n', 'bar'] >>> vtor.check("string_list(default=list('\n'))", '', missing=True) ['\n'] """ if __name__ == '__main__': # run the code tests in doctest format import sys import doctest m = sys.modules.get('__main__') globs = m.__dict__.copy() globs.update({ 'vtor': Validator(), }) doctest.testmod(m, globs=globs) whyteboard-0.41.1/whyteboard/lib/icon.py0000777000175000017500000001037111443255427017243 0ustar stevesteve#---------------------------------------------------------------------- # This file was generated by C:\Python26\Scripts\img2py # from wx.lib.embeddedimage import PyEmbeddedImage whyteboard = PyEmbeddedImage( "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAACkNJ" "REFUWIWNlnt01dWVxz+/+3vdZ3KT3FySAElIQgIkEBRUoDyiAYqDVcQWGeowfeDqiLZO69Ri" "dbm0a8a1ptS2tpXOTHWplRIftKU6YhaIokReARMgPPJ+50Jy877v+3vMHxdibsCpZ629ztnn" "7LP397vPU2BSSZ3v3D7r+9un/fb+gedWpL4SnTzWvvus+nhFxson5730leeP9s2v8wVz/RHd" "DYLgkIRQYZp6+d656Y1dO249ebD70gen/vdpH1+iCJOVhmJ2di2p+Lf7H/reR4u/2rz+o7Gn" "w//oebZ03tnRR5/+uG9TzGlPlewqmCa+nmGOHGpE1w2WrSoit9CLABi6QWxo3KicYT/y8Ceb" "f/+96t1vB468ZnwpANM2cfOTDeKptY/8WNhwT+W7t0cOD//q7JUH1Ay3ZbJda6OPrQcsNN+9" "FVMUyd//Bi/dPEj54llJzvVIlAItcP5fv/vn7//IN/bR3wUAsH+RsL8/JffOu3/yE4bDAxRk" "Xw/+mbMKz67+aVLfQwd/ya7yUUb84xyqOkZ/7zAFpdO5fdNtEAwaBzfk7Nz83td/GlhTlORQ" "nOp8+Zub0833Pr1zeECjrGIln31cS9+ZUwjROPbMaVisdvbl3c4JZUbSvOJsB3eFW3mnahcr" "Hm9hxSODkNZOTdUgZatWCYUPRJd3Tj9WvOV03l8PdJ8zbwjgkd2PLn7KmrXHNxSQBxqOI0hZ" "FFdW8MYfqzhx8iTDzc0UlJaRZpV5XSrBsCSmC6bBc/FaMgbOs2j7YVKzwSJDRh6kF/mx6Xch" "Wh38Q1Cd3/nMTNdfzOoD1wEo/12ucrro5oOP5czOunjoQ6JhP/1NLUzLnUf5nXdqS2PNvznc" "2JkXudLvqlhxK+v1HqwYLDL8PK/Xskby48iOIWYcTsqMM0PCMv51EBUsqp3iTaNLpCOjRw4P" "9bUDTGwu7cAftnVnzSuJB4JEAr72W9Kk123qMEf3vEqKKUmp6x5eVvLYhqVDl7t7tFCERfYY" "LyjneVE+xwo1CIodwSiByMLkNR27FxQHqHZQ7agzZglvVa3/z2vDE5uwsPa5+paU2eVaNMRH" "c+p+sKr+jqr3/v3+lvGhcKojZyl37XiUB9LGfrZarhvfFly5U3S6EhPN5HimYIDtFEj9ECsi" "6s+l5k/VXGnvJX9BEUs2rUEfvMIep23Rt0u2fiYCPPsrPWu3t/7nstMjCDYn2/xRuVZd/9L+" "wH5tdXBsjb+tnXG/ybZpeUvWdYW9T81emIMocyMRRAXByEfQSjHimbz1tR8yZ++HLDzbSuhg" "Lcdb+yj7p43M2Gjt3On7U40FIH9nbrGcOVMwRRXDlDnkXry6wdxz4PjS+tOpzpkdaVkWmj7+" "C331LWp9LHURip0vI4ONPWxsaGUOkA7cBCx59xOimsDzdaPFE3vgZOV/eLG6wKKgaQKCM52a" "vMo7xvKqD6Wv/mZeWnY+bq/Jp6//D+blQeoa2kC2/13xOlOxTznmORYLqi2VQxe6vRMAFu/Y" "nWnE4hiCjG6KhPzjmLIdeeZs5m36pqAu3EjmjEzsziC1777PkeONmF/I3Pa5LJgPy29JRvCd" "TeBM4fjxugwACaCl/UI47OrGln8TugWigRiRrn7EjGxicY3C+x7klfrzzMs+zLmeXtavvY9T" "53opzJ+JICT28Y1qAWDfGwh79mHt7kG6rRTuXYepxeg7fzo6cQ+o70j28nzzu96bKtE0g7hh" "Ut/Ux29e3kt6eg7BsIm3dClv+xzE5lbiUBQMU8bERiCgEwgZBIMGobBJMGQSjkAoDKGISUST" "iJeVcypjGnlrb8ciygTrP8Woqzr4ixORdy0A2ourTzXUVI8YgRF0QcIQFP58oIZVy5Zi6eyg" "LBKm0pPBM/dswKqo1F1sxOVwkjLop8wiUCrAXExKDJ1iQ6dIi1OoxSmIx8mNxXDbHZTNm0N7" "lx/TUDn11u/ZVRH5YCIDHfsu6M+viOU0tfTclr/mnznb2MqZi8143G4KhocY6GjHCIXJ9Xgo" "dNg43utjcGgIhyzgCUdRMSESgWg0UU9qC5EIQS2ONcNDW0cXtksfcOitF/vXvZr9L/8VGNUm" "ruLqAuq9lzoezLEK1qozA8ydXYRt0M+0wDixUJj+3h4io2MUZ2eTp0gcautAi/dRIh+hr/UK" "GY6sz4NPARMJBgm53Qz4B7lQ/Rq/XdT5+DN7B45NZABg6CKBuf9d2DX+qbSxPWoXFpSVsRQT" "cWSY8ZER4pEI/T4foeFhyrKzyR07zdoVCqkL1+MYeZ++SzFSVVcyiKtAlFCIDpsdh9NJZm5h" "w5svXN420NVsJgEAePHkDy4cu8nz4KxZBS5rLMrKeJjMlBTcVpVQYJzhkWF8V3wM+C7T0+On" "tFzCUvINJGc2QyfeJ02dmWAeu5qBcBB8fVja2qgbGsYvSozFRYc90/Xrmurq+MQxvFY8x360" "uWH7QzmO1DQWh6MIogaA2wa3lMygZLqbhvZuLnS3EYhF+awmzi0LOuHKGRBGwNcEHW3Q3Ql9" "vTDgBz3x/+jPyufS5gdRFNlx/szZu4GqpAysq7xDGAlFXm9qaZmW4nBwv20EK2Eww2CEwYyg" "yjozM11kuBTOd3bT3NRHSWojDnGcy+1RMt/ZA75LELoMliC4TEgBXIAZ5Hj+YkRR4tWXX1bM" "eCwZQPb8xXdVV1f/sK29k6zeOram9IIZBEJghkAbhfAAjHbhDnYi+du52HyFkzUdpC/fgqN8" "JX2nP8GbEgYn14k3xWCvPh0lNZ1vbd0666zVs2u04XR4Ygk237dhx493nMQ0TbZM90O4F8IX" "EoOTn9yr7VvzwOuGDzs0XnnsCTIXVZBeeA/OkX3kqUPXPdMWE4ojLfhiRVxqblYa337lPiv8" "QQKou+xfsWHN2mUAHjXO+rJo8nd1irNrer4HvlOQUAPRw4RjhxlSZGJuUKTr55X42uiKxwEY" "GRzcjDslAaDz3LknBvyDAsCW0iCK54uD3kgXzMQyuwAv8WSbSXbLrKNU9Q2Q4fHw2mt/XPVm" "fVOO9MLe/QufePKpddfsYxaTurD8/wef2mdOaZhT0ne12x+R0bUosViM1o4OsWRezybhs6bW" "quWr7tj8ubkJ5o0iTimCgCBYsIgWFEVJiCwhyzKqoqLIMrIso8hyol+RUVUrNqsVWZaQJZlH" "H95eK7o93l0nak87pnj/EvI5Y+MaYEGYYCtYhMSTLAhJ7YStiYmJO8WVIpSvXH1x1szpcya7" "nHjTLRYsFgFRFBFFEUlKIL/GQJKlBFMpwVyWJCRZwulw4HQ4sFqtWFUFVVWxKiqKqqBezZaq" "KLjT03ukZUuXcPToUQzDwDTNZLlKxzTBNJMX/ZpqXjOYRMCTkU6W14vL6cTldEwAcjjsOO2J" "2uV0kpefj7SgYs2bqs22xTAMQdd1NE1H13U0Q8PQDDRDQ9d0NF1D1wziuo6uaWhX68l6PK6h" "6zqy3Ykgq+iiSBwLEQMscR1iGqYUx5TjCDHNXPCVir/9Hw+in8yPLoz7AAAAAElFTkSuQmCC") getwhyteboardData = whyteboard.GetData getwhyteboardImage = whyteboard.GetImage getwhyteboardBitmap = whyteboard.GetBitmap whyteboard-0.41.1/whyteboard/gui/menu.py0000777000175000017500000004131511444707746017306 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ Creates the menu bar for the GUI """ import os import wx from whyteboard.misc import get_image_path from whyteboard.gui import (ID_CLEAR_ALL, ID_CLEAR_ALL_SHEETS, ID_CLEAR_SHEETS, ID_CLOSE_ALL, ID_COLOUR_GRID, ID_DESELECT, ID_EXPORT, ID_EXPORT_ALL, ID_EXPORT_PDF, ID_FEEDBACK, ID_EXPORT_PREF, ID_FULLSCREEN, ID_HISTORY, ID_IMPORT_IMAGE, ID_IMPORT_PDF, ID_IMPORT_PREF, ID_IMPORT_PS, ID_MOVE_UP, ID_MOVE_DOWN, ID_MOVE_TO_TOP, ID_MOVE_TO_BOTTOM, ID_NEW, ID_NEXT, ID_PASTE_NEW, ID_PDF_CACHE, ID_PREV, ID_RECENTLY_CLOSED, ID_RELOAD_PREF, ID_RENAME, ID_REPORT_BUG, ID_RESIZE, ID_SHAPE_VIEWER, ID_STATUSBAR, ID_SWAP_COLOURS, ID_TOOL_PREVIEW, ID_TOOLBAR, ID_TRANSPARENT, ID_TRANSLATE, ID_UNDO_SHEET, ID_UPDATE, ID_BACKGROUND, ID_FOREGROUND) _ = wx.GetTranslation #---------------------------------------------------------------------- class Menu(object): """ Menu bar and its bindings. """ def __init__(self, gui): self.gui = gui self.menu = wx.MenuBar() self.closed_tabs_menu = wx.Menu() self.recent = wx.Menu() self.file = wx.Menu() self.view = wx.Menu() edit = wx.Menu() shapes = wx.Menu() sheets = wx.Menu() _help = wx.Menu() _import = wx.Menu() _export = wx.Menu() gui.filehistory.UseMenu(self.recent) gui.filehistory.AddFilesToMenu() self.make_closed_tabs_menu() _import.Append(ID_IMPORT_IMAGE, _('&Image...')) _import.Append(ID_IMPORT_PDF, '&PDF...') _import.Append(ID_IMPORT_PS, 'Post&Script...') _import.Append(ID_IMPORT_PREF, _('P&references...'), _("Load in a Whyteboard preferences file")) _export.Append(ID_EXPORT, _("&Export Sheet...") + "\tCtrl+E", _("Export the current sheet to an image file")) _export.Append(ID_EXPORT_ALL, _("Export &All Sheets...") + "\tCtrl+Shift+E", _("Export every sheet to a series of image files")) _export.Append(ID_EXPORT_PDF, _('As &PDF...'), _("Export every sheet into a PDF file")) _export.Append(ID_EXPORT_PREF, _('P&references...'), _("Export your Whyteboard preferences file")) new = wx.MenuItem(self.file, ID_NEW, _("New &Window") + "\tCtrl-N", _("Opens a new Whyteboard instance")) pnew = wx.MenuItem(edit, ID_PASTE_NEW, _("Paste to a &New Sheet") + "\tCtrl+Shift-V", _("Paste from your clipboard into a new sheet")) undo_sheet = wx.MenuItem(edit, ID_UNDO_SHEET, _("&Undo Last Closed Sheet") + "\tCtrl+Shift-T", _("Undo the last closed sheet")) if os.name != "nt": new.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_NEW, wx.ART_MENU)) pnew.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_PASTE, wx.ART_MENU)) undo_sheet.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_UNDO, wx.ART_MENU)) self.file.AppendItem(new) self.file.Append(wx.ID_NEW, _("&New Sheet") + "\tCtrl-T", _("Add a new sheet")) self.file.Append(wx.ID_OPEN, _("&Open...") + "\tCtrl-O", _("Load a Whyteboard save file, an image or convert a PDF/PS document")) self.file.AppendMenu(-1, _('Open &Recent'), self.recent, _("Recently Opened Files")) self.file.AppendSeparator() self.file.Append(wx.ID_SAVE, _("&Save") + "\tCtrl+S", _("Save the Whyteboard data")) self.file.Append(wx.ID_SAVEAS, _("Save &As...") + "\tCtrl+Shift+S", _("Save the Whyteboard data in a new file")) self.file.AppendSeparator() self.file.AppendMenu(-1, _('&Import File'), _import, _("Import various file types")) self.file.AppendMenu(-1, _('&Export File'), _export, _("Export your data files as images/PDFs")) self.file.Append(ID_RELOAD_PREF, _('Re&load Preferences'), _("Reload your preferences file")) self.file.AppendSeparator() self.file.Append(wx.ID_PRINT_SETUP, _("Page Set&up"), _("Set up the page for printing")) self.file.Append(wx.ID_PREVIEW_PRINT, _("Print Pre&view"), _("View a preview of the page to be printed")) self.file.Append(wx.ID_PRINT, _("&Print...") + "\tCtrl+P", _("Print the current page")) self.file.AppendSeparator() self.file.Append(wx.ID_EXIT, _("&Quit") + "\tAlt+F4", _("Quit Whyteboard")) edit.Append(wx.ID_UNDO, _("&Undo") + "\tCtrl+Z", _("Undo the last operation")) edit.Append(wx.ID_REDO, _("&Redo") + "\tCtrl+Y", _("Redo the last undone operation")) edit.AppendSeparator() edit.Append(wx.ID_COPY, _("&Copy") + "\tCtrl+C", _("Copy a Bitmap Selection region")) edit.Append(wx.ID_PASTE, _("&Paste") + "\tCtrl+V", _("Paste text or an image from your clipboard into Whyteboard")) edit.AppendItem(pnew) edit.AppendSeparator() edit.Append(wx.ID_PREFERENCES, _("Prefere&nces"), _("Change your preferences")) self.view.Append(ID_SHAPE_VIEWER, _("&Shape Viewer...") + "\tF3", _("View and edit the shapes' drawing order")) self.view.Append(ID_HISTORY, _("&History Viewer...") + "\tCtrl+H", _("View and replay your drawing history")) self.view.Append(ID_PDF_CACHE, _("&PDF Cache...") + "\tF4", _("View and modify Whyteboard's PDF Cache")) self.view.AppendSeparator() self.view.Append(ID_TOOLBAR, u" " + _("&Toolbar"), _("Show and hide the toolbar"), kind=wx.ITEM_CHECK) self.view.Append(ID_STATUSBAR, u" " + _("&Status Bar"), _("Show and hide the status bar"), kind=wx.ITEM_CHECK) self.view.Append(ID_TOOL_PREVIEW, u" " + _("Tool &Preview"), _("Show and hide the tool preview"), kind=wx.ITEM_CHECK) self.view.Append(ID_COLOUR_GRID, u" " + _("&Color Grid"), _("Show and hide the color grid"), kind=wx.ITEM_CHECK) self.view.AppendSeparator() self.view.Append(ID_FULLSCREEN, u" " + _("&Full Screen") + "\tF11", _("View Whyteboard in full-screen mode"), kind=wx.ITEM_CHECK) shapes.Append(ID_MOVE_UP, _("Move Shape &Up") + "\tCtrl-Up", _("Moves the currently selected shape up")) shapes.Append(ID_MOVE_DOWN, _("Move Shape &Down") + "\tCtrl-Down", _("Moves the currently selected shape down")) shapes.Append(ID_MOVE_TO_TOP, _("Move Shape To &Top") + "\tCtrl-Shift-Up", _("Moves the currently selected shape to the top")) shapes.Append(ID_MOVE_TO_BOTTOM, _("Move Shape To &Bottom") + "\tCtrl-Shift-Down", _("Moves the currently selected shape to the bottom")) shapes.AppendSeparator() shapes.Append(wx.ID_DELETE, _("&Delete Shape") + "\tDelete", _("Delete the currently selected shape")) shapes.Append(ID_DESELECT, _("&Deselect Shape") + "\tCtrl-D", _("Deselects the currently selected shape")) shapes.AppendSeparator() shapes.AppendCheckItem(ID_TRANSPARENT, " " + _("T&ransparent"), _("Toggles the selected shape's transparency")) shapes.Append(ID_FOREGROUND, _("&Color..."), _("Change the selected shape's color")) shapes.Append(ID_BACKGROUND, _("&Background &Color..."), _("Change the selected shape's background color")) shapes.Append(ID_SWAP_COLOURS, _("Swap &Colors"), _("Swaps the foreground and background colors")) sheets.Append(wx.ID_CLOSE, _("Re&move Sheet") + "\tCtrl+W", _("Close the current sheet")) sheets.Append(ID_CLOSE_ALL, _("&Close All Sheets") + "\tCtrl+Shift+W", _("Close every sheet")) sheets.Append(ID_RENAME, _("&Rename Sheet...") + "\tF2", _("Rename the current sheet")) sheets.Append(ID_RESIZE, _("Resi&ze Canvas...") + "\tCtrl+R", _("Change the canvas' size")) sheets.AppendSeparator() sheets.Append(ID_NEXT, _("&Next Sheet") + "\tCtrl+Tab", _("Go to the next sheet"))# sheets.Append(ID_PREV, _("&Previous Sheet") + "\tCtrl+Shift+Tab", _("Go to the previous sheet")) sheets.AppendItem(undo_sheet) sheets.AppendMenu(ID_RECENTLY_CLOSED, _("Recently &Closed Sheets"), self.closed_tabs_menu, _("View all recently closed sheets")) sheets.AppendSeparator() sheets.Append(wx.ID_CLEAR, _("&Clear Sheets' Drawings"), _("Clear drawings on the current sheet (keep images)")) sheets.Append(ID_CLEAR_ALL, _("Clear &Sheet"), _("Clear the current sheet")) sheets.AppendSeparator() sheets.Append(ID_CLEAR_SHEETS, _("Clear All Sheets' &Drawings"), _("Clear all sheets' drawings (keep images)")) sheets.Append(ID_CLEAR_ALL_SHEETS, _("Clear &All Sheets"), _("Clear all sheets")) _help.Append(wx.ID_HELP, _("&Contents") + "\tF1", _("View Whyteboard's help documents")) _help.AppendSeparator() _help.Append(ID_UPDATE, _("Check for &Updates...") + "\tF12", _("Search for updates to Whyteboard")) _help.Append(ID_REPORT_BUG, _("&Report a Problem"), _("Report any bugs or issues with Whyteboard")) _help.Append(ID_TRANSLATE, _("&Translate Whyteboard"), _("Translate Whyteboard to your language")) _help.Append(ID_FEEDBACK, _("Send &Feedback"), _("Send feedback directly to Whyteboard's developer")) _help.AppendSeparator() _help.Append(wx.ID_ABOUT, _("&About"), _("View information about Whyteboard")) self.menu.Append(self.file, _("&File")) self.menu.Append(edit, _("&Edit")) self.menu.Append(self.view, _("&View")) self.menu.Append(shapes, _("Sha&pes")) self.menu.Append(sheets, _("&Sheets")) self.menu.Append(_help, _("&Help")) self.gui.SetMenuBar(self.menu) def bindings(self): """ Binds the menu items to the GUI """ self.gui.Bind(wx.EVT_MENU_RANGE, self.gui.on_file_history, id=wx.ID_FILE1, id2=wx.ID_FILE9) self.gui.Bind(wx.EVT_MENU_OPEN, self.gui.load_recent_files) # "Import" sub-menu ids = {'pdf': ID_IMPORT_PDF, 'ps': ID_IMPORT_PS, 'img': ID_IMPORT_IMAGE} [self.gui.Bind(wx.EVT_MENU, lambda evt, text=key: self.gui.on_open(evt, text), id=ids[key]) for key in ids] # idle event handlers ids = [ID_BACKGROUND, ID_CLOSE_ALL, ID_DESELECT, ID_FOREGROUND, ID_MOVE_DOWN, ID_MOVE_TO_BOTTOM, ID_MOVE_TO_TOP, ID_MOVE_UP, ID_NEXT, ID_PASTE_NEW, ID_PREV, ID_RECENTLY_CLOSED, ID_SWAP_COLOURS, ID_TRANSPARENT, ID_UNDO_SHEET, wx.ID_CLOSE, wx.ID_COPY, wx.ID_DELETE, wx.ID_PASTE, wx.ID_REDO, wx.ID_UNDO] [self.gui.Bind(wx.EVT_UPDATE_UI, self.gui.update_menus, id=x) for x in ids] # menu items bindings = { ID_BACKGROUND: "background", ID_CLEAR_ALL: "clear_all", ID_CLEAR_ALL_SHEETS: "clear_all_sheets", ID_CLEAR_SHEETS: "clear_sheets", ID_CLOSE_ALL: "close_all_sheets", ID_COLOUR_GRID: "colour_grid", ID_DESELECT: "deselect_shape", ID_EXPORT: "export", ID_EXPORT_ALL: "export_all", ID_EXPORT_PDF: "export_pdf", ID_EXPORT_PREF: "export_pref", ID_FEEDBACK: "feedback", ID_FOREGROUND: "foreground", ID_FULLSCREEN: "fullscreen", ID_HISTORY: "history", ID_IMPORT_PREF: "import_pref", ID_MOVE_DOWN: "move_down", ID_MOVE_TO_BOTTOM: "move_bottom", ID_MOVE_TO_TOP: "move_top", ID_MOVE_UP: "move_up", ID_NEW: "new_win", ID_NEXT: "next_sheet", ID_PASTE_NEW: "paste_new", ID_PDF_CACHE: "pdf_cache", ID_PREV: "previous_sheet", ID_RELOAD_PREF: "reload_preferences", ID_RENAME: "rename", ID_REPORT_BUG: "report_bug", ID_RESIZE: "resize", ID_SHAPE_VIEWER: "shape_viewer", ID_STATUSBAR: "statusbar", ID_SWAP_COLOURS: "swap_colours", ID_TOOL_PREVIEW: "tool_preview", ID_TOOLBAR: "toolbar", ID_TRANSLATE: "translate", ID_TRANSPARENT: "transparent", ID_UNDO_SHEET: "undo_tab", ID_UPDATE: "update", wx.ID_ABOUT: "about", wx.ID_CLEAR: "clear", wx.ID_CLOSE: "close_tab", wx.ID_COPY: "copy", wx.ID_DELETE: "delete_shape", wx.ID_EXIT: "exit", wx.ID_HELP: "help", wx.ID_NEW : "new_tab", wx.ID_OPEN: "open", wx.ID_PASTE: "paste", wx.ID_PREFERENCES: "preferences", wx.ID_PREVIEW_PRINT: "print_preview", wx.ID_PRINT: "print", wx.ID_PRINT_SETUP: "page_setup", wx.ID_REDO: "redo", wx.ID_SAVE: "save", wx.ID_SAVEAS: "save_as", wx.ID_UNDO: "undo" } for _id, name in bindings.items(): method = getattr(self.gui, u"on_" + name) self.gui.Bind(wx.EVT_MENU, method, id=_id) def make_closed_tabs_menu(self): """ Recreates the undo tab menu """ gui = self.gui for menu in self.closed_tabs_menu.GetMenuItems(): self.closed_tabs_menu.Remove(menu.GetId()) gui.Unbind(wx.EVT_MENU, id=menu.GetId()) for x, tab in enumerate(reversed(gui.closed_tabs)): _id = wx.NewId() name = tab['name'] self.closed_tabs_menu.Append(_id, u"&%i: %s" % (x + 1, name), _('Restore sheet "%s"') % name) func = lambda evt, tab=tab: self.gui.on_undo_tab(tab=tab) gui.Bind(wx.EVT_MENU, func, id=_id) def toggle_fullscreen(self, value): menu = self.menu.FindItemById(ID_FULLSCREEN) menu.Check(value) def remove_all_recent(self): for x in self.recent.GetMenuItems(): self.recent.RemoveItem(x) def is_checked(self, _id): menu = self.menu.FindItemById(_id) return menu.IsChecked() def is_file_menu(self, menu): return menu == self.file def check(self, _id, value): self.menu.Check(_id, value) def enable(self, _id, value): self.menu.Enable(_id, value) #---------------------------------------------------------------------- class Toolbar(object): @staticmethod def create(gui): """ Creates a toolbar, Pythonically :D Move to top/up/down/bottom must be created with a custom bitmap. """ toolbar = gui.CreateToolBar() _move = [ID_MOVE_UP, ID_MOVE_DOWN, ID_MOVE_TO_BOTTOM, ID_MOVE_TO_TOP] ids = [wx.ID_NEW, wx.ID_OPEN, wx.ID_SAVE, wx.ID_COPY, wx.ID_PASTE, wx.ID_UNDO, wx.ID_REDO, wx.ID_DELETE] arts = [wx.ART_NEW, wx.ART_FILE_OPEN, wx.ART_FILE_SAVE, wx.ART_COPY, wx.ART_PASTE, wx.ART_UNDO, wx.ART_REDO, wx.ART_DELETE] tips = [_("New Sheet"), _("Open a File"), _("Save Drawing"), _("Copy a Bitmap Selection"), _("Paste an Image/Text"), _("Undo the Last Action"), _("Redo the Last Undone Action"), _("Delete the currently selected shape"), ("Move Shape Up"), ("Move Shape Down"), _("Move Shape To Top"), ("Move Shape To Bottom")] ids.extend(_move) arts.extend(_move) icons = [u"up", u"down", u"top", u"bottom"] bmps = {} for icon, _id in zip(icons, _move): bmps[_id] = wx.Bitmap(get_image_path(u"icons", u"move-%s-small" % icon)) # add tools, add a separator and bind paste/undo/redo for UI updating for x, (_id, art_id, tip) in enumerate(zip(ids, arts, tips)): if _id in _move: art = bmps[_id] else: art = wx.ArtProvider.GetBitmap(art_id, wx.ART_TOOLBAR) toolbar.AddSimpleTool(_id, art, tip) if x in [2, 6]: toolbar.AddSeparator() toolbar.EnableTool(wx.ID_PASTE, gui.can_paste) toolbar.Realize() return toolbarwhyteboard-0.41.1/whyteboard/gui/app.py0000777000175000017500000001222111444517222017100 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ Contains the application that is used to launch the program, provide command line arguments/parsing and setting the program's locale/language. """ import os import sys import wx from optparse import OptionParser from whyteboard.gui import GUI from whyteboard.lib import ConfigObj, Validator from whyteboard.misc import meta, get_path, get_home_dir, is_exe #---------------------------------------------------------------------- class WhyteboardApp(wx.App): def OnInit(self): """ Load config file, apply translation, parse arguments and delete any temporary filse left over from an update """ wx.SetDefaultPyEncoding("utf-8") self.SetAppName(u"whyteboard") # used to identify app in $HOME/ parser = OptionParser(version="Whyteboard %s" % meta.version) parser.add_option("-f", "--file", help="load FILE on load") parser.add_option("-c", "--conf", help="load configurations from CONF file") parser.add_option("--width", type="int", help="set canvas to WIDTH") parser.add_option("--height", type="int", help="set canvas to HEIGHT") parser.add_option("-u", "--update", action="store_true", help="check for a newer version of whyteboard") parser.add_option("-l", "--lang", help="set language. can be a country code or language (e.g. fr, french; nl, dutch)") (options, args) = parser.parse_args() path = options.conf or os.path.join(get_home_dir(), u"user.pref") config = ConfigObj(path, configspec=meta.config_scheme, encoding=u"utf-8") config.validate(Validator()) self.set_language(config, options.lang) self.frame = GUI(config) self.frame.Show(True) try: _file = options.file or sys.argv[1] _file = os.path.abspath(_file) if os.path.exists(_file): self.frame.do_open(_file.decode("utf-8")) except IndexError: pass x = options.width or self.frame.canvas.area[0] y = options.height or self.frame.canvas.area[1] self.frame.canvas.resize((x, y)) self.delete_temp_files() if options.update: self.frame.on_update() return True def delete_temp_files(self): """ Delete temporary files from an update. Remove a backup exe, otherwise iterate over the current directory (where the backup files will be) and remove any that matches the random file extension """ if is_exe() and os.path.exists(u"wtbd-bckup.exe"): os.remove(u"wtbd-bckup.exe") else: path = get_path() for f in os.listdir(path): if f.find(meta.backup_extension) is not - 1: os.remove(os.path.join(path, f)) def set_language(self, config, option_lang=None): """ Sets the user's language. """ set_lang = False lang_name = config.get('language', '') if option_lang: country = wx.Locale.FindLanguageInfo(option_lang) if country: set_lang = True lang_name = country.Description self.locale = wx.Locale(country.Language) if not set_lang: for x in meta.languages: if lang_name.capitalize() == 'Welsh': self.locale = wx.Locale() self.locale.Init(u"Cymraeg", u"cy", u"cy_GB.utf8") break elif lang_name == x[0]: nolog = wx.LogNull() self.locale = wx.Locale(x[2]) if not hasattr(self, "locale"): # now try sytem language self.locale = wx.Locale(wx.LANGUAGE_DEFAULT) config['language'] = wx.Locale.GetLanguageName(wx.LANGUAGE_DEFAULT) config.write() if not wx.Locale.IsOk(self.locale): wx.MessageBox(u"Error setting language to %s - reverting to English" % lang_name, u"Whyteboard") if not set_lang: config['language'] = 'English' config.write() self.locale = wx.Locale(wx.LANGUAGE_ENGLISH) langdir = os.path.join(get_path(), u'locale') self.locale.AddCatalogLookupPathPrefix(langdir) self.locale.AddCatalog(u"whyteboard") reload(meta) # fix for some translated strings not being appliedwhyteboard-0.41.1/whyteboard/gui/event_ids.py0000777000175000017500000000647611443222121020305 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ wxPython event IDs used by the program for event binding and for a common ID to be used for multiple events (menu/toolbar/idle) """ import wx #---------------------------------------------------------------------- ID_BACKGROUND = wx.NewId() # change shape's background ID_CLEAR_ALL = wx.NewId() # remove everything from current tab ID_CLEAR_ALL_SHEETS = wx.NewId() # remove everything from all tabs ID_CLEAR_SHEETS = wx.NewId() # remove all drawings from all tabs, keep imgs ID_CLOSE_ALL = wx.NewId() # close all sheets ID_COLOUR_GRID = wx.NewId() # toggle colour grid ID_DESELECT = wx.NewId() # deselect shape ID_EXPORT = wx.NewId() # export sheet to image file ID_EXPORT_ALL = wx.NewId() # export every sheet to numbered image files ID_EXPORT_PDF = wx.NewId() # export->PDF ID_FEEDBACK = wx.NewId() # help->feedback ID_FOREGROUND = wx.NewId() # change shape's foreground colour ID_EXPORT_PREF = wx.NewId() # export->preferences ID_FULLSCREEN = wx.NewId() # toggle fullscreen ID_HISTORY = wx.NewId() # history viewer ID_IMPORT_IMAGE = wx.NewId() # import->Image ID_IMPORT_PDF = wx.NewId() # import->PDF ID_IMPORT_PREF = wx.NewId() # import->Preferences ID_IMPORT_PS = wx.NewId() # import->PS ID_MOVE_UP = wx.NewId() # move shape up ID_MOVE_DOWN = wx.NewId() # move shape down ID_MOVE_TO_TOP = wx.NewId() # move shape to the top ID_MOVE_TO_BOTTOM = wx.NewId() # move shape to the bottom ID_NEW = wx.NewId() # new window ID_NEXT = wx.NewId() # next sheet ID_PASTE_NEW = wx.NewId() # paste as new selection ID_PDF_CACHE = wx.NewId() # view->PDF Cache ID_PREV = wx.NewId() # previous sheet ID_RECENTLY_CLOSED = wx.NewId() # list of recently closed sheets ID_RELOAD_PREF = wx.NewId() # reload preferences ID_RENAME = wx.NewId() # rename sheet ID_REPORT_BUG = wx.NewId() # report a problem ID_RESIZE = wx.NewId() # resize dialog ID_SHAPE_VIEWER = wx.NewId() # view/edit shapes ID_STATUSBAR = wx.NewId() # toggle statusbar ID_SWAP_COLOURS = wx.NewId() # swap colour ID_TOOL_PREVIEW = wx.NewId() # toggle preview of tool ID_TOOLBAR = wx.NewId() # toggle toolbar ID_TRANSPARENT = wx.NewId() # toggle shape transparency ID_TRANSLATE = wx.NewId() # open translation URL ID_UNDO_SHEET = wx.NewId() # undo close sheet ID_UPDATE = wx.NewId() # update selfwhyteboard-0.41.1/whyteboard/gui/sheets.py0000777000175000017500000000217311443222121017606 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ Classes for managing Whyteboard's sheets. """ import wx class SheetManager(object): def __init__(self): self.sheets = [] class UndoSheetManager(object): """ Contains sheets that have been closed and can be restored """ def __init__(self, gui): self.gui= gui self.sheets = []whyteboard-0.41.1/whyteboard/gui/__init__.py0000777000175000017500000000267511444476216020102 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- from event_ids import (ID_HISTORY, ID_REPORT_BUG, ID_FEEDBACK, ID_CLEAR_SHEETS, ID_UPDATE, ID_PREV, ID_RECENTLY_CLOSED, ID_PASTE_NEW, ID_MOVE_TO_TOP, ID_RENAME, ID_FULLSCREEN, ID_PDF_CACHE, ID_EXPORT_PREF, ID_EXPORT_PDF, ID_RESIZE, ID_MOVE_TO_BOTTOM, ID_CLEAR_ALL, ID_CLOSE_ALL, ID_EXPORT, ID_COLOUR_GRID, ID_UNDO_SHEET, ID_SWAP_COLOURS, ID_NEW, ID_BACKGROUND, ID_IMPORT_IMAGE, ID_RELOAD_PREF, ID_DESELECT, ID_STATUSBAR, ID_TRANSLATE, ID_MOVE_UP, ID_TOOL_PREVIEW, ID_IMPORT_PDF, ID_MOVE_DOWN, ID_TOOLBAR, ID_CLEAR_ALL_SHEETS, ID_IMPORT_PREF, ID_TRANSPARENT, ID_IMPORT_PS, ID_FOREGROUND, ID_EXPORT_ALL, ID_NEXT, ID_SHAPE_VIEWER) from menu import Menu, Toolbar from sheets import UndoSheetManager from canvas import Canvas, CanvasDropTarget from dialogs import (ExceptionHook, AboutDialog, Feedback, FindIM, History, PDFCacheDialog, ProgressDialog, PromptForSave, Resize, ShapeViewer, TextInput, UpdateDialog) from popups import NotesPopup, ThumbsPopup, ShapePopup, SheetsPopup from panels import ControlPanel, MediaPanel, SidePanel from preferences import Preferences from printing import Print from frame import GUI from app import WhyteboardAppwhyteboard-0.41.1/whyteboard/gui/canvas.py0000777000175000017500000006630611443222121017576 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ This module contains the Canvas class, a window that can be drawn upon. Each Canvas panel gets added to a tab in the GUI, and each Canvas maintains a list of undo/redo actions for itself; thus each Canvas tab on the GUI has its own undo/redo. The canvas to be drawn on is managed by a buffer bitmap, and the rest of the area is flooded with a grey, to indicate it is the background. This background can be grabbed with the mouse to resize the canvas' size. If the canvas is larger than the client size, then scrollbars are displayed, and a slight "border" is shown around the canvas - this can be grabbed to resize. """ import os import copy import wx #import wx.lib.wxcairo as wxcairo from whyteboard.lib import DragScroller, pub from whyteboard.misc import get_image_path from whyteboard.tools import (Highlighter, Image, Line, Media, Note, OverlayShape, Polygon, Select, Text, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, CENTER_TOP, CENTER_RIGHT, CENTER_BOTTOM, CENTER_LEFT, HANDLE_ROTATE, EDGE_TOP, EDGE_RIGHT, EDGE_LEFT, EDGE_BOTTOM) EDGE = 15 # distance from canvas edge before shape will scroll canvas TO_MOVE = 5 # pixels shape will cause canvas to scroll # area user clicked on canvas to resize RIGHT = 1 DIAGONAL = 2 BOTTOM = 3 DRAG_LEFT = 1 DRAG_RIGHT = 2 DRAG_UP = 3 DRAG_DOWN = 4 _ = wx.GetTranslation #---------------------------------------------------------------------- class Canvas(wx.ScrolledWindow): """ The drawing frame of the application. References to self.shape.drawing are for the Polygon tool, mainly avoiding isinstance() checks """ CANVAS_BORDER = 15 # border pixels in size (overridable by user) def __init__(self, tab, gui, area): """ Initalise the window, class variables and bind mouse/paint events """ wx.ScrolledWindow.__init__(self, tab, style=wx.NO_FULL_REPAINT_ON_RESIZE | wx.CLIP_CHILDREN) self.SetVirtualSizeHints(2, 2) self.SetScrollRate(1, 1) self.SetBackgroundColour('Grey') self.SetDropTarget(CanvasDropTarget()) if os.name == "nt": self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) # no flicking on Win! else: self.ClearBackground() self.area = area self.scroller = DragScroller(self) self.overlay = wx.Overlay() self.buffer = wx.EmptyBitmap(*self.area) self.gui = gui self.scale = (1.0, 1.0) self.shapes = [] # list of shapes for re-drawing/saving self.shape = None # currently selected shape *to draw with* self.medias = [] # list of Media panels self.selected = None # selected shape *with Select tool* self.text = None # current Text object for redraw all self.copy = None # BitmapSelect instance self.resizing = False self.cursor_control = False # toggle resize canvas cursor on/off self.resize_direction = None self.undo_list = [] self.redo_list = [] self.drawing = False self.prev_drag = (0, 0) self.SetScrollRate(3, 3) img = wx.Image(get_image_path(u"cursors", u"rotate")) self.rotate_cursor = wx.CursorFromImage(img) self.gui.change_tool(canvas=self) self.redraw_all() self.Bind(wx.EVT_SIZE, self.on_size) self.Bind(wx.EVT_LEFT_DOWN, self.left_down) self.Bind(wx.EVT_LEFT_UP, self.left_up) self.Bind(wx.EVT_RIGHT_UP, self.right_up) self.Bind(wx.EVT_LEFT_DCLICK, self.left_double) self.Bind(wx.EVT_MIDDLE_DOWN, self.middle_down) self.Bind(wx.EVT_MIDDLE_UP, self.middle_up) self.Bind(wx.EVT_MOTION, self.motion) self.Bind(wx.EVT_PAINT, self.on_paint) pub.subscribe(self.set_border, 'canvas.set_border') def left_down(self, event): """Starts drawing""" x, y = self.convert_coords(event) if os.name == "nt" and not isinstance(self.shape, Media) and not self.shape.drawing: self.CaptureMouse() if not self.shape.drawing and self.check_resize_direction(x, y): # don't draw outside canvas self.resizing = True return if not isinstance(self.shape, Select) and self.selected: self.deselect_shape() self.shape.left_down(x, y) # Crashes without the Text check if not isinstance(self.shape, (Text, Media)): self.drawing = True def motion(self, event): """ Checks if the canvas can be updated, changes the cursor to show it can Updates the shape if the user is drawing. Indicate shape may be changed when using Select tool by changing the cursor """ x, y = self.convert_coords(event) if self.resizing: self.resize((x, y), self.resize_direction) return else: if not self.check_mouse_for_resize(x, y): return self.gui.SetStatusText(u" %s, %s" % (x, y)) if self.drawing or self.shape.drawing: self.shape.motion(x, y) if not self.shape.drawing: # polygon self.draw_shape(self.shape) elif isinstance(self.shape, Select): # change cursor to indicate action self.select_tool_cursor(x, y) def left_up(self, event): """ Called when the left mouse button is released. """ if os.name == "nt" and not self.shape.drawing: if self.HasCapture(): self.ReleaseMouse() if self.resizing: self.resizing = False self.redraw_all(True) # update thumb for new canvas size self.Layout() if self.copy: self.draw_shape(self.copy) # draw back the GCDC return if self.drawing or isinstance(self.shape, Text): before = len(self.shapes) self.shape.left_up(*self.convert_coords(event)) if not isinstance(self.shape, Media): if len(self.shapes) - before: pub.sendMessage('canvas.change_tool') pub.sendMessage('thumbs.update_current') self.drawing = False def right_up(self, event): """Called when the right mouse button is released - used for zoom""" self.shape.right_up(*self.convert_coords(event)) def left_double(self, event): """Double click for the Select tool - edit text""" self.shape.double_click(*self.convert_coords(event)) def set_border(self, border_size): self.CANVAS_BORDER = border_size self.resize(self.area) def select_tool_cursor(self, x, y): if self.selected: if self.select_tool_cursor_change(self.selected, x, y): return for shape in reversed(self.shapes): if self.select_tool_cursor_change(shape, x, y): break else: self.change_cursor() def select_tool_cursor_change(self, shape, x, y): res = shape.handle_hit_test(x, y) ret = True if res and isinstance(shape, (Line, Polygon)): if wx.GetKeyState(wx.WXK_SHIFT): self.set_cursor(wx.CURSOR_SIZENWSE) elif wx.GetKeyState(wx.WXK_CONTROL): self.SetCursor(self.rotate_cursor) else: self.set_cursor(wx.CURSOR_SIZING) elif res == HANDLE_ROTATE: self.SetCursor(self.rotate_cursor) elif res in [TOP_LEFT, BOTTOM_RIGHT]: self.set_cursor(wx.CURSOR_SIZENWSE) elif res in [TOP_RIGHT, BOTTOM_LEFT]: self.set_cursor(wx.CURSOR_SIZENESW) elif res in [CENTER_TOP, CENTER_BOTTOM]: self.set_cursor(wx.CURSOR_SIZENS) elif res in [CENTER_LEFT, CENTER_RIGHT]: self.set_cursor(wx.CURSOR_SIZEWE) elif shape.hit_test(x, y): self.set_cursor(wx.CURSOR_HAND) else: ret = False return ret def set_cursor(self, cursor): self.SetCursor(wx.StockCursor(cursor)) def check_mouse_for_resize(self, x, y): """ Sees if the user's mouse is outside of the canvas, and updates the cursor if it's in a different resizable area than it previously was Returns the cursor to its normal state if it's moved back in """ direction = self.check_resize_direction(x, y) if not self.drawing and direction and not self.shape.drawing: if not self.resize_direction: self.resize_direction = direction if not self.cursor_control or direction != self.resize_direction: self.resize_direction = direction self.cursor_control = True self.set_resize_cursor(direction) # change cursor return False else: if self.cursor_control: self.change_cursor() self.cursor_control = False return False return True def check_resize_direction(self, x, y): """Which direction the canvas can be resized in, if any""" if x > self.area[0] and y > self.area[1]: return DIAGONAL elif x > self.area[0]: return RIGHT elif y > self.area[1]: return BOTTOM return False def set_resize_cursor(self, direction): cursors = {DIAGONAL: wx.CURSOR_SIZENWSE, RIGHT: wx.CURSOR_SIZEWE, BOTTOM: wx.CURSOR_SIZENS} self.SetCursor(wx.StockCursor(cursors.get(direction))) def resize(self, size, direction=None): """ Performs the canvas resizing. Size = (w, h) tuple """ if size[0] < 1 or size[1] < 1: return if direction == RIGHT: size = (size[0], self.area[1]) self.Scroll(self.GetVirtualSizeTuple()[0], -1) elif direction == BOTTOM: size = (self.area[0], size[1]) self.Scroll(-1, size[1]) elif direction is not None: self.Scroll(*size) self.buffer = wx.EmptyBitmap(*size) self.area = size size = (size[0] + self.CANVAS_BORDER, size[1] + self.CANVAS_BORDER) self.SetVirtualSize(size) self.redraw_all(resizing=True) def redraw_dirty(self, dc): """ Figure out what part of the window to refresh. """ x1, y1, x2, y2 = dc.GetBoundingBox() rect = wx.Rect() rect.SetTopLeft(self.CalcScrolledPosition(x1, y1)) rect.SetBottomRight(self.CalcScrolledPosition(x2, y2)) self.RefreshRect(rect.Inflate(2, 2)) def redraw_all(self, update_thumb=False, dc=None, resizing=False): """ Redraws all shapes that have been drawn. self.text is used to show text characters as they're being typed, as new Text/Note objects have not been added to self.shapes at this point. dc is used as the DC for printing. """ if not dc: dc = wx.BufferedDC(None, self.buffer) dc.Clear() for s in self.shapes: if not resizing: s.draw(dc, True) else: if not isinstance(s, Highlighter): s.draw(dc, True) if self.text: self.text.draw(dc, True) if self.copy: self.copy.draw(dc, True) self.Refresh() if update_thumb: pub.sendMessage('thumbs.update_current') def change_tool(self): if self.HasCapture(): self.ReleaseMouse() self.change_cursor() def change_cursor(self): if isinstance(self.shape.cursor, wx.Cursor): self.SetCursor(self.shape.cursor) else: self.SetCursor(wx.StockCursor(self.shape.cursor)) def add_shape(self, shape): """ Adds a shape to the "to-draw" list. """ self.add_undo() self.shapes.append(shape) if self.selected: self.deselect_shape() self.redraw_all() if self.text: self.text = None if self.copy: self.copy = None self.redraw_all() pub.sendMessage('update_shape_viewer') def add_undo(self): """Creates an undo point. NEED to change this for memory improvements""" l = [copy.copy(x) for x in self.shapes] self.undo_list.append(l) if self.redo_list: self.redo_list = [] pub.sendMessage('gui.mark_unsaved') def undo(self): """ Undoes an action, and adds it to the redo list. """ self.perform(self.undo_list, self.redo_list) def redo(self): """ Redoes an action, and adds it to the undo list. """ self.perform(self.redo_list, self.undo_list) def perform(self, list_a, list_b): """ perform undo/redo. list_a: to remove from / list b: append to """ if not list_a: return list_b.append(list(self.shapes)) self.shapes = list_a.pop() self.deselect_shape() self.redraw_all(True) pub.sendMessage('note.delete_sheet_items') # lazy way of doing things... for x in self.shapes: if isinstance(x, Note): pub.sendMessage('note.add', note=x) pub.sendMessage('gui.mark_unsaved') pub.sendMessage('update_shape_viewer') def restore_sheet(self, shapes, undo_list, redo_list, size, medias, viewport): """ Restores itself (e.g. from undoing closing a sheet.) """ self.shapes = shapes self.undo_list = undo_list self.redo_list = redo_list self.medias = medias for media in medias: media.canvas = self media.make_panel() for shape in shapes: shape.canvas = self if isinstance(shape, Note): pub.sendMessage('note.add', note=shape) wx.Yield() self.resize(size) self.Scroll(viewport[0], viewport[1]) pub.sendMessage('thumbs.update_current') def toggle_transparent(self): """Toggles the selected item's transparency""" if not self.selected or isinstance(self.selected, (Media, Image, Text)): return self.add_undo() val = wx.TRANSPARENT if self.selected.background == wx.TRANSPARENT: val = self.gui.get_background_colour() self.selected.background = val self.selected.make_pen() self.redraw_all(True) def delete_selected(self): """Deletes the selected shape""" if not self.selected: return if isinstance(self.selected, Media): self.selected.remove_panel() else: if isinstance(self.selected, Note): self.gui.notes.tree.Delete(self.selected.tree_id) self.add_undo() self.shapes.remove(self.selected) pub.sendMessage('update_shape_viewer') self.selected = None self.redraw_all(True) def clear(self, keep_images=False): """ Removes all shapes from the 'to-draw' list. """ if not self.medias and not self.shapes: return for m in self.medias: m.remove_panel() self.medias = [] images = [] if self.shapes: self.add_undo() if keep_images: for x in self.shapes: if isinstance(x, Image): images.append(x) self.shapes = images pub.sendMessage('update_shape_viewer') self.redraw_all(update_thumb=True) def drag_direction(self, x, y): """ Work out the direction a shape's being moved in so that we don't scroll the canvas as a shape is being dragged away from a canvas edge. """ direction = [] if self.prev_drag[0] > x: direction.append(DRAG_LEFT) elif self.prev_drag[0] < x: direction.append(DRAG_RIGHT) if self.prev_drag[1] < y: direction.append(DRAG_DOWN) elif self.prev_drag[1] > y: direction.append(DRAG_UP) self.prev_drag = (x, y) return direction def shape_near_canvas_edge(self, x, y, direction, moving=False): """ Check that the x/y coords is within X pixels from the edge of the canvas and scroll the canvas accordingly. If the shape is being moved, we need to check specific edges of the shape (e.g. left/right side of rectangle) """ size = self.GetClientSizeTuple() if not self.area > size: # canvas is too small to need to scroll return # no point continuing if we're not near the canvas' border start = self.GetViewStart() end_x = self.GetClientSize()[0] + start[0] end_y = self.GetClientSize()[1] + start[1] rect = wx.Rect(start[0] + EDGE, start[1] + EDGE, end_x - EDGE, end_y - EDGE) if rect.ContainsXY(x, y): return scroll = (-1, -1) if moving: if self.selected.edges[EDGE_RIGHT] > start[0] + size[0] - EDGE and DRAG_RIGHT in direction: scroll = (start[0] + TO_MOVE, -1) if self.selected.edges[EDGE_BOTTOM] > start[1] + size[1] - EDGE and DRAG_DOWN in direction: scroll = (-1, start[1] + TO_MOVE) if self.selected.edges[EDGE_LEFT] < start[0] + EDGE and DRAG_LEFT in direction: scroll = (start[0] - TO_MOVE, -1) if self.selected.edges[EDGE_TOP] > start[1] + EDGE and DRAG_RIGHT in direction: scroll = (-1, start[1] - TO_MOVE) else: if x > start[0] + size[0] - EDGE: scroll = (start[0] + TO_MOVE, -1) if y > start[1] + size[1] - EDGE: scroll = (-1, start[1] + TO_MOVE) if x < start[0] + EDGE: # x left scroll = start[0] - TO_MOVE, -1 if y < start[1] + EDGE: # y top scroll = (-1, start[1] - TO_MOVE) self.Scroll(*scroll) def check_move(self, pos): """ Checks whether a selected shape can be moved in the shape order """ if not self.selected or isinstance(self.selected, Media): return False if not self.selected in self.shapes: return False if pos in [u"top", u"up"]: length = len(self.shapes) - 1 if length < 0: length = 0 if self.shapes.index(self.selected) != length: return True elif pos in [u"down", u"bottom"]: if self.shapes.index(self.selected) != 0: return True return False def do_move(self, shape): """ Performs the move, by popping the item to be moved """ self.add_undo() x = self.shapes.index(shape) return (x, self.shapes.pop(x)) def move_shape(fn): def wrapper(self, shape, x=None, item=None): x, item = self.do_move(shape) fn(self, shape, x, item) pub.sendMessage('update_shape_viewer') self.redraw_all(True) return wrapper @move_shape def move_up(self, shape, x=None, item=None): self.shapes.insert(x + 1, item) @move_shape def move_down(self, shape, x=None, item=None): self.shapes.insert(x - 1, item) @move_shape def move_top(self, shape, x=None, item=None): self.shapes.append(item) @move_shape def move_bottom(self, shape, x=None, item=None): self.shapes.insert(0, item) def convert_coords(self, event): """ Translate mouse x/y coords to virtual scroll ones. """ return self.CalcUnscrolledPosition(event.GetX(), event.GetY()) def middle_down(self, event): """ Begin dragging the scroller to move around the panel """ self.set_cursor(wx.CURSOR_SIZENESW) self.scroller.Start(event.GetPosition()) def middle_up(self, event): """ Stop dragging the scroller. """ self.scroller.Stop() self.change_cursor() # bugfix with custom cursor def on_paint(self, event=None): """ Called when the window is exposed. Paint the buffer, and then create a region, remove the buffer rectangle then clear it with grey. """ wx.BufferedPaintDC(self, self.buffer, wx.BUFFER_VIRTUAL_AREA) if os.name == "nt": relbuf = self.CalcScrolledPosition(self.area) cli = self.GetClientSize() if cli.x > relbuf[0] or cli.y > relbuf[1]: bkgregion = wx.Region(0, 0, cli.x, cli.y) bkgregion.SubtractRect(wx.Rect(0, 0, relbuf[0], relbuf[1])) dc = wx.ClientDC(self) dc.SetClippingRegionAsRegion(bkgregion) dc.SetBrush(wx.GREY_BRUSH) dc.Clear() dc.DestroyClippingRegion() def paste_image(self, bitmap, x, y, ignore=False): shape = Image(self, bitmap, None) shape.left_down(x, y) wx.Yield() if ignore: self.resize((bitmap.GetWidth(), bitmap.GetHeight())) self.redraw_all(True) def paste_text(self, text, x, y, colour): self.shape = Text(self, colour, 1) self.shape.text = text self.shape.left_down(x, y) self.shape.left_up(x, y) self.text = None pub.sendMessage('canvas.change_tool') self.redraw_all(True) def get_selection_bitmap(self): """ If a rectangle selection is made, copy the selection as a bitmap. NOTE: The bitmap selection can be larger than the actual canvas bitmap, so we must only selection the region of the selection that is visible on the canvas """ self.copy.update_rect() # ensure w, h are correct bmp = self.copy area = self.area if bmp.x + bmp.width > area[0]: bmp.rect.SetWidth(area[0] - bmp.x) if bmp.y + bmp.height > area[1]: bmp.rect.SetHeight(area[1] - bmp.y) self.copy = None self.redraw_all() return self.buffer.GetSubBitmap(bmp.rect) def deselect_shape(self): """Deselects the currently selected shape""" for x in self.shapes: if isinstance(x, OverlayShape): x.selected = False if self.selected: self.selected = None self.redraw_all() def select_shape(self, shape): """Selects the selected shape""" self.overlay = wx.Overlay() if self.selected: self.deselect_shape() self.selected = shape shape.selected = True x = self.shapes.index(shape) self.shapes.pop(x) self.redraw_all() # hide 'original' self.shapes.insert(x, shape) shape.draw(self.get_dc(), False) # draw 'new' def change_colour(self): x = self.colour_data(self.selected.colour) if x: self.selected.colour = x self.redraw_all(True) def change_background(self,): x = self.colour_data(self.selected.background) if x: self.selected.background = x self.redraw_all(True) def colour_data(self, colour): """Shows a colour info box""" data = wx.ColourData() data.SetChooseFull(True) data.SetColour(colour) dlg = wx.ColourDialog(self.gui, data) if dlg.ShowModal() == wx.ID_OK: x = dlg.GetColourData() self.add_undo() return x.GetColour().Get() return False def get_mouse_position(self): x, y = self.ScreenToClient(wx.GetMousePosition()) if x < 0 or y < 0 or x > self.area[0] or y > self.area[1]: x, y = 0, 0 return self.CalcUnscrolledPosition(x, y) def get_dc(self): cdc = wx.ClientDC(self) self.PrepareDC(cdc) return wx.BufferedDC(cdc, self.buffer, wx.BUFFER_VIRTUAL_AREA) def draw_shape(self, shape, replay=False): """ Redraws a single shape efficiently""" dc = self.get_dc() #dc.SetUserScale(self.scale[0], self.scale[1]) if replay: shape.draw(dc, replay) else: shape.draw(dc) self.redraw_dirty(dc) def can_swap_transparency(self): return self.selected and not isinstance(self.selected, (Media, Image, Text)) def can_swap_colours(self): return self.can_swap_transparency() and not self.is_transparent() def is_transparent(self): return self.selected.background == wx.TRANSPARENT def swap_colours(self): self.selected.colour, self.selected.background = self.selected.background, self.selected.colour self.redraw_all() def show_text_edit_dialog(self, text_shape): return self.gui.show_text_edit_dialog(text_shape) def capture_mouse(self): if not self.HasCapture(): self.CaptureMouse() def release_mouse(self): if self.HasCapture(): self.ReleaseMouse() def resize_if_large_image(self, size): """ Check whether the canvas should be resized (for large images) """ if size[0] > self.area[0] or size[1] > self.area[1]: self.resize(size) def on_size(self, event): """ Updates the scrollbars when the window is resized. """ size = self.GetClientSize() if size[0] < self.area[0] or size[1] < self.area[1]: self.SetVirtualSize((self.area[0] + 20, self.area[1] + 20)) #---------------------------------------------------------------------- class CanvasDropTarget(wx.PyDropTarget): """Implements drop target functionality to receive files and text""" def __init__(self): wx.PyDropTarget.__init__(self) self.do = wx.DataObjectComposite() self.filedo = wx.FileDataObject() self.textdo = wx.TextDataObject() self.bmpdo = wx.BitmapDataObject() self.do.Add(self.filedo) self.do.Add(self.bmpdo) self.do.Add(self.textdo) self.SetDataObject(self.do) def OnData(self, x, y, d): """ Handles drag/dropping files/text or a bitmap """ if self.GetData(): df = self.do.GetReceivedFormat().GetType() if df in [wx.DF_UNICODETEXT, wx.DF_TEXT]: pub.sendMessage('canvas.paste_text', text=self.textdo.GetText(), x=x, y=y) elif df == wx.DF_FILENAME: for x, name in enumerate(self.filedo.GetFilenames()): pub.sendMessage('gui.open_file', filename=name) elif df == wx.DF_BITMAP: pub.sendMessage('canvas.paste_image', image=self.bmpdo.GetBitmap(), x=x, y=y, ignore=True) return dwhyteboard-0.41.1/whyteboard/gui/preferences.py0000777000175000017500000005365011443222121020622 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ This module contains a base Dialog class and several Panel classes that create Whyteboard's preferences dialog. It has been separated from the dialog module as it's a large unit of functionality. NOTE: A ConfigObj is stored inside the GUI, so this module first creates its own copy of the ConfigObj. All changes are then written to this object. Only when the user presses ok on the preferences dialog window will be updated configobj be written to disk and updates the GUI to its new state, and updated its config file. """ import os import wx from copy import copy from wx.lib.wordwrap import wordwrap as wordwrap from wx.lib import scrolledpanel as scrolled from whyteboard.gui import FindIM from whyteboard.lib import pub from whyteboard.misc import meta, create_colour_bitmap _ = wx.GetTranslation #---------------------------------------------------------------------- class Preferences(wx.Dialog): """ Contains a tabbed bar corresponding to each "page" of different options """ def __init__(self, gui): wx.Dialog.__init__(self, gui, title=_("Preferences"), size=(450, 500), style=wx.CLOSE_BOX | wx.CAPTION) self.gui = gui self.config = copy(gui.util.config) sizer = wx.BoxSizer(wx.VERTICAL) self.tabs = wx.Notebook(self) params = [self.tabs, gui, self.config] self.tabs.AddPage(General(*params), _("General")) self.tabs.AddPage(FontAndColours(*params), _("Fonts and Color")) self.tabs.AddPage(View(*params), _("View")) self.tabs.AddPage(PDF(*params), _("PDF Conversion")) okay = wx.Button(self, wx.ID_OK, _("&OK")) cancel = wx.Button(self, wx.ID_CANCEL, _("&Cancel")) _help = wx.Button(self, wx.ID_HELP, _("&Help")) okay.SetDefault() btnSizer = wx.StdDialogButtonSizer() btnSizer.AddButton(okay) btnSizer.AddButton(cancel) btnSizer.Add(_help, 0, wx.ALIGN_LEFT | wx.LEFT, 10) btnSizer.Realize() sizer.Add(self.tabs, 2, wx.EXPAND | wx.ALL, 10) sizer.Add((10, 10)) sizer.Add(btnSizer, 0, wx.ALIGN_CENTRE | wx.BOTTOM, 10) self.SetSizer(sizer) sizer.Layout() self.SetFocus() cancel.Bind(wx.EVT_BUTTON, self.on_cancel) okay.Bind(wx.EVT_BUTTON, self.on_okay) _help.Bind(wx.EVT_BUTTON, self.on_help) def on_okay(self, event=None): """ Write the config file - update the *true* config file to new prefs Just updates all the GUI instead of figuring out which parts actually need updating -- laziness! """ old = self.gui.util.config if self.config['language'] != old['language']: wx.MessageBox(_("Whyteboard will be translated into %s when restarted") % _(self.config['language']), u"Whyteboard") if self.config['handle_size'] != old['handle_size']: pub.sendMessage('tools.set_handle_size', handle_size=self.config['handle_size']) if self.config['canvas_border'] != old['canvas_border']: pub.sendMessage('canvas.set_border', border_size=self.config['canvas_border']) if 'default_font' in self.config: if self.config['default_font'] and not self.gui.util.font: self.gui.util.font = wx.FFont(1, 1) self.gui.util.font.SetNativeFontInfoFromString(self.config['default_font']) # view toggles/menu. do the colour grid later keys = ['statusbar', 'toolbar', 'tool_preview'] for x in keys: method = getattr(self.gui, "on_" + x) if self.config[x]: method(None, True) else: method(None, False) self.config.write() self.gui.util.config = self.config ctrl = self.gui.control x = self.config['undo_sheets'] if x < old['undo_sheets']: del self.gui.closed_tabs[0:x] if x != old['undo_sheets']: self.gui.menu.make_closed_tabs_menu() if self.config['bmp_select_transparent'] != old['bmp_select_transparent']: self.gui.canvas.copy = None if self.config['toolbox_columns'] != old['toolbox_columns']: ctrl.toolsizer.SetCols(self.config['toolbox_columns']) ctrl.toolsizer.SetHGap(5) ctrl.toolsizer.SetVGap(5) wx.CallAfter(ctrl.toolsizer.Layout) if not self.config['tool_preview']: ctrl.preview.Hide() else: ctrl.preview.Show() pub.sendMessage('gui.preview.refresh') if self.config['toolbox'] != old['toolbox']: cols = 1 if self.config['toolbox'] != 'text': cols = int(self.config['toolbox_columns']) ctrl.toolsizer.Clear(True) ctrl.toolsizer.SetCols(cols) ctrl.make_toolbox(self.config['toolbox']) ctrl.toolsizer.Layout() do = True if not self.config['colour_grid']: do = False wx.CallAfter(self.gui.on_colour_grid, None, do) # too lazy to check if each colour has changed - just remake it all self.gui.canvas.redraw_all() ctrl.grid.Clear(True) ctrl.make_colour_grid() ctrl.grid.Layout() self.Destroy() def on_help(self, event=None): self.gui.on_help(page="preferences") def on_cancel(self, event): self.Destroy() #---------------------------------------------------------------------- class General(wx.Panel): """ Select language and toolbar/status bar visiblity """ def __init__(self, parent, gui, config): wx.Panel.__init__(self, parent) self.config = config self.gui = gui if os.name == "posix": self.SetBackgroundColour("White") sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(sizer) translated = [i[1] for i in meta.languages] translated.sort() self.lang = wx.ComboBox(self, choices=translated, style=wx.CB_READONLY, size=(240, 30)) self.lang.Layout() undo = wx.StaticText(self, label=_("Number of Recently Closed Sheets")) self.undoctrl = wx.SpinCtrl(self, min=5, max=50) handle = wx.StaticText(self, label=_("Selection Handle Size")) self.handlectrl = wx.SpinCtrl(self, min=3, max=15) border = wx.StaticText(self, label=_("Canvas Border")) self.borderctrl = wx.SpinCtrl(self, min=10, max=35) langText = wx.StaticText(self, label=_("Choose Your Language:")) font = langText.GetClassDefaultAttributes().font font.SetWeight(wx.FONTWEIGHT_BOLD) langText.SetFont(font) undo.SetFont(font) handle.SetFont(font) border.SetFont(font) self.handlectrl.SetValue(self.config['handle_size']) self.undoctrl.SetValue(self.config['undo_sheets']) self.lang.SetValue(_(self.config['language'])) self.borderctrl.SetValue(self.config['canvas_border']) self.lang.Bind(wx.EVT_COMBOBOX, self.on_lang) self.undoctrl.Bind(wx.EVT_SPINCTRL, self.on_undo) self.handlectrl.Bind(wx.EVT_SPINCTRL, self.on_handle) self.borderctrl.Bind(wx.EVT_SPINCTRL, self.on_border) sizer.Add(langText, 0, wx.ALL, 15) sizer.Add(self.lang, 0, wx.LEFT, 30) sizer.Add(undo, 0, wx.ALL, 15) sizer.Add(self.undoctrl, 0, wx.LEFT, 30) sizer.Add(handle, 0, wx.ALL, 15) sizer.Add(self.handlectrl, 0, wx.LEFT, 30) sizer.Add(border, 0, wx.ALL, 15) sizer.Add(self.borderctrl, 0, wx.LEFT, 30) def on_lang(self, event): for lang in meta.languages: if self.lang.GetValue() == lang[1]: self.config['language'] = lang[0] # english string def on_undo(self, event): self.config['undo_sheets'] = self.undoctrl.GetValue() def on_handle(self, event): self.config['handle_size'] = self.handlectrl.GetValue() def on_border(self, event): self.config['canvas_border'] = self.borderctrl.GetValue() #---------------------------------------------------------------------- class FontAndColours(wx.Panel): """ Allows the user to select their custom colours for the grid in the left pane Their default font may be chosen, too Pretty ugly code to ensure that """ def __init__(self, parent, gui, config): wx.Panel.__init__(self, parent) if os.name == "posix": self.SetBackgroundColour("White") sizer = wx.BoxSizer(wx.VERTICAL) self.gui = gui self.parent = parent self.config = config self.SetSizer(sizer) self.buttons = [] self.grid = wx.GridSizer(cols=3, hgap=2, vgap=2) colours = [] for x in range(1, 10): col = self.config["colour" + str(x)] colours.append([int(c) for c in col]) for x, colour in enumerate(colours): method = lambda evt, _id = x: self.on_colour(evt, _id) b = wx.BitmapButton(self, bitmap=create_colour_bitmap(colour)) self.buttons.append(b) self.grid.Add(b, 0) b.Bind(wx.EVT_BUTTON, method) self.grid.Layout() self.button = wx.Button(self, label=_("Select Font")) self.button.Bind(wx.EVT_BUTTON, self.on_font) font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT) self.font = font # the correct font, w/ right size self.size = font.GetPointSize() # size to use regardless of font labCol = wx.StaticText(self, label=_("Choose Your Custom Colors:")) labFont = wx.StaticText(self, label=_("Default Font:")) transparency = wx.CheckBox(self, label=wordwrap(_("Transparent Bitmap Select (may draw slowly)"), 350, wx.ClientDC(gui))) new_font = labFont.GetClassDefaultAttributes().font new_font.SetWeight(wx.FONTWEIGHT_BOLD) labCol.SetFont(new_font) labFont.SetFont(new_font) if self.config['bmp_select_transparent']: transparency.SetValue(True) if 'default_font' in self.config: f = wx.FFont(1, 1) f.SetNativeFontInfoFromString(self.config['default_font']) self.font = f self.button.SetFont(f) self.button.SetLabel(self.config['default_font']) if os.name == "nt": self.font_label(f) f = wx.FFont(1, 1) f.SetNativeFontInfoFromString(self.config['default_font']) f.SetPointSize(self.size) self.button.SetFont(f) else: if os.name == "nt": self.font_label(self.font) else: self.button.SetLabel(self.button.GetFont().GetNativeFontInfoDesc()) sizer.Add(labCol, 0, wx.ALL, 15) sizer.Add(self.grid, 0, wx.LEFT | wx.BOTTOM, 30) sizer.Add(labFont, 0, wx.LEFT, 15) sizer.Add((10, 15)) sizer.Add(self.button, 0, wx.LEFT, 30) sizer.Add((10, 25)) sizer.Add(transparency, 0, wx.LEFT, 15) transparency.Bind(wx.EVT_CHECKBOX, self.on_transparency) def on_transparency(self, event): self.config['bmp_select_transparent'] = event.Checked() def on_font(self, event): """ Change the font button's font, and text description of the font, but the button's font size must not change, or it'll "grow" too big. """ data = wx.FontData() data.SetInitialFont(self.font) dlg = wx.FontDialog(self, data) if dlg.ShowModal() == wx.ID_OK: font = dlg.GetFontData().GetChosenFont() if os.name == "nt": self.font_label(font) else: self.button.SetLabel(font.GetNativeFontInfoDesc()) self.font = font font2 = dlg.GetFontData().GetChosenFont() font2.SetPointSize(self.size) self.button.SetFont(font2) self.GetSizer().Layout() self.config['default_font'] = font.GetNativeFontInfoDesc() dlg.Destroy() def font_label(self, font): """Sets the font label on Windows""" size = font.GetPointSize() weight = font.GetWeightString() style = font.GetStyle() w = s = u"" if weight == "wxBOLD": w = _("Bold") elif weight == "wxLIGHT": w = _("Light") if style == wx.ITALIC: s = _("Italic") self.button.SetLabel(u"%s %s %s %s" % (font.GetFaceName() , w, s, size)) def on_colour(self, event, _id): """ Change the colour of the selected button. We need to remove the current button's button, recreate it with the new colour and re-layout the sizer """ pref = "colour%s" % (_id + 1) colour = ([int(c) for c in self.config[pref]]) data = wx.ColourData() data.SetColour(colour) dlg = wx.ColourDialog(self, data) if dlg.ShowModal() == wx.ID_OK: self.config[pref] = list(dlg.GetColourData().Colour.Get()) col = create_colour_bitmap(dlg.GetColourData().Colour) self.buttons[_id].SetBitmapLabel(col) self.grid.Layout() dlg.Destroy() #---------------------------------------------------------------------- class View(scrolled.ScrolledPanel): """ Select language and toolbar/status bar visiblity """ def __init__(self, parent, gui, config): scrolled.ScrolledPanel.__init__(self, parent) self.config = config self.gui = gui if os.name == "posix": self.SetBackgroundColour("White") sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(sizer) self.SetupScrolling(False, True) radio1 = wx.RadioButton(self, label=" " + _("Icons")) radio2 = wx.RadioButton(self, label=" " + _("Text")) cols = wx.ComboBox(self, choices=('2', '3'), size=(60, -1), style=wx.CB_READONLY) self.width = wx.SpinCtrl(self, min=1, max=12000) self.height = wx.SpinCtrl(self, min=1, max=12000) statusbar = wx.CheckBox(self, label=_("View the status bar")) toolbar = wx.CheckBox(self, label=_("View the toolbar")) title = wx.CheckBox(self, label=_("Show the title when printing")) preview = wx.CheckBox(self, label=_("Show the tool preview")) colour = wx.CheckBox(self, label=_("Show the color grid")) label = wx.StaticText(self, label=_("Toolbox View:")) cols_label = wx.StaticText(self, label=_("Number of Toolbox Columns:")) width = wx.StaticText(self, label=_("Default Canvas Width")) height = wx.StaticText(self, label=_("Default Canvas Height")) font = label.GetFont() font.SetWeight(wx.FONTWEIGHT_BOLD) label.SetFont(font) width.SetFont(font) cols_label.SetFont(font) height.SetFont(font) sizer.Add(label, 0, wx.ALL, 15) if self.config['toolbox'] == 'icon': radio1.SetValue(True) else: radio2.SetValue(True) if self.config['statusbar']: statusbar.SetValue(True) if self.config['toolbar']: toolbar.SetValue(True) if self.config['print_title']: title.SetValue(True) if self.config['tool_preview']: preview.SetValue(True) if self.config['colour_grid']: colour.SetValue(True) cols.SetValue(str(self.config['toolbox_columns'])) self.width.SetValue(self.config['default_width']) self.height.SetValue(self.config['default_height']) for x, btn in enumerate([radio1, radio2]): sizer.Add(btn, 0, wx.LEFT, 30) sizer.Add((10, 5)) method = lambda evt, _id = x: self.on_view(evt, _id) btn.Bind(wx.EVT_RADIOBUTTON, method) sizer.Add(cols_label, 0, wx.ALL, 15) sizer.Add(cols, 0, wx.LEFT, 30) sizer.Add((10, 15)) sizer.Add(width, 0, wx.ALL, 15) sizer.Add(self.width, 0, wx.LEFT, 30) sizer.Add(height, 0, wx.ALL, 15) sizer.Add(self.height, 0, wx.LEFT, 30) sizer.Add((10, 15)) sizer.Add(statusbar, 0, wx.ALL, 10) sizer.Add(toolbar, 0, wx.LEFT | wx.BOTTOM, 10) sizer.Add(title, 0, wx.LEFT | wx.BOTTOM, 10) sizer.Add(preview, 0, wx.LEFT | wx.BOTTOM, 10) sizer.Add(colour, 0, wx.LEFT, 10) self.Scroll(0, 0) cols.Bind(wx.EVT_COMBOBOX, self.on_columns) statusbar.Bind(wx.EVT_CHECKBOX, self.on_statusbar) toolbar.Bind(wx.EVT_CHECKBOX, self.on_toolbar) title.Bind(wx.EVT_CHECKBOX, self.on_title) preview.Bind(wx.EVT_CHECKBOX, self.on_preview) colour.Bind(wx.EVT_CHECKBOX, self.on_colour) self.width.Bind(wx.EVT_SPINCTRL, self.on_width) self.height.Bind(wx.EVT_SPINCTRL, self.on_height) def on_statusbar(self, event): self.config['statusbar'] = event.Checked() def on_columns(self, event): self.config['toolbox_columns'] = int(event.GetEventObject().GetValue()) def on_toolbar(self, event): self.config['toolbar'] = event.Checked() def on_preview(self, event): self.config['tool_preview'] = event.Checked() def on_colour(self, event): self.config['colour_grid'] = event.Checked() def on_title(self, event): self.config['print_title'] = event.Checked() def on_width(self, event): self.config['default_width'] = self.width.GetValue() def on_height(self, event): self.config['default_height'] = self.height.GetValue() def on_view(self, event, _id): if _id == 0: self.config['toolbox'] = 'icon' else: self.config['toolbox'] = 'text' #---------------------------------------------------------------------- class PDF(wx.Panel): """ PDF conversion quality """ def __init__(self, parent, gui, config): wx.Panel.__init__(self, parent) self.config = config self.gui = gui self.im_result = None if os.name == "posix": self.SetBackgroundColour("White") sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(sizer) label = wx.StaticText(self, label=_("Conversion Quality:")) note = wx.StaticText(self, label=wordwrap(_("Note: Higher quality takes longer to convert"), 350, wx.ClientDC(gui))) radio1 = wx.RadioButton(self, label=_("Highest")) radio2 = wx.RadioButton(self, label=_("High")) radio3 = wx.RadioButton(self, label=_("Normal")) font = label.GetFont() font.SetWeight(wx.FONTWEIGHT_BOLD) label.SetFont(font) sizer.Add(label, 0, wx.ALL, 15) for x, btn in enumerate([radio1, radio2, radio3]): sizer.Add(btn, 0, wx.LEFT, 30) sizer.Add((10, 5)) method = lambda evt, _id = x: self.on_quality(evt, _id) btn.Bind(wx.EVT_RADIOBUTTON, method) sizer.Add((10, 10)) sizer.Add(note, 0, wx.LEFT | wx.BOTTOM, 30) if os.name == "nt": label_im = wx.StaticText(self, label=_("ImageMagick Location")) label_im.SetFont(font) self.im_button = wx.Button(self) self.im_button.Bind(wx.EVT_BUTTON, self.on_im) self.set_im_button() sizer.Add(label_im, 0, wx.LEFT, 15) sizer.Add((10, 15)) sizer.Add(self.im_button, 0, wx.LEFT, 30) if self.config['convert_quality'] == 'highest': radio1.SetValue(True) if self.config['convert_quality'] == 'high': radio2.SetValue(True) if self.config['convert_quality'] == 'normal': radio3.SetValue(True) def on_quality(self, event, _id): if _id == 0: self.config['convert_quality'] = 'highest' elif _id == 1: self.config['convert_quality'] = 'high' else: self.config['convert_quality'] = 'normal' def set_im_button(self): """Sets the label to IM's path""" s = _("Find...") if "imagemagick_path" in self.config: s = self.config["imagemagick_path"] self.im_button.SetLabel(s) self.GetSizer().Layout() def on_im(self, event): dlg = FindIM(self, self.gui, self.check_im_path) dlg.ShowModal() if self.im_result: self.config['imagemagick_path'] = self.im_result self.set_im_button() def check_im_path(self, path): _file = os.path.join(path, u"convert.exe") if not os.path.exists(_file): wx.MessageBox(_('Folder "%s" does not contain convert.exe') % path, u"Whyteboard") self.im_result = None return False self.im_result = path return True #---------------------------------------------------------------------- whyteboard-0.41.1/whyteboard/gui/dialogs.py0000777000175000017500000014214711444677005017763 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ This module contains classes extended from wx.Dialog used by the GUI. """ from __future__ import division from __future__ import with_statement import os import sys import zipfile import time import wx import wx.lib.mixins.listctrl as listmix from wx.lib.agw.hyperlink import HyperLinkCtrl from urllib import urlopen, urlretrieve, urlencode from whyteboard.lib import BaseErrorDialog, icon, pub import whyteboard.tools as tools from whyteboard.misc import meta from whyteboard.misc import (get_home_dir, bitmap_button, is_exe, extract_tar, fix_std_sizer_tab_order, format_bytes, version_is_greater, get_image_path) _ = wx.GetTranslation #---------------------------------------------------------------------- class History(wx.Dialog): """ Creates a history replaying dialog and methods for its functionality """ def __init__(self, gui): wx.Dialog.__init__(self, gui, title=_("History Player"), size=(400, 200), style=wx.CLOSE_BOX | wx.CAPTION) self.gui = gui self.looping = False self.paused = False self.playButton = bitmap_button(self, get_image_path(u"icons", u"play"), True, toggle=True) self.pauseButton = bitmap_button(self, get_image_path(u"icons", u"pause"), True, toggle=True) self.stopButton = bitmap_button(self, get_image_path(u"icons", u"stop"), True, toggle=True) closeButton = wx.Button(self, wx.ID_CANCEL, _("&Close")) sizer = wx.BoxSizer(wx.VERTICAL) historySizer = wx.BoxSizer(wx.HORIZONTAL) historySizer.Add(self.playButton, 0, wx.ALL, 2) historySizer.Add(self.pauseButton, 0, wx.ALL, 2) historySizer.Add(self.stopButton, 0, wx.ALL, 2) sizer.Add(historySizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 13) sizer.Add((10, 5)) sizer.Add(closeButton, 0, wx.ALIGN_CENTRE | wx.BOTTOM, 13) self.SetSizer(sizer) sizer.Fit(self) self.SetEscapeId(closeButton.GetId()) self.toggle_buttons() self.playButton.Bind(wx.EVT_BUTTON, self.play) self.pauseButton.Bind(wx.EVT_BUTTON, self.pause) self.stopButton.Bind(wx.EVT_BUTTON, self.stop) self.Bind(wx.EVT_CLOSE, self.on_close) closeButton.Bind(wx.EVT_BUTTON, self.on_close) def play(self, event): """ Starts the replay if it's not already started, unpauses if paused """ if self.looping: self.paused = False self.toggle_buttons(True, False, False) return if self.paused: self.paused = False tmp_shapes = list(self.gui.canvas.shapes) shapes = [] for shape in tmp_shapes: if not isinstance(shape, tools.Image): shapes.append(shape) if shapes: self.toggle_buttons(True, False, False) self.looping = True self.draw(shapes) def draw(self, shapes): """ Replays the users' last-drawn pen strokes. The loop can be paused/unpaused by the user. """ dc = wx.ClientDC(self.gui.canvas) dc.SetBackground(wx.WHITE_BRUSH) buff = self.gui.canvas.buffer bkgregion = wx.Region(0, 0, buff.GetWidth(), buff.GetHeight()) dc.SetClippingRegionAsRegion(bkgregion) dc.Clear() self.gui.canvas.PrepareDC(dc) # paint any images first for s in self.gui.canvas.shapes: if isinstance(s, tools.Image): s.draw(dc) for pen in shapes: # draw pen outline if isinstance(pen, tools.Pen): if isinstance(pen, tools.Highlighter): gc = wx.GraphicsContext.Create(dc) colour = (pen.colour[0], pen.colour[1], pen.colour[2], 50) gc.SetPen(wx.Pen(colour, pen.thickness)) path = gc.CreatePath() else: dc.SetPen(wx.Pen(pen.colour, pen.thickness)) for x, p in enumerate(pen.points): if self.looping and not self.paused: try: wx.MilliSleep((pen.time[x + 1] - pen.time[x]) * 950) wx.Yield() except IndexError: pass if isinstance(pen, tools.Highlighter): path.MoveToPoint(p[0], p[1]) path.AddLineToPoint(p[2], p[3]) gc.DrawPath(path) else: dc.DrawLine(p[0], p[1], p[2], p[3]) else: # loop is paused, wait for unpause/close/stop while self.paused: wx.MicroSleep(100) wx.Yield() else: if self.looping and not self.paused: wx.MilliSleep(350) wx.Yield() pen.draw(dc, True) else: # loop is paused, wait for unpause/close/stop while self.paused: wx.MicroSleep(100) wx.Yield() self.stop() # restore other drawn items def pause(self, event=None): """Pauses/unpauses the replay.""" if self.looping: self.paused = not self.paused self.toggle_buttons(not self.paused, self.paused, False) def stop(self, event=None): """Stops the replay.""" if self.looping or self.paused: self.toggle_buttons(False, False, True) self.looping = False self.paused = False self.gui.canvas.Refresh() # restore def on_close(self, event=None): """ Called when the dialog is closed; stops the replay and ends the modal view, allowing the GUI to Destroy() the dialog. """ self.stop() self.EndModal(1) def toggle_buttons(self, play=False, pause=False, stop=True): """ Toggles the buttons on/off as indicated by the bool params """ self.playButton.SetValue(play) self.pauseButton.SetValue(pause) self.stopButton.SetValue(stop) #---------------------------------------------------------------------- class ProgressDialog(wx.Dialog): """ Shows a Progres Gauge while an operation is taking place. May be cancellable which is possible when converting pdf/ps """ def __init__(self, gui, title, cancellable=False): """Defines a gauge and a timer which updates the gauge.""" wx.Dialog.__init__(self, gui, title=title, style=wx.CAPTION) self.gui = gui self.timer = wx.Timer(self) self.gauge = wx.Gauge(self, range=100, size=(180, 30)) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.gauge, 0, wx.ALL, 10) if cancellable: cancel = wx.Button(self, wx.ID_CANCEL, _("&Cancel")) cancel.SetDefault() cancel.Bind(wx.EVT_BUTTON, self.on_cancel) btnSizer = wx.StdDialogButtonSizer() btnSizer.AddButton(cancel) btnSizer.Realize() sizer.Add(btnSizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10) self.SetSizer(sizer) sizer.Fit(self) self.SetFocus() self.Bind(wx.EVT_TIMER, self.on_timer, self.timer) self.timer.Start(95) def on_timer(self, event): """Increases the gauge's progress.""" self.gauge.Pulse() def on_cancel(self, event): """Cancels the conversion process""" self.SetTitle(_("Cancelling...")) self.FindWindowById(wx.ID_CANCEL).Disable() self.timer.Stop() self.gui.convert_cancelled = True if os.name == "nt": wx.Kill(self.gui.pid, wx.SIGKILL) else: wx.Kill(self.gui.pid) #---------------------------------------------------------------------- class UpdateDialog(wx.Dialog): """ Updates Whyteboard. First, connect to server, parse HTML to check for new version. Then, when the user clicks update, fetch the file and show the download progress. Then, depending on exe/python source, we update the program accordingly """ def __init__(self, gui): """ Builds the UI - then wx.CallAfter()s the update check to the server """ wx.Dialog.__init__(self, gui, title=_("Updates"), size=(350, 200)) self.gui = gui self.downloaded = 0 self.new_version = None self._file = None self._type = None self.text = wx.StaticText(self, label=_("Connecting to server..."), size=(300, 80)) self.text2 = wx.StaticText(self, label="") # for download progress self.btn = wx.Button(self, wx.ID_OK, _("Update")) cancel = wx.Button(self, wx.ID_CANCEL, _("&Cancel")) self.btn.Enable(False) self.btn.SetDefault() btnSizer = wx.StdDialogButtonSizer() btnSizer.AddButton(cancel) btnSizer.AddButton(self.btn) btnSizer.SetCancelButton(cancel) btnSizer.Realize() sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.text, 0, wx.LEFT | wx.TOP | wx.RIGHT, 10) sizer.Add(self.text2, 0, wx.LEFT | wx.RIGHT, 10) sizer.Add((10, 20)) sizer.Add(btnSizer, 0, wx.ALIGN_CENTRE) self.SetSizer(sizer) self.SetFocus() self.btn.Bind(wx.EVT_BUTTON, self.update) wx.CallAfter(self.check) # we want to show the dialog *then* fetch URL def check(self): """ Parses the "control" file giving information about the latest release """ try: f = urlopen("http://whyteboard.org/latest") except IOError: self.text.SetLabel(_("Could not connect to server.")) return html = f.read().split(u"\n") f.close() self.new_version = html[0] if version_is_greater(meta.version, self.new_version): self.text.SetLabel(_("You are running the latest version.")) return self._file, size = html[3], html[4] self._type = ".tar.gz" if os.name == "nt" and is_exe(): self._file, size = html[1], html[2] self._type = ".zip" s = (_("There is a new version available, %(version)s\nFile: %(filename)s\nSize: %(filesize)s") % {'version': html[0], 'filename': self._file, 'filesize': format_bytes(size)} ) self.text.SetLabel(s) self.btn.Enable(True) self._file = u"http://whyteboard.googlecode.com/files/%s" % self._file def update(self, event=None): """ Updates the program by downloading the correct file and extracting it. On Linux, the Tar file is extracted into the current directory, and on Windows the .exe is renamed, the new one renamed to replace it and on both platforms the program is then restarted (after asking the user to save or not) """ path = self.gui.util.path args = [] # args to reload running program, may include filename tmp = None tmp_file = os.path.join(path[0], 'tmp-wb-' + self._type) try: tmp = urlretrieve(self._file, tmp_file, self.reporter) except IOError: self.text.SetLabel(_("Could not connect to server.")) self.btn.SetLabel(_("Retry")) return if os.name == "nt" and is_exe(): os.rename(path[1], u"wtbd-bckup.exe") _zip = zipfile.ZipFile(tmp_file) _zip.extractall() _zip.close() os.remove(tmp_file) wb = os.path.abspath(sys.argv[0]) args = [wb, [wb]] else: if os.name == "posix": os.system("tar -xf \"%s\" --strip-components=1" % tmp[0]) else: extract_tar(self.gui.util.path[0], os.path.abspath(tmp[0]), self.new_version, meta.backup_extension) os.remove(tmp[0]) args = [u'python', [u'python', sys.argv[0]]] # for os.execvp if self.gui.util.filename: name = u'"%s"' % self.gui.util.filename # gotta escape for Windows args[1].append(u"-f") args[1].append(name) # restart, load .wtbd self.gui.prompt_for_save(os.execvp, wx.YES_NO, args) def reporter(self, count, block, total): self.downloaded += block self.text2.SetLabel(_("Downloaded %s of %s") % (format_bytes(self.downloaded), format_bytes(total))) #---------------------------------------------------------------------- class TextInput(wx.Dialog): """ Shows a text input screen, updates the canvas' text as text is being input and has methods for """ def __init__(self, gui, note=None, text=u""): """ Standard constructor - sets text to supplied text variable, if present. """ wx.Dialog.__init__(self, gui, title=_("Enter text"), style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.WANTS_CHARS, size=(350, 280)) self.ctrl = wx.TextCtrl(self, style=wx.TE_MULTILINE, size=(300, 120)) fontBtn = wx.Button(self, label=_("Select Font")) self.colourBtn = wx.ColourPickerCtrl(self) self.okButton = wx.Button(self, wx.ID_OK, _("&OK")) self.okButton.SetDefault() self.cancelButton = wx.Button(self, wx.ID_CANCEL, _("&Cancel")) extent = self.ctrl.GetFullTextExtent(u"Hy") lineHeight = extent[1] + extent[3] self.ctrl.SetSize(wx.Size(-1, lineHeight * 4)) if not gui.util.font: gui.util.font = self.ctrl.GetFont() self.gui = gui self.note = None self.colour = gui.util.colour gap = wx.LEFT | wx.TOP | wx.RIGHT font = gui.util.font if note: self.note = note self.colour = note.colour text = note.text font = wx.FFont(1, 1) font.SetNativeFontInfoFromString(note.font_data) self.set_text_colour(text) self.ctrl.SetFont(font) self.colourBtn.SetColour(self.colour) _sizer = wx.BoxSizer(wx.HORIZONTAL) _sizer.Add(fontBtn, 0, wx.RIGHT, 5) _sizer.Add(self.colourBtn, 0) btnSizer = wx.StdDialogButtonSizer() btnSizer.AddButton(self.okButton) btnSizer.AddButton(self.cancelButton) btnSizer.Realize() sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.ctrl, 1, gap | wx.EXPAND, 10) sizer.Add(_sizer, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.LEFT | wx.TOP, 10) sizer.Add((10, 10)) # Spacer. sizer.Add(btnSizer, 0, wx.BOTTOM | wx.ALIGN_CENTRE, 10) self.SetSizer(sizer) fix_std_sizer_tab_order(btnSizer) self.set_focus() self.Bind(wx.EVT_BUTTON, self.on_font, fontBtn) self.Bind(wx.EVT_COLOURPICKER_CHANGED, self.on_colour, self.colourBtn) self.Bind(wx.EVT_TEXT, self.update_canvas, self.ctrl) ac = [(wx.ACCEL_CTRL, wx.WXK_RETURN, self.okButton.GetId())] tbl = wx.AcceleratorTable(ac) self.SetAcceleratorTable(tbl) if text: self.update_canvas() self.gui.canvas.redraw_all(True) def on_font(self, evt): """ Shows the font dialog, sets the input text's font and returns focus to the text input box, at the user's selected point. """ data = wx.FontData() data.SetInitialFont(self.ctrl.GetFont()) dlg = wx.FontDialog(self, data) if dlg.ShowModal() == wx.ID_OK: data = dlg.GetFontData() self.gui.util.font = data.GetChosenFont() self.ctrl.SetFont(self.gui.util.font) self.set_text_colour() self.update_canvas() # Update dialog with new text height dlg.Destroy() self.set_focus() def on_colour(self, event): """Change text colour to the chosen one""" self.colour = event.GetColour() self.set_text_colour() self.update_canvas() self.set_focus() def set_text_colour(self, text=None): """Updates (or forces...) the text colour""" if not text: text = self.ctrl.GetValue() self.ctrl.SetValue("") self.ctrl.SetForegroundColour(self.colour) self.ctrl.SetValue(text) def set_focus(self): """Gives the text focus, places the cursor at the end of the text""" self.ctrl.SetFocus() self.ctrl.SetInsertionPointEnd() def update_canvas(self, event=None): """Updates the canvas with the inputted text""" if self.note: shape = self.note canvas = shape.canvas else: canvas = self.gui.canvas shape = canvas.shape self.transfer_data(shape) shape.find_extent() canvas.redraw_all() # stops overlapping text def transfer_data(self, text_obj): """Transfers the dialog's data to an object.""" text_obj.text = self.ctrl.GetValue() text_obj.font = self.ctrl.GetFont() text_obj.colour = self.colour #---------------------------------------------------------------------- class FindIM(wx.Dialog): """ Asks a user for the location of ImageMagick (Windows-only) Method is called on the ok button (for preference use) """ def __init__(self, parent, gui, method): wx.Dialog.__init__(self, gui, title=_("ImageMagick Notification")) self.gui = gui self.method = method self.path = u"C:/Program Files/" t = (_("Whyteboard uses ImageMagick to load PDF, SVG and PS files. \nPlease select its installed location.")) text = wx.StaticText(self, label=t) btn = wx.Button(self, label=_("Find location...")) gap = wx.LEFT | wx.TOP | wx.RIGHT self.okButton = wx.Button(self, wx.ID_OK, _("&OK")) self.okButton.SetDefault() self.cancelButton = wx.Button(self, wx.ID_CANCEL, _("&Cancel")) btnSizer = wx.StdDialogButtonSizer() btnSizer.AddButton(self.okButton) btnSizer.AddButton(self.cancelButton) btnSizer.Realize() sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(text, 1, gap | wx.EXPAND, 10) sizer.Add(btn, 0, gap | wx.ALIGN_CENTRE, 20) sizer.Add((10, 20)) # Spacer. sizer.Add(btnSizer, 0, wx.BOTTOM | wx.ALIGN_CENTRE, 12) self.SetSizer(sizer) sizer.Fit(self) self.SetFocus() btn.Bind(wx.EVT_BUTTON, self.browse) self.okButton.Bind(wx.EVT_BUTTON, self.ok) self.cancelButton.Bind(wx.EVT_BUTTON, self.cancel) def browse(self, event=None): dlg = wx.DirDialog(self, _("Choose a directory"), self.path, style=wx.DD_DIR_MUST_EXIST) if dlg.ShowModal() == wx.ID_OK: self.path = dlg.GetPath() else: dlg.Destroy() def ok(self, event=None): if self.method(self.path): self.Close() def cancel(self, event=None): self.Close() #---------------------------------------------------------------------- class Feedback(wx.Dialog): """ Sends feedback to myself by POSTing to a PHP script """ def __init__(self, gui): wx.Dialog.__init__(self, gui, title=_("Send Feedback")) t_lbl = wx.StaticText(self, label=_("Your Feedback:")) email_label = wx.StaticText(self, label=_("E-mail Address")) self.feedback = wx.TextCtrl(self, size=(350, 250), style=wx.TE_MULTILINE) self.email = wx.TextCtrl(self) cancel_b = wx.Button(self, wx.ID_CANCEL, _("&Cancel")) send_b = wx.Button(self, wx.ID_OK, _("Send &Feedback")) send_b.SetDefault() btnSizer = wx.StdDialogButtonSizer() btnSizer.AddButton(send_b) btnSizer.AddButton(cancel_b) btnSizer.Realize() font = t_lbl.GetClassDefaultAttributes().font font.SetWeight(wx.FONTWEIGHT_BOLD) t_lbl.SetFont(font) email_label.SetFont(font) vsizer = wx.BoxSizer(wx.VERTICAL) vsizer.Add((10, 10)) vsizer.Add(t_lbl, 0, wx.LEFT | wx.RIGHT, 10) vsizer.Add(self.feedback, 0, wx.EXPAND | wx.ALL, 10) vsizer.Add((10, 10)) vsizer.Add(email_label, 0, wx.ALL, 10) vsizer.Add(self.email, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 10) vsizer.Add((10, 10)) vsizer.Add(btnSizer, 0, wx.TOP | wx.BOTTOM | wx.ALIGN_CENTRE, 15) self.SetSizerAndFit(vsizer) self.SetFocus() self.SetAutoLayout(True) self.Bind(wx.EVT_BUTTON, self.submit, send_b) def submit(self, event): """Submit feedback.""" if not self.email.GetValue() or self.email.GetValue().find("@") == -1: wx.MessageBox(_("Please fill out your email address"), u"Whyteboard") return if len(self.feedback.GetValue()) < 10: wx.MessageBox(_("Please provide some feedback"), u"Whyteboard") return params = urlencode({'submitted': 'fgdg', 'feedback': self.feedback.GetValue(), 'email': self.email.GetValue()}) f = urlopen(u"http://www.whyteboard.org/feedback_submit.php", params) wx.MessageBox(_("Your feedback has been sent, thank you."), _("Feedback Sent")) self.Destroy() #---------------------------------------------------------------------- class PromptForSave(wx.Dialog): """ Prompts the user to confirm quitting without saving. Style can be wx.YES_NO | wx.CANCEL or just wx.YES_NO. 2nd is used when prompting the user after updating the program. """ def __init__(self, gui, name, method, style, args): wx.Dialog.__init__(self, gui, title=_("Save File?")) self.gui = gui self.method = method self.args = args warning = wx.ArtProvider.GetBitmap(wx.ART_WARNING, wx.ART_CMN_DIALOG) bmp = wx.StaticBitmap(self, bitmap=warning) btnSizer = wx.StdDialogButtonSizer() mainSizer = wx.BoxSizer(wx.VERTICAL) iconSizer = wx.BoxSizer(wx.HORIZONTAL) textSizer = wx.BoxSizer(wx.VERTICAL) container = wx.BoxSizer(wx.HORIZONTAL) top_message = wx.StaticText(self, label=_('Save changes to "%s" before closing?') % name) bottom_message = wx.StaticText(self, label=self.get_time()) font = top_message.GetClassDefaultAttributes().font font.SetWeight(wx.FONTWEIGHT_BOLD) font.SetPointSize(font.GetPointSize() + 1) top_message.SetFont(font) if not self.gui.util.filename: saveButton = wx.Button(self, wx.ID_SAVE, _("Save &As...")) else: saveButton = wx.Button(self, wx.ID_SAVE, _("&Save")) if style == wx.YES_NO | wx.CANCEL: cancelButton = wx.Button(self, wx.ID_CANCEL, _("&Cancel")) btnSizer.AddButton(cancelButton) noButton = wx.Button(self, wx.ID_NO, _("&Don't Save")) saveButton.SetDefault() btnSizer.AddButton(noButton) btnSizer.AddButton(saveButton) btnSizer.Realize() iconSizer.Add(bmp, 0) textSizer.Add(top_message) textSizer.Add((10, 10)) textSizer.Add(bottom_message) container.Add(iconSizer, 0, wx.LEFT, 15) container.Add((15, -1)) container.Add(textSizer, 1, wx.RIGHT, 15) container.Layout() mainSizer.Add((10, 15)) mainSizer.Add(container, wx.ALL, 30) mainSizer.Add((10, 10)) mainSizer.Add(btnSizer, 0, wx.TOP | wx.BOTTOM | wx.ALIGN_CENTRE, 15) self.SetSizerAndFit(mainSizer) self.SetFocus() self.SetAutoLayout(True) fix_std_sizer_tab_order(btnSizer) self.Bind(wx.EVT_BUTTON, self.okay, saveButton) self.Bind(wx.EVT_BUTTON, self.no, noButton) def get_time(self): m, s = divmod(time.time() - self.gui.util.save_time, 60) h, m = divmod(m, 60) hours, mins, seconds = "", "", "" # ugly.... if m > 0 and h < 1: mins = (u"%i " % m) + _("minutes") if m == 1 and h < 1: mins = _("minute") if h > 0: hours = (u"%i " % h) + _("hours") if h == 1: hours = _("hour") if s == 1 and m < 1: seconds = _("second") elif s > 1 and m < 1: seconds = (u"%i " % s) + _("seconds") ms = u"%s%s%s" % (hours, mins, seconds) return _("If you don't save, changes from the last\n%s will be permanently lost.") % ms def okay(self, event): self.gui.on_save() if self.gui.util.saved: self.Close() if self.gui.util.saved or self.method == os.execvp: self.method(*self.args) # force restart, otherwise 'cancel' # returns to application def no(self, event): self.method(*self.args) self.Close() if self.method == self.gui.Destroy: sys.exit() def cancel(self, event): self.Close() #---------------------------------------------------------------------- class Resize(wx.Dialog): """ Allows the user to resize a sheet's canvas """ def __init__(self, gui): """ Two spinctrls are used to set the width/height. Canvas updates as the values change """ wx.Dialog.__init__(self, gui, title=_("Resize Canvas")) self.gui = gui gap = wx.LEFT | wx.TOP | wx.RIGHT width, height = self.gui.canvas.buffer.GetSize() self.size = (width, height) self.wctrl = wx.SpinCtrl(self, min=1, max=12000) self.hctrl = wx.SpinCtrl(self, min=1, max=12000) csizer = wx.GridSizer(cols=2, hgap=1, vgap=2) csizer.Add(wx.StaticText(self, label=_("Width:")), 0, wx.TOP | wx.ALIGN_RIGHT, 10) csizer.Add(self.wctrl, 1, gap, 7) csizer.Add(wx.StaticText(self, label=_("Height:")), 0, wx.TOP | wx.ALIGN_RIGHT, 7) csizer.Add(self.hctrl, 1, gap, 7) self.wctrl.SetValue(width) self.hctrl.SetValue(height) okButton = wx.Button(self, wx.ID_OK, _("&OK")) okButton.SetDefault() cancelButton = wx.Button(self, wx.ID_CANCEL, _("&Cancel")) applyButton = wx.Button(self, wx.ID_APPLY, _("&Apply")) btnSizer = wx.StdDialogButtonSizer() btnSizer.AddButton(okButton) btnSizer.AddButton(cancelButton) btnSizer.AddButton(applyButton) btnSizer.Realize() sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(csizer, 0, gap, 7) sizer.Add((10, 15)) sizer.Add(btnSizer, 0, wx.ALIGN_CENTRE | wx.ALL, 5) sizer.Add((10, 5)) self.SetSizer(sizer) self.SetFocus() self.SetEscapeId(cancelButton.GetId()) self.Fit() cancelButton.Bind(wx.EVT_BUTTON, self.cancel) okButton.Bind(wx.EVT_BUTTON, self.ok) applyButton.Bind(wx.EVT_BUTTON, self.apply) self.hctrl.Bind(wx.EVT_SPINCTRL, self.resize) self.wctrl.Bind(wx.EVT_SPINCTRL, self.resize) fix_std_sizer_tab_order(sizer) def apply(self, event): self.size = self.gui.canvas.buffer.GetSize() def ok(self, event): self.resize() self.Close() def resize(self, event=None): value = (self.wctrl.GetValue(), self.hctrl.GetValue()) self.gui.canvas.resize(value) def cancel(self, event): self.gui.canvas.resize(self.size) self.Close() #---------------------------------------------------------------------- class ErrorDialog(BaseErrorDialog): def __init__(self, msg): BaseErrorDialog.__init__(self, None, title=_("Error Report"), message=msg) self.SetDescriptionLabel(_("An error has occured - please report it")) self.gui = wx.GetTopLevelWindows()[0] def Abort(self): self.gui.prompt_for_save(self.gui.Destroy) def GetEnvironmentInfo(self): """ Need to stick in extra information: preferences, helps with debugging """ info = super(ErrorDialog, self).GetEnvironmentInfo() path = os.path.join(get_home_dir(), u"user.pref") if os.path.exists(path): info.append(u"#---- Preferences ----#") with open(path) as f: for preference in f: preference = preference.rstrip() info.append(unicode(preference, "utf-8")) info.append(u"") info.append(u"") return os.linesep.join(info) def GetProgramName(self): return u"Whyteboard %s" % meta.version def Send(self): """Send the error report. PHP script calls isset($_POST['submitted'])""" params = urlencode({'submitted': 'fgdg', 'message': self._panel.err_msg, 'desc': self._panel.action.GetValue(), 'email': self._panel.email.GetValue()}) f = urlopen(u"http://www.whyteboard.org/bug_submit.php", params) self.gui.prompt_for_save(self.gui.Destroy) #---------------------------------------------------------------------- def ExceptionHook(exctype, value, trace): """ Handler for all unhandled exceptions """ ftrace = ErrorDialog.FormatTrace(exctype, value, trace) print ftrace # show in console if ErrorDialog.ABORT: os._exit(1) if not ErrorDialog.REPORTER_ACTIVE and not ErrorDialog.ABORT: dlg = ErrorDialog(ftrace) dlg.ShowModal() dlg.Destroy() #---------------------------------------------------------------------- class WhyteboardList(wx.ListCtrl, listmix.ListRowHighlighter, listmix.ListCtrlAutoWidthMixin): def __init__(self, parent): wx.ListCtrl.__init__(self, parent, style=wx.DEFAULT_CONTROL_BORDER | wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_HRULES) listmix.ListRowHighlighter.__init__(self, (206, 218, 255)) listmix.ListCtrlAutoWidthMixin.__init__(self) #---------------------------------------------------------------------- class ShapeViewer(wx.Dialog): """ Presents a list of the current sheet's shapes, in accordance to their position in the list, which is the order that the shapes are drawn in. Allows the user to move shapes up/down/to top/to bottom, as well as info about the shape such as its colour/thickness """ def __init__(self, gui): """ Initialise and populate the listbox """ wx.Dialog.__init__(self, gui, title=_("Shape Viewer"), size=(550, 400), style=wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX | wx.RESIZE_BORDER | wx.WANTS_CHARS) self.gui = gui self.count = 0 self.shapes = list(self.gui.canvas.shapes) self.SetSizeHints(550, 400) label = wx.StaticText(self, label=_("Shapes at the top of the list are drawn over shapes at the bottom")) sizer = wx.BoxSizer(wx.VERTICAL) bsizer = wx.BoxSizer(wx.HORIZONTAL) nextprevsizer = wx.BoxSizer(wx.HORIZONTAL) self.moveUp = self.make_button(u"move-up", _("Move Shape Up")) self.moveDown = self.make_button(u"move-down", _("Move Shape Down")) self.moveTop = self.make_button(u"move-top", _("Move Shape To Top")) self.moveBottom = self.make_button(u"move-bottom", _("Move Shape To Bottom")) self.deleteBtn = self.make_button(u"delete", _("Delete Shape")) self.prev = self.make_button(u"prev_sheet", _("Previous Sheet")) self.next = self.make_button(u"next_sheet", _("Next Sheet")) self.pages = wx.ComboBox(self, size=(125, 25), style=wx.CB_READONLY) self.list = WhyteboardList(self) self.list.InsertColumn(0, _("Position")) self.list.InsertColumn(1, _("Type")) self.list.InsertColumn(2, _("Thickness")) self.list.InsertColumn(3, _("Color")) self.list.InsertColumn(4, _("Properties")) bsizer.AddMany([(self.moveUp, 0, wx.RIGHT, 5), (self.moveDown, 0, wx.RIGHT, 5), (self.moveTop, 0, wx.RIGHT, 5), (self.moveBottom, 0, wx.RIGHT, 5), (self.deleteBtn, 0, wx.RIGHT, 5)]) nextprevsizer.Add(self.prev, 0, wx.RIGHT, 5) nextprevsizer.Add(self.next) bsizer.Add((1, 1), 1, wx.EXPAND) # align to the right bsizer.Add(nextprevsizer, 0, wx.RIGHT, 10) bsizer.Add(self.pages, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 10) okButton = wx.Button(self, wx.ID_OK, _("&OK")) okButton.SetDefault() cancelButton = wx.Button(self, wx.ID_CANCEL, _("&Cancel")) applyButton = wx.Button(self, wx.ID_APPLY, _("&Apply")) btnSizer = wx.StdDialogButtonSizer() btnSizer.AddButton(okButton) btnSizer.AddButton(cancelButton) btnSizer.AddButton(applyButton) btnSizer.Realize() sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(label, 0, wx.ALL, 15) sizer.Add((10, 5)) sizer.Add(bsizer, 0, wx.LEFT | wx.EXPAND, 10) sizer.Add((10, 15)) sizer.Add(self.list, 1, wx.LEFT | wx.RIGHT |wx.EXPAND, 10) sizer.Add((10, 5)) sizer.Add(btnSizer, 0, wx.TOP | wx.BOTTOM | wx.ALIGN_CENTRE, 15) self.SetSizer(sizer) self.populate() self.Fit() self.SetFocus() self.SetEscapeId(cancelButton.GetId()) cancelButton.Bind(wx.EVT_BUTTON, self.cancel) okButton.Bind(wx.EVT_BUTTON, self.ok) applyButton.Bind(wx.EVT_BUTTON, self.apply) self.moveUp.Bind(wx.EVT_BUTTON, self.on_up) self.moveDown.Bind(wx.EVT_BUTTON, self.on_down) self.moveTop.Bind(wx.EVT_BUTTON, self.on_top) self.moveBottom.Bind(wx.EVT_BUTTON, self.on_bottom) self.prev.Bind(wx.EVT_BUTTON, self.on_prev) self.next.Bind(wx.EVT_BUTTON, self.on_next) self.deleteBtn.Bind(wx.EVT_BUTTON, self.on_delete) self.pages.Bind(wx.EVT_COMBOBOX, self.on_change_sheet) self.Bind(wx.EVT_CLOSE, self.on_close) ac = [(wx.ACCEL_NORMAL, wx.WXK_DELETE, self.deleteBtn.GetId())] tbl = wx.AcceleratorTable(ac) self.list.SetAcceleratorTable(tbl) self.Bind(wx.EVT_CHAR_HOOK, self.delete_key) pub.subscribe(self.sheet_rename, 'sheet.rename') pub.subscribe(self.update, 'update_shape_viewer') ids = [self.moveUp.GetId(), self.moveTop.GetId(), self.moveDown.GetId(), self.moveBottom.GetId(), self.deleteBtn.GetId(), self.prev.GetId(), self.next.GetId()] [self.Bind(wx.EVT_UPDATE_UI, self.update_buttons, id=x) for x in ids] def make_button(self, filename, tooltip): btn = bitmap_button(self, get_image_path(u"icons", filename), False) btn.SetToolTipString(tooltip) return btn def sheet_rename(self, _id, text): self.populate() def update(self): self.shapes = list(self.gui.canvas.shapes) self.populate() def populate(self): """ Creates all columns and populates with the current sheets' data """ self.pages.SetItems(self.gui.get_tab_names()) self.pages.SetSelection(self.gui.current_tab) selection = self.list.GetFirstSelected() self.list.DeleteAllItems() if not self.shapes: index = self.list.InsertStringItem(sys.maxint, "") self.list.SetStringItem(index, 3, _("No shapes drawn")) else: for x, shape in enumerate(reversed(self.shapes)): index = self.list.InsertStringItem(sys.maxint, str(x + 1)) self.list.SetStringItem(index, 0, str(x + 1)) self.list.SetStringItem(index, 1, _(shape.name)) self.list.SetStringItem(index, 2, str(shape.thickness)) self.list.SetStringItem(index, 3, str(shape.colour)) self.list.SetStringItem(index, 4, shape.properties()) self.list.Select(selection) self.list.EnsureVisible(selection) def update_buttons(self, event): _id = event.GetId() do = False if _id == self.next.GetId() and self.gui.current_tab + 1 < self.gui.tab_count: do = True elif _id == self.prev.GetId() and self.gui.current_tab > 0: do = True elif _id == self.deleteBtn.GetId() and self.shapes and self.list.GetFirstSelected() >= 0: do = True elif _id in [self.moveUp.GetId(), self.moveTop.GetId()] and self.list.GetFirstSelected() > 0: do = True elif _id in [self.moveDown.GetId(), self.moveBottom.GetId()] and self.is_not_last_item(): do = True event.Enable(do) for x in [self.moveBottom, self.moveDown, self.moveUp, self.moveTop, self.deleteBtn, self.prev, self.next]: x.Refresh() def is_not_last_item(self): return (self.list.GetFirstSelected() != len(self.shapes) - 1 and self.shapes and self.list.GetFirstSelected() >= 0) def find_shape(self): """Find the selected shape's index and actual object""" count = 0 for x in reversed(self.shapes): if count == self.list.GetFirstSelected(): return (self.shapes.index(x), x) count += 1 def move_shape(fn): """ Passes the selected shape and its index to the decorated function, which handles moving the shape. function returns the list index to select """ def wrapper(self, event, index=None, item=None): index, item = self.find_shape() self.shapes.pop(index) x = fn(self, event, index, item) self.populate() self.list.Select(x) return wrapper @move_shape def on_top(self, event, index=None, item=None): self.shapes.append(item) return 0 @move_shape def on_bottom(self, event, index=None, item=None): self.shapes.insert(0, item) return len(self.shapes) - 1 @move_shape def on_up(self, event, index=None, item=None): self.shapes.insert(index + 1, item) return self.list.GetFirstSelected() - 1 @move_shape def on_down(self, event, index=None, item=None): self.shapes.insert(index - 1, item) return self.list.GetFirstSelected() + 1 @move_shape def on_delete(self, event, index=None, item=None): if self.list.GetFirstSelected() - 1 <= 0: return 0 return self.list.GetFirstSelected() - 1 def delete_key(self, event): if event.GetKeyCode() == wx.WXK_DELETE and self.shapes: self.on_delete(None) event.Skip() def change(self, selection): """Change the sheet, repopulate""" self.gui.tabs.SetSelection(selection) self.pages.SetSelection(selection) self.gui.on_change_tab() self.update() def on_change_sheet(self, event): self.change(self.pages.GetSelection()) def on_next(self, event): self.change(self.gui.current_tab + 1) def on_prev(self, event): self.change(self.gui.current_tab - 1) def ok(self, event): self.apply() self.Close() def cancel(self, event=None): self.Close() def on_close(self, event): self.gui.shape_viewer_open = False event.Skip() def apply(self, event=None): self.gui.canvas.add_undo() self.gui.canvas.shapes = self.shapes self.gui.canvas.redraw_all(True) #---------------------------------------------------------------------- class PDFCacheDialog(wx.Dialog): """ Views a list of all cached PDFs - showing the amount of pages, location, conversion quality and date saved. Has options to remove items to re-convert """ def __init__(self, gui, cache): wx.Dialog.__init__(self, gui, title=_("PDF Cache Viewer"), size=(650, 300), style=wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX | wx.RESIZE_BORDER) self.cache = cache self.files = cache.entries() self.original_files = dict(cache.entries()) self.list = WhyteboardList(self) self.SetSizeHints(450, 300) label = wx.StaticText(self, label=_("Whyteboard will load these files from its cache instead of re-converting them")) sizer = wx.BoxSizer(wx.VERTICAL) bsizer = wx.BoxSizer(wx.HORIZONTAL) self.deleteBtn = bitmap_button(self, get_image_path(u"icons", u"delete"), False) self.deleteBtn.SetToolTipString(_("Remove cached item")) bsizer.Add(self.deleteBtn, 0, wx.RIGHT, 5) okButton = wx.Button(self, wx.ID_OK, _("&OK")) cancelButton = wx.Button(self, wx.ID_CANCEL, _("&Cancel")) okButton.SetDefault() btnSizer = wx.StdDialogButtonSizer() btnSizer.AddButton(okButton) btnSizer.AddButton(cancelButton) btnSizer.Realize() sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(label, 0, wx.ALL, 15) sizer.Add((10, 5)) sizer.Add(bsizer, 0, wx.LEFT | wx.EXPAND, 10) sizer.Add((10, 5)) sizer.Add(self.list, 1, wx.LEFT | wx.RIGHT |wx.EXPAND, 10) sizer.Add((10, 5)) sizer.Add(btnSizer, 0, wx.TOP | wx.BOTTOM | wx.ALIGN_CENTRE, 15) self.SetSizer(sizer) self.populate() self.check_buttons() self.SetEscapeId(cancelButton.GetId()) okButton.Bind(wx.EVT_BUTTON, self.ok) self.deleteBtn.Bind(wx.EVT_BUTTON, self.on_remove) cancelButton.Bind(wx.EVT_BUTTON, lambda x: self.Close()) self.Bind(wx.EVT_LIST_ITEM_SELECTED, lambda x: self.check_buttons()) self.Bind(wx.EVT_LIST_ITEM_DESELECTED, lambda x: self.check_buttons()) ac = [(wx.ACCEL_NORMAL, wx.WXK_DELETE, self.deleteBtn.GetId())] tbl = wx.AcceleratorTable(ac) self.list.SetAcceleratorTable(tbl) self.Bind(wx.EVT_CHAR_HOOK, self.delete_key) def populate(self): """ Creates all columns and populates them with the PDF cache list """ self.list.ClearAll() self.list.InsertColumn(0, _("File Location")) self.list.InsertColumn(1, _("Quality")) self.list.InsertColumn(2, _("Pages")) self.list.InsertColumn(3, _("Date Cached")) if not self.files: index = self.list.InsertStringItem(sys.maxint, "") self.list.SetStringItem(index, 0, _("There are no cached items to display")) else: for x, key in self.files.items(): f = self.files[x] index = self.list.InsertStringItem(sys.maxint, str(x + 1)) self.list.SetStringItem(index, 0, f['file']) self.list.SetStringItem(index, 1, f['quality'].capitalize()) self.list.SetStringItem(index, 2, u"%s" % len(f['images'])) self.list.SetStringItem(index, 3, f.get('date', _("No Date Saved"))) self.list.SetColumnWidth(0, wx.LIST_AUTOSIZE) self.list.SetColumnWidth(1, 70) self.list.SetColumnWidth(2, 60) self.list.SetColumnWidth(3, wx.LIST_AUTOSIZE) def check_buttons(self): """ Enable / Disable the appropriate buttons """ if not self.list.GetItemCount() or self.list.GetFirstSelected() == -1: self.deleteBtn.Disable() else: self.deleteBtn.Enable() def ok(self, event): self.cache.write_dict(self.files) self.Close() def delete_key(self, event): if event.GetKeyCode() == wx.WXK_DELETE and self.files.items(): self.on_remove(None) event.Skip() def on_remove(self, event): """Remove the dictionary item that matches the selected item's path""" item = self.list.GetFirstSelected() if item == -1: return quality = self.list.GetItem(item, 1).GetText() text = self.list.GetItemText(item) files = dict(self.files) for x, key in self.files.items(): if (self.files[x]['file'] == text and self.files[x]['quality'].capitalize() == quality): del files[x] self.files = files self.populate() #---------------------------------------------------------------------- class AboutDialog(wx.Dialog): """ A replacement About Dialog for Windows, as it uses a generic frame that well...sucks. """ def __init__(self, parent, info): wx.Dialog.__init__(self, parent, title=_("About Whyteboard")) self.info = info image = wx.StaticBitmap(self, bitmap=icon.GetBitmap()) name = wx.StaticText(self, label="%s %s" % (info.Name, info.Version)) description = wx.StaticText(self, label=info.Description) copyright = wx.StaticText(self, label=info.Copyright) url = HyperLinkCtrl(self, label=info.WebSite[0], URL=info.WebSite[1]) font = name.GetClassDefaultAttributes().font font.SetWeight(wx.FONTWEIGHT_BOLD) font.SetPointSize(18) name.SetFont(font) credits = wx.Button(self, id=wx.ID_ABOUT, label=_("C&redits")) license = wx.Button(self, label=_("&License")) close = wx.Button(self, id=wx.ID_CLOSE, label=_("&Close")) btnSizer = wx.BoxSizer(wx.HORIZONTAL) btnSizer.Add(credits, flag=wx.CENTER | wx.LEFT | wx.RIGHT, border=5) btnSizer.Add(license, flag=wx.CENTER | wx.RIGHT, border=5) btnSizer.Add(close, flag=wx.CENTER | wx.RIGHT, border=5) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(image, flag=wx.CENTER | wx.TOP | wx.BOTTOM, border=5) sizer.Add(name, flag=wx.CENTER | wx.BOTTOM, border=10) sizer.Add(description, flag=wx.CENTER | wx.BOTTOM, border=10) sizer.Add(copyright, flag=wx.CENTER | wx.BOTTOM, border=10) sizer.Add(url, flag=wx.CENTER | wx.BOTTOM, border=15) sizer.Add(btnSizer, flag=wx.CENTER | wx.BOTTOM, border=5) container = wx.BoxSizer(wx.VERTICAL) container.Add(sizer, flag=wx.ALL, border=10) self.SetSizer(container) self.Layout() self.Fit() self.Centre() self.Show(True) self.SetEscapeId(close.GetId()) credits.Bind(wx.EVT_BUTTON, self.on_credits) license.Bind(wx.EVT_BUTTON, self.on_license) close.Bind(wx.EVT_BUTTON, lambda evt: self.Destroy()) def on_license(self, event): LicenseDialog(self, self.info.License) def on_credits(self, event): CreditsDialog(self, self.info) #---------------------------------------------------------------------- class CreditsDialog(wx.Dialog): def __init__(self, parent, info): wx.Dialog.__init__(self, parent, title=_("Credits"), size=(475, 320), style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) self.SetIcon(icon.GetIcon()) self.SetMinSize((300, 200)) notebook = wx.Notebook(self) close = wx.Button(self, id=wx.ID_CLOSE, label=_("&Close")) close.SetDefault() developer = wx.TextCtrl(notebook, style=wx.TE_READONLY | wx.TE_MULTILINE) translators = wx.TextCtrl(notebook, style=wx.TE_READONLY | wx.TE_MULTILINE) developer.SetValue(u'\n'.join(info.Developers)) translators.SetValue(u'\n'.join(info.Translators)) notebook.AddPage(developer, text=_("Written by")) notebook.AddPage(translators, text=_("Translated by")) btnSizer = wx.BoxSizer(wx.HORIZONTAL) btnSizer.Add(close) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(notebook, 1, wx.EXPAND | wx.ALL, 10) sizer.Add(btnSizer, flag=wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, border=10) self.SetSizer(sizer) self.Layout() self.Show() self.SetEscapeId(close.GetId()) close.Bind(wx.EVT_BUTTON, lambda evt: self.Destroy()) #---------------------------------------------------------------------- class LicenseDialog(wx.Dialog): def __init__(self, parent, license): wx.Dialog.__init__(self, parent, title=_("License"), size=(500, 400), style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) self.SetMinSize((400, 300)) self.SetIcon(icon.GetIcon()) close = wx.Button(self, id=wx.ID_CLOSE, label=_("&Close")) ctrl = wx.TextCtrl(self, style=wx.TE_READONLY | wx.TE_MULTILINE) ctrl.SetValue(license) btnSizer = wx.BoxSizer(wx.HORIZONTAL) btnSizer.Add(close) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(ctrl, 1, wx.EXPAND | wx.ALL, 10) sizer.Add(btnSizer, flag=wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, border=10) self.SetSizer(sizer) self.Layout() self.Show() self.SetEscapeId(close.GetId()) close.Bind(wx.EVT_BUTTON, lambda evt: self.Destroy()) #----------------------------------------------------------------------whyteboard-0.41.1/whyteboard/gui/panels.py0000777000175000017500000011051211444517222017604 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ This module contains classes for the GUI side panels and pop-up menus. """ from __future__ import division import os import wx import wx.media import wx.lib.colourselect as csel from wx.lib import scrolledpanel as scrolled from wx.lib.buttons import GenBitmapToggleButton from wx.lib.wordwrap import wordwrap as wordwrap from whyteboard.lib import pub from whyteboard.misc import (meta, create_colour_bitmap, get_time, file_dialog, get_image_path) from whyteboard.gui import NotesPopup, ThumbsPopup _ = wx.GetTranslation #---------------------------------------------------------------------- class ControlPanel(wx.Panel): """ This class implements a control panel for the GUI. It creates buttons or icons for each tool that can be drawn with on the Whyteboard; a drop-down menu for the line thickness and a ColourPicker for choosing the drawing colour. A preview of what the tool will look like is also shown. It is contained within a collapsed pane, to give extra screen space when in full screen mode """ def __init__(self, gui): """ Stores a reference to the drawing preview and the toggled drawing tool. """ wx.Panel.__init__(self, gui, style=0 | wx.RAISED_BORDER) cp = wx.CollapsiblePane(self, style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE) self.pane = cp.GetPane() # every widget's parent self.gui = gui self.toggled = 1 # Pen, initallly self.tools = {} self.thickness_timer = None self.thickness_scrolling = False sizer = wx.BoxSizer(wx.VERTICAL) csizer = wx.BoxSizer(wx.VERTICAL) box = wx.BoxSizer(wx.VERTICAL) self.grid = wx.GridSizer(cols=3, hgap=4, vgap=4) self.toolsizer = wx.GridSizer(cols=1, hgap=5, vgap=5) self.make_toolbox(gui.util.config['toolbox']) self.make_colour_grid() colour = self.colour_buttons() thickness = wx.StaticText(self.pane, label=_("Thickness:")) choices = u''.join(u"%s " % i for i in range(1, 35)).split() self.thickness = wx.ComboBox(self.pane, choices=choices, size=(25, 25), style=wx.CB_READONLY) self.thickness.SetSelection(0) self.thickness.SetToolTipString(_("Sets the drawing thickness")) line = wx.StaticLine(self, size=(-1, 30)) line.SetBackgroundColour((0, 0, 0)) self.preview = DrawingPreview(self.pane, self.gui) spacing = 4 box.AddMany([(self.toolsizer, 0, wx.ALIGN_CENTER | wx.ALL, spacing), ((5, 8)), (self.grid, 0, wx.EXPAND | wx.ALL, spacing), ((5, 8)), (colour, 0, wx.EXPAND | wx.ALL, spacing), ((5, 10)), (thickness, 0, wx.ALL | wx.ALIGN_CENTER, spacing), (self.thickness, 0, wx.EXPAND | wx.ALL, spacing), ((5, 5)), (self.preview, 0, wx.EXPAND | wx.ALL, spacing)]) csizer.Add(box, 1, wx.EXPAND) sizer.Add(cp, 1, wx.EXPAND) self.SetSizer(sizer) cp.GetPane().SetSizer(csizer) cp.Expand() self.control_sizer = box self.background.Raise() if not self.gui.util.config['tool_preview']: self.preview.Hide() if not self.gui.util.config['colour_grid']: box.Hide(self.grid) self.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, self.toggle) #self.thickness.Bind(wx.EVT_MOUSEWHEEL, self.scroll) self.thickness.Bind(wx.EVT_COMBOBOX, self.change_thickness) pub.subscribe(self.set_colour, 'change_colour') pub.subscribe(self.set_background, 'change_background') def colour_buttons(self): panel = wx.Panel(self.pane) self.colour = csel.ColourSelect(panel, pos=(0, 0), size=(60, 60)) parent = panel if os.name == "nt": parent = self.colour # segfaults otherwise sizer = wx.BoxSizer() swap_sizer = wx.BoxSizer(wx.HORIZONTAL) self.background = csel.ColourSelect(parent, pos=(0, 30), size=(30, 30)) self.background.SetValue("White") swap = wx.BitmapButton(panel, bitmap=wx.Bitmap(get_image_path(u"icons", u"swap_colours")), pos=(70, 0), style=wx.NO_BORDER) self.transparent = wx.CheckBox(panel, label=_("Transparent"), pos=(0, 69)) self.transparent.SetValue(True) self.colour.Bind(csel.EVT_COLOURSELECT, self.change_colour) self.background.Bind(csel.EVT_COLOURSELECT, self.change_background) self.transparent.Bind(wx.EVT_CHECKBOX, self.on_transparency) swap.Bind(wx.EVT_BUTTON, self.on_swap) self.colour.SetToolTipString(_("Set the foreground color")) self.background.SetToolTipString(_("Set the background color")) self.transparent.SetToolTipString(_("Ignores the background color")) swap.SetToolTipString(_("Swaps the foreground and background colors")) sizer.AddMany([(self.background), (self.colour), (self.transparent)]) swap_sizer.Add(sizer) swap_sizer.Add(swap, flag=wx.ALIGN_RIGHT) return panel def get_background_colour(self): return self.background.GetColour() def get_colour(self): return self.colour.GetColour() def set_colour(self, colour): self.colour.SetColour(colour) self.preview.Refresh() def set_background(self, colour): self.background.SetColour(colour) self.preview.Refresh() def make_toolbox(self, _type=u"text"): """Creates a toolbox made from toggleable text or icon buttons""" items = [_(i.name) for i in self.gui.util.items] self.toolsizer.SetCols(1) if _type == u"icon": items = [_(i.icon) for i in self.gui.util.items] self.toolsizer.SetCols(int(self.gui.util.config['toolbox_columns'])) for x, val in enumerate(items): if _type == u"icon": path = get_image_path(u"tools", val) b = GenBitmapToggleButton(self.pane, x + 1, wx.Bitmap(path), style=wx.NO_BORDER) evt = wx.EVT_BUTTON else: b = wx.ToggleButton(self.pane, x + 1, val) evt = wx.EVT_TOGGLEBUTTON b.SetToolTipString(u"%s\n%s %s" % (_(self.gui.util.items[x].tooltip), _("Shortcut Key:"), self.gui.util.items[x].hotkey.upper())) b.Bind(evt, self.change_tool, id=x + 1) self.toolsizer.Add(b, 0, wx.EXPAND | wx.RIGHT, 2) self.tools[x + 1] = b self.tools[self.toggled].SetValue(True) def make_colour_grid(self): """Builds a colour grid from the user's preferred colours""" colours = [] for x in range(1, 10): col = self.gui.util.config["colour%s" % x] colours.append([int(c) for c in col]) for colour in colours: method = lambda evt, col = colour: self.change_colour(evt, col) method2 = lambda evt, col = colour: self.change_background(evt, col) b = wx.BitmapButton(self.pane, bitmap=create_colour_bitmap(colour)) self.grid.Add(b, 0) b.Bind(wx.EVT_BUTTON, method) b.Bind(wx.EVT_RIGHT_UP, method2) def toggle(self, evt): """Toggles the collapsible pane and its widgets""" self.gui.Layout() self.gui.canvas.redraw_all() # fixes a windows redraw bug def toggle_colour_grid(self, value): self.control_sizer.Show(self.grid, value) self.pane.Layout() def scroll(self, event): """Scrolls the thickness drop-down box (for Windows)""" val = self.thickness.GetSelection() if event.GetWheelRotation() > 0: # mousewheel down val -= 1 if val <= 0: val = 0 else: val += 1 self.thickness.SetSelection(val) def change_tool(self, event=None, _id=None): """ Toggles the tool buttons on/off """ new = self.gui.util.tool if event and not _id: new = int(event.GetId()) # get widget ID elif _id: new = _id self.tools[self.toggled].SetValue(True) if new != self.toggled: # toggle old button self.tools[self.toggled].SetValue(False) self.tools[new].SetValue(True) self.toggled = new pub.sendMessage('canvas.change_tool', new=new) def on_transparency(self, event): """Toggles transparency in the shapes' background""" if event.Checked() and not self.gui.canvas.selected: self.gui.util.transparent = True else: self.gui.util.transparent = False if self.gui.canvas.selected: self.gui.canvas.toggle_transparent() pub.sendMessage('canvas.change_tool') def on_swap(self, event): """Swaps foreground/background colours""" a, b = self.get_background_colour(), self.get_colour() self.background.SetColour(b) self.colour.SetColour(a) self.gui.util.colour = a if not self.transparent.IsChecked(): self.gui.util.background = a pub.sendMessage('canvas.change_tool') def change_colour(self, event=None, colour=None): """Event can also be a string representing a colour (from the grid)""" if event and not colour: colour = event.GetValue() # from the colour button self.colour.SetColour(colour) self.update(colour, u"colour") def change_background(self, event=None, colour=None): """Event can also be a string representing a colour (from the grid)""" if event and not colour: colour = event.GetValue() # from the colour button self.background.SetColour(colour) self.update(colour, u"background") def update(self, value, var_name, add_undo=True): """Updates the given utility variable and the selected shape""" setattr(self.gui.util, var_name, value) if self.gui.canvas.selected: if add_undo: self.gui.canvas.add_undo() if var_name == u"background" and not self.transparent.IsChecked(): self.gui.canvas.selected.background = value elif var_name != u"background": setattr(self.gui.canvas.selected, var_name, value) self.gui.canvas.redraw_all(True) pub.sendMessage('update_shape_viewer') pub.sendMessage('canvas.change_tool') self.preview.Refresh() def change_thickness(self, event=None): """ This uses a timer to know when a series of scroll events begin so we don't add many undo points """ if not self.thickness_scrolling: self.thickness_scrolling = True self.thickness_timer = wx.CallLater(250, self.reset_hotkey) self.update(self.thickness.GetSelection(), u"thickness") else: self.thickness_timer.Restart(250) self.update(self.thickness.GetSelection(), u"thickness", False) def reset_hotkey(self): self.thickness_scrolling = False #---------------------------------------------------------------------- class DrawingPreview(wx.Window): """ Shows a sample of what the current tool's drawing will look like. Pane is the collapsible pane, its parent. """ def __init__(self, pane, gui): """ Stores gui reference to access utility colour/thickness attributes. """ wx.Window.__init__(self, pane) self.gui = gui self.SetBackgroundColour(wx.WHITE) self.SetSize((45, 45)) self.SetToolTipString(_("A preview of your current tool")) self.Bind(wx.EVT_PAINT, self.on_paint) pub.subscribe(self.redraw, 'gui.preview.refresh') def on_paint(self, event=None): """ Draws the tool inside the box when tool/colour/thickness is changed """ if self.gui.canvas: dc = wx.PaintDC(self) dc.SetPen(wx.Pen(self.gui.canvas.shape.colour, self.gui.canvas.shape.thickness, wx.SOLID)) if self.gui.util.transparent: dc.SetBrush(wx.TRANSPARENT_BRUSH) else: dc.SetBrush(self.gui.canvas.shape.brush) width, height = self.GetClientSize() self.gui.canvas.shape.preview(dc, width, height) dc.SetPen(wx.Pen((0, 0, 0), 1, wx.SOLID)) dc.SetBrush(wx.TRANSPARENT_BRUSH) width, height = self.GetClientSize() dc.DrawRectangle(0, 0, width, height) # draw a border.. def redraw(self): self.Refresh() #---------------------------------------------------------------------- class MediaPanel(wx.Panel): """ A panel that contains a MediaCtrl for playing videos/audio, and buttons for controlling it: open (file)/pause/stop/play, and a slider bar. Used by the Media tool. """ def __init__(self, parent, pos, tool): wx.Panel.__init__(self, parent, pos=pos, style=wx.CLIP_CHILDREN) self.gui = parent.gui self.tool = tool self.offset = (0, 0) self.directory = None self.mc = wx.media.MediaCtrl(self, style=wx.SIMPLE_BORDER) self.timer = wx.Timer(self) # updates the slider as the file plays self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW)) self.total = "" # total time self.file_drop = MediaDropTarget(self) self.SetDropTarget(self.file_drop) self.open = wx.BitmapButton(self, bitmap=wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR)) self.play = wx.BitmapButton(self, bitmap=wx.Bitmap(get_image_path(u"icons", u"play"))) self.pause = wx.BitmapButton(self, bitmap=wx.Bitmap(get_image_path(u"icons", u"pause"))) self.stop = wx.BitmapButton(self, bitmap=wx.Bitmap(get_image_path(u"icons", u"stop"))) self.play.Disable() self.pause.Disable() self.stop.Disable() self.file = wx.StaticText(self) self.elapsed = wx.StaticText(self) timesizer = wx.BoxSizer(wx.HORIZONTAL) timesizer.Add(self.file, 1, wx.LEFT | wx.RIGHT, 5) timesizer.Add(self.elapsed, 0, wx.ALIGN_RIGHT | wx.RIGHT, 5) self.slider = wx.Slider(self) self.slider.SetToolTipString(_("Skip to a position")) self.volume = wx.Slider(self, value=100, style=wx.SL_VERTICAL | wx.SL_INVERSE) self.volume.SetToolTipString(_("Set the volume")) self.slider.SetMinSize((150, -1)) self.volume.SetMinSize((-1, 75)) sizer = wx.GridBagSizer(6, 5) sizer.Add(self.mc, (1, 1), span=(5, 1)) sizer.Add(self.open, (1, 3), flag=wx.RIGHT, border=10) sizer.Add(self.play, (2, 3), flag=wx.RIGHT, border=10) sizer.Add(self.pause, (3, 3), flag=wx.RIGHT, border=10) sizer.Add(self.stop, (4, 3), flag=wx.RIGHT, border=10) sizer.Add(self.volume, (5, 3), flag=wx.RIGHT, border=10) sizer.Add(self.slider, (6, 1), flag=wx.EXPAND) sizer.Add(timesizer, (7, 1), flag=wx.EXPAND | wx.BOTTOM, border=10) self.SetSizer(sizer) self.Layout() self.Fit() self.Bind(wx.media.EVT_MEDIA_LOADED, self.media_loaded) self.Bind(wx.media.EVT_MEDIA_STOP, self.media_stopped) self.Bind(wx.EVT_BUTTON, self.load_file, self.open) self.Bind(wx.EVT_BUTTON, self.on_play, self.play) self.Bind(wx.EVT_BUTTON, self.on_pause, self.pause) self.Bind(wx.EVT_BUTTON, self.on_stop, self.stop) self.Bind(wx.EVT_SLIDER, self.on_seek, self.slider) self.Bind(wx.EVT_SLIDER, self.on_volume, self.volume) self.Bind(wx.EVT_LEFT_UP, self.left_up) self.Bind(wx.EVT_LEFT_DOWN, self.left_down) self.Bind(wx.EVT_MOTION, self.left_motion) self.Bind(wx.EVT_TIMER, self.on_timer) self.timer.Start(650) def left_down(self, event): """Grab the mouse offset of the window relative the the top-left""" self.gui.canvas.selected = self.tool self.tool.selected = True self.CaptureMouse() pos = self.Parent.ScreenToClient(self.ClientToScreen(event.Position)) self.offset = (pos[0] - self.tool.x, pos[1] - self.tool.y) def left_up(self, event): if self.HasCapture(): self.ReleaseMouse() self.Layout() def left_motion(self, event): """Reposition the window with an offset""" if event.Dragging(): pos = self.Parent.ScreenToClient(self.ClientToScreen(event.Position)) pos = (pos[0] - self.offset[0], pos[1] - self.offset[1]) self.tool.x, self.tool.y = pos self.SetPosition(pos) def load_file(self, evt): """ Display a file chooser window and try to load the file """ vids = u"*.avi; *.mkv; *.mov; *.mpg; *ogg; *.wmv" audio = u"*.mp3; *.oga; *.ogg; *.wav" wildcard = _("Media Files") + u" |%s;%s|" % (vids, audio) wildcard += _("Video Files") + u" (%s)|%s|" % (vids, vids) wildcard += _("Audio Files") + u" (%s)|%s" % (audio, audio) _dir = u"" if self.directory: _dir = self.directory name = file_dialog(self, _("Choose a media file"), wx.OPEN, wildcard, _dir) if name: self.do_load_file(name) def do_load_file(self, path): """ Loads a file from a given path, sets up instance variables and enables and disabled buttons """ if not self.mc.Load(path): wx.MessageBox(_("Unable to load %s: Unsupported format?") % path, u"Whyteboard", wx.ICON_ERROR | wx.OK) self.play.Disable() self.pause.Disable() self.stop.Disable() else: if os.name == "posix": self.mc.Load(path) self.directory = path self.tool.filename = path def media_loaded(self, evt): """ Called when a media file has finished loading. Calculates the total time of the file and updates the filename label """ self.play.Enable() self.total = get_time(self.mc.Length() / 1000) wordwrap(os.path.basename(self.tool.filename), 350, wx.ClientDC(self.gui)) self.file.SetLabel(os.path.basename(self.tool.filename)) self.elapsed.SetLabel(u"00:00/" + self.total) self.mc.SetInitialSize() self.slider.SetRange(0, self.mc.Length()) self.GetSizer().Layout() self.Fit() def media_stopped(self, evt): self.on_stop(ignore=True) def on_timer(self, evt): """Keep updating the timer label/scrollbar...""" if self.mc.GetState() == wx.media.MEDIASTATE_PLAYING: offset = self.mc.Tell() self.slider.SetValue(offset) self.elapsed.SetLabel(get_time(offset / 1000) + "/" + self.total) def on_play(self, evt): if not self.mc.Play(): wx.MessageBox(_("Unable to play file %s") % self.tool.filename, u"Whyteboard", wx.ICON_ERROR | wx.OK) else: self.play.Disable() self.pause.Enable() self.stop.Enable() def on_pause(self, evt): self.mc.Pause() self.play.Enable() self.pause.Disable() def on_stop(self, evt=None, ignore=False): if not ignore: self.mc.Stop() self.slider.SetValue(0) self.elapsed.SetLabel(u"00:00/" + self.total) self.play.Enable() self.pause.Disable() self.stop.Disable() def on_seek(self, evt): self.mc.Seek(self.slider.GetValue()) self.elapsed.SetLabel(u"%s/%s" % (get_time(self.slider.GetValue() / 1000), self.total)) def on_volume(self, evt): self.mc.SetVolume(float(self.volume.GetValue() / 100)) #--------------------------------------------------------------------- class MediaDropTarget(wx.FileDropTarget): """Implements drop target functionality to receive files""" def __init__(self, panel): wx.FileDropTarget.__init__(self) self.panel = panel def OnDropFiles(self, x, y, filenames): self.panel.do_load_file(filenames[0]) #--------------------------------------------------------------------- class SidePanel(wx.Panel): """ The side panel is a tabbed window, allowing the user to switch between thumbnails and notes. It can be toggled on and off via CollapsiblePane """ def __init__(self, gui): wx.Panel.__init__(self, gui, style=wx.RAISED_BORDER) cp = wx.CollapsiblePane(self, style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE) self.tabs = wx.Notebook(cp.GetPane()) self.thumbs = Thumbs(self.tabs, gui) self.notes = Notes(self.tabs, gui) self.tabs.AddPage(self.thumbs, _("Thumbnails")) self.tabs.AddPage(self.notes, _("Notes")) sizer = wx.BoxSizer(wx.VERTICAL) csizer = wx.BoxSizer(wx.VERTICAL) csizer.Add(self.tabs, 1, wx.EXPAND) sizer.Add(cp, 1, wx.EXPAND) self.SetSizer(sizer) cp.GetPane().SetSizer(csizer) cp.Expand() self.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, self.toggle) def toggle(self, evt): """Toggles the pane and its widgets""" self.GetTopLevelParent().Layout() #---------------------------------------------------------------------- class Notes(wx.Panel): """ Contains a Tree which shows an overview of all sheets' notes. Each sheet is a child of the tree, with each Note a child of a sheet. Sheets can be right clicked to pop-up a menu; or double clicked to change to that sheet. Notes can be double/right clicked upon to be edited. """ def __init__(self, parent, gui): wx.Panel.__init__(self, parent) self.gui = gui self.tree = wx.TreeCtrl(self, style=wx.TR_HAS_BUTTONS) self.root = self.tree.AddRoot("Whyteboard") self.tabs = [] self.add_tab() self.tree.Expand(self.root) self.sizer = wx.BoxSizer(wx.VERTICAL) self.sizer.Add(self.tree, 1, wx.EXPAND) # fills vert space self.SetSizer(self.sizer) self.tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.on_click) self.tree.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.pop_up) pub.subscribe(self.add_note, 'note.add') pub.subscribe(self.edit_note, 'note.edit') pub.subscribe(self.rename, 'sheet.rename') pub.subscribe(self.sheet_moved, 'sheet.move') pub.subscribe(self.remove_current_sheet_items, 'note.delete_sheet_items') def add_tab(self, name=None): """Adds a new tab as a child to the root element""" _id = len(self.tabs) if not _id: _id = 0 if not name: name = _("Sheet") + u" %s" % (_id + 1) data = wx.TreeItemData(_id) t = self.tree.AppendItem(self.root, name, data=data) self.tabs.insert(_id, t) def add_note(self, note, _id=None): """ Adds a note to the current tab tree element. The notes' text is the element's text in the tree - newlines are replaced to stop the tree's formatting becoming too wide. """ text = note.text.replace(u"\n", u" ")[:15] _id = self.tabs[self.gui.tabs.GetSelection()] data = wx.TreeItemData(note) note.tree_id = self.tree.AppendItem(_id, text, data=data) self.tree.Expand(_id) def remove_tab(self, note): """Removes a tab and its children.""" item = self.tabs[note] self.tree.DeleteChildren(item) self.tree.Delete(item) del self.tabs[note] # now ensure all nodes are linked to the right tab for x in range(self.gui.current_tab, len(self.tabs)): self.tree.SetItemData(self.tabs[x], wx.TreeItemData(x)) def rename(self, _id, text): """Renames a given sheet""" self.tree.SetItemText(self.tabs[_id], text) def remove_all(self): """Removes all tabs.""" self.tree.DeleteChildren(self.root) self.tabs = [] def remove_current_sheet_items(self): self.tree.DeleteChildren(self.tabs[self.gui.tabs.GetSelection()]) def on_click(self, event): """ Changes to the selected tab if a tab node is double clicked upon, otherwise we're editing the note. """ item = self.tree.GetPyData(event.GetItem()) if item is None: # clicked on the root node return if isinstance(item, int): self.gui.tabs.SetSelection(item) self.gui.on_change_tab() else: item.edit() def pop_up(self, event): """Brings up the context menu on right click (except on root node)""" if self.tree.GetPyData(event.GetItem()) is not None: self.PopupMenu(NotesPopup(self, self.gui, event)) def select(self, event, draw=True): """ Selects a Note if unselected, otherwise it de-selects the note. draw forces a canvas redraw """ item = self.tree.GetPyData(event.GetItem()) if not item.selected: self.gui.canvas.deselect_shape() item.selected = True self.gui.canvas.selected = item else: self.gui.canvas.deselect_shape() if draw: self.gui.canvas.redraw_all() def delete(self, event): self.select(event, False) self.gui.canvas.delete_selected() def edit_note(self, tree_id, text): """Edit a non-blank Note by changing its tree item's text""" text = text.replace(u"\n", u" ")[:15] self.tree.SetItemText(tree_id, text) def sheet_moved(self, event, tab_count): """ Drag/drop sheet: move a tree item and its associated notes """ tree = self.tree old_item = self.tabs[event.GetOldSelection()] text = tree.GetItemText(old_item) children = [] # Save all Note item data to re-create it in the new Tree node if tree.ItemHasChildren(old_item): (child, cookie) = tree.GetFirstChild(old_item) while child.IsOk(): item = (tree.GetItemPyData(child), tree.GetItemText(child)) children.append(item) (child, cookie) = tree.GetNextChild(old_item, cookie) # Remove the old tree node, re-add it before = event.GetSelection() if event.GetSelection() >= tab_count: before = event.GetSelection() - 1 if event.GetOldSelection() < event.GetSelection(): # drag to the right before += 1 if before < 0: before = 0 new = tree.InsertItemBefore(self.root, before, text) tree.Delete(old_item) # Restore the notes to the new tree item for item in children: data = wx.TreeItemData(item[0]) item[0].tree_id = tree.AppendItem(new, item[1], data=data) # Reposition the tab in the list of wx.TreeItemID's for the loop below item = self.tabs.pop(event.GetOldSelection()) self.tabs.insert(event.GetSelection(), item) # Update each tree's node data so it is pointing to the correct tab ID (child, cookie) = tree.GetFirstChild(self.root) count = 0 while child.IsOk(): self.tabs[count] = child tree.SetItemData(self.tabs[count], wx.TreeItemData(count)) (child, cookie) = tree.GetNextChild(self.root, cookie) count += 1 tree.Expand(new) #---------------------------------------------------------------------- class Thumbs(scrolled.ScrolledPanel): """ Thumbnails of all tabs' drawings. """ def __init__(self, parent, gui): scrolled.ScrolledPanel.__init__(self, parent, style=wx.VSCROLL) self.sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self.sizer) self.SetScrollRate(0, 120) self.gui = gui self.thumbs = [] # ThumbButtons self.text = [] # StaticTexts self.new_thumb() # inital thumb self.thumbs[0].current = True pub.subscribe(self.highlight_current, 'thumbs.text.highlight') pub.subscribe(self.rename, 'sheet.rename') pub.subscribe(self.sheet_moved, 'sheet.move') pub.subscribe(self.update_current, 'thumbs.update_current') def highlight_current(self, tab, select): if self.text: try: font = self.text[tab].GetClassDefaultAttributes().font font.SetWeight(wx.FONTWEIGHT_NORMAL) if select: font.SetWeight(wx.FONTWEIGHT_BOLD) self.text[tab].SetFont(font) except IndexError: pass # ignore a bug closing the last tab from the pop-up menu # temp fix, can't think how to solve it otherwise def new_thumb(self, _id=0, name=None): """ Creates a new thumbnail button and manages its ID, along with a label. """ if _id: bmp = self.redraw(_id) else: if len(self.thumbs): _id = len(self.thumbs) bmp = wx.EmptyBitmap(150, 150) memory = wx.MemoryDC() memory.SelectObject(bmp) memory.SetPen(wx.Pen((255, 255, 255), 1)) memory.SetBrush(wx.Brush((255, 255, 255))) memory.Clear() memory.FloodFill(0, 0, (255, 255, 255), wx.FLOOD_BORDER) if os.name == "nt": memory.SetPen(wx.Pen(wx.BLACK, 1, wx.SOLID)) memory.SetBrush(wx.TRANSPARENT_BRUSH) memory.DrawRectangle(0, 0, 150, 150) memory.SelectObject(wx.NullBitmap) btn = ThumbButton(self, _id, bmp, name) if not name: name = _("Sheet") + u" %s" % (_id + 1) text = wx.StaticText(self, label=name) self.text.insert(_id, text) self.thumbs.insert(_id, btn) self.sizer.Add(text, 0, wx.ALIGN_CENTER | wx.TOP, 13) self.sizer.Add(btn, 0, wx.ALIGN_CENTER | wx.TOP, 7) self.SetVirtualSize(self.GetBestVirtualSize()) def remove(self, _id): """ Removes a thumbnail/label from the sizer and the managed widgets list. """ self.sizer.Remove(self.thumbs[_id]) self.sizer.Remove(self.text[_id]) self.thumbs[_id].Hide() # 'visibly' remove self.text[_id].Hide() del self.thumbs[_id] # 'physically' remove del self.text[_id] self.SetVirtualSize(self.GetBestVirtualSize()) # now ensure all thumbnail classes are pointing to the right tab for x in range(self.gui.current_tab, len(self.thumbs)): self.thumbs[x].thumb_id = x def remove_all(self): """ Removes all thumbnails. """ for x in range(len(self.thumbs)): self.sizer.Remove(self.thumbs[x]) self.sizer.Remove(self.text[x]) self.thumbs[x].Hide() # 'visibly' remove self.text[x].Hide() self.sizer.Layout() # update sizer self.thumbs = [] self.text = [] self.SetVirtualSize(self.GetBestVirtualSize()) def redraw(self, _id): """ Create a thumbnail by grabbing the currently selected Whyteboard's contents and creating a bitmap from it. This bitmap is then converted to an image to rescale it, and converted back to a bitmap to be displayed on the button as the thumbnail. """ canvas = self.gui.tabs.GetPage(_id) img = wx.ImageFromBitmap(canvas.buffer) img.Rescale(150, 150) bitmap = wx.BitmapFromImage(img) if os.name == "nt": memory = wx.MemoryDC() memory.SelectObject(bitmap) memory.SetPen(wx.Pen(wx.BLACK, 1, wx.SOLID)) memory.SetBrush(wx.TRANSPARENT_BRUSH) memory.DrawRectangle(0, 0, 150, 150) memory.SelectObject(wx.NullBitmap) return bitmap def sheet_moved(self, event, tab_count): for x in range(tab_count): self.text[x].SetLabel(self.gui.tabs.GetPageText(x)) def rename(self, _id, text): self.text[_id].SetLabel(text) self.Layout() def update_current(self): """ Updates current thumnbail """ self.update(self.gui.tabs.GetSelection()) def update(self, _id): """ Updates a single thumbnail. """ bmp = self.redraw(_id) thumb = self.thumbs[_id] thumb.SetBitmapLabel(bmp) self.thumbs[_id].buffer = bmp if thumb.current and meta.transparent: thumb.highlight() def update_all(self): """ Updates all thumbnails (i.e. upon loading a Whyteboard file). """ for count, item in enumerate(self.thumbs): self.update(count) #---------------------------------------------------------------------- class ThumbButton(wx.BitmapButton): """ This class has an extra attribute, storing its related tab ID so that when the button is pressed, it can switch to the proper tab. """ def __init__(self, parent, _id, bitmap, name=None): wx.BitmapButton.__init__(self, parent, size=(150, 150)) self.thumb_id = _id self.parent = parent self.SetBitmapLabel(bitmap) self.buffer = bitmap self.current = False # active thumb? self.Bind(wx.EVT_BUTTON, self.on_press) self.SetBackgroundColour(wx.WHITE) self.Bind(wx.EVT_RIGHT_UP, self.tab_popup) def tab_popup(self, event): """ Pops up the tab context menu. """ self.PopupMenu(ThumbsPopup(self.parent, self.parent.gui, self.thumb_id)) def on_press(self, event): """ Changes the tab to the selected button, deselect previous one """ self.parent.gui.tabs.SetSelection(self.thumb_id) self.parent.gui.on_change_tab() def update(self): """ Iterates over each thumb and unhighlights the previously selected thumb Then, sets this thumb as the currently highlighted one and redraws """ for thumb in self.parent.thumbs: if thumb.thumb_id != self.thumb_id: if thumb.current: thumb.current = False self.parent.update(thumb.thumb_id) self.current = True self.parent.update(self.thumb_id) def highlight(self): """ Highlights the current thumbnail with a light transparent overlay. """ dc = wx.MemoryDC() dc.SelectObject(self.buffer) gcdc = wx.GCDC(dc) gcdc.SetBrush(wx.Brush(wx.Color(0, 0, 255, 50))) # light blue gcdc.SetPen(wx.Pen((0, 0, 0), 1, wx.TRANSPARENT)) gcdc.DrawRectangle(0, 0, 150, 150) dc.SelectObject(wx.NullBitmap) self.SetBitmapLabel(self.buffer)whyteboard-0.41.1/whyteboard/gui/frame.py0000777000175000017500000012075311444677077017443 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ This module implements the Whyteboard application. It takes a Canvas class and wraps it in a GUI with a menu, toolbar, statusbar and some control panels. The GUI acts as a controller for the application - it delegates method calls to the appropriate classes when certain actions take place. """ from __future__ import with_statement import os import sys import time import shutil import wx import wx.lib.newevent from wx.html import HtmlHelpController from whyteboard.lib import ConfigObj, Validator, icon, fnb, pub from whyteboard.misc import Utility, meta from whyteboard.tools import Highlighter, EDGE_LEFT, EDGE_TOP from whyteboard.gui import (Canvas, CanvasDropTarget, ControlPanel, MediaPanel, Menu, Preferences, Print, SidePanel, ShapePopup, SheetsPopup, Toolbar) from whyteboard.gui import (ExceptionHook, AboutDialog, Feedback, FindIM, History, PDFCacheDialog, ProgressDialog, PromptForSave, Resize, ShapeViewer, TextInput, UpdateDialog) from whyteboard.gui import (ID_BACKGROUND, ID_CLOSE_ALL, ID_COLOUR_GRID, ID_DESELECT, ID_FOREGROUND, ID_MOVE_UP, ID_MOVE_DOWN, ID_MOVE_TO_TOP, ID_MOVE_TO_BOTTOM, ID_NEXT, ID_PASTE_NEW, ID_PREV, ID_RECENTLY_CLOSED, ID_STATUSBAR, ID_SWAP_COLOURS, ID_TOOL_PREVIEW, ID_TOOLBAR, ID_TRANSPARENT, ID_UNDO_SHEET) from whyteboard.misc import (get_home_dir, is_save_file, get_clipboard, check_clipboard, download_help_files, file_dialog, get_path, set_clipboard, show_dialog, open_url, new_instance, help_file_path) _ = wx.GetTranslation PASTE_CHECK_COUNT = 7 # only check clipboard every x value of EVT_UPDATE_MENU SCROLL_AMOUNT = 3 #---------------------------------------------------------------------- class GUI(wx.Frame): """ This class contains a ControlPanel, a Canvas frame and a SidePanel and manages their layout with a wx.BoxSizer. A menu, toolbar and associated event handlers call the appropriate functions of other classes. """ title = u"Whyteboard" LoadEvent, LOAD_DONE_EVENT = wx.lib.newevent.NewEvent() def __init__(self, config): """ Initialise utility, status/menu/tool bar, tabs, ctrl panel + bindings. """ wx.Frame.__init__(self, None, title=_("Untitled") + u" - %s" % self.title) self.util = Utility(self, config) self._oldhook = sys.excepthook sys.excepthook = ExceptionHook meta.find_transparent() # important if meta.transparent: try: x = self.util.items.index(Highlighter) except ValueError: self.util.items.insert(1, Highlighter) self.can_paste = check_clipboard() self.process = None self.pid = None self.dialog = None self.convert_cancelled = False self.shape_viewer_open = False self.help = None self.hotkey_pressed = False # for hotkey timer self.hotkey_timer = None self.tab_count = 1 self.tab_total = 1 self.current_tab = 0 self.closed_tabs = [] self.hotkeys = [] style = (fnb.FNB_X_ON_TAB | fnb.FNB_NO_X_BUTTON | fnb.FNB_VC8 | fnb.FNB_DROPDOWN_TABS_LIST | fnb.FNB_MOUSE_MIDDLE_CLOSES_TABS | fnb.FNB_NO_NAV_BUTTONS) self.control = ControlPanel(self) self.tabs = fnb.FlatNotebook(self, agwStyle=style) self.canvas = Canvas(self.tabs, self, (config['default_width'], config['default_height'])) self.panel = SidePanel(self) self.thumbs = self.panel.thumbs self.notes = self.panel.notes self.tabs.AddPage(self.canvas, _("Sheet") + u" 1") box = wx.BoxSizer(wx.HORIZONTAL) # position windows side-by-side box.Add(self.control, 0, wx.EXPAND) box.Add(self.tabs, 1, wx.EXPAND) box.Add(self.panel, 0, wx.EXPAND) self.SetSizer(box) self.SetSizeWH(800, 600) if os.name == "posix": self.canvas.SetFocus() # makes EVT_CHAR_HOOK trigger if 'mac' != os.name: self.Maximize(True) self.paste_check_count = PASTE_CHECK_COUNT - 1 wx.UpdateUIEvent.SetUpdateInterval(75) #wx.UpdateUIEvent.SetMode(wx.UPDATE_UI_PROCESS_SPECIFIED) self.SetIcon(icon.getIcon()) self.SetExtraStyle(wx.WS_EX_PROCESS_UI_UPDATES) self.SetDropTarget(CanvasDropTarget()) self.statusbar = self.CreateStatusBar() self._print = Print(self) self.filehistory = wx.FileHistory(8) self.load_history_file() self.filehistory.Load(self.config) self.menu = Menu(self) self.toolbar = Toolbar.create(self) self.SetMenuBar(self.menu.menu) self.set_menu_from_config() self.do_bindings() self.find_help() pub.sendMessage('thumbs.update_current') self.update_panels(True) wx.CallAfter(self.UpdateWindowUI) def __del__(self): sys.excepthook = self._oldhook def do_bindings(self): """ Performs event binding. """ self.Bind(fnb.EVT_FLATNOTEBOOK_PAGE_CHANGED, self.on_change_tab) self.Bind(fnb.EVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU, self.tab_popup) self.Bind(fnb.EVT_FLATNOTEBOOK_PAGE_DROPPED, self.on_drop_tab) self.Bind(self.LOAD_DONE_EVENT, self.on_done_load) self.Bind(wx.EVT_CHAR_HOOK, self.hotkey) self.Bind(wx.EVT_CLOSE, self.on_exit) self.Bind(wx.EVT_END_PROCESS, self.on_end_process) # end pdf conversion self.menu.bindings() topics = {'shape.add': self.shape_add, 'shape.popup': self.shape_popup, 'shape.selected': self.shape_selected, 'canvas.capture_mouse': self.capture_mouse, 'canvas.change_tool': self.pubsub_change_tool, 'canvas.paste_image': self.paste_image, 'canvas.paste_text': self.paste_text, 'canvas.release_mouse': self.release_mouse, 'gui.mark_unsaved': self.mark_unsaved, 'gui.open_file': self.open_file, 'media.create_panel': self.make_media_panel, 'text.show_dialog': self.show_text_dialog} [pub.subscribe(value, key) for key, value in topics.items()] self.hotkeys = [x.hotkey for x in self.util.items] ac = [] if os.name == "nt": for x, item in enumerate(self.util.items): blah = lambda evt, y = x + 1: self.on_change_tool(evt, y) _id = wx.NewId() ac.append((wx.ACCEL_NORMAL, ord(item.hotkey.upper()), _id)) self.Bind(wx.EVT_MENU, blah, id=_id) else: ac = [(wx.ACCEL_CTRL, ord(u'\t'), ID_NEXT), (wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord(u'\t'), ID_PREV) ] tbl = wx.AcceleratorTable(ac) self.SetAcceleratorTable(tbl) def set_menu_from_config(self): """ Sets up the program's initial menu state from the config parameters """ values = {ID_TOOLBAR: u'toolbar', ID_STATUSBAR: u'statusbar', ID_TOOL_PREVIEW: u'tool_preview', ID_COLOUR_GRID: u'colour_grid'} for _id, config_key in values.items(): if self.util.config[config_key]: self.menu.check(_id, True) else: getattr(self, u"on_" + config_key)(None, False) def shape_selected(self, shape): """ Shape getting selected (by Select tool) """ self.canvas.select_shape(shape) change = (shape.background == wx.TRANSPARENT) self.util.transparent = change self.control.transparent.SetValue(change) def make_media_panel(self, size, media): media.mc = MediaPanel(self.canvas, size, media) def release_mouse(self): self.canvas.release_mouse() def shape_popup(self, shape): self.PopupMenu(ShapePopup(self.canvas, self, shape)) def capture_mouse(self): self.canvas.capture_mouse() def shape_add(self, shape): self.canvas.add_shape(shape) def on_save(self, event=None): """ Saves file if filename is set, otherwise calls 'save as'. """ if not self.util.filename: # no wtbd file active, prompt for location self.on_save_as() else: self.util.save_file() def on_save_as(self, event=None): """ Prompts for the filename and location to save to. """ wildcard = _("Whyteboard file ") + u"(*.wtbd)|*.wtbd" _dir = self.util.config.get('last_opened_dir') or u"" _file = self.util.filename if not _file: _file = time.strftime(u"%x %X") _file = _file.replace(u":", u"-").replace(u"/", u"-") name = file_dialog(self, _("Save Whyteboard As..."), wx.SAVE | wx.OVERWRITE_PROMPT, wildcard, _dir, _file) if name: if not os.path.splitext(name)[1]: # no file extension name += u'.wtbd' if is_save_file(name): self.util.filename = name self.on_save() def on_open(self, event=None, text=None): """ Opens a file, sets Utility's temp. file to the chosen file, prompts for an unsaved file and calls do_open(). text is img/pdf/ps for the "import file" menu item """ wildcard = meta.dialog_wildcard if text == u"img": wildcard = wildcard[wildcard.find(_(u"Image Files")) : wildcard.find(u"|" + _(u'Whyteboard files')) ] # image to page elif text: wildcard = wildcard[wildcard.find(u"PDF/PS/SVG") : wildcard.find(u"*.SVG|")] # page descriptions _dir = self.util.config.get('last_opened_dir') or u"" filename = file_dialog(self, _("Open file..."), wx.OPEN, wildcard, _dir) self.open_file(filename) def open_file(self, filename): if filename: if is_save_file(filename): self.prompt_for_save(self.do_open, args=[filename]) else: self.do_open(filename) def do_open(self, path): """ Updates the appropriate variables in the utility file class and loads the selected file. """ self.filehistory.AddFileToHistory(path) self.filehistory.Save(self.config) self.config.Flush() self.util.save_last_path(path) if is_save_file(path): self.util.load_wtbd(path) else: self.util.temp_file = path self.util.load_file() def on_export_pdf(self, event=None): """ Exports the all the sheets as a PDF. Must first export all sheets as imgages, convert to PDF (displaying a progress bar) and then remove all the temporary files """ if not self.util.im_location: self.util.prompt_for_im() if not self.util.im_location: return filename = file_dialog(self, _("Export data to..."), wx.SAVE | wx.OVERWRITE_PROMPT, u"PDF (*.pdf)|*.pdf") if filename: ext = os.path.splitext(filename)[1] if not ext: # no file extension filename += u'.pdf' elif ext.lower() != u".pdf": wx.MessageBox(_("Invalid filetype to export as:") + u" .%s" % ext, u"Whyteboard") return names = [] canvas = self.canvas for x in range(self.tab_count): self.canvas = self.tabs.GetPage(x) name = u"%s-tempblahhahh-%s-.jpg" % (filename, x) names.append(name) self.util.export(name) self.canvas = canvas self.process = wx.Process(self) files = "" for x in names: files += u'"%s" ' % x # quote filenames for windows cmd = u'%s -define pdf:use-trimbox=true %s"%s"' % (self.util.im_location, files, filename) self.pid = wx.Execute(cmd, wx.EXEC_ASYNC, self.process) self.show_progress_dialog(_("Converting..."), True, True) [os.remove(x) for x in names] def on_export(self, event=None): """Exports the current sheet as an image, or all as a PDF.""" filename = self.export_prompt() if filename: self.util.export(filename) def on_export_all(self, event=None): """ Iterate over the chosen filename, add a numeric value to each path to separate each sheet's image. """ filename = self.export_prompt() if filename: name = os.path.splitext(filename) canvas = self.canvas for x in range(self.tab_count): self.canvas = self.tabs.GetPage(x) self.util.export(u"%s-%s%s" % (name[0], x + 1, name[1])) self.canvas = canvas def on_export_pref(self, event=None): """ Copies the user's preferences file to another file. """ if not os.path.exists(self.util.config.filename): wx.MessageBox(_("You have not set any preferences"), _("Export Error")) return wildcard = _("Whyteboard Preference Files") + u" (*.pref)|*.pref" filename = file_dialog(self, _("Export preferences to..."), wx.SAVE | wx.OVERWRITE_PROMPT, wildcard) if filename: if not os.path.splitext(filename)[1]: filename += u".pref" shutil.copy(os.path.join(get_home_dir(), u"user.pref"), filename) def on_import_pref(self, event=None): """ Imports the preference file. Backsup the user's current prefernce file into a directory, with a timestamp on the filename """ wildcard = _("Whyteboard Preference Files") + u" (*.pref)|*.pref" filename = file_dialog(self, _("Import Preferences From..."), wx.OPEN, wildcard, get_home_dir()) if filename: config = ConfigObj(filename, configspec=meta.config_scheme) validator = Validator() config.validate(validator) _dir = os.path.join(get_home_dir(), u"pref-bkup") if not os.path.isdir(_dir): os.makedirs(_dir) home = os.path.join(get_home_dir(), u"user.pref") if os.path.exists(home): stamp = time.strftime(u"%d-%b-%Y_%Hh-%Mm_%Ss", time.gmtime()) os.rename(home, os.path.join(_dir, stamp + u".user.pref")) pref = Preferences(self) pref.config = config pref.config.filename = home pref.on_okay() def on_reload_preferences(self, event): home = os.path.join(get_home_dir(), u"user.pref") if os.path.exists(home): config = ConfigObj(home, configspec=meta.config_scheme) validator = Validator() config.validate(validator) pref = Preferences(self) pref.config = config pref.config.filename = home pref.on_okay() def export_prompt(self): """ Find out the filename to save to """ val = None # return value wildcard = (u"PNG (*.png)|*.png|JPEG (*.jpg, *.jpeg)|*.jpeg;*.jpg|" + u"BMP (*.bmp)|*.bmp|TIFF (*.tiff)|*.tiff") dlg = wx.FileDialog(self, _("Export data to..."), style=wx.SAVE | wx.OVERWRITE_PROMPT, wildcard=wildcard) if dlg.ShowModal() == wx.ID_OK: filename = dlg.GetPath() _name = os.path.splitext(filename)[1].replace(u".", u"") types = {0: u"png", 1: u"jpg", 2: u"bmp", 3: u"tiff"} if not os.path.splitext(filename)[1]: _name = types[dlg.GetFilterIndex()] filename += u"." + _name val = filename if not _name in meta.types[2:]: wx.MessageBox(_("Invalid filetype to export as:") + u" .%s" % _name, u"Whyteboard") else: val = filename dlg.Destroy() return val def on_new_tab(self, event=None, name=None, wb=None): """ Opens a new tab, selects it, creates a new thumbnail and tree item name: unique name, sent by PDF convert/load file. wb: Passed by undo_tab to ensure the tab total is correct """ if not wb: self.tab_total += 1 if not name: name = _("Sheet") + u" %s" % self.tab_total self.tab_count += 1 self.thumbs.new_thumb(name=name) self.notes.add_tab(name) self.tabs.AddPage(Canvas(self.tabs, self, (self.util.config['default_width'], self.util.config['default_height'])), name) self.update_panels(False) # unhighlight current self.thumbs.thumbs[self.current_tab].current = True self.current_tab = self.tab_count - 1 self.tabs.SetSelection(self.current_tab) # fires on_change_tab self.on_change_tab() def on_change_tab(self, event=None): """Updates tab vars, scrolls thumbnails and selects tree node""" self.canvas = self.tabs.GetCurrentPage() self.update_panels(False) self.current_tab = self.tabs.GetSelection() self.update_panels(True) self.thumbs.thumbs[self.current_tab].update() self.thumbs.ScrollChildIntoView(self.thumbs.thumbs[self.current_tab]) self.control.change_tool() # updates canvas' shape if self.notes.tabs: tree_id = self.notes.tabs[self.current_tab] self.notes.tree.SelectItem(tree_id, True) pub.sendMessage('update_shape_viewer') def prompt_for_save(self, method, style=wx.YES_NO | wx.CANCEL, args=None): """ Ask the user to save, quit or cancel (quitting) if they haven't saved. Can be called through "Update", "Open (.wtbd)", or "Exit". If updating, don't show a cancel button, and explicitly restart if the user cancels out of the "save file" dialog method(*args) specifies the action to perform if user selects yes or no """ if not args: args = [] if not self.util.saved: name = _("Untitled") if self.util.filename: name = os.path.basename(self.util.filename) dialog = PromptForSave(self, name, method, style, args) dialog.ShowModal() else: method(*args) if method == self.Destroy: sys.exit() def prompt_for_im(self): dlg = FindIM(self.util, self, self.util.check_im_path) dlg.ShowModal() def on_drop_tab(self, event): """ Update the thumbs/notes so that they're poiting to the new tab position. Show a progress dialog, as all thumbnails must be updated. """ if event.GetSelection() == event.GetOldSelection(): return self.show_progress_dialog(_("Loading...")) self.dialog.Show() self.on_change_tab() pub.sendMessage('sheet.move', event=event, tab_count=self.tab_count) self.on_done_load() wx.MilliSleep(100) # try and stop user dragging too many tabs quickly wx.SafeYield() pub.sendMessage('update_shape_viewer') def update_panels(self, select): """Updates thumbnail panel's text""" pub.sendMessage('thumbs.text.highlight', tab=self.current_tab, select=select) def on_close_tab(self, event=None): """ Closes the current tab (if there are any to close). """ if not self.tab_count - 1: return self.tab_count -= 1 self.notes.remove_tab(self.current_tab) self.thumbs.remove(self.current_tab) for x in self.canvas.medias: x.remove_panel() self.create_sheet_undo_point(self.canvas, self.current_tab) if os.name == "posix": self.tabs.RemovePage(self.current_tab) else: self.tabs.DeletePage(self.current_tab) self.on_change_tab() # updates self.canvas def create_sheet_undo_point(self, canvas, tab_number, recreate_menu=True): """ Creates an undo entry for a tab that's being closed """ if len(self.closed_tabs) == self.util.config['undo_sheets']: del self.closed_tabs[0] item = {'shapes': canvas.shapes, 'undo': canvas.undo_list, 'redo': canvas.redo_list, 'size': canvas.area, 'name': self.tabs.GetPageText(tab_number), 'medias': canvas.medias, 'viewport': canvas.GetViewStart()} self.closed_tabs.append(item) if recreate_menu: self.menu.make_closed_tabs_menu() def on_close_all_sheets(self, event=None): """ Closes every sheet, creating undo points for each one. """ if not self.tab_count - 1: # must have at least one sheet open return for x in reversed(range(self.tab_count)): self.create_sheet_undo_point(self.tabs.GetPage(x), x, False) self.menu.make_closed_tabs_menu() self.remove_all_sheets() self.on_new_tab() def remove_all_sheets(self): self.canvas.shapes = [] self.canvas.redraw_all() self.tabs.DeleteAllPages() self.thumbs.remove_all() self.notes.remove_all() self.tab_count = 0 self.tab_total = 0 def on_undo_tab(self, event=None, tab=None): """ Undoes the last closed tab from the list. Re-creates the canvas from the saved shapes/undo/redo lists """ if not self.closed_tabs: return if not tab: tab = self.closed_tabs.pop() else: tab = self.closed_tabs.pop(self.closed_tabs.index(tab)) self.on_new_tab(name=tab['name'], wb=True) self.canvas.restore_sheet(tab['shapes'], tab['undo'], tab['redo'], tab['size'], tab['medias'], tab['viewport']) pub.sendMessage('update_shape_viewer') self.menu.make_closed_tabs_menu() def on_rename(self, event=None, sheet=None): if sheet is None: sheet = self.current_tab dlg = wx.TextEntryDialog(self, _("Rename this sheet to:"), _("Rename sheet")) dlg.SetValue(self.tabs.GetPageText(sheet)) if dlg.ShowModal() == wx.ID_CANCEL: dlg.Destroy() else: val = dlg.GetValue() if val: self.tabs.SetPageText(sheet, val) pub.sendMessage('sheet.rename', _id=sheet, text=val) def load_recent_files(self, event): """ Re-creates the Recent Files menu by reloading the config file. """ if self.menu.is_file_menu(event.GetMenu()): self.menu.remove_all_recent() self.load_history_file() self.filehistory.Load(self.config) event.Skip() # otherwise interferes with EVT_UPDATE_UI def update_menus(self, event): """ Enables/disables GUI menus and toolbar items. It uses a counter for the clipboard check as it can be too performance intense and cause segmentation faults """ if not self.canvas: return _id = event.GetId() if _id in [wx.ID_PASTE, ID_PASTE_NEW]: # check this less frequently, possibly expensive self.paste_check_count += 1 if self.paste_check_count == PASTE_CHECK_COUNT: self.can_paste = False if check_clipboard(): self.can_paste = True self.paste_check_count = 0 try: self.menu.enable(ID_PASTE_NEW, self.can_paste) self.menu.enable(wx.ID_PASTE, self.can_paste) except wx.PyDeadObjectError: pass return canvas = self.canvas if _id == ID_TRANSPARENT: if canvas.can_swap_transparency(): if canvas.is_transparent(): event.Check(True) else: event.Check(False) event.Enable(True) else: event.Enable(False) return do = False if _id == wx.ID_REDO and canvas.redo_list: do = True elif _id == wx.ID_UNDO and canvas.undo_list: do = True elif _id == ID_PREV and self.current_tab: do = True elif (_id == ID_NEXT and self.can_change_next_sheet()): do = True elif _id in [wx.ID_CLOSE, ID_CLOSE_ALL] and self.tab_count > 1: do = True elif _id in [ID_UNDO_SHEET, ID_RECENTLY_CLOSED] and self.closed_tabs: do = True elif _id in [wx.ID_DELETE, ID_DESELECT, ID_FOREGROUND] and canvas.selected: do = True elif _id == ID_MOVE_UP and canvas.check_move(u"up"): do = True elif _id == ID_MOVE_DOWN and canvas.check_move(u"down"): do = True elif _id == ID_MOVE_TO_TOP and canvas.check_move(u"top"): do = True elif _id == ID_MOVE_TO_BOTTOM and canvas.check_move(u"bottom"): do = True elif _id in [ID_SWAP_COLOURS, ID_BACKGROUND] and canvas.can_swap_colours(): do = True elif _id == wx.ID_COPY: if canvas: if canvas.copy: do = True event.Enable(do) def can_change_next_sheet(self): return self.tab_count > 1 and self.current_tab + 1 < self.tab_count def on_delete_shape(self, event=None): self.canvas.delete_selected() def on_deselect_shape(self, event=None): self.canvas.deselect_shape() def on_copy(self, event): set_clipboard(self.canvas.get_selection_bitmap()) def paste_image(self, bitmap, x, y, ignore=False): self.canvas.paste_image(bitmap, x, y, ignore) def paste_text(self, text, x, y): self.canvas.paste_text(text, x, y, self.util.colour) def on_paste_new(self, event): """ Pastes the text/image into a new tab """ self.on_new_tab() self.on_paste(ignore=True) def on_paste(self, event=None, ignore=False): """ Grabs the image from the clipboard and places it on the panel Ignore is used when pasting into a new sheet """ data = get_clipboard() if not data: return x, y = 0, 0 if not ignore: x, y = self.canvas.get_mouse_position() if isinstance(data, wx.TextDataObject): self.paste_text(data.GetText(), x, y) else: self.paste_image(data.GetBitmap(), x, y, ignore) def on_change_tool(self, event, _id): """ Change tool -- used when being used as a hotkey """ if not self.canvas.shape.drawing and not self.canvas.drawing: self.control.change_tool(_id=_id) def pubsub_change_tool(self, new=None): if self.canvas: self.change_tool(new) def change_tool(self, new=None, canvas=None): if not canvas: canvas = self.canvas self.util.change_tool(canvas, new) canvas.change_tool() pub.sendMessage('gui.preview.refresh') def on_fullscreen(self, event=None, val=None): """ Toggles fullscreen. val forces fullscreen on/off """ flag = wx.FULLSCREEN_NOBORDER | wx.FULLSCREEN_NOCAPTION | wx.FULLSCREEN_NOSTATUSBAR if not val: val = not self.IsFullScreen() self.ShowFullScreen(val, flag) self.menu.toggle_fullscreen(val) def hotkey(self, event=None): """ Checks for hotkeys to either change tools or to move the canvas' viewport. Checks for the arrow keys to move shapes about. """ code = event.GetKeyCode() if os.name == "posix": for x, key in enumerate(self.hotkeys): if code in [ord(key), ord(key.upper())]: self.on_change_tool(None, _id=x + 1) return if code == wx.WXK_ESCAPE: # close fullscreen/deselect shape if self.canvas.selected: self.canvas.deselect_shape() # check this before fullscreen return if self.IsFullScreen(): self.on_fullscreen(None, False) elif code in [wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP]: if self.canvas.selected: shape = self.canvas.selected _map = { wx.WXK_UP: (shape.x, shape.y - SCROLL_AMOUNT), wx.WXK_DOWN: (shape.x, shape.y + SCROLL_AMOUNT), wx.WXK_LEFT: (shape.x - SCROLL_AMOUNT, shape.y), wx.WXK_RIGHT: (shape.x + SCROLL_AMOUNT, shape.y) } if not self.hotkey_pressed: self.hotkey_pressed = True self.canvas.add_undo() shape.start_select_action(0) self.hotkey_timer = wx.CallLater(150, self.reset_hotkey) else: self.hotkey_timer.Restart(150) shape.move(_map.get(code)[0], _map.get(code)[1], offset=shape.offset(shape.x, shape.y)) self.canvas.draw_shape(shape) #shape.find_edges() #self.canvas.shape_near_canvas_edge(shape.edges[EDGE_LEFT], # shape.edges[EDGE_TOP], True) return self.hotkey_scroll(code, event) event.Skip() def hotkey_scroll(self, code, event): """Scrolls the viewport depending on the key pressed""" x, y = None, None if code == wx.WXK_HOME: x, y = 0, -1 # beginning of viewport if event.ControlDown(): x, y = -1, 0 # top of document elif code == wx.WXK_END: x, y = self.canvas.area[0], -1 # end of viewport if event.ControlDown(): x, y = -1, self.canvas.area[1] # end of page elif code in [wx.WXK_PAGEUP, wx.WXK_PAGEDOWN, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP]: x, y = self.canvas.GetViewStart() x2, y2 = self.canvas.GetClientSizeTuple() _map = { wx.WXK_PAGEUP: (-1, y - y2), wx.WXK_PAGEDOWN: (-1, y + y2), wx.WXK_UP: (-1, y - SCROLL_AMOUNT), wx.WXK_DOWN: (-1, y + SCROLL_AMOUNT), wx.WXK_LEFT: (x - SCROLL_AMOUNT, -1), wx.WXK_RIGHT: (x + SCROLL_AMOUNT, -1) } x, y = _map.get(code)[0], _map.get(code)[1] if x != None and y != None: self.canvas.Scroll(x, y) def reset_hotkey(self): """Reset the system for the next stream of hotkey up/down events""" self.hotkey_pressed = False if not self.canvas.selected: return self.canvas.selected.end_select_action(0) pub.sendMessage('update_shape_viewer') def get_canvases(self): return [self.tabs.GetPage(x) for x in range(self.tab_count)] def get_tab_names(self): return [self.tabs.GetPageText(x) for x in range(self.tab_count)] def get_background_colour(self): return self.control.get_background_colour() def get_colour(self): return self.control.get_colour() def toggle_control(self, menu, control, force=None): """Menu ID to check, enable/disable; view: Control to show/hide""" val = self.get_toggle_value(menu, force) control.Show(val) self.menu.check(menu, val) self.SendSizeEvent() def get_toggle_value(self, menu, force): val = False if self.menu.is_checked(menu) or force: val = True if force is False: val = False return val def on_toolbar(self, event=None, force=None): self.toggle_control(ID_TOOLBAR, self.toolbar, force) def on_tool_preview(self, event=None, force=None): self.toggle_control(ID_TOOL_PREVIEW, self.control.preview, force) def on_statusbar(self, event=None, force=None): self.toggle_control(ID_STATUSBAR, self.statusbar, force) def on_colour_grid(self, event=None, force=None): val = self.get_toggle_value(ID_COLOUR_GRID, force) self.control.toggle_colour_grid(val) self.menu.check(ID_COLOUR_GRID, val) pub.sendMessage('gui.preview.refresh') def convert_dialog(self, cmd): """Called when the PDF convert process begins""" self.process = wx.Process(self) self.pid = wx.Execute(cmd, wx.EXEC_ASYNC, self.process) self.show_progress_dialog(_("Converting..."), True, True) def on_end_process(self, event=None): """ Destroy the progress process after Convert finishes """ self.process.Destroy() self.dialog.Destroy() del self.process self.pid = None def show_text_dialog(self, text): dlg = TextInput(self, text=text) if dlg.ShowModal() == wx.ID_CANCEL: self.canvas.text = None self.canvas.redraw_all() self.pubsub_change_tool() return False dlg.transfer_data(self) # grab font and text data def show_text_edit_dialog(self, text_shape): dlg = TextInput(self, text_shape) if dlg.ShowModal() == wx.ID_CANCEL: return False dlg.transfer_data(text_shape) # grab font and text data return True def show_progress_dialog(self, title, cancellable=False, modal=False): self.dialog = ProgressDialog(self, title, cancellable) if modal: self.dialog.ShowModal() else: self.dialog.Show() def on_done_load(self, event=None): """ Refreshes thumbnails, destroys progress dialog after loading """ self.dialog.SetTitle(_("Updating Thumbnails")) wx.MilliSleep(50) wx.SafeYield() self.on_refresh() # force thumbnails self.dialog.Destroy() def on_file_history(self, evt): """ Handle file load from the recent files menu """ num = evt.GetId() - wx.ID_FILE1 path = self.filehistory.GetHistoryFile(num) if not os.path.exists(path): wx.MessageBox(_("File %s not found") % path, u"Whyteboard") self.filehistory.RemoveFileFromHistory(num) self.filehistory.Save(self.config) self.config.Flush() return self.filehistory.AddFileToHistory(path) # move up the list self.open_file(path) def on_exit(self, event=None): self.prompt_for_save(self.Destroy) def tab_popup(self, event): self.PopupMenu(SheetsPopup(self, self, event.GetSelection())) def on_undo(self, event=None): self.canvas.undo() def on_redo(self, event=None): self.canvas.redo() def on_move_top(self, event=None): self.canvas.move_top(self.canvas.selected) def on_move_bottom(self, event=None): self.canvas.move_bottom(self.canvas.selected) def on_move_up(self, event=None): self.canvas.move_up(self.canvas.selected) def on_move_down(self, event=None): self.canvas.move_down(self.canvas.selected) def on_previous_sheet(self, event=None): if not self.current_tab: return self.tabs.SetSelection(self.current_tab - 1) self.on_change_tab() def on_next_sheet(self, event=None): if not self.can_change_next_sheet(): return self.tabs.SetSelection(self.current_tab + 1) self.on_change_tab() def on_clear(self, event=None): self.canvas.clear(keep_images=True) def on_clear_all(self, event=None): self.canvas.clear() self.thumbs.update_all() def on_clear_sheets(self, event=None): for tab in range(self.tab_count): self.tabs.GetPage(tab).clear(keep_images=True) def on_clear_all_sheets(self, event=None): for tab in range(self.tab_count): self.tabs.GetPage(tab).clear() self.thumbs.update_all() def on_foreground(self, event): self.canvas.change_colour() def on_background(self, event): self.canvas.change_background() def on_refresh(self): self.thumbs.update_all() def on_transparent(self, event=None): self.canvas.toggle_transparent() def on_swap_colours(self, event=None): self.canvas.swap_colours() def on_page_setup(self, evt): self._print.page_setup() def on_print_preview(self, event): self._print.print_preview() def on_print(self, event): self._print.do_print() def on_new_win(self, event=None): new_instance() def on_translate(self, event): open_url(u"https://translations.launchpad.net/whyteboard") def on_report_bug(self, event): open_url(u"https://bugs.launchpad.net/whyteboard") def on_resize(self, event=None): show_dialog(Resize(self)) def on_preferences(self, event=None): show_dialog(Preferences(self)) def on_update(self, event=None): show_dialog(UpdateDialog(self)) def on_history(self, event=None): show_dialog(History(self)) def on_pdf_cache(self, event=None): show_dialog(PDFCacheDialog(self, self.util.library)) def on_feedback(self, event): show_dialog(Feedback(self), False) def load_history_file(self): self.config = wx.Config(u"Whyteboard", style=wx.CONFIG_USE_LOCAL_FILE) def on_shape_viewer(self, event=None): if not self.shape_viewer_open: self.shape_viewer_open = True show_dialog(ShapeViewer(self), False) def mark_unsaved(self): if self.util.saved: self.util.saved = False self.SetTitle(u"*" + self.GetTitle()) def find_help(self): """Locate the help files, update self.help var""" self.help = None if os.path.exists(help_file_path()): self.help = HtmlHelpController() self.help.AddBook(help_file_path()) def on_help(self, event=None, page=None): """ Shows the help file, if it exists, otherwise prompts the user to download it. """ if self.help and os.path.exists(help_file_path()): if page: self.help.Display(page) else: self.help.DisplayIndex() else: if self.download_help(): self.on_help(page=page) def download_help(self): """Downloads the help files""" msg = _("Help files not found, do you want to download them?") d = wx.MessageDialog(self, msg, style=wx.YES_NO | wx.ICON_QUESTION) if d.ShowModal() == wx.ID_YES: try: download_help_files(self.util.path[0]) self.find_help() return True except IOError: return False def on_about(self, event=None): inf = wx.AboutDialogInfo() inf.Name = u"Whyteboard" inf.Version = meta.version inf.Copyright = u"© 2009-2010 Steven Sproat" inf.Description = _("A simple whiteboard and PDF annotator") inf.Developers = [u"Steven Sproat "] inf.Translators = meta.translators inf.WebSite = (u"http://www.whyteboard.org", u"http://www.whyteboard.org") inf.Licence = u"GPL 3" license = os.path.join(get_path(), u"LICENSE.txt") if os.path.exists(license): with open(license) as f: inf.Licence = f.read() if os.name == "nt": AboutDialog(self, inf) else: wx.AboutBox(inf)whyteboard-0.41.1/whyteboard/gui/popups.py0000777000175000017500000001671611444707761017674 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ Popup menu items. """ import wx from whyteboard.gui import (ID_BACKGROUND, ID_CLOSE_ALL, ID_FOREGROUND, ID_MOVE_DOWN, ID_MOVE_TO_BOTTOM, ID_MOVE_TO_TOP, ID_MOVE_UP, ID_SWAP_COLOURS, ID_TRANSPARENT) _ = wx.GetTranslation #---------------------------------------------------------------------- class Popup(wx.Menu): """ A general pop-up menum providing default menu items. Easy to subclass to add new functionality. The "extra" (of type wx.Event*) variable must be passed around a lot as different subclasses access different functions of different events e.g. a TreeCtrl event to get its item, or a notebook tab change event """ def __init__(self, parent, gui, extra): wx.Menu.__init__(self) self.parent = parent self.gui = gui self.item = None self.set_item(extra) self.make_menu(extra) def make_menu(self, extra): SELECT, RENAME, EXPORT = wx.NewId(), wx.NewId(), wx.NewId() self.Append(SELECT, _("&Select")) self.AppendSeparator() self.Append(wx.ID_NEW, _("&New Sheet")) self.Append(wx.ID_CLOSE, _("Re&move Sheet")) self.Append(ID_CLOSE_ALL, _("Close All Sheets")) self.AppendSeparator() self.Append(RENAME, _("&Rename...")) self.Append(EXPORT, _("&Export...")) self.Bind(wx.EVT_MENU, self.select_tab_method(extra), id=SELECT) self.Bind(wx.EVT_MENU, self.rename, id=RENAME) self.Bind(wx.EVT_MENU, self.export, id=EXPORT) self.Bind(wx.EVT_MENU, self.close, id=wx.ID_CLOSE) self.Bind(wx.EVT_MENU, self.close_all, id=ID_CLOSE_ALL) def select_tab_method(self, extra): """Guess this is the class' interface...""" pass def set_item(self, extra): self.item = extra def close(self, event): self.gui.current_tab = self.item self.gui.canvas = self.gui.tabs.GetPage(self.item) self.gui.on_close_tab() def close_all(self, event): self.gui.on_close_all_sheets() def export(self, event): """ Change canvas temporarily to 'trick' the gui into exporting the selected tab. Then, restore the GUI to the correct one """ canvas = self.gui.canvas # reference to restore self.gui.canvas = self.gui.tabs.GetPage(self.item) self.gui.on_export() self.gui.canvas = canvas def rename(self, event): self.gui.on_rename(sheet=self.item) #---------------------------------------------------------------------- class NotesPopup(Popup): """ Parent = Notes panel - needs access to tree's events and methods. Overwrites the menu for a note, adding in extra items """ def make_menu(self, extra): if self.item is None: # root node return if isinstance(self.item, int): # sheet node super(NotesPopup, self).make_menu(extra) else: SELECT, EDIT, DELETE = wx.NewId(), wx.NewId(), wx.NewId() text = _("&Select") if self.item.selected: text = _("De&select") self.Append(SELECT, text) self.Append(EDIT, _("&Edit Note...")) self.AppendSeparator() self.Append(DELETE, _("&Delete")) self.Bind(wx.EVT_MENU, lambda x: self.parent.select(extra), id=SELECT) self.Bind(wx.EVT_MENU, self.select_tab_method(extra), id=EDIT) self.Bind(wx.EVT_MENU, lambda x: self.parent.delete(extra), id=DELETE) def select_tab_method(self, extra): return lambda x: self.parent.on_click(extra) def set_item(self, extra): self.item = self.parent.tree.GetPyData(extra.GetItem()) #---------------------------------------------------------------------- class SheetsPopup(Popup): """ Brought up by right-clicking the tab list. Its parent is the GUI """ def select_tab_method(self, extra): return lambda x: self.bleh() def bleh(self): self.parent.tabs.SetSelection(self.item) self.parent.on_change_tab() #---------------------------------------------------------------------- class ShapePopup(Popup): """ Brought up by right-clicking on a shape with the Select tool """ def make_menu(self, extra): SELECT, EDIT, POINT = wx.NewId(), wx.NewId(), wx.NewId() self.SetTitle(_(self.item.name)) text, _help = _("&Select"), _("Selects this shape") if self.item.selected: text, _help = _("De&select"), _("Deselects this shape") self.Append(SELECT, text, _help) self.Append(EDIT, _("&Edit..."), _("Edit the text")) self.Append(POINT, _("&Add New Point"), _("Adds a new point to the Polygon")) self.Append(wx.ID_DELETE, _("&Delete")) self.AppendSeparator() self.AppendCheckItem(ID_TRANSPARENT, _("T&ransparent")) self.Append(ID_FOREGROUND, _("&Color...")) self.Append(ID_BACKGROUND, _("&Background &Color...")) self.Append(ID_SWAP_COLOURS, _("Swap &Colors")) self.AppendSeparator() self.Append(ID_MOVE_UP, _("Move &Up")) self.Append(ID_MOVE_DOWN, _("Move &Down")) self.Append(ID_MOVE_TO_TOP, _("Move To &Top")) self.Append(ID_MOVE_TO_BOTTOM, _("Move To &Bottom")) if not self.item.name in [u"Text", u"Note"]: self.Enable(EDIT, False) if not self.item.name == u"Polygon": self.Enable(POINT, False) self.Bind(wx.EVT_MENU, lambda x: self.select(), id=SELECT) self.Bind(wx.EVT_MENU, lambda x: self.edit(), id=EDIT) self.Bind(wx.EVT_MENU, lambda x: self.add_point(), id=POINT) def edit(self): self.item.edit() def select(self, draw=True): if not self.item.selected: self.gui.canvas.deselect_shape() self.item.selected = True self.gui.canvas.selected = self.item else: self.gui.canvas.deselect_shape() if draw: self.gui.canvas.redraw_all() def add_point(self): self.gui.canvas.add_undo() self.item.points = list(self.item.points) x, y = self.gui.canvas.ScreenToClient(wx.GetMousePosition()) x, y = self.gui.canvas.CalcUnscrolledPosition(x, y) self.item.points.append((float(x), float(y))) self.gui.canvas.redraw_all() #---------------------------------------------------------------------- class ThumbsPopup(SheetsPopup): """ Just need to set the item to the current tab number. parent: thumb panel """ def bleh(self,): self.parent.gui.tabs.SetSelection(self.item) self.parent.gui.on_change_tab() #----------------------------------------------------------------------whyteboard-0.41.1/whyteboard/gui/printing.py0000777000175000017500000001154311443222121020146 0ustar stevesteve#! /usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ Print-related classes """ import wx _ = wx.GetTranslation #---------------------------------------------------------------------- class Print(object): def __init__(self, gui): self.gui = gui self.printData = wx.PrintData() self.printData.SetPaperId(wx.PAPER_LETTER) self.printData.SetPrintMode(wx.PRINT_MODE_PRINTER) def page_setup(self): psdd = wx.PageSetupDialogData(self.printData) psdd.CalculatePaperSizeFromId() dlg = wx.PageSetupDialog(self.gui, psdd) dlg.ShowModal() self.printData = wx.PrintData(dlg.GetPageSetupData().GetPrintData()) dlg.Destroy() def print_preview(self): data = wx.PrintDialogData(self.printData) printout = PrintOut(self.gui) printout2 = PrintOut(self.gui) preview = wx.PrintPreview(printout, printout2, data) if not preview.Ok(): wx.MessageBox(_("There was a problem printing.\nPerhaps your current printer is not set correctly?"), _("Printing Error")) return pfrm = wx.PreviewFrame(preview, self.gui, _("Print Preview")) pfrm.Initialize() pfrm.SetPosition(self.gui.GetPosition()) pfrm.SetSize(self.gui.GetSize()) pfrm.Show(True) def do_print(self): pdd = wx.PrintDialogData(self.printData) pdd.SetToPage(2) printer = wx.Printer(pdd) printout = PrintOut(self.gui) if not printer.Print(self.gui.canvas, printout, True): if printer.GetLastError() is not wx.PRINTER_CANCELLED: wx.MessageBox(_("There was a problem printing.\nPerhaps your current printer is not set correctly?"), _("Printing Error"), wx.OK) else: self.printData = wx.PrintData(printer.GetPrintDialogData().GetPrintData()) printout.Destroy() #---------------------------------------------------------------------- class PrintOut(wx.Printout): def __init__(self, gui): title = _("Untitled") if gui.util.filename: title = gui.util.filename wx.Printout.__init__(self, title) self.gui = gui def OnBeginDocument(self, start, end): return super(PrintOut, self).OnBeginDocument(start, end) def OnEndDocument(self): super(PrintOut, self).OnEndDocument() def OnBeginPrinting(self): super(PrintOut, self).OnBeginPrinting() def OnEndPrinting(self): super(PrintOut, self).OnEndPrinting() def OnPreparePrinting(self): super(PrintOut, self).OnPreparePrinting() def HasPage(self, page): return page <= self.gui.tab_count def GetPageInfo(self): return (1, self.gui.tab_count, 1, self.gui.tab_count) def OnPrintPage(self, page): dc = self.GetDC() canvas = self.gui.tabs.GetPage(page - 1) canvas.deselect_shape() maxX = canvas.buffer.GetWidth() maxY = canvas.buffer.GetHeight() marginX = 50 marginY = 50 maxX = maxX + (2 * marginX) maxY = maxY + (2 * marginY) (w, h) = dc.GetSizeTuple() scaleX = float(w) / maxX scaleY = float(h) / maxY actualScale = min(scaleX, scaleY) posX = (w - (canvas.buffer.GetWidth() * actualScale)) / 2.0 posY = (h - (canvas.buffer.GetHeight() * actualScale)) / 2.0 dc.SetUserScale(actualScale, actualScale) dc.SetDeviceOrigin(int(posX), int(posY)) dc.DrawText(_("Page:") + u" %d" % page, marginX / 2, maxY - marginY + 100) if self.gui.util.config['print_title']: filename = _("Untitled") if self.gui.util.filename: filename = self.gui.util.filename font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) dc2 = wx.WindowDC(self.gui) x = dc2.GetMultiLineTextExtent(filename, font) extent = x[0], x[1] dc.SetFont(font) dc.DrawText(_(filename), marginX / 2, -120) dc.SetDeviceOrigin(int(posX), int(posY)) canvas.redraw_all(dc=dc) return Truewhyteboard-0.41.1/whyteboard/misc/utility.py0000777000175000017500000005677111443222121020202 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ This module contains a utility helper class to reduce the amount of code inside gui.py - whyteboard-file saving/loading, pdf/ps loading/conversion and loading a standard image. The saved .wtbd file structure is: dictionary { 0: [colour, thickness, tool, tab, version, font], - app settings 1: shapes { 0: [shape1, shape2, .. shapeN], 1: [shape1, shape2, .. shapeN], .. N: [shape1, shape2, .. shapeN] } 2: files { 0: { 0: filename, ----------- not used any more 1: temp-file-1.png, 2: temp-file-2.png, ... }, 1: { 0: filename, 1: temp-file-1.png, 2: temp-file-2.png, ... }, ... } 3: names: [ 0: 'sheet 1' 1: 'sheet 2' 2: 'sheet 3' ... ] 4: sizes [ 0: (canvas_x, canvas_y), 1: (canvas_x, canvas_y), ..... ] } Image Tools have the assosicated image removed from their class upon saving, but are restored with it upon loading the file. """ from __future__ import with_statement import os import sys #import poppler import time import zipfile import wx import shutil try: import cPickle as pickle except ImportError: import pickle from whyteboard.lib import pub from whyteboard.gui import printing from whyteboard.misc import (meta, get_home_dir, load_image, convert_quality, make_filename, get_wx_image_type, version_is_greater, open_url) import whyteboard.tools as tools _ = wx.GetTranslation #---------------------------------------------------------------------- class Utility(object): """ The class defines some class variables which are set/accessed through the GUI - supported filetypes, names of the drawng tools, a save file's associated converted files (e.g. a PDF) Trying to achieve a data-driven system, focusing on "don't repeat yourself" """ def __init__(self, gui, config): """ Initialise "shared" variables, and set up a wxPython wildcard from the supported filetypes. """ self.gui = gui self.filename = None # ACTIVE .wtbd file self.temp_file = None # selected file (.wtdb/png/pdf - doesn't matter) self.to_archive = [] # image files to add to the save archive self.is_zipped = False self.zip = None # zip archive to read images from self.saved = True self.save_time = time.time() self.colour = wx.BLACK self.background = u"White" self.transparent = True # overwrites background self.thickness = 1 self.font = None # default font for text input self.tool = 1 # Current tool ID that is being drawn with self.items = tools.items # shortcut self.update_version = True self.saved_version = u"" self.im_location = None # location of ImageMagick on windows self.path = os.path.split(os.path.abspath(sys.argv[0])) self.library = PDFCache(u"library.known") self.config = config pub.subscribe(self.set_colour, 'change_colour') pub.subscribe(self.set_background, 'change_background') tools.HANDLE_SIZE = self.config['handle_size'] pub.sendMessage('canvas.set_border', border_size=self.config['canvas_border']) if 'default_font' in self.config: self.font = wx.FFont(1, 1) self.font.SetNativeFontInfoFromString(self.config['default_font']) def save_file(self): """ Saves the file by wrapping a pickled dictionary into a .data file and added any images into a zip archive along with the .data file. """ if not self.filename: return version = meta.version if not self.update_version: version = self.saved_version self.is_zipped = True self.mark_saved() self.save_last_path(self.filename) self.gui.show_progress_dialog(_("Saving...")) canvases = self.gui.get_canvases() save = Save(self, canvases, self.gui.get_tab_names()) self.write_save_file(save, version) self.zip = zipfile.ZipFile(self.filename, "r") save.restore_items(canvases) self.zip.close() self.gui.dialog.Destroy() self.gui.SetTitle(u"%s - %s" % (os.path.basename(self.filename), self.gui.title)) def write_save_file(self, save, version): """ An existing .wtbd zip must be re-created by copying all files except the pickled file, otherwise it gets added twice """ tmp_file = os.path.join(os.path.dirname(self.filename), u'whyteboard_temp_new.wtbd') _zip = zipfile.ZipFile(tmp_file, 'w') self.save_bitmap_data(_zip) save.save_items() data = save.create_save_list(self.gui.current_tab, version) with open("save.data", 'wb') as f: try: pickle.dump(data, f) except pickle.PickleError: wx.MessageBox(_("Error saving file data"), u"Whyteboard") self.saved = False self.filename = None _zip.write("save.data") _zip.close() os.remove("save.data") if os.path.exists(self.filename): os.remove(self.filename) shutil.move(tmp_file, self.filename) def save_bitmap_data(self, _zip): """ Will save all Image tools to disk as temporary files, and then removes them. This function is lengthy because it will not save two idential images twice. """ data = {} # list of bitmap data, check if image has been pasted to_remove = [] for canvas in self.gui.get_canvases(): for shape in canvas.shapes: if isinstance(shape, tools.Image): img = shape.image.ConvertToImage() img_data = img.GetData() for key, value in data.items(): if value == img_data: if not shape.filename: shape.filename = key break # the above iteration didn't find any common pastes if not shape.filename: tmp_name = make_filename() + u".png" img.SaveFile(tmp_name, wx.BITMAP_TYPE_PNG) img = wx.Image(tmp_name) name = make_filename() + u".jpg" img.SaveFile(name, wx.BITMAP_TYPE_JPEG) shape.filename = name data[shape.filename] = img_data _zip.write(name, os.path.join(u"data", name)) to_remove.append(name) to_remove.append(tmp_name) else: name = shape.filename if not name in to_remove: data[name] = img_data img.SaveFile(name, get_wx_image_type(name)) _zip.write(name, os.path.join("data", name)) [os.remove(x) for x in to_remove] def load_file(self, filename=None): """ Loads in a file, passes it to convert if it is a convertable file, then either loads an image or unpickles a whyteboard file """ if filename is None: filename = self.temp_file _file, _type = os.path.splitext(filename) # convert to lowercase to _type = _type.replace(u".", u"").lower() # save typing filename[1:] :) if _type in meta.types[:3]: self.convert() elif _type in meta.types: load_image(self.temp_file, self.gui.canvas, tools.Image) self.gui.canvas.redraw_all() else: wx.MessageBox(_("Whyteboard doesn't support the filetype") + u" .%s" % _type, u"Whyteboard") def load_wtbd(self, filename): """ Closes all tabs, loads in a Whyteboard save, which can be a zipped file or a single pickled file. """ f = None try: f = zipfile.ZipFile(filename) except zipfile.BadZipfile: self.is_zipped = False self.load_wtbd_pickle(filename) # old save format return self.is_zipped = True data = None self.zip = f try: data = f.read("save.data") except KeyError: wx.MessageBox(_('"%s" is missing the file save.data') % os.path.basename(filename)) f.close() return self.load_wtbd_pickle(filename, data) self.zip.close() def load_wtbd_pickle(self, filename, pickle_data=None): """ Loads in the old .wtbd format (just a pickled file). Takes in either a filename (path) or a Python file object (from the zip archive) Pretty messy code, to support old save files written in "w", not "wb" """ sys.modules['tools'] = tools # monkey patch for new src layout (0.4) temp = {} method = pickle.load if not pickle_data: f = open(filename, 'rb') else: f = pickle_data method = pickle.loads try: temp = method(f) except (pickle.UnpicklingError, AttributeError, ValueError, TypeError, EOFError): wx.MessageBox(_('"%s" has corrupt data.\nThis file cannot be loaded.') % os.path.basename(filename), u"Whyteboard") return except ImportError: # older windows/linux incompatible type if not pickle_data: f.close() f = open(filename, 'r') try: temp = method(f) except (pickle.UnpicklingError, AttributeError, ImportError, ValueError, TypeError, EOFError): wx.MessageBox(_('"%s" has corrupt data.\nThis file cannot be loaded.') % os.path.basename(filename), u"Whyteboard") return finally: if not pickle_data: f.close() finally: if not pickle_data: f.close() del sys.modules['tools'] self.recreate_save(filename, temp) def recreate_save(self, filename, temp): """ Recreates the saved .wtbd file's state """ self.filename = filename self.gui.show_progress_dialog(_("Loading...")) self.gui.remove_all_sheets() # change program settings and update the Preview window self.colour = temp[0][0] self.thickness = temp[0][1] self.tool = temp[0][2] self.gui.control.change_tool(_id=self.tool) # toggle button self.gui.control.colour.SetColour(self.colour) self.gui.control.thickness.SetSelection(self.thickness - 1) # re-create tabs and its saved drawings for x in temp[1]: self.gui.on_new_tab(name=temp[3][x]) self.gui.canvas.resize(temp[4][x]) try: media = temp[5][x] for m in media: m.canvas = self.gui.canvas m.load() self.gui.canvas.medias.append(m) except KeyError: break for shape in temp[1][x]: try: shape.canvas = self.gui.canvas # restore canvas shape.load() # restore unpickleable settings self.gui.canvas.add_shape(shape) except Exception: break self.gui.canvas.redraw_all(True) # close progress bar, handle older file versions gracefully wx.PostEvent(self.gui, self.gui.LoadEvent()) self.mark_saved() self.saved_version = temp[0][4] pub.sendMessage('canvas.change_tool') self.gui.tabs.SetSelection(temp[0][3]) self.gui.on_change_tab() self.gui.SetTitle(u"%s - %s" % (os.path.basename(filename), self.gui.title)) self.gui.closed_tabs = list() try: if temp[0][5]: font = wx.FFont(1, 1) font.SetNativeFontInfoFromString(temp[0][5]) self.font = font except IndexError: pass # Don't save .wtbd file of future versions as current, older version if version_is_greater(self.saved_version, meta.version): self.update_version = False def save_last_path(self, path): self.config['last_opened_dir'] = os.path.dirname(path) self.config.write() def mark_saved(self): self.saved = True self.save_time = time.time() def convert(self, _file=None): """ If the filetype is PDF/PS, convert to a (temporary) series of images and loads them. Find out the directory length before/after the conversion to know how many 'pages' were converted - used then to create a new Whyteboard tabs for each page. The PDF's file location, convert quality and converted images are written into a "library" file, effectively caching the conversion. An attempt at randomising the temp. file name is made using alphanumeric characters to help minimise conflict. """ if not self.im_location: self.prompt_for_im() if not self.im_location: # above will have changed this if IM exists return if _file is None: _file = self.temp_file quality = self.config['convert_quality'] cached = self.library.lookup(_file, quality) if cached: self.display_converted(_file, cached) else: path = get_home_dir(u"wtbd-tmp") # directory to store the images tmp_file = make_filename() before = os.walk(path).next()[2] # file count before convert full_path = path + tmp_file + u".png" cmd = convert_quality(quality, self.im_location, _file, full_path) self.gui.convert_dialog(cmd) # show progress bar, kick off convert if self.gui.convert_cancelled: return after = os.walk(path).next()[2] count = len(after) - len(before) images = [] ignore = False if not count: wx.MessageBox(_("Failed to convert file. Ensure GhostScript is installed\nhttp://pages.cs.wisc.edu/~ghost/"), _("Conversion Failed")) open_url(u"http://pages.cs.wisc.edu/~ghost/") return if count == 1: images.append(path + tmp_file + u".png") ignore = True else: for x in range(count): # store the temp file path for this file in the dictionary images.append(u"%s%s-%i.png" % (path, tmp_file, x)) self.display_converted(_file, images, ignore) self.library.write(_file, images, quality) # Just in case it's a file with many pages self.gui.show_progress_dialog(_("Loading...")) self.gui.on_done_load() def display_converted(self, _file, images, ignore_close=False): """ Display converted items. _file: PDF/PS name. Images: list of files """ if not ignore_close and self.gui.tab_count == 1 and not self.gui.canvas.shapes: self.gui.remove_all_sheets() for x in range(len(images)): name = u"%s - %s" % (os.path.basename(_file)[:15], x + 1) self.gui.on_new_tab(name=name) load_image(images[x], self.gui.canvas, tools.Image) self.gui.canvas.redraw_all() def export(self, filename): """ Exports the current view as a file. Select the appropriate wx constant depending on the filetype. gif is buggered for some reason :-/ """ const = get_wx_image_type(filename) self.gui.canvas.deselect_shape() context = wx.MemoryDC(self.gui.canvas.buffer) memory = wx.MemoryDC() x, y = self.gui.canvas.buffer.GetSize() bitmap = wx.EmptyBitmap(x, y, -1) memory.SelectObject(bitmap) memory.Blit(0, 0, x, y, context, 0, 0) memory.SelectObject(wx.NullBitmap) bitmap.SaveFile(filename, const) # write to disk def prompt_for_im(self): """ Prompts a Windows user for ImageMagick's directory location on initialisation. Save location to config file. """ if os.name == "posix": value = os.system(u"which convert") if value == 256: wx.MessageBox(_("ImageMagick was not found. You will be unable to load PDF and PS files until it is installed."), u"Whyteboard") else: self.im_location = u"convert" elif os.name == "nt": if not 'imagemagick_path' in self.config: self.gui.prompt_for_im() if self.im_location: self.config['imagemagick_path'] = os.path.dirname(self.im_location) self.config.write() else: self.check_im_path(self.config['imagemagick_path']) def check_im_path(self, path): """ Checks the ImageMagick path before getting/setting the string to ensure convert.exe exists """ _file = os.path.join(path, u"convert.exe") if not os.path.exists(_file): wx.MessageBox(_('Folder "%s" does not contain convert.exe') % path, u"Whyteboard") return False self.im_location = _file return True def change_tool(self, canvas, new=None): """ Changes the canvas' shape that is being drawn with, or creates a new instance of the currently selected shape """ if not new: new = self.tool else: self.tool = new colour = self.colour thickness = self.thickness params = [canvas, colour, thickness] if not self.transparent: params.append(self.background) canvas.shape = self.items[new - 1](*params) # create new Tool def set_colour(self, colour): self.colour = colour def set_background(self, colour): self.background = colour #---------------------------------------------------------------------- class PDFCache(object): """ Represents a cache of any converted PDF files """ def __init__(self, filename): self.path = os.path.join(get_home_dir(), filename) if not os.path.exists(self.path): self.write_dict(dict()) def lookup(self, _file, quality): """Check whether a file is inside our known file library""" files = self.entries() for x, key in files.items(): if files[x]['file'] == _file and files[x]['quality'] == quality: return files[x]['images'] return False def write(self, location, images, quality): """Adds a newly converted file to the library""" files = self.entries() files[len(files)] = {'file': location, 'images': images, 'quality': quality, 'date': time.asctime()} self.write_dict(files) def write_dict(self, files): with open(self.path, "w") as f: pickle.dump(files, f) def entries(self): with open(self.path) as f: return pickle.load(f) #---------------------------------------------------------------------- class Save(object): """ Stores the data required to save a file. """ def __init__(self, util, canvases, names): self.util = util self.names = names self.medias = [] self.canvas_sizes = [] self.tree_ids = [] self.items = {} for x, canvas in enumerate(canvases): for media in canvas.medias: media.save() self.items[x] = list(canvas.shapes) self.canvas_sizes.append(canvas.area) self.medias.append(canvas.medias) def save_items(self): """ Remove any unpickleable items from the list of shapes """ for x in self.items: for shape in self.items[x]: if isinstance(shape, tools.Note): self.tree_ids.append(shape.tree_id) shape.tree_id = None try: shape.save() except Exception: break def create_save_list(self, tab, version): """ Creates the save list. This *really* needs to be revised, using ints as dictionary keys! ugh. """ font = None if self.util.font: font = self.util.font.GetNativeFontInfoDesc() return { 0: [self.util.colour, self.util.thickness, self.util.tool, tab, version, font], 1: self.items, 2: None, # was self.to_convert, but wasn't used. 3: self.names, 4: self.canvas_sizes, 5: self.medias } def restore_items(self, canvases): """ Fixes a bug with Windows, where the save_items() fuction unlinks each shape's canvas' """ count = 0 for x in self.items: canvas = canvases[x] for shape in self.items[x]: shape.canvas = canvas if isinstance(shape, tools.Note): shape.load(False) shape.tree_id = self.tree_ids[count] count += 1 else: shape.load() for m in canvas.medias: m.canvas = canvas m.load()whyteboard-0.41.1/whyteboard/misc/functions.py0000777000175000017500000002511211443272712020503 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ Contains generic functions for the program that are not dependent on any other classes, only Python and wx functionality. """ import os import subprocess import sys import random import tarfile import urllib import webbrowser import wx from wx.lib.buttons import GenBitmapButton, GenBitmapToggleButton from distutils.dir_util import copy_tree, remove_tree from whyteboard.lib import pub _ = wx.GetTranslation path = os.path.split(os.path.abspath(sys.argv[0])) #---------------------------------------------------------------------- def get_home_dir(extra_path=None): """ Returns the home directory for Whyteboard in a cross-platform way If the extra path is supplied, it is appended to the home directory. The directory is verified to see if it exists: if doesn't, it is created. """ std_paths = wx.StandardPaths.Get() path = wx.StandardPaths.GetUserLocalDataDir(std_paths) if extra_path: path = os.path.join(path, extra_path, "") # "" forces slash at end if not os.path.isdir(path): os.makedirs(path) return path def get_time(seconds): """Returns an (h:)m:s time from a seconds value - hour not shown if < 0""" m, s = divmod(seconds, 60) h, m = divmod(m, 60) if h > 0: h = u"%d:" % h else: h = u"" return h + u"%02d:%02d" % (m, s) def file_dialog(gui, title, style, wildcard, defaultDir="", defaultFile=""): """ Returns the result of a file dialog """ dlg = wx.FileDialog(gui, title, style=style, wildcard=wildcard, defaultDir=defaultDir, defaultFile=defaultFile) if dlg.ShowModal() == wx.ID_OK: return dlg.GetPath() return False def load_image(path, canvas, image_class): """ Loads an image into the given Whyteboard tab. bitmap is the path to an image file to create a bitmap from. image_class = tools.Image *CLASS ITSELF* """ image = wx.Bitmap(path) shape = image_class(canvas, image, path) shape.left_down(0, 0) # renders, updates scrollbars pub.sendMessage('thumbs.update_current') def create_colour_bitmap(colour): """ Draws a small coloured bitmap for a colour grid button. Can take a name, RGB tupple or RGB-packed int. """ bmp = wx.EmptyBitmap(20, 20) dc = wx.MemoryDC() dc.SelectObject(bmp) dc.SetBackground(wx.Brush(colour)) dc.Clear() dc.SelectObject(wx.NullBitmap) return bmp def bitmap_button(parent, path, border=True, toggle=False): """ Creates a platform-dependent bitmap button that's toggleable or not. """ _type = GenBitmapToggleButton if not toggle: _type = GenBitmapButton if os.name == "posix": _type = wx.BitmapButton style = 0 if not border: style = wx.NO_BORDER return _type(parent, bitmap=wx.Bitmap(path), style=style) def get_wx_image_type(filename): """ Returns the wx.BITMAP_TYPE_X for a given filename """ _name = os.path.splitext(filename)[1].replace(".", "").lower() types = {"png": wx.BITMAP_TYPE_PNG, "jpg": wx.BITMAP_TYPE_JPEG, "jpeg": wx.BITMAP_TYPE_JPEG, "bmp": wx.BITMAP_TYPE_BMP, "tiff": wx.BITMAP_TYPE_TIF, "pcx": wx.BITMAP_TYPE_PCX } return types[_name] # grab the right image type from dict. above def convert_quality(quality, im_location, _file, path): """Returns a string for controlling the convert quality""" density = 200 resample = 88 if quality == 'highest': density = 300 resample = 120 if quality == 'high': density = 250 resample = 100 cmd = (u'"%s" -density %i "%s" -resample %i -unsharp 0x.5 -trim +repage -bordercolor white -border 20 "%s"' % (im_location, density, _file, resample, path)) return cmd def make_filename(): """ Create a random filename using letters, numbers and other characters """ alphabet = (u"abcdefghijklmnopqrstuvwxyz1234567890-+!^&()=[]@$%_ ABCDEFGHIJKLMNOPQRSTUVWXYZ") _list = [] for x in random.sample(alphabet, random.randint(8, 20)): _list.append(x) string = u"".join(_list) return string + u"-temp-%s" % (random.randrange(0, 999999)) def get_clipboard(): """ Gets the clipboard's contents, or False for any valid image/text data """ bmp = wx.BitmapDataObject() wx.TheClipboard.Open() success = wx.TheClipboard.GetData(bmp) wx.TheClipboard.Close() if success: return bmp text = wx.TextDataObject() wx.TheClipboard.Open() success = wx.TheClipboard.GetData(text) wx.TheClipboard.Close() if success: return text return False def check_clipboard(): """ Checks whether supported data is on the clipboard """ if not wx.TheClipboard.IsOpened(): wx.TheClipboard.Open() success = wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_BITMAP)) success2 = wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT)) wx.TheClipboard.Close() return success or success2 return False def set_clipboard(bitmap): """ Sets the clipboard with bitmap image data """ bmp = wx.BitmapDataObject() bmp.SetBitmap(bitmap) wx.TheClipboard.Open() wx.TheClipboard.SetData(bmp) wx.TheClipboard.Close() def transparent_supported(): """ Does this wxPython build support transparency? """ try: dc = wx.MemoryDC() dc.SelectObject(wx.EmptyBitmap(10, 10)) x = wx.GCDC(dc) return True except NotImplementedError: return False def is_exe(): """ Determine if Whyteboard is being run as an exe """ return hasattr(sys, u"frozen") def is_save_file(name): return name.lower().endswith(u".wtbd") def show_dialog(_class, modal=True): if modal: _class.ShowModal() else: _class.Show() def open_url(url): wx.BeginBusyCursor() webbrowser.open_new_tab(url) wx.CallAfter(wx.EndBusyCursor) def new_instance(): program = (u'python', os.path.abspath(sys.argv[0])) if is_exe(): program = os.path.abspath(sys.argv[0]) subprocess.Popen(program) def fix_std_sizer_tab_order(sizer): """ Fixes wx.StdDialogButtonSizer's tab ordering """ buttons = [] for child in sizer.GetChildren(): win = child.GetWindow() if win is not None: buttons.append(win) if len(buttons) >= 1: buttons[1].MoveAfterInTabOrder(buttons[0]) def format_bytes(total): """ Turn an amount of byte into readable KB/MB format http://www.5dollarwhitebox.org/drupal/node/84 """ _bytes = float(total) if _bytes >= 1048576: megabytes = _bytes / 1048576 size = u'%.2fMB' % megabytes elif _bytes >= 1024: kilobytes = _bytes / 1024 size = u'%.2fKB' % kilobytes else: size = u'%.2fb' % _bytes return size def get_version_int(version): """ Turns a version string like 0.40.2 into [0, 40, 2] """ num = [int(x) for x in version.split(u".")] if len(num) == 2: num.append(0) return num def version_is_greater(version1, version2): """ Checks whether the first version is greater than the 2nd """ a = get_version_int(version1) b = get_version_int(version2) return b[1] < a[1] or (b[1] == a[1] and b[2] < a[2]) def download_help_files(path): """ Downloads the help files to the user's directory and shows them """ _file = os.path.join(path, u"whyteboard-help.tar.gz") url = u"http://whyteboard.googlecode.com/files/help-files.tar.gz" tmp = None try: tmp = urllib.urlretrieve(url, _file) except IOError: wx.MessageBox(_("Could not connect to server.\n Check your Internet connection and firewall settings"), u"Whyteboard") raise IOError if os.name == "posix": os.system(u"tar -xf " + tmp[0]) else: tar = tarfile.open(tmp[0]) tar.extractall(path) tar.close() os.remove(tmp[0]) def extract_tar(path, _file, version, backup_extension): """ Extract a .tar.gz source file on Windows, without needing to use the 'tar' command, and with no other downloads! """ tar = tarfile.open(_file) tar.extractall(path) tar.close() # remove 2 folders that will be updated, may not exist src = os.path.join(path, u"whyteboard-" + version) # rename all relevant files - ignore any dirs for f in os.listdir(path): location = os.path.join(path, f) if not os.path.isdir(location): _type = os.path.splitext(f) if _type[1] in [u".py", u".txt"]: new_file = os.path.join(path, _type[0]) + backup_extension os.rename(location, new_file) # move extracted file to current dir, remove tar, remove extracted dir copy_tree(src, path) remove_tree(src) def help_file_path(): return os.path.join(get_path(), u'whyteboard-help', u'whyteboard.hhp') def get_path(): """ Root directory from wherever the application is installed to. We must follow through any symlinks to find the actual install directory for Unix. """ _file = os.path.abspath(sys.argv[0]) path = os.path.dirname(_file) if os.path.islink(_file): path = os.path.dirname(os.path.join(os.path.dirname(_file), os.readlink(_file))) return path.decode("utf-8") def get_image_path(directory, filename): """ Fetch an image from the correct directory """ return os.path.join(get_path(), u"images", directory, u"%s.png" % filename)whyteboard-0.41.1/whyteboard/misc/topic_tree.py0000777000175000017500000000714111443222121020617 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ Specifices a "PyPubSub" topic tree which is a list of all pub/sub events that the system is listening to. Helps give an overview of the program as well as prevent any invalid parameters being passed into the message broadcasts. """ from whyteboard.lib import pub from pub.utils import TopicTreeDefnSimple _ = wx.GetTranslation #---------------------------------------------------------------------- class WhyteboardTopicTree(TopicTreeDefnSimple): class sheet: '''Operations performed on the sheets (tabs)''' class move: '''When a sheet has been dragged/dropped''' event = 'the wx.Event that the listener can use as needed' tab_count = 'total number of tabs to iterate over for updating' _required = ('event', 'tab_count') # class rename: # '''sheet being renamed''' # _id = 'ID (tab number) of the sheet being renamed' # text = 'new sheet name' # _required = ('_id', 'text') class canvas: '''Operations performed on the canvas''' class set_border: '''Updates the "grabbable" border size the canvas has''' border_size = 'size in pixels to set the border to' _required = 'border_size' class capture_mouse: pass class release_mouse: pass class note: '''The Note Tool''' class add: '''when a note is added''' note = 'the Note instance' _id = 'wx.TreeId' _required = 'note' class edit: '''when a note is edited''' tree_id = 'wx.TreeId' text = "new Note's text" _required = ('tree_id', 'text') class thumbs: '''Thumbnails''' class text: '''static text labels''' class highlight: '''to turn a thumbnail label bold or not''' tab = 'which label to update' select = 'Whether to highlight the text or not' _required = ('tab', 'select') class shape: '''Operations performed on shapes''' class selected: '''shape has been select''' shape = 'selected shape' _required = 'shape' class add: '''shape has been drawn''' shape = 'drawn shape' _required = 'shape' class shape_viewer: '''actions for the Shape Viewer dialog''' class update: '''an action has been performed to update the dialog''' pass class tools: '''update a Tool''' class set_handle_size: '''change tools' selection handle size''' handle_size = 'size in pixels' _required = 'handle_size' pub.addTopicDefnProvider(WhyteboardTopicTree())whyteboard-0.41.1/whyteboard/misc/meta.py0000777000175000017500000002071211444535677017437 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ Contains meta data for the program, such as version, whether transparency is supported, language mappings, translator credits All attributes are global, but this module will always be imported, and can be used as a class in a way. There's simply *no need* to make it a class The function find_transparency is called from the GUI upon its creation, instead of before the GUI is created because wx must first initialise its App object before allowing DC operations """ import wx from whyteboard.misc import transparent_supported _ = wx.GetTranslation #---------------------------------------------------------------------- # Creates a wxPython wildcard filter from a list of known/supported filetypes. _all = [ (_('PDF/PS/SVG'), [u'ps', u'pdf', u'svg']), (_('Image Files'), [u"jpeg", u"jpg", u"png", u"tiff", u"bmp", u"pcx"]), (_('Whyteboard files'), [u'wtbd']) ] wc_list, types, tmp = [], [], [] for label, exts in _all: [types.append(x) for x in exts] exts = [u'*.%s' % a for a in exts] visexts = u', '.join(exts) exts.extend([e.upper() for e in exts]) tmp.extend(exts) [types.append(x.replace(u"*.", u"")) for x in exts] wc_list.append(u'%s (%s)|%s' % (label, visexts, u';'.join(exts))) wc_list.insert(0, u'%s|%s' % (_('All files') + u' (*.*)', u'*.*')) wc_list.insert(0, u'%s|%s' % (_("All suppported files"), u';'.join(tmp))) dialog_wildcard = u'|'.join(wc_list) transparent = True version = u"0.41.1" backup_extension = u".blah5bl8ah123bla6h" languages = ( (u"English", _("English"), wx.LANGUAGE_ENGLISH), (u"English (U.K.)", _("English (U.K.)"), wx.LANGUAGE_ENGLISH_UK), (u"Japanese", _("Japanese"), wx.LANGUAGE_JAPANESE), (u"Portuguese", _("Portuguese"), wx.LANGUAGE_PORTUGUESE), (u"Dutch", _("Dutch"), wx.LANGUAGE_DUTCH), (u"German", _("German"), wx.LANGUAGE_GERMAN), (u"Russian", _("Russian"), wx.LANGUAGE_RUSSIAN), (u"Arabic", _("Arabic"), wx.LANGUAGE_ARABIC), (u"Hindi", _("Hindi"), wx.LANGUAGE_HINDI), (u"Spanish", _("Spanish"), wx.LANGUAGE_SPANISH), (u"French", _("French"), wx.LANGUAGE_FRENCH), (u"Welsh", _("Welsh"), wx.LANGUAGE_WELSH), (u"Chinese (Traditional)", _("Chinese (Traditional)"), wx.LANGUAGE_CHINESE_TRADITIONAL), (u"Czech", _("Czech"), wx.LANGUAGE_CZECH), (u"Italian", _("Italian"), wx.LANGUAGE_ITALIAN), (u"Galician", _("Galician"), wx.LANGUAGE_GALICIAN) ) _langs = "'%s'" % "', '".join(str(x[0]) for x in languages) config_scheme = """ bmp_select_transparent = boolean(default=False) canvas_border = integer(min=10, max=35, default=15) colour_grid = boolean(default=True) colour1 = list(min=3, max=3, default=list('280', '0', '0')) colour2 = list(min=3, max=3, default=list('255', '255', '0')) colour3 = list(min=3, max=3, default=list('0', '255', '0')) colour4 = list(min=3, max=3, default=list('255', '0', '0')) colour5 = list(min=3, max=3, default=list('0', '0', '255')) colour6 = list(min=3, max=3, default=list('160', '32', '240')) colour7 = list(min=3, max=3, default=list('0', '255', '255')) colour8 = list(min=3, max=3, default=list('255', '165', '0')) colour9 = list(min=3, max=3, default=list('211', '211', '211')) convert_quality = option('highest', 'high', 'normal', default='normal') default_font = string default_width = integer(min=1, max=12000, default=640) default_height = integer(min=1, max=12000, default=480) imagemagick_path = string handle_size = integer(min=3, max=15, default=6) language = option(""" + _langs + """) last_opened_dir = string print_title = boolean(default=True) statusbar = boolean(default=True) tool_preview = boolean(default=True) toolbar = boolean(default=True) toolbox = option('icon', 'text', default='icon') toolbox_columns = option(2, 3, default=2) undo_sheets = integer(min=5, max=50, default=10) """ config_scheme = config_scheme.split("\n") translators = [ u'A. Emmanuel Mendoza https://launchpad.net/~a.emmanuelmendoza (Spanish)', u'Alexey Reztsov https://launchpad.net/~ariafan (Russian)', u'Aljosha Papsch https://launchpad.net/~joschi-papsch (German)', u'"Amy" https://launchpad.net/~anthropofobe (German)', u'Antoine Jouve https://launchpad.net/~aj94tj (French)', u'"Auduf" https://launchpad.net/~5097-mail (Russian)', u'Billy Robshaw https://launchpad.net/~billyrobshaw (Spanish)', u'"Cheesewheel" https://launchpad.net/~wparker05 (Arabic)', u'"cmdrhenner" https://launchpad.net/~cmdrhenner (German)', u'Cristian Asenjo https://launchpad.net/~apu2009 (Spanish)', u"David https://launchpad.net/~3-admin-dav1d-de (German)", u'David Aller https://launchpad.net/~niclamus (Italian)', u'"Dennis" https://launchpad.net/~dlinn83 (German)', u'Diejo Lopez https://launchpad.net/~diegojromerolopez (Spanish)', u'"Donkade" https://launchpad.net/~donkade (Dutch)', u'Fabian Riechsteiner https://launchpad.net/~ruffy91-gmail (German)', u'Federico Vera https://launchpad.net/~fedevera (Spanish)', u'Fernando Muñoz https://launchpad.net/~munozferna (Spanish)', u'"fgp" https://launchpad.net/~komakino (Spanish)', u'Gonzalo Testa https://launchpad.net/~gonzalogtesta (Spanish)', u'Hiroshi Tagawa https://launchpad.net/~kuponuga (Japanese)', u'Javier Acuña Ditzel https://launchpad.net/~santoposmoderno (Spanish)', u'James Maloy https://launchpad.net/~jamesmaloy (Spanish)', u'John Y. Wu https://launchpad.net/~johnwuy (Traditional Chinese, Spanish)', u'"kentxchang" https://launchpad.net/~kentxchang (Traditional Chinese)', u'"Kuvaly" https://launchpad.net/~kuvaly (Czech)', u'"Lauren" https://launchpad.net/~lewakefi (French)', u'Lorenzo Baracchi https://launchpad.net/~baracchi-lorenzo (Italian)', u'Lukáš Machyán https://launchpad.net/~phobulos (Czech)', u'Marcel Schmücker https://launchpad.net/~versus666 (German)', u'"melvinor" https://launchpad.net/~aka-melv (Russian)', u'Medina https://launchpad.net/~medina-colpaca (Spanish)', u'Miguel Anxo Bouzada https://launchpad.net/~mbouzada/ (Galician)', u'Milan Jensen https://launchpad.net/~milanjansen (Dutch)', u'"MixCool" https://launchpad.net/~mixcool (German)', u'"nafergo" https://launchpad.net/~nafergo (Portuguese)', u'Nkolay Parukhin https://launchpad.net/~parukhin (Russian)', u'"Pallas" https://launchpad.net/~v-launchpad-geekin-de (German)', u"Papazu https://launchpad.net/~pavel-z (Russian)", u'"pmkvodka" https://launchpad.net/~jazon23 (French)', u'"pygmee" https://launchpad.net/~pygmee (French)', u'"Rarulis" https://launchpad.net/~rarulis (French)', u'Roberto Bondi https://launchpad.net/~bondi (Italian)', u'"RodriT" https://launchpad.net/~rodri316 (Spanish)', u'Sergey Sedov https://launchpad.net/~serg-sedov (Russian)', u'Sérgio Marques https://launchpad.net/~sergio+marques (Portuguese)', u'Simon Junga https://launchpad.net/~simonthechipmunk (German)', u'"SimonimNetz" https://launchpad.net/~s-marquardt (German)', u'Steven Sproat https://launchpad.net/~sproaty (Welsh, misc.)', u'"Tobberoth" https://launchpad.net/~tobberoth (Japanese)', u'Tobias Baldauf https://launchpad.net/~technopagan (German)', u'"tjalling" https://launchpad.net/~tjalling-taikie (Dutch)', u'"ucnj" https://launchpad.net/~ucn (German)', u'"VonlisT" https://launchpad.net/~hengartt (Spanish)', u'Will https://launchpad.net/~willbickerstaff (UK English)', u'Wouter van Dijke https://launchpad.net/~woutervandijke (Dutch)'] def find_transparent(): """Has to be called by the GUI""" global transparent transparent = transparent_supported() whyteboard-0.41.1/whyteboard/misc/undo.py0000777000175000017500000000423011436276303017440 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2009, 2010 by Steven Sproat # # GNU General Public Licence (GPL) # # Whyteboard 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 3 of the License, or (at your option) any later # version. # Whyteboard 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 # Whyteboard; if not, write to the Free Software Foundation, Inc., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA """ Classes representing undo actions that can be performed """ import os import wx from whyteboard.lib import pub _ = wx.GetTranslation #---------------------------------------------------------------------- class BaseUndo(object): """ The base undo class. """ def __init__(self, data): self.data = data def undo(self): pass def redo(self): pass class ShapeUndo(BaseUndo): """ An action that's performed on a shape, such as change position/size/colour """ def undo(self): self.data.undo() def redo(self): self.data.redo() class ClearSheetUndo(BaseUndo): """ Restore all shapes after clearing a given a sheet """ def undo(self): self.data.undo() def redo(self): self.data.redo() class ClearAllSheetUndo(BaseUndo): """ Restores all shapes to each canvas after clearing all sheets """ def undo(self): self.data.undo() def redo(self): self.data.redo() class CanvasResizeUndo(BaseUndo): """ Restores the canvas to its previous size """ def undo(self): self.data.undo() def redo(self): self.data.redo() class CanvasRenameUndo(BaseUndo): """ Undo renaming a canvas """ def undo(self): self.data.undo() def redo(self): self.data.redo()whyteboard-0.41.1/whyteboard/misc/__init__.py0000777000175000017500000000127111443222121020217 0ustar stevesteve#!/usr/bin/env python # -*- coding: utf-8 -*- from functions import (bitmap_button, check_clipboard, convert_quality, create_colour_bitmap, download_help_files, extract_tar, file_dialog, fix_std_sizer_tab_order, format_bytes, get_clipboard, get_home_dir, get_image_path, get_path, get_time, get_version_int, get_wx_image_type, help_file_path, is_exe, is_save_file, load_image, make_filename, new_instance, open_url, set_clipboard, show_dialog, transparent_supported, version_is_greater) from utility import Utility import meta whyteboard-0.41.1/images/tools/select.png0000777000175000017500000000177510416324745017424 0ustar stevesteve‰PNG  IHDR‰ sRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs  ÒÝ~ütIMEÖ %{¯SIDAT8Oc` °›(¡Œ8%fåúz‘S/ɸ6§Ÿ*õT]£ø™7R¦žùo“±ð#¯q©Ù†ŠÚ×X¥.¼š6ýìÿ°¶½ÿA´cβ÷ÒÎÞd*ãڼƧþЗ¢5ÿÝËÖÿwÈ_ù? ùЯö;d¨èÕî-áÖQ/íÚôÀ»fÛe¿®Ï¢ö-ò^í DÈoWc¨à?uš’w'<¬¼:v‡´þ¯Òÿf‚w»¾‚ÿôi"65>¸ ×Ê2°L[xÕ§áðqûÚ¸fÏö} Oý× ð (Æç·(t*Ý Žgl†¾è†2ò[”ÇN¿6í8̤](zwa%ŸÎÉŽüטôSÖ½íH  W–ÿOŸqþ¿]ö²çV~ ÿÁ–1ðªè†Mº–2å 86AàS³ýHÛÁÿ — Ë›}úêÄÃÿ'þÖ~ø¿oÝ.°º ¦]ÿÓ§Ÿÿo™2ÿ·q¹Ä¥ZõlÒ.MA6 >¥“(6A Üÿ\;lÂsÝðI?S&ùo–0ë/Pì…fÈ„ç*þÝï,RüO¦O÷Òu?d=Ú ÄõйQ¼ÎkVæí”·âu@Ó¡ÿ"6U= ’i\ ®Ürî­‡“&FJß Qñn^³’8—Òmÿýª¶~¶­ËÆ1Òî-Þ*¡³*x4ÇÃ#Å«}OXÇ‘ÿZ!à±,çÙj­2óÐ2܆¡Û"íZa.äØŒ k¾u;ÿ«øu½ç5-Jwo…’z™– °1'(ž€˜MÊ©f»SÉŽÿ Øt+[÷f.eÛþË{4?Ê‹±0T-0hÀzᆂ¢›ˆA* Ä @¬Î§æ_î”»ì}0i€b3uÚ™ÿ^eëÿŠ™ÄOʱKB eÇf PPªÐH›±È8ÔÙ¤ÏÿJonE«þój†ÍŠÛ±%k±ƒô±1$Bȹ [@Þ•bU Öb].ß2³¤ÙïÅÌS@.3b¨¼8ù Ż膂¼ GA VœáA@ûlPA]r3²¸Ø §Ãb¤¦ $bƒhï!“0/ÞéIEND®B`‚whyteboard-0.41.1/images/tools/eraser.png0000777000175000017500000000205110416322241017377 0ustar stevesteve‰PNG  IHDR‰ sRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs  ÒÝ~ütIMEÖ  OžØïIDAT8OÕ”mLSWǤV×Ëh!@*ÆeqÆ/âPc´S,‰ÙbŒ~HéL–,ñS4uÁ¸*Q°Àj¡ämSð…ÛÔÌį&[é½½ÚÒw‹‹ŽÿžÓVƒ:ü¸Ä“ürî˹¿<ÿûœ{{/G@§K—[9ÉØúhžÞÍØgaÆ d­öSëŠóÿ3hC~~JD¥Êö,]ZܱÝéý²ü¹»¬ä…§t÷Œ§´Þ²8SD˜./ÃÔÚ5Ì̯gÖ­[ð¦tžqÉe`Ù²oBû÷JþÁ^H÷G ÙnA%Fáä B¦Yþù&ÆØ<} OWæyü9yoUV(6Gví|ºýìwÄŸÖfØ;€½£ãk N3DScLéí Åð/ bB06ï¥9°|yÎÔA¼oÃ_$°w™bÂq;¬œˆ$äÇâ­^DëÏz=ìÍæ×* ¤¦Â;wØ=wn@¼Ù;=ÈE."ʨɤkM{à5·ÀO‘Ÿ¾ Ul{%¤*“ü[ Ž¿hn„slöö«q¯b¶¨½N³r§ NŠ;il€¿æä¹A¡("am+¦#'àÇœlBK,JˆxÌ9GÝINÀÿo\þ^‡GãkøÚÿgü #~'jµçÌIEND®B`‚whyteboard-0.41.1/images/tools/arrow.png0000777000175000017500000000054411300535414017256 0ustar stevesteve‰PNG  IHDR‰ gAMA± üatEXtSoftwarePaint.NET v3.36©çâ%÷IDAT8OÝÑ!kBaÆñ«Á) ‘14MmD¬.™VüºbPü ‚…­ l&³AM‚ ÉdSШŲ!‚Ê‚èÿÀû¼çbð…çráy¸ç½–u'ÌRI§ PÔÅ 9xµÅO|ág4‘Ò–J>‹¡)1KxпPPÃ'´Ð–J>ƒùÚ©¼xGE dò¨`‚)\b‡­MäÖyÂ|¢aCL=³²ü}Ë·™ò| ɧ±Àruª%=†ÜÜ£GÓ"Ü7«~35e~ÂmSÖa5eÏ„LوєI6† æˆkË$ïÃ^(»}Çu¸D¹Œ‹ËÀIEND®B`‚whyteboard-0.41.1/images/tools/circle.png0000777000175000017500000000201011321477215017362 0ustar stevesteve‰PNG  IHDR‰ sRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÚ&%ÃÕˆIDAT8ËÕ”mh[UÇÿçÞ›Üä&qe6E²Ö¾Œjé¤LÇêDñÃ6u?úu£ aøÛ*ÅJéˆMÓ© "~TtSŠRÅôejâºUZp¡­nI“&é}˽ç<~HoÖÖ‚ˆŸ|àááÎóãþ<çÿƒƒƒ›ê¿Žx<þŸÎ±‹D2ŽþÓ€Óý§"Z(p;c,ì:nPñ)ÖLÝ*ŒŽŒ• 1Gÿ™ícoŒâÔ gp䉃¬§ûÞÞÎÎΧ‚±n×qŧÀ® …‰¹¹¹÷usmú­7ßq½¾M‘HÔä ¿Ižyö£óüžÉ¤iuµ@Žc“.9N•ÊåUºzí }üÉù›c¯'^:;­õ¿öw/ž9vBJŽô}vñÓj¡'Ë2©TZ¥bqE¬¬ä¨XÌ‹R©H¦iP©T¤/¿úÜMž8ÙwRÙÈ‘ÇÇÇGŸŸˆsÆùý~4440EQðãO?TæVÅø†“÷Þòw3“8°ÿ!?Ñ×лãxWW×s{ºïiÅb5ȲÎ,ËÀŸ7nbvö×\:ýËÛ×~›ï‡²Û~ÍÍ»°¸¸¤ˆ>räðƒ¸ÿ©ÖÖ¶ý‘p¸C–e&¸@e­’½ž½žžšºt1õÍDªêTÿ…Bkº®óm¿/EQ$×ueÒ¾Þ½;[šïŒ†4m€[×òÒòRaæûËy\Q×u]ŽÿUü‰FŸCˆÍ¼IEND®B`‚whyteboard-0.41.1/images/tools/text.png0000777000175000017500000000174410341356164017122 0ustar stevesteve‰PNG  IHDR‰ sRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarewww.inkscape.org›î<=IDAT8O•”ËOQÆé‹G¾(M }Q@[’RZc©©¦¥6Ä0A qAm€¤ ÃF]Œa!‰ ê ý ‰1.5Ѹc©;LÔHHÇïL:¦1…À$¿Ì̽÷|óÝsÎ\IÕñ×°R©|,R©ôߪÃÃÃb6›Ý*‹Nˆ­8µ>==M›››´±±AëëëÂs*•¢ÚÚÚ]DhÎ"hÒëõ»™L†VVV(‹ÏÍÍÑÒÒõööîClæ,‚·‚Á`vmmfff(ÒÔÔMLLP,£‘‘‚Ø3 ;¨²±±ñy2™ÜMNN’Ïç+:μßï/ŽÓÐÐoû3ÄN#èøºººJ‰D‚à”Z[[ÿÈåòÂðð0ŽŽÒàà ±ËÕÓfÂá0-//S  ÇCuuußøÞl6âc·d2™H&“½Æø¹“DÍíííowcccd³Ùr| XCCï¾¾>êîî&ˆó‡~bV ×Qœ<»EëëëÙeâA)¦Yh~~ž L===d0Ž z~—8hii)ðv;::„m755qqv ¨+…{ýGnbå…Ä=ÇM‰D¸¹¹/¹Ê(žàÒh4rZ²) ÀQ-ã…mmmÔÜÜLp$ôœ¿ó8„òâ¶ñ# å,Zƒ¸ÉsÓò—Y )ØÃÜKðl—ÁïoPÀ¼øq.ÖÁ¸íØ…BdµZ‡Z­–ý RåJc~kGâ¶9ŸèÉ"æ®UAùÚ#Çù²ÛíB²ÑÀG˜Œ—‚ù0+Úå;c ŠSDq^UaïïмBµ Ä?=Wí€ \ƒeôâÙ¶ Jp*€Âp¿± '¸ "€û)Yºq‚›à¸[ºó{Ü)p»´ŽwWJ“ƒZÀÍ|\—Áà|dFЄʊ×_“á8祺iËIEND®B`‚whyteboard-0.41.1/images/tools/highlighter.png0000777000175000017500000000237411326357537020445 0ustar stevesteve‰PNG  IHDRÄ´l;sRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÚ/MØ]|IDAT8ËÅ”kˆ”eÇÿçyÞwfggo¾;3Î^f]]u+­,u%Ã( j¥(¼Ô‡ ²BP+‚R‚èC~LºI v@ûA)[‹nÛÅ{®«ë:ÎíÛ;;×wÞË郩ˆ¡V:p>žÿsþ‡CøQ®U•JbyÃr3—K­Ío]~ÿjš>Õ™xJªò„×ë¾^N}ÓÈÌ"6Á”Î; [1g@ ãý‚ôvÕXßÜbª”×3µ©o}kpðX¬ˆsôO•¦/Œ,ñ4©»áŒõøÛ[a™1¨Åƒ°ôÐ+ó xØÞ9ûÁ-Ê Ušq·ó/ÅŽªžÒ»ujºlRÀ¥q¨Æ~Pá'xY¢«x OoÐÂn&o'˜Ù_+Œ fRCûýÎj¿Ïà"›µãäMJc`éŧpqb&´ÐýöT¥¡¨Ü¬õ¼~t‡*++ýÅ€×gƒê¬ÜRËÀ– a9¨Ÿs8»“Ìù›ÑÛ»xS¹eÖ‘ëÀÙòEjìâ'¦ÉSû6y²O{Ô Èë²S‹’šø¢~¼†‰êç«ËH.x-;mÎí•Â=¿v¹ D£QD"|½ç´û»¹–Ÿ§×ïî˜åÎ$Õ‚Õ8Djz' …¼àXÅÓÄäJ–K·˜ÁÎÞ Z08 úDâêºE£Ñp$Iž<6üRH‹mkïhn‘®€ãÄÙÎÿHª1‡ Øn;<ã:ªc.Ov¼HÍ 7êhn¿«',³¤´Ù¹œMk» Îf³ÊÈÈÈŒ·¶n_óÄV­½ =avbÈ dIŸÎ#oÇ…¯"tßʯZZ7ÚC)pJIȦð%ã —ËAÓ4|ðþ‡Ïâ~¾ë³=Ô%%ž]£ãá$¼` Pƒ=nr.¤Ô[-máª÷"ÝW “K! M¿Æ+áš)hš˜˜<µª6ÍïïÇáØ¬ÛœÀÇ»üÈë *GmÄ’J-ýÝ«ßèî*¼¹}ÛpôŠbøhçÎp.•ø2/ÿáûo©ìZèíž‹jRÇŠÙ.V>2áE›ÐÖ·`T ´.ù»ÕÔu¡P ”yªÓZ:zìÐÛûOOöþr½}}˜3k6| ÓШ5Á©pïY'†ƒÒéXêöa&Ê !*ÊRÊx04.UáRéQ³ZÙqwð ¶è{^DA(*BÔQ´1)‚]Ú4›t³µ¤©Ué‹FQJ­&lTZ ‘ˆH|Pû“ín¶ÉÚìììÇÌlvî½Ç‡ÝÙlŠŠúÐøg¸ó»ÿÿ½s.p/c4vr{=:üÿ@‘Hç>œôím:6ØõÚ«/k0~:úß`±Ø‰Æ¸÷ÈáÝ\xïøÅKŸ—ÏNžîí;òú.8'Äã‘aq|Ëb_ÿúÙTbñ矨Zuiaaž>žúhª¯ÿÍÇü9#§FþÞbüݱF=ôÎÛ/ÎÌ\þ=“I“ëÚT(˜rsÓ¡dr™fg§¯ íi8Š!‰4[Ü*^êy!t*62zåê79Ë2©\.’inP.wGšæ•ËE² &Í?gÅâÑñžWzð¿®ó_ õwì~°ãŸ<°¯ã¡ûˆ$<Ï#`Œ1""ÆÀEƒª*¸½–©ÌÍÍ-\»~ý­ØX|Ñç¨püı¶={öýðôSÏ<ÚÞÞ®{ž!ÕdŒ10Ƙ”Î9 !XkëýZggç^Û±ŸíÚÿÈôÕ+ßm€RS EKËŽüÎÖŒ{œ¤”> Œ1¤”~2Î9¹®K#ÀB-¡"ˆ<_¡V©d»•Ê'É•½YU&„€”Ò¯™®il}} e»|Ù,˜ÙmÀøØ™ÍÕõÌ·7–nÂ0@D´MYsMDdH¯¦±²²¦zÊzÞ›øÃÿ ¥pòÝ IEND®B`‚whyteboard-0.41.1/images/tools/line.png0000777000175000017500000000033511266462145017064 0ustar stevesteve‰PNG  IHDR‰ gAMA± üa pHYs  ÒÝ~ütIMEÙ * ±üuRtEXtCommentCreated with GIMPWGIDAT8Oc`£!08B èŒx Ž¡ÇõG€¼tˆ?ñ{ 1ÈŒ§ ëx÷PˆûúÛ@2QŽˆuň¢¯$ÁÛàL›IEND®B`‚whyteboard-0.41.1/images/tools/flood.png0000777000175000017500000000253310340454027017232 0ustar stevesteve‰PNG  IHDR‰ sRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarewww.inkscape.org›î<´IDAT8O•” LSgÇ ¨Ì –Œi‚fÉÈÊØd<nR©@/ú¼mÇ@¡/LZ@‚‰¾¾¾ošï5ï25uÞ0+ Pú)f&ÕöïÚw¤§§oì³i&öàB^&ŽóäL¦§ÐÇÇ‹.qãBÞÏ|ûä+Á1FÛÅåsZ1_ì침ˆÆðˆ/ïË»ù"þu¾€ŸòÊ@GÇ÷B|¦àZž±óQ'T×T͸œpaðD<‰@ÄÏØ1ÐÚÚz¿§'=2-]ÖÏŸk`qqFFžAJêÅj?æw¸OÞ Êy+>Í:“/ã„\“ #oÉðÈod>iqÒ¸ÆÚÚQ€4Q(:¥R ÐÜÒ¬gqügù|nì6`HlÉå„Ü{+Ù•OáRIˆ“ª ("n•VÂð³!@ÏÜ?s”Cww7ôõõQŽ@FfÐèŸÄl†J+T§ ´E _¥)6dYÐÞþçÆòÒ" 3P[W§ƒ¡¼¢œ„uvv‚vB ÍÍMÀÅÙYÐl &­ÐýÞÿrË€4Aê‘AXXX€ÙÙYX[[ƒ¦ûM}> $Ä^l\ -ƒÞÞ^èîéññq¸~£pŒJ¥ºn‰µ¿ý½ U ô\º²«c}ff†††`ii ró²!1)!;ç*ÈÒSappˆŒR£ÑºvÀùد‹  %}Kp³a˜ÂDýÍ¢üñééiR£ÉÉI¨ª¾CêWP˜““ ´ì{r¯‡ˆAÑárE±‘îíîEàL¶€ŠŸÔÀ Îû10•ÚúG+Ìêgaxx˜„Ößý²®fÂíÊ 2*µZMjÙÕÕð”ø¾”šÔ`iiù%TR¡kí_†oÑ/LQbûŽ5//W…ôAŽccc°¼¼Lê‰$Ðëõä’U½++/ óJ†ñࡃv”0éí©æ'ó ¸7 üpy’é‡ÉüÚ²†RGŽ(­õõuX]]…¹¹9Ðjµ R©È¢ ÿÞ'½p!.¦ÝÆÆÆš"–”Õe—t‚4§ „ò$tpv°K¿œ¦Ô“RDÎóóód”hR  ×é´p·¡Þu¶Þå¸ËQ²(xd±7¤0'. Wì{Y~šÇ§Ñ÷›› :ލê á¬#{r³7W¡ía\ÍÎj£Ói!„ÏÞÿgZ0í5‹eéêêz˜h‡¿FG7uÔju$ Íq±¼húŒ8(Ïö=Çÿ Ô=Ø\.ã€?îmËåzèï}à‰Ñ¯Èòyü¦¦¦ˆ¦.Ý$Æ·ºŸt‹swwváp<ŽlÚ3 ''§Ýäfk.cûqÜë0÷qàà 7>;åËb°>û\T“’–¼”’š¬æâ¬'èÇCüØØ)éÁá0ܽ?á‡ã¸º7_FlBÜÊf†™³¾`Yúøíãr±·ÙlÆ!ƒþѱcÎ>Îõd0<ì9"ìƒXg ˜ÖÞ¸·ágA%2”Éd¦ö/å“é¬ãÖÈëIEND®B`‚whyteboard-0.41.1/images/tools/pen.png0000777000175000017500000000151110342201072016674 0ustar stevesteve‰PNG  IHDR‰ sRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarewww.inkscape.org›î<¢IDAT8O¥”_HSQÇÏl7]V¶0‡bj:¶ér3ÿ¤ef=lÌòŠºi¤5PK3šSçü³tË?ØÄÙV>E,˜hÄŠð!{lMèÁ'É ³0ðAKŠoçBAÔÓ]>ø{>çË9÷ù¿&¬ÍfÊ„aaiÛ?Âîxæ·Þ‡¬“^—-4·Fl]=þ®&—Q„,;#!‘z5cf~wWÄ¢>+M&Q! sU©™¡Aö^;Šà¬LÁ9) IV@ˆ°"ƒé½u> o à7å ¾(úNFd! +UÂæVmÂöœEyKle©0d†÷”ÊÈNÞBýQÆÐT·î¿¡FÀ” gµ-gã— éäoYU&Ñ6œŽYò6ª±`NÀDMºY9.Û9ª/ ¼„Õé$ÿÊñ¨ »N‰ÅÎd<ÔGÃʦ¡]»¬Ï żdVS}aGÓÅûu/LJø/íC›Š~ö0Œ9;œ¼÷®ÛÖ9>ýxþ§O0æ0Ã^“…Á*®Œ|yAI’x¥këj“ V>¬àãÚMNaØÞkù‘/µY!üwf‹©ufæ9¶67àõzá¹çB¥ÉÅ+÷1D¹=cÏÖ×?cuuÓSS¸?áÝas[œÎÝ|…{ró²{­]Á`³³³¿ëBËh4ò–Å©T*7˲_µZ-JJJÐÞAãvߨÃÑÉ7Ù~:áF£ÏçCCC#‘®TÌétº|eÜ]´ (  p ¥R)hm„ŽÅSb(œ˜[x/eEHü½W¬£lsB†a ‹!‘H ‰¾ÑúMJ>%’I‘SR(q1埇¡œ)”O”eÊ+*÷ѾšÂ=M©¿$¿“r".!C û3áOÓŠüÉ‚7ÁíIEND®B`‚whyteboard-0.41.1/images/tools/rectangle.png0000777000175000017500000000114210341142620020060 0ustar stevesteve‰PNG  IHDR‰ sRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<tEXtSoftwarewww.inkscape.org›î<»IDAT8Oµ”]K†»ê¦û~Bý!pöa™–†ta(† ‚àEdawK+WôJ­è»ûRó[ë¦ÔmÎv#Fšzàe Ï9쬧§EQT¯Óé0´ m Á~‹Å¢ ÉÕ>™ÂÖ7Ö>öƒØ;ð Ùm2~ðílÇãVÊ€½fè€ ÀWÕš Ïsb÷íÝØV–Õ2 މV{{+6•××ð|.¯/Àî°m‹Å<Ôj 9¨VY¸ê„!Ž›Ïg Ó>GE»l6 ,Û&P‚¡]:†©üÏG¬‡¡]*õ"Ë­Á2™$“  éõ°\.#î ah—HÄ R)5o(Áp_ ÷†0´‹Åž \þøˆ»Â H2BBÐ*aÑè#”Jᅢ‡ß·Ìó5á 8à8V ~À,K‹aZÜŽ‹uÿp6Û[¶:¬ƒÛ¾Mˆœ…„œB8ra| Á÷†9ÇÞSàv»T²[&I²Ïåv)—Ì„yÑDÌÍÏŽfõZfztœ0©Ôà #cª¹‰©1ÃŒQ7i2G±cwØ ¯×Ûßÿtw˜ŸüòèÿÓâIEND®B`‚whyteboard-0.41.1/images/tools/rounded-rect.png0000777000175000017500000000110311322277101020507 0ustar stevesteve‰PNG  IHDR‰ sRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÚ  ϼߒÃIDAT8Ë­”ËnÔ0†ÿßNjwCAʰ¢PnË ª¾BªØ÷=ØTêñ*U/o€ê ÄØ0¬fƒ„„㌻pœÌСʈqäø$r¾sùÃ#V9V<Šlœž4Mób鈴>UÕÛçÏöŒ1âôìøÀ‰=~ôdãÁöÃÒ3öíûW¹¸øøÇXû:C4MórkóîÆ¨º]Šxñƒ€÷ïm›§ëM¯®ÔðÖ›¥wÞ œ¼ݼ³¥ÖЉÈ4¨Ö^+Ëke9¿¡zqP`º€ @ÓGË«,âA &“ÉMQË¥%™›Áé<E¡‡½9ª¨Úô³/ ­õÀ½Ÿöâtw¦÷Ö(¥—H¹$ÁØ&Ì,VZ5PJ] u£ëkǘ”§b'œµ6íùê‘¶†½8ü[¨éºA¨C½¨ È4ÀÔuÝ×ìlfÅÙGIEŒŽ›+@­õ¹ZS»ÀÓ“~;vg³Rw©ŽÇ?Üç/Ÿ‚µödXUÕ›Éd‚P„ýÞWD ‡Ú9üv~-VÕZ{2ªªÃüÌܱßx÷_u®®r\ó_àò÷æIEND®B`‚whyteboard-0.41.1/images/tools/zoom.png0000777000175000017500000000256410423206621017114 0ustar stevesteve‰PNG  IHDR‰ sRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs  d_‘tIMEÖ :&NÏÊIDAT8O}” Le€a² LœL‰ÑL³lº¢Æe!S—-ºâbP~:Øä·¸-dáǶ®¥£XËíõ®Úñ³ŽÒÚ^ ý½^[ ¶ü‹ŠfÓ0ul,8v~‡ÌЈ^òä}¿»ïžïýîî½Ð A[VVîíØ¾=1"üé·†…í L®þ¼²òhðþƒß !«¸¸\xiã}›ætî¹Ê*Ná¥z‘O®TÎuê{pÏàC÷ÀÐ#“•¸{åjçM "ž«ªæ«Ê*ÊÞý_á9 ã ¸hß3Z‰³gl^‰ùú߉è¸Ü0êÖØ‡wô'E›j±V$˜âUAGþSÊãCEb©d ³{¿o7ßhÐŽÚá.¿îò¨èwˆµ~»L ÄØT[‡æ>_ÀwBUЮIÙìâ}Ô6M6×Âß2Ÿ éöá°Î‡#z?ŽêükcI—ߎ)Føf[Um‹·´$ÍÝ$­pKäÊ–9Kßøƒd°ŠéAÂz±Èg°wR9Xû±"=šÄéiJØ™B§2¯Þ£Ì{”¹ nEvÑL«¶7&–ÑcEi£v`@s½ãÏÒÒbZ°VT…´ªµs"•—x¯ [Ï4 ŸqzÑ/.Ø‘”*'’^ëBNRkp4…oG?fá‰[¥6¹fňx¸ŒËü HU0âerdÞ⽕\amþ„Õ'óm²“²¬ú>Y®Ä+ËmðÊr@žVíD>åX$˜7ö˜–Ë+Ø Ú$,,,ÜVs‘¯ºªÖ,^³ú†ÒxÓqÈ"¦ªÊ²3èìké š.ꃓ„ä’Ö‡a6Ï0" q›v Õ›BQÍ´¦[ÿ@‹¾¨}Ýg`¯‚Vçg\¾qù¬låvTíöIÜh÷üØÚÞB²JK<‡ŽOHHˆØTÊå2ò<§Zsí7³Ãý‹Ú22¢uÍôu{oºµÄäæœí1÷.Èd„Å.q²Ë™$è’–žŠaä¦R¯t˜Ì¬Ö@wpÜBö{ R£éx #ð›Ã”ÖÖòŽÄ½࣢âówA$Ï!óÏÒ;6JCÁ` àIO†ggÓârèY§òò3‹èô,FfÎé‚´´¤ä˜·böƒy±±±;+òè9ä~Ū¬IJfçeukÛ¨J)aàÀó€Që9©ñ«€7Ôð àõèW¢ß¦e¤õ2Y%$¯’KÊ(™GÏÔoR—ú^Zìq/€jþwÔ=´€¸;rGd\Ò—Ÿ»Xl‰È%äWY§VŸŸI8àÙõÊ^ñeÀNÀk€]€=€7Ö#•S FGEEíO¡%Ogf,Å:xþ/†¼°(ÐÇ‹IEND®B`‚whyteboard-0.41.1/images/tools/eyedrop.png0000777000175000017500000000170710365003111017567 0ustar stevesteve‰PNG  IHDR‰ sRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs  d_‘tIMEÖ 0½¨{IDAT8O•”mHSaÇ7çÛœºJs¥VÒ|Á¹7ïµm*¥YA«†VŽ,£gj¾ä2µDsš†Nµ°$"…FB?õB¢†eúRY– )šœž£WØŒˆ{áÇsïîÙÿ9ÏÃårX^"‘H ôõ5ó½}RxœùŸŸ_~zÿθ¸¸ø¥j­<céÓIBWMPbÝ%Ä)b—˜V0øŠPlס›=ß ¥]s Ú€Õ 3ðœqöíw<üc-ùšÅ1Ã2!Íð@Aáª{¨ê‹ßÚ¤>qMU!(ˆ^èî?âè§_¹ç ~þá;ûÇ|ùÅ6+Ë}L˜QUÊŒW¸¼­CÕˆõ³¬Áæ’‚3G»ºÈÅ'0Ö@}­ÇöÆ2ý^Ž÷ G'°¸`X¬0‹”¾ ‚ˆãÍ óQÃXÿæ.ïç Yî2ÒÌsq9›(ßßkÙºé¸ûy@bj”`nÈÿÆÀ0Ò4°µ¹L„ºRÚÆ8<êØ{9x¡d©P‚`˜ÕÄ z`nNyË3¼ïj¦<ø³#FøûyËÆz`gÛs}Õ“ˆ1â¬ÁbK¡À,`&ÿ½ç5ãð0Õ¸œ*]'¬=·7߸ÈV–ZT‚ø‹)"&Ìjãå‘r|ªì¿ˆ”¥£?p,£®aï òÏK¥ª ª®'‚—†66çR„Œó åt'“ÈÕT±:öÜÙpôûðtߨ})3X[õlÜ®fF]e!xס±&ø€Ë8(÷w#7o8¶ny¼Ú=Ž<~®4ñá»ÑÈ1½2NŽÑ¢Ñ+…ÄGbSD\ÈÈrö…kŽÍuO¯~ù½åøÔˆ\9Æ+ŽA_¸œ*O÷•AOp~îŠà•®À£–±²àøì#Ïp(M e!¬`aà‰gpð2’¥ÂÒ‚°µîéJHŒØÍ"Žh)Iâ( Á˜àΦ§W‡GÊî£ÈƺçàÄxï¶'Ï„¨`&ˆƒà®"‚H†!tÑ£W8ÄCÓ Ã¾Ã ¬Ž„ñ²Ã‰UßøÖˆ@)Ães±y5ȪfH„<7ÞYÖFŽ€F#b†m£LN…Ó)0qˆG¤xãÉy /ÏSÔtJ3UaV ç—žW³ŒºíãÒåð†Bn.ñàThãuu uëic )¥D+0J\Ò!eÈJïãÑ"Y‚Þ1©ªlé¯'}N&S4æø´ ¤%!鑤%iV’öKzyI’ä„’¦E‘’¥o+ODª_AêÓbIDAT8O…” PW€Ÿµâ­µRTl)‚U)•Œ”#°› ˆ%@Â!"A­FÔâX°("BPà R4!¢ä‘£€ÙÐbU±ã`ÝfcËÐÛ¾™oæÍûg¾÷oÞ,À,;:ÝÈàùóÙëÉ%ÖÛ &Á¤îøÍš=Eþñ°^ù€ 06öûóæææ©éàÛ64í]6†m¡Óéî;wE1sJ®Fd*/‡Qd•†'gd (Bg¢ãø’ÿ”QA?Ëãz GvŠ%q™ÊòâŒüKz K “dYrʉâàx ¶±†ß³³ Æÿ*†áà< Žï—ñC#ËÄ' šEñ¹ ¹M¡GS<}|2…‚ 9jç–·ñî¥{‰Êp™Ó†·J}w/ôð.xs½zƒÄ{º kµñÙ•EB^5q$U©aãx³/ Þ™gï&U965’)¤¬)x0®H –཈˜FÞ<ß<.’Hõ—Ö>Ð**[Š¢­Ús •ÖO èäû îºù| G”|Ù¤x@Ê'2½ÛwêdPÉÿÂvZÊd2PW¸1 4&fpìÙÑO<Ñ3øËQ×ÔFxâx‚rÚ!ž#"̲ª¯C÷7®<ýh3)ÿÍ‹<Õˆvˆrì}Ì”Ó 1oL9œ_¦".*ËôÈ‹¯)ç/jtã>þíÌ@gM3®åÛò—ÎS™u7jCLú’‡¯Á?y³7Ñ u=R d€¾»kï…;Ú¨45¡GV£¯$˜^Þw8·…è„aé©\w¤JtŽýÀ:è²YNjF&Öâ­!i›L%äà ›A%Ò¡ôê1íÕA‘r}H7˜ÛÅQ”ÛÆ¸°PÙâjq¹µèïž­ôïÇT¸\‘–;vJ:¡>CGÁn}»¤17»ŸŒ©Z5EUçc"·¢AcˆšåÉnuåÙáÌdCuH™Å´‹•3vo˜!Ô•ììî:²/:¦üÅkíàø3bøÑ ¢G3Nä¨Ï?Þ#C^ì—#OC³¿R³ÏõñKŒÃg>•0…p0§gd¼³½w´¢óg¢ýf}‹ò`+è F¡Óv¿µÛš˜¯ô|ÐLÊȪ—GÈŒ§0ytØ’ôÊ5¬ü,,ëaxÄ`‰h;vì‰$ªº¦„òêÛÇ)*n6Ï)©ˆv¢»tƒÑ,:Ç‚Æ7[$<é‘·ñ*?÷“ϬýL™Aö¶¨Ù®`=˜¬¬¬ \!ÈiË{çÀ¨XøLIã·IEõÑz”·cOdWŠé,O77؉É\þWç‚Àr9 6¯dêHï8::š˜›››H$q¦…5}¶ÙU=6…u6YªŽ5› ÈÔÖÖvÏŸ÷?öŸSL£°>ç†õIEND®B`‚whyteboard-0.41.1/images/tools/select-rectangular.png0000777000175000017500000000166010423206724021714 0ustar stevesteve‰PNG  IHDR‰ sRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs  ÒÝ~ütIMEÖ98eÙ|tEXtCommentid logo†Æw¹ëIDAT8Oå‘IL“Q…Q⌬PWÆÄ¤%Uâ‚T"Cù11qAˆ$˜°qÁ‚UqGYAb˜ÜF#PJQÄ¢Flé<2¹PFfÑÙï}m”Ù»ð%'¯÷ý÷|=÷½„„ÿz¥(ÒRÁõ+’*W))g%M…FJJJ’ªkª¤„ısÍçü]©RŠ~öÑÍ¥Èn¯¤¤(¿µý! ƒèëïEwOF_›ÐÖÖ Ë¸--ͰXÍ¢}cBOO7õ=ƒÁ8ö±_”¤RuGgxE7ÂØØŒ" cskÁÐúï=ºu8ÂzpMôwt¶ƒý2`eå5§b×ï‚ _À oÀEµn˧‡ä¶Ãá±ÁbC(DÏã.°_ÌË»¤Öéñs{ þI/&¦Lù˜ô‰Ú7á!¸?Éç‚Ûë„Íi¡i"hjjûeÀûuµj£Ñ ²ybʃqRª}À8ÌEÀq»…ÆÂ84öË€J¥R­Õj±M }Ï.¥Éb£»¼<ºV»Y$lhЂý2`}}zäÕ0"ш+v‡1æ0‡›îÐ:&fÄôì—³³³/ó?mQBKÀxLz ?cÆ’1Ìá²ÁL¥ öHX}¯ªÐ8d@(¤Ë/éF+ìN+lŽqXº73½.Éúoßbm톞¢¦öîÕ] 3Ëø•y-¯,áë·Ú±ú} KóX¥z~q+«K˜_˜Ãâò¾Ì}ý:99YÅ2`qYÑm½¡¯N—‹Ó3Ó0™Lø4ûÃÃ/0K;×3¦Åwîã~ö•–ß¼µ¸ŠÌ¬ìŒæ‚ÂüGsU]çÒSŸ^»QØwòTŠ^ªÐô''×—Whô\óyzzêÕ…œnî'_ ù3HÌùk£“¤3¤TÒyR)}Ç® ß§ã}Iqž0>ÜOJ$ !Ý¡Ãñ³C´Œ÷qïž°½Òþû³_)vœÔv¡<1Òž=Ú3³Ö7ë[k}{àÿý}ñÀÁ§www?­µfnnîO¿ÿÝNÿO‘>?°|}èðÁŸúíºá5ôÕñ1ýó_üTùê3¿\~vðÙ–íkÝ8öÜÃã|îñÏîñ|ÿùþþþ—9ôƒ_>pô+GŽŠmÛ"hÒ™”Ž¢à{úv÷?Ô³µÿv¥ÒýpïÎÛcW®Þ<6ÀHÓVËô<Ü#O>öø§lÇ9ÙßÿDÏSO>>‡ñ@‡N<­[0øÜ'^=ÉôýW_xáÅc;¶ï ]«U¥\.ÓÙÙI¹\áôéßÐð<â(F<{t€éÙ)•JlèèÀ²m¡\¹2Æ›|óø¯ŸúÚ’íu)åßüú·^~ùÇ?ééÙF…Äq, I§2äóE>¸rÓ4q]‡¶¶ …B0Šèì),Ó¢R½MÇ¢u¦MéééÙgZæäkÇ_¿p¯?s r¹¦a¢-Á4 D)¦gnP,äI§]|ß'Š"¢8"IbþþÎYD‘; Ã\.‹aÀ¯Z -cccôööÒÖÖŽRŠ Ðh¶lÞBGøž‡ç{A@øG£Q§V¯Ç†a`(ƒz£Îää5’$Q«ùZ:ð¼ï¼;D>Ÿgû¶äsy4ùø¾OúaHœDˆ(Òé ét†(ŽY˜Ÿgrê:åR‰$I¼–Ëp÷ž]cŽã|gÿ£û¥Q¯S.——xÅ´,ÇÁ±m,ËÆ4-D aR©T˜Ÿ›gêú „QD:f|bB—J¥G†G§ïÁÁA¢(¾üÁåù‹—.núîKßÃuüÐ'|êu…R ¯I×ðð<?ð—×LÓ _(P¯×9sæ Zëù$IFZ¢àĉ:|]»v±oï#íÛÁq\¶oÛF÷C[ÙØ±×u°,Çr–s¤V©1=3ÍÌì,år‰(ŽÈårˆ•J…–s$Ö´çÚèëë£/`ÙÞ»Àðè0µZ›7oây¦ibš&žç‘Íféêê¤R¹ã¸lÊ·S­T ‚hÍVl®×§‚ã8ds9²Ùvö=²b±H.—gâÚ8³3³tuváçÞ=GGG…Bž‹—.!–eƒ¬¯wëXêJ˲°l‹t*M.Ÿ£X)xÅb‘Z£N:Æu],˃Dë–ÄHÝW¯eñ$"h­Ñ°8¯‚¾çùÖUVµ¼quO\>ª(²ä]ÌDµ—Ë­Wl‹£Öß.ɳRË^DA}D>µäT% š‰µ¼Þ¤FÔêú.ÿ ˆ–¢+J–וRè&PwE`EJŠ`æ@5?I‰Zô ²ètI@”qwr*¹» VØqg±RÖ gUãWÇÑÈ"¿ÜヒÕ¼'4geÓ4±- ×uÑZËØ•«N«(333W¥ÔÂÍ¢(Â4 ß_¡A5# ÑÍü¸S–ibY)7…2 Lm µVÓ7¦‹@ÔÖ`©0 Ýz£ž8Ž«ã$–éÙinÍã¸.ÅbÏo`Z&)'E¶½Rù7l “i# C6oéÂó|üE­ÐQ%Aº@ ðh½ý€ø'a¡»{ëgööíÅ4LÇ‘ 5žß Ö¨b™¶mQ÷T«UlÇ"ˆZklÛѦeÉÔõ)ùõèÈų@£9ôZôº‰ñÉ¡©©ëçÿùþû_LgÒöþýŸ¤{óVq]Û¶1 ƒ$ÑÄqˆˆiZ¤Ü4ÙlAôèè¨*?wþÛ=û·W€JsÄ­üˆˆ8ZëÙÝ·ëÓ™LfïÎ;Ýܵ¹·=›í²-+¯ ÃI’„ <ÏóÊ•JåÆüüÜØoü£Z­ ÿkäP‘†ÖÚ_­·Ò+°§1£Y=jEéæ—%Í9nrpexàŸÓUÞÕbäß:uщãäÐAIEND®B`‚whyteboard-0.41.1/images/icons/move-down-small.png0000644000175000017500000000126511360605440021115 0ustar stevesteve‰PNG  IHDRóÿasRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÚ ;äÁÐ5IDAT8Ë…’MH”Q†Ÿ{çûæGLa A‹ú!, 3£ E!’› [(ZBP.‚hQ(-‚ÑJŠ"P°`2’þD]”º¨0!%²““Nƒ:3ß7§EiêŒö®.çÞóž{%",×±kùŽI'oÖFº¯~WËkÆê¦p$Ȇ.¬d+™ ²"²0 ÀΖS#Ý$¥ÓO‰Ys„¢“LüxÏÉ=—Ó¦ÒéŠË—r¬§´ˆ ’DD0´s]ãÜíƒ2òåí¿‰.7I±±Å‹_±éwe—Ì¥€»·”b”×E8[Ùˆá0Ib‹(´MÔaº5ÚPÔ¿‰Dœî¾NÊ‹kQ"BÓý*q8“ì/:ʧ©lÀ&Ƽ5ËøÏaù»–æÐ¦jFFGð™4×t( ÐXÕ®>N¼ãkð3ù¾mˆŽƒÃ"Çô(œéq°7ÿ0Ó3a‚SS4×t¨¥Gô¸2¹~&ÀãÞ{¸õ²3|ÌI˜˜c§[ãô8(ðn%×ãgàÍkÚNRÁŸW¨ÎW´ð¨÷.›³‹°Ô¦ëÏä¬ÌlvåáEß3*Zñç.ѨV£Üô JtFÿΆÂ1]š’œS|ÃóÑ\ݱåƒùX”ê›ERVz¼a†FÏxéï ëÊ„ú/HW&mu^õ='‡ø’ÛéyÙCk] -H) õd¨]ºopbß*KêUZ×B´²¤^~”ÅóZ}¿ó1Ý_ìgüÊIEND®B`‚whyteboard-0.41.1/images/icons/move-up-small.png0000644000175000017500000000127011360606035020567 0ustar stevesteve‰PNG  IHDRóÿasRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÚ ) ¢¾Z8IDAT8Ë•’OHTq…¿ßû73a”58¢‚Bf+­E…X¶iÑÆ-¬M¶p¡…-¤I᪤1…A…µ ) Ú i¦$¦•èˆVꢓÓ8O7ï¶( s´ºp7÷¾Ë=%"lTÍOÎ @é;j#¶Ñ¢£7$#Sý|œê££7´ñY×á™!©h È£7Ír¿§Q*ž’tZõç örœÓ-ER^zÃ+$SK,,ÄèìæAÝ òy26¡©½Zöäܙ˗è>Eß³m›Ÿœ`.MÏʦtô†äóÜ(‡÷Už}‹®™šIÏäSö3XçÇ*`<2,·ž_¦êè¦c£ˆJbY+*Ž­æy7û’#*h}v‰ñȰ¬ØËqêïVRu¬W%I8Q,…é1‰9ÓX>9g‚yw‚C%e\¼W‰½ÿ hj¯–â ²‹˜MLb˜:¦©“pçXÒ¿aù4,ŸÆèB`&YÁ,®þòCßÌS©ƒ±L&3Ük~4·”»cÛ6Ê rG@(þ`CR ø~ôõõóýñøS93{"ÒÔdåóËw}>DZÿ5@2™ ƒþÓCÃÇQE0"±¿R Ä¢1êaÀT7A¼X þZ[S»ÞØØ(LÓ¬ØTi²§§§˜œ½zõ X¶¢Šƒ3Žû««ÿà¼7—Î|ßZ{›I–v]wifffSGX¥£Ñh‰’8çàŒƒsÆJ(¥ ¥€Wô`;6VV ªˆÑÑwôѱ±ÃÁººïõu¯%‰øÈÈHÍ„Ãa€R–mÁv,ضÇuày„(BH (FkðùjÀÇÂü‚´, œX:cmmmÓ(×*Äb1Üþåˆ4®± ¨´ PåPÔ5™Ì&&Ç]Çq?}öÐ3~ÿ¾…|>oNOOoz2*D£Qº"úk c ÆÀˆ°ÑBœs LNNxwç2?'û϶µ=ù[0ÌöööVLË-Ðu€1c ¬Ü œsXë®]ûÒ»~ýël¸1|æÔ©WB人ºV*­]½†@* ’ Ƥ¸uó¦¼üÉE»¾¾áógÎMÕÖÖæ#‘ˆIDUQU(¥ D±ô‚¦#{ï.^úØU Ÿ¿|ò•ñÎÎÎ,€,=ðåT ÀRš¦C …+W.{éôï·¦ž~sppè€Ue÷¶t]ç~úñFñÛ™oÌx¼åÜûç'n(”‹Wm÷ö ï¾7fûñ_xãõ·.…B¡UlÓîm´¶¶Žêzþ«T*•`Qa' o¨b¸+¥ ”–…rñýÜRJ)])åÛÕ¢{úßéOz*|•é^ÙIEND®B`‚whyteboard-0.41.1/images/icons/stop.png0000777000175000017500000000107310443521756017075 0ustar stevesteve‰PNG  IHDR szzôsBIT|dˆòIDATX…í–íŠÓ@†Ÿ¤IÛ“²¡ ±‹½"¿ðæDAoÈ Xd„íâÝÌyý‘¶Û¢ÛFþ±/ ™!sæ}2çÌ8餓þwE=æœù¯ÀÅ}/“ dOž?þ”$ÉêOÛ¶xÿñÑ¡9GʲÀÛ×ïÆîŽ»÷2OÓ”g/žR–¥®¯¯‡Hâòê3®~çnãÄÇ:;;@ð€Ë‘÷lkÐMü_Ìçs‡nþD¾ž¿‰¿OGS0›Í$IÄQŒï"k¿s7Ô¶Vf³ÙAò£EQèæÛ$â8&ÒþÉÕÆvǦ -ZE1  ®ký¸øÀh4Ú¦B!´wBhiCÀCKœÁ¼!MSêºe™›îâ¶m ¡EÁ [èn´Q<"Ž;HI˜Y– «Åb¡«/—DqDDW$±FÝö«K‚´éwæ¢X,Ãv ª*™M‰"H’1’¯¿ؘéÎtÓG`6¥ªªaeYº™“&)î¾cäû; À̘L&ÃÌLfFAwgPŽÓ]#Z·ýToj ªªa5Ð4Ì IŒ'“mÎ÷„õ±¸3£iša;ç¹Ì¦¼zóòØÔ_d6%ÏóƒGÿ$À¸¯ér¹Ü¯V«Ûº®oúÆŸtÒIÿ\?–ù;:`oIEND®B`‚whyteboard-0.41.1/images/icons/move-bottom.png0000777000175000017500000000235710443521756020366 0ustar stevesteve‰PNG  IHDR szzôsBIT|dˆ¦IDATX…í—]ˆUUÇ{Ÿsï\?‚RqPûì‡˯ÊJKȇÆ@£Dƒ ƒ ‚B{éaÂzP{ìÁ #$•ˆPȯÁ™4t*1§Ô1umrR›fî×9g¯Õù÷:ãÜ›÷C/mج}ö9ç¿~gí}Ö:þoÿq3Õ\4oµÕZ…[7HUÚ~µ‚ï­ú'A”Åi„“¸‹Fñ±‹-6íÜd«Ò­ tyŽ]ØOî ¡ä %Oär„. (Ø|”aÑ=/U+ €­öBU%’§!‘D. t¡ËÆyÂ(‡“pxªi‚Ô|O ñ>Tlã3ñØI4œ *(:âÚXáP•Ø!Jü̘Pàr¥H €ª EaœDäÃtÍΡÌkX1é¨"âP¬`P¬Q\˜##—ñ’ j0ÞHBùä44Xó̤±“7¿Ò¸6e­EÔžØÅŽ­`! /è!ëz± °XT!™H±jéš8Bâ4¹cïÇaÏÕ‹¯–{®²éòá·ìÚiw>øæÓ¾8âû®=¤Ã^”gBy®d»è þ«` &¨¢ "JÂŒdAà ZÛög;:ü¨e½¼^ΗWn²³UŒ¸·k޵~ÃôÉø—Ò8i–‹ý'Éé_Ø„Áó Ö3X?¶Æ3 ¾ç3tœ=?}øð„_ty{{ùZij&¥Ïîûngǹ‹ínjý\”ˆîô)œÍá% ~Âà%-~Òâ'ŠÖà',÷Mz‚¾Þ´~{l_wFµqÛ6\?•ß‚¶&2.EÛ›7ý™Ï:nô­„&‹ç›¸ ü"DÒâ%-SÆÍb´©gwóŽþÈȶuôVòQq Š­ë0}·Íq{Oþz|Åýw?”/ ßõ`}‹Wêñk?ª©7Íçó/?Éf³¹ÆoÖËÑÒ¿!@ç!~ÿ@xú|×™§›±$Ñëº èÃKX¼„‰­oÃ̱Kعkk¦çòïo·¾ï¶ÜH»*€®m¯Ÿ›M^ºÜ={ÁôÆÄ¥üÄæK¨K¦˜=vÍ­ûsçÚѲNÖT£[5@g‹6ß<ãê¬(’†Yw=îwç~+xÖ2cL#§ÎF‡ŽØòNÙÈ”ˆ$dß…xÖÃXÅúK±Ã©Ã‰ .Š‹•D‚HäP ê´T–L[]ÖyE€|”a^ýÊk¦ƒ)FØÒS,FñqxNE)­‰ úl* °qÛ»‰ÿe³ÄØ%¢J/HzìÀ^+ * €»®Gƃ¶è~ŸLÁÑ@;`¾xMQT ã¢ã¨0Wñ{½ªÿ·*ï­ý‹ø§6„»\ô <IEND®B`‚whyteboard-0.41.1/images/icons/cursor.png0000777000175000017500000000115311323044175017415 0ustar stevesteve‰PNG  IHDR ¹ðÜsRGB®ÎégAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs  ›Én6tEXtSoftwarewww.inkscape.org›î<¯IDAT8O“1Ha†O;ƒ jÉÔAÒE…\"!¤!)·œZ‚jkls¸†Bœ£ÍTâ$D3£ARpª(ii ‰¯÷;TÔ.뇼Ÿ÷ùß»ïNA„ ƒYð¯u«Óé¾<3ƒAI$äõz?>Ó%œ®4 j6›d·ÛßÞâ$I©V«Ä«R©Íf{CxLý&)ì¯r¹LV«õ á ×’F‹Å"™Íæ6Â[ZÒ¥B¡@‹åÂ&Ð 7i ,e³Y¾½:ÂëÿúMn·ûÂj_Òlèt:”Ï片‰DáF¿i t»]*•JêÀ2™ ‡øe^ƒ {Ü¢Ôj55‹ÅÈãñ¨¿s¹ ÷`yd´z½^©×ë”N§Õ€Éd:Ç韭V‹ü~ÿ öq ‰¢XˆF£„à6WÀ’$IjKï›ñÞxÁÞ)F‡Ã‘H¥RÄ-@àûÃ-Û/Gòù|jK<ç[½‹#Ï2v1çt:O“É$µÛm …BÜÂ_ñÄ?ÙZ8V[dYæ–K°0©e#> ƒär¹x Àð ¦`9ÜâöÒ6IEND®B`‚whyteboard-0.41.1/images/icons/swap_colours.png0000777000175000017500000000152611321475243020626 0ustar stevesteve‰PNG  IHDRóÿasRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÚ7­×­ÖIDAT8ËSkH“a~ÞoÎ>Ù”áœNMÒ¼ódþ)Í?å´‹Ù!cD‘•a!–y©5‹.BZE””¢©™¦“b–nëBn󈊰r_¸oîûúaßÁðùóÞsžóžóžçà~cœ/=¨ÕÕ,Ôêµ6½Awë@oÐùlêlq  HS˜ù,ÿpA`ô–˜8†a±¼+=£<.6^£ÎÉëÖ ¼7Q¥#„ÚšŽ:ÝMãê*ÊË*@J/–)ÔOÓRwä¨sòxxÓÙ‹ÕLhšö½ÄqœÑétž&„ت+¯{|hu5Lþ¡Z¥J¦ðàõzÁq^Âó!€XìÏßðâås»¿¿attÌ°æø D{³3§Y»ã8…R°XÍüЉŒ;0jçå¢@TTTðìÜlêääÄ÷~ãˆzßõYarEQI‰T!—ËÉà#1ûÎÌÌ´LMOš# U„ÊRRÒ`ú¸yѵØ7Ð?h ï=h2[F.¼nmYrþrBD‰@ÓtgΠ1ÔÝÒØÇl}6Ç(<v¥qK¾1 Fý†ÎåååÍzìÛ‚D"5 >%EQàù|žsû>QEyYÅÊyåÒšX”H$¿Y–dþ0j÷ÒRîžÝY‘¹ê<Ü­¿ùù9MôJ¥Òdí\TÕTd5ÆÅƧz9/¶ÆÄrɪÒý¶nÖMƒñ‰1›Ÿ@XM–uGŠÅbUVf6d2™Otû÷©_¾Z=vÇh0ùŸLõÊÏÏÏtð@>ŸÀ ÷ímdÄ<ÜÁ0®£¢õ–¥»«Ý]=sIÛM,ëN'„(”ÊpÒÞÑFì[Óˆy¸²¡¾qÙ¸âs§4Ÿ4³Z-ÞZ½¶½HS˜´6fÝ$BÐ5mÕ±ë7ª™«U—%«É5t3vX˜aIEND®B`‚whyteboard-0.41.1/images/icons/move-down.png0000777000175000017500000000224310443521756020023 0ustar stevesteve‰PNG  IHDR szzôsBIT|dˆZIDATX…í—KlTUÇçÜ{§-ˆPÀ4j¤¦J…&1 #â#M”èÆQnи%î4AcbbÐÄ­˜@â£<Ú‚ò0B©<¡´´ú Ç½çœÏÅÌ´Ó2-#a×üsgæœ9ÿß÷ǽîÇ=§Ó¢u¾ø^€ˆ`¹í“LYÃzÒ·ëçÇøtÍ×8g±b°Î`ÅàœÁ:‹+ø-²!ßÔ×…ý)Sÿ#À Û‰LšÈeˆl†È¦‰l†Ð¦‰lHÆ$Y6û]$î˜:nG•›-¥nµ|Û÷Îâ„ÃÝ=ÉUUŠd™o‹³8ÿ3@ÎA©|›»›"‚—Ë6/†€"—ªÄ]ÈZ9D®@KGƒwlE¶á¢u~6 E @ !Ÿi@P‚Ò‚ˆa0ºŽ(‹Þ %%Öï^ô&Ç^ÐTgnÙB·ž¢>XX³ôóå‹W•CÈf*"X±  ‡VBhè »Ïâ{H$JXóú:Ä "¿|^ëíø¤XŠÅÏì}¹rñû«Ÿ¨¬NœèhÀØ#!Ž+!)ÛÏt›q.ˆâ@œ x©jÇOþ™n=¬þàFûf1¯¢k Ýg?ÜѸåDOo­œ:‡mA[zÂ6ºRç0*…hü@á'^ †¾{æÙÊZ®vw¸Sç_ô'Ú·‹ùŒ pô+¢È¤k·þüYß=E*&W"Úr=s™~Ó‰À tÖ8¡sŸ5^μºbeºœ†C»Ä™WÆ»)¹ š6Ñ50xsÙw{¾È̘<‡„_B_xÏ×xþp¦~"[l%4=ø3˲ko}*2æ¦:.åàרvÈ]™:o ³÷æõ¥s«Ÿ :’­8e3õ5žŸ½jO¡pîèVóžÄ~ÎÃcøÔS `(ãQJäTÒuVö«G:k“É䔦#“g÷„oõ] U`–Ï^0ò±i¼ ŒÖÐ 6…ã~OOèX}íŒ}çÒ~9ØvÔµPf4Àxï:XXÆÂìòPùQf6g˜—£ÈZˆõbRз˜  uçO¨÷ã^Ä¿ë5/v”mÛIEND®B`‚whyteboard-0.41.1/images/icons/next_sheet.png0000777000175000017500000000230310443521756020253 0ustar stevesteve‰PNG  IHDR szzôsBIT|dˆzIDATX…í–hUeÇ?ï{ÎqÞdê¦Î3S Š~—.ç(*- ËúCŠ2#( B7#  ¢Èþ‘D#,„夅e¬i³6Ür*æ¯ÐÍ™S7›Ú¶»»{~¼ïÛ÷œywÝæœà_{à¹ÏË=çy¾ßó}žó¾mÐhE¶™Ql=w½uäõ$O{çÆÂb{% ZÃhâ-r凯}kÇ“­0µnzÞl½íÜoø×ZçºR0¿pñн1'–pöå¿Ë„JÀS.G›«¸gj¾óú¼÷ofe,(¶gÜ0Z$ƒ8G›*9b¸|û…rF [^°ÌZÔß=OA‘mtö™iˆ}úÖwh,ÃS.ÆhÆdOdLl›ËÖ$ë›=¾A-ݺ5 ,^‹e9H$B¢fJaóç©ñµ‹Ö ƒ&;k4SF?HyMi²úèÎ=I̯ýŒÖÞؽS3ì>± )¬.B¤‘¹,…Öm Ú(Z:NÓž¼À£Í:*gláÏU›ÿÊ_.Ÿ¬þÜ=ÞL¯3 ŒÂW¾ö´‡¯]|•Zx( —¸ßÂÅd#ç;Nr®­ŽÝÇ7áYqñê¼w²sFäm›¹ÜYÚïh­´‡@"¥ÄVН”aÿÊtx—hK¶ðÛÑJ¡•Ishþ»;ò¦óòÜ%±m%S||Våªà™«ðµKÂkÃ’6Žãhƒ FÆÅÕq<•Àƒ´ gR?c FC =<í¢M €)ýR áµÓÔV`9ËX¶@Úi ¤é¤Þ ƒÀhƒŽÀ…#Àîÿy±ÉlÙ±®³µýâêÊUÁŠ«è6ÒH+Œ¶À £”Þi )`ƒ-b<<éYü¸6ß—mèLtt¾Y½ZmL_DúB¦ö{)çŽÀ»T!ƒÑá“Ü\¦Ýü<õ'낊š²¶ÖF³àÐfUKêðSé$2 Èt^|`EP¤‚)ù…Lõ»ìÌ—ø‹V©šcb¹7÷i*÷îö©­;õG°ðL gúá$_Þ{jX»å“.u·â%+°‡ïÖìû˜Ëgû/¥É†Ó ;lTËÚ[ˆ§×%c÷Í$`]õE08¡§¯À),¶+dY–@‹»F>NÌGIé÷ß.®©ýF­ü°fºw;2 èЄl#ï"ØUD†fÝÄý¹³¸t¶Ó”ü´>qîXré±Ô®0?r•¶îsI¾B™´B€-†0-g!û÷ï výþkKã>ýÊÉru"ÜO‹W|1õDÀ^á½Ñ Œò¡¬¢Ü=pdïÁúþ¢¦Ãü—A4Š.ô|,÷çcÒ¾dA‘}!/wœßtþlÉ¡õê½xœ€Ë-ŒHû½÷¥@¦ERF³Ðõª677-ß³Z}E÷‹T´Aë—ýÌ)ÉŠX‰IEND®B`‚whyteboard-0.41.1/images/icons/prev_sheet.png0000777000175000017500000000226010443521756020253 0ustar stevesteve‰PNG  IHDR szzôsBIT|dˆgIDATX…í–mhÕUÇ?çœÿ»ÎÌŠ¤"ôY³Ý©eV›ÙƒJ SM{‘Øæ^Æ ¤^¨AAÐÌ7EMÊ0ðEJ–èE‰êeê|ÚuÞ=Þ»{ïyèÅÿ¿Û½×­ö@ïö…¿?çÏùý¾ç÷tLa “ÄâFU_Ý้ð-¢;¼ ï•NØÈ„<ÝÈôš&ïÀâûV¼õú oG<åO˜€7Þ UMÌMâZ³tÓüE÷.ó•TvãŒ@u£÷ØtïÖ¶7V¿³ðáÊ%~{×I¬Ó“"0æDÔúY3g´õÅæ2å+:ºN’1i Àüš‘Ѥ·»rÎC[6Ôíˆôeâœïþc5ÎYpj,p|·ã"mbƒÞþšꪟ‹nˆ\êi'‘ºŠuk Æi¤4¿ö ÖYël(jc ï¶¼94®ÔlÌwÂ^»lãœE•Oø]'Hfúp¸œaë ±¿¾ÅX±jc³h›Íéå÷¼:ê!G$PÓè=^âûß­«Û1cöóDÛåÉštø×á° ƒÅà¤Æ¢q.ÐÃä¬38,Ξ›Dü-wÌ,ß³®vû4éCÛå#A®…@+ ' ä¥'¿,Fì×Ñ&ÿ«ŠòÊçׯlŒ\ê;Kg÷Y!RZBœ KÊô“Ìô2¤“h“Ƈp é|%amXpŒ8­ Xk«…ƒ™}C×Ñ&Εƒ™ú3qú³×ÉØTÐ œkV;¬qíðˆÖ©±¥`îySq޳»>Þ¿só³K_*SÒç̵¥%>ÒC-Ò R „xRààÖ‚0a†ç ›MåPÍŒ‚IØÚŠ9¶KoïíKlÝ÷ÃÞ¤oÊÜ£u¤í Ùn2 "”Eyå‰@|ç‹Ö”@z©RT­ïST #bÛc¾î¿’©ýþ§}‰+W¯ê'®ç¶²Y¡ÑÀò$Ê(_¢|‰ôCÇ9çAê‚Ú)ˆxAÔGjC ÈS-ºmÞ–³‡[o$ºî^ZõJéo݉§ÿDå9YYÑŽ°‚4ÚauXÆØÅŽF ‡ G‰÷vèUº¾mOO_¢vÕSk#C¿r~𗀄ÔÁ{4ÿ›™|Œi»lï5†N|h¶e7vnþb ¥aͪúÈíååœî?„P&—ãã»uСdó$“÷móìçBR S$Ч>ןvžîÙôå7-ƒqçªÊë™æß ¦I`H9ÕybµF{M¸¾]üŒ» /§Î¬ˆ¨²²G<£JT)GcG¸³ï¸˜D6$WІ£Õ€ Ø<Ñ€wîgÝ>p‰åÎý¬«;^µº¶>’áðGp˜ÐM—½§†›p³t2Aêb̶–.¸1½ãö“CÞŘÝYtòtžLîÙT÷ñ6õrøñõÜu5…)ŒcF¦Œ˜IEND®B`‚whyteboard-0.41.1/images/icons/pause.png0000777000175000017500000000074110443521756017226 0ustar stevesteve‰PNG  IHDR szzôsBIT|dˆ˜IDATX…í–ÝŠÓ@†ŸI&!_RÈ&mõÜ+R\ðâñ@ü»!/Ài—ꪰ6óz°µPlÙÙ95ïÑ0ïÃäa†sæÌùßã"˜'@s¡û|‰dÎÆGÔϯŸ}>W||÷éé#˜4¶mðþíî~ßPø‚ë—/hÛV»Ý.й”,V@›ÍWnn6ÿt1L²ÀÕÕÕñðiÚ³ŸöLÓtÒÅ0É«Õ*…ãžë¿] “,°\.u8—e8’Nº&Y`±X HäY~øŽNº&Y`•e ßo¿ñó×-A²,†A±Ì¥<8†u]‡Ê*„(Šp ¨¬¢®ëË$ßÀz½–™ŽÂ—xïlfÇ‹a’o ,K™ðÞßÖÌŽ#Ã$ ô}/«*pï=!¯*ú¾W,“,Ðu]¸¿^È\Y@3£ëºË$ Œã(3Cf5’kcGÅ2ÉMÓȬâõ›W'ûfMÓ(–¹”ÿ¶Ûm›çy~®›¦iêû~Ã$ H:;®Î¹ðfΜ9çòH '0ÞôÓIEND®B`‚whyteboard-0.41.1/images/icons/move-top.png0000777000175000017500000000241510443521756017657 0ustar stevesteve‰PNG  IHDR szzôsBIT|dˆÄIDATX…í—]lTEÇÿgæÞÒnÁ‰†&„ZðQLÔb*kÄh"…~$&ÆXôÁ'$P‚¼YEc„'¿bŒQ#Á˜ˆŠ†h°¨ ‰¢<(BI)”ÚÝîÞ{gÎñaî.ËvÛ²¢žääÎÎÎÎÿwæœ;3 üËF¥ÉO®•X÷f3LÏ+7ð¥gÞ‡" E ˆ@À`±`a0Û¸maÙžV,XŒëc‹×¶¾Xª,€"…½Ç¾€V>´ò@¤@ ä¢G°!²! ‡ˆlpÑ9@drˆ8DhrXvó³#®ÊÉ›†"]'"ˆH‚…!…h]”6qòâwlxóñ·†úüÔÒ jóÇë10”süH6¬(ƲÖ¿9‘¨ûaõ£›j}ßGÿ…SxûóM²œ™÷Ë+è©t>UÉà»Vázòhg{ÛšÄ@؇½ ¿JaÅ=í×iñv5w"qÍš:Qek¼+®œX›¨¥ÃýûaÙàPßL›2ÝÚÖP=ä}Š WU_îÀ™óõ{ šî[Ô2w± · Ɔ°lqˆ³é4O_¬ûÏõMMÌ;“8ÑÍ»®*@²Ã_ucýÌÕËïn¯þ­÷Gd£¸Ú]ÅçLç³}HÎyÀ?|üÐüÉó3ôtË¡«°`·ðºñß}rÉÚđԯH õ‚ãÆŠ)¼z™à<"Ð2û^ÿ÷¿ö·M¹¿éù‰O5ÿ¨ùj}Ó4Õl_º¶.À Žž=n?¾dt fÝЂ(ge{×Ö”p4·ûUœMcÄ"lîD‚ÙûnIëc”/øóô>Ž`âÏH+¬D0âvÅÀd±ûÈgÐãˆî¼eqÁû¶©UW@ÕokKÓ††úéêàÉïa8‚•BÐжà¬B š3¸ôÂH?ÛŽÆ©3½YófMLûŽP¶Z×yÏO­ŸñÄýw{=„@XÀFðÑŽ7²™ÌùGöl±_—ê ? „¾J âõm/äâ3˜D0®cåFX 0Ä)dyä¼8ƒ@ L`2xë“-°ˆo"¨ÑŽr¨š`jLÎÔDù™°ñRFVR0ÏW Q, !°w‚*S_¼Ì ƒ°\ †tu" WÜ—ì„ IC{å¹\ÇúNœÅA)×¹ûeœ+'8&ÀHf%„Òˆ”vb—o2ƒ*:Þ*°Ôn´GPÚEKÖåJ„ i¹ê ·äWn5be0¹š¨ô‚q9ùJƒœÇo‹œ9îAÑ•º €ÀW øE ­q]¡ÐˆJß‚<˜»Ô˜ Š!¢¢ö˜^LŸw¶¼³q´@ÊÙxaìÜ®›ÿ\°r)óKÄKaòO?ž”X¸èB¸H‹ŸÅÄý—D[jQ°’•=—=ëÿÖÚkÏ Ü´ë´…;È)ßb«n4Žw½ö™=¾ä)¯õ_ÿÏ*6û¯Ì¼}þÚWŸ~›[ò Þ-¯µ+þ3€Š-¶rJÁ­»ª—=ï .²úáçò­õ?_²)v÷¿°ìMfù&çÐSlˆ_¸rŠãg>#0 ª–¬.ðlðUé6¦LÀLtbéâ&´uk–¿8-·ÒÐò-NCZ»ÿ`Áô‡ÄšX~[{SùâénC:Ѹ­€äöÚË=1gÖs¼†Ö〠"8 ùþÜ!-Xœs×´ùeÍs쮉ŠÃ+PQkß*ž]òÂʲ5¹'šŽ0êÃi˜ö€dÐOGo#Kï{Ò?Ûtêþ;JMõúÓ?P^k/š:c÷3Uã¿¶ÕÓh5<îè¤?ÕCÙ‚•þoçNVÍ\ÌÑÆ:×<^ü1— bkìÞX,þéúU›â-=§éìÿkÌ`^ú…ÖÞÓ<º´&_=—meæu”ncŠºàèó«ÞȤ ]¿_s_ûìj¿©:~nþ› •%+ }µGJwŸ4ÀÚµ˜˜³_VW®/*š:]N·Ÿ@U#QQÄcÏ‚g1J@•E©?{Ù3æšâ¹ÌËíµ `Ä(|Ì~ø`ñ²ê%5±“­ß £àN5!Îs¨¤H¸naxÏħ-]g©\Xm[Û›gݶèŠßX玤5Œli­Y7£hÞÞ5;ó!öã1žáØ™} ¸$=©ºm8 Q@¨*5ÅÛPŽÕ •ý‡?îïè\Wÿ~x0[Ïf_p*Ÿ´tœgûÞ ¡àTñ¶¿ôbAéJ^ 7¸Œø`ð`PPðŒ°{ß;ˆ’Â#$‚Ëä‹‘P·+ÏK_ÆØ$ž‚q\ì?GB;±9’Ž—wN¢„ahï±~¢ @ „$FZ‚aÙöÃRå›”KçIhÆ$½ÞQ†‚:EœàyaÊ&.÷]ãÅ c)MжalTfÏ€¤+ ª¸PPoÔ†¿1€¾ Ï€g$‚°‚—®€s‚Hô>ÉéO éúðm ¼ÔU©I¨Q#:cÇß €¢‚ ‘ˆ0à2Â2iý Ø4AzG™j(„¸èV˜¾¦W_Ì?ˆ¶T0Rà± àq¬äP5û54°Kgde›2&ÓâÉl±ÑÌË‚°CD3ÂC¿¦nÈP˜ŒhÃÿ'Ó´™f¤g<’9¿iÿoûúâã”|~ÕIEND®B`‚whyteboard-0.41.1/images/icons/move-bottom-small.png0000644000175000017500000000124611360661554021461 0ustar stevesteve‰PNG  IHDRóÿasRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÚ  œñ¥õ&IDAT8Ë¥“MH”qÆÿ÷cß}cQØõ#!‘(ÊRJ3Œ.…¥Fuè¡å¥°KÚ±:ÔEMºu ¤:TàA„¤RPƒñƒ ¤¨ Â%XlYY÷ýšŠ©mAôÀ\†gfž™Q"Âz4ÞÜ&‰dœlˆæ2xã‹ZÏ›“É8½íOð/pð/pqý ]}W~5²URJcâó /Ͳ·DÆ]¢¦ôdÖ®´¬, ¥VJáø"""0.Ý­“Ù¯×Ë #"H€H€ã§qË S{Õ\s}oi F}U ©L’‹'®cè!D|ñ”ºx,,Σôc´î×u}L}U JD¸vÿŒ¨G]esñ—øÊÁÇ!±ü•o©w¸"ˆh*ifvn–˜YÌ­s”Ò™g{+ähí)òb1>ý˜a!=O<ýMW«~ÀîèaÜd˜ééIv¾Q¶Y1Ѷ"ܾ0@ÿH† cš&ߘaPX#dëG·S`—1þj„žóØVdãÊŠÊÕå†.ú‡ï‘·¥˜ˆ‹ii˜¶NN$—=Gx>ú”öãÝ”•«¬klªnS%±]L½`ÿÖF4Ca„4ö6253IY~%MÕmNYmþ…t&Eó ©­9Ѻ¡¡-FçAÇÊÜ=$ÛŠÐÓ:Àðè3òÙI,ØÁЋ!º[ͽ¡ƒ†ðÐøOür·Û0ùfïIEND®B`‚whyteboard-0.41.1/whyteboard/lib/pubsub/pubsubconf.py0000777000175000017500000001156711443222121021752 0ustar stevesteve""" Allows user to configure pubsub. Most important: - setVersion(N): State which version of pubsub should be used (N=1, 2 or 3; defaults to latest). E.g. to use version 1 of pubsub: # in your main script only: import pubsubconf pubsubconf.setVersion(1) # in main script and all other modules imported: from pubsub import pub # usual line - Several functions specific to version 3: - setListenerExcHandler(handler): set handling of exceptions raised in listeners (default: None). - setTopicUnspecifiedFatal(val=True): state whether unspecified topics should be creatable (default: False). - setNotificationhandler(notificationhandler): what class to instantiate for processing notification events (default: None). - transitionV1ToV3(commonName, stage=1): set policies that support migrating an application from pubsub version 1 to version 3. """ packageImported = False class Version: DEFAULT = 3 value = None output = None def setVersion(val, output=None): '''Set the version of package to be used when imported. If output is set to a file object (has write() method), a message will be written to that file indicating which version of pubsub has been imported. E.g. setVersion(2, sys.stdout).''' if val < 1 or val > 3: raise ValueError('val = %s invalid, need 1 <= val <= 3' % val) Version.value = val Version.output = output def getVersion(): '''Get version number selected for import (via setVersion, or default version if setVersion not called).''' return Version.value or Version.DEFAULT def isVersionChosen(): '''Return True if setVersion() was called at least once.''' return Version.value is not None def getDefaultVersion(): '''Get version number imported by default.''' return Version.default def getVersionOutput(): '''Return the file object to be used for messaging about imported version''' return Version.output class Policies: ''' Define the policies used by pubsub, when several alternatives exist. ''' _notificationHandler = None _listenerExcHandler = None _raiseOnTopicUnspecified = False _msgDataProtocol = 'kwargs' _msgDataArgName = None def setTopicUnspecifiedFatal(val=True): '''When called with val=True (default), causes pubsub to raise an UnspecifiedTopicError when attempting to create a topic that has no specification. This happens when pub.addTopicDefnProvider() was never called, or none of the given providers specify the topic (or a super topic of it) that was given to pub.subscribe(). If True, the topic will be created with argument specification inferred from first listener subscribed. ''' Policies._raiseOnTopicUnspecified = val def setNotificationHandler(notificationHandler): '''The notifier should be a class that follows the API of pubsub.utils.INotificationHandler. If no notifier is set, then the default will be used. ''' Policies._notificationHandler = notificationHandler def setListenerExcHandler(handler): '''Set the handler to call when a listener raises an exception during a sendMessage(). Without a handler, the send operation aborts, whereas with one, the exception information is sent to it (where it can be logged, printed, whatever), and sendMessage() continues to send messages to remaining listeners. ''' Policies._listenerExcHandler = handler def isPackageImported(): '''Can be used to determine if pubsub package has been imported by your application (or by any modules imported by it). ''' return packageImported def setMsgProtocol(protocol): '''Messaging protocol defaults to 'kwargs'. It can be set to 'dataArg' to support legacy code or simple pub-sub architectures. ''' if protocol not in ('dataArg', 'kwargs'): raise NotImplementedError('The protocol "%s" is not supported' % protocol) Policies._msgDataProtocol = protocol def transitionV1ToV3(commonName, stage=1): '''Use this to help with migrating code from protocol DATA_ARG to KW_ARGS. This only makes sense in an application that has been using setMsgProtocol('dataArg') and wants to move to the more robust 'kwargs'. This function is designed to support a three-stage process: (stage 1) make all listeners use the same argument name (commonName); (stage 2) make all senders use the kwargs protocol and all listeners use kwargs rather than Message.data. The third stage, for which you don't use this function, consists in splitting up your message data into more kwargs and further refining your topic specification tree. See the docs for more info. ''' Policies._msgDataArgName = commonName if stage <= 1: Policies._msgDataProtocol = 'dataArg' whyteboard-0.41.1/whyteboard/lib/pubsub/pub.py0000777000175000017500000000244311443222121020363 0ustar stevesteve""" Pubsub package initialization. This module loads the pubsub API selected via pubsubconf. The default is to load latest pubsub API. The Publisher variable is deprecated but is made available for backward compatibility. This module assumes one of two imports:: from pubsub import pub from pubsub import Publisher (do not use "import pubsub" or "from pubsub import *"). """ import pubsubconf # indicate that package has been loaded: pubsubconf.packageLoaded = True __all__ = [] def _notify(output, version): if output is not None: output.write('Importing version %s of pubsub\n' % version) _output = pubsubconf.getVersionOutput() _version = pubsubconf.getVersion() if _version <= 1: msg = 'BUG: Should not be loaded for version (%s) <= 1' % _version raise NotImplementedError(msg) if _version <= 2: _notify(_output, 2) from core.pubsub2 import * from core import pubsub2 as _pubsubMod elif _version <= 3: _notify(_output, 3) from core.pubsub3 import * from core import pubsub3 as _pubsubMod else: _output.write('Warning: pubsub version %s doesn\'t exist') raise NotImplementedError('pubsub version %s doesn\'t exist' % _version) assert PUBSUB_VERSION == _version __doc__ = _pubsubMod.__doc__ __all__.extend(_pubsubMod.__all__) whyteboard-0.41.1/whyteboard/lib/pubsub/__init__.py0000777000175000017500000000161211443222121021331 0ustar stevesteve''' Publisher-subscribe module, simple form. This package provides the pub and utils modules. Use for instance from pubsub import pub help(pub) pub.sendMessage(topic, ...) from pubsub import utils as pubutils pubutils.... Do not use Publisher in new code, it is deprecated (see below). DEPRECATED use: old code can still use from pubsub import Publisher help(Publisher.__class__) Publisher.sendMessage(topic) # OR: Publisher().sendMessage(topic) but should be converted to new format. ''' __all__ = ['pub', 'utils'] # if user wants old pubsub, main API is a singleton Publisher instance # ie no way to fake via module so must import here; but that's ok since # for version 1 there are no utilities etc. import pubsubconf if pubsubconf.getVersion() <= 1: from core.pubsub1 import Publisher as pub Publisher = pub __all__.append('Publisher') whyteboard-0.41.1/locale/zh_TW/LC_MESSAGES/whyteboard.mo0000777000175000017500000005546511444706714021625 0ustar stevesteveÞ•s´ óL2"Lov…Œ”¬³ Å Ñ Ûåë ó   # 1 : G X c i v | ™ ¦ ¯ º Æ Ê Ó á è ø !!! !*!,E,N,V,\,Ba,%¤, Ê, Ö,á, ç, ó,þ,--'-7-M-`-u- ‡-•- ¥-'²-0Ú-- .%9. _. k. u. €.Ž.ž.¥.,ª.×. Ý.þ./ ./ ;/ G/T/t/„/•/ ¤/ ¯/»/Á/Ç/*Û/:0A0 W0x0%|0"¢0Å0â0 ê0õ0 þ0 11&1 51C1Z1 i1t1|1Œ1“1 §1µ1Í1 ã1í1 2(2E2 X2e2~2)”2¾2 Ð2Þ2ñ2 ÷23 3 3 %303$F3k3&„3 «3 µ3 Ö3-â3$454K4^4 m40{4¬4Å4Þ4í4 5%5 -5 ;5AH5Š5 5ž5»5Ø5÷56%6B6X6k6 s6*€6 «6¸6$½6Sâ6P67 ‡7 ‘7 œ7)§7 Ñ7 ß7%í7 8 !8+-8Y8&^8…8œ8±8Ì8ä8í8ô8ü8 99#!9 E9(f99'¯9&×9$þ9!#:E:Y:j:p:'Œ:´:Å:aÖ:48;Mm;»; Â;#Í; ñ;<'!<I<N<l<q<w<€<‡<<–<«ž<,J>w>Ž> £> ±> ¼>Ç> ä>ï> ? ? %? 0? ;?F?W?n? ?Š? ›?©?º? Î? Ü? ç? õ?@ @%@ 6@A@U@ h@ s@~@ @š@ ±@ ¿@Ê@Û@õ@ A A %A0A MA [A iAwA ŒA(—A ÀAËAáAýAB /BC ]C jCwCC”C®C%ÂCèC/þC/.D^DtDŠD£D¹DÀD ÙD æD óDEE2EKE dE qE {E ˆE–E¬E ÂE ÏEÜEøEF'F :FGF WFdF wF „F‘F ¡F ®F¸F ËF ØFâF éF ÷FG G 'G4GGG dGqG$„G$©GÎGáG"H #HDHRKH žH«H¿H ßH ìHúH I I!I#4IXIhIoII†II£I ¹I'ÃIëIïIöI J J&JL KLUL eL pL{LŒL LªLÄLÞLôL MM+M?M![M!}MŸM¸M ÉMÖMæMùM NN-NHNONnN‡N™N ­NºNÊNçN÷N O OO0O5OaV V§V¯V¶VÕVæV%ùVW &W'0WXW$_W„WšW°WÏW åWïWöWýW XX$X4X QX$^X$ƒX%¨XÎXíXYY $Y1Y%HYnY€YT‘Y1æYUZ nZ xZ…Z!ŸZÁZ$ÑZöZ ýZ [[[[&[-[1[Ò$HáXöLèp1Ü’w@‰‡G/d0)ŽÔÝlAÉŒÎûÅ×N[UVDšÏ\O!Ö¯bs·Kø§(ªóà4ïyÈtHF f"=²Ì-zù“Þ…'¥2,Ð(I<5 Xl@à ç;÷>+î LMʃºæ«%Aieq#,ð/€þr”'C^P& ^MEí±v>Sk©F‹ê¹Ù{†NhÚ])."7œ•8$=eìQmÁq¢S „xI½ä]ò\™–V}õWËp Ø#Wôm5giˆ+J`—2joŸ¬À³ñ¡ÛZ<Oc[¾;ý_annPCÍT?‚a43Ó¸~:­´1GoY¿-¦BǨb*U?Bg89JüdßZՑĤ*TRfã  ˜70éÑ%°®kY EhÆ`sžrµâcŠu¼ú|_Q:3£R»&¶6. jK9ëåÿ!6D ›Â"%s" has corrupt data. This file cannot be loaded."%s" is missing the file save.data&About&Add New Point&Apply&Cancel&Clear Sheets' Drawings&Close&Close All Sheets&Color Grid&Color...&Contents&Copy&Delete&Delete Shape&Deselect Shape&Don't Save&Edit&Edit Note...&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&License&New Sheet&Next Sheet&OK&Open...&PDF Cache...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Shape Viewer...&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAdd a new sheetAdds a new point to the PolygonAll filesAll suppported filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectBoldC&reditsCancelling...Canvas BorderChange the canvas' sizeChange your preferencesCheck for &Updates...Chinese (Traditional)Choose Your Custom Colors:Choose Your Language:Choose a directoryChoose a media fileCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose All SheetsClose every sheetClose the current sheetColorConnecting to server...Conversion FailedConversion Quality:Converting...Copy a Bitmap SelectionCopy a Bitmap Selection regionCould not connect to server.Could not connect to server. Check your Internet connection and firewall settingsCreditsCzechDate CachedDe&selectDefault Canvas HeightDefault Canvas WidthDefault Font:Delete ShapeDelete the currently selected shapeDeselects the currently selected shapeDeselects this shapeDownloaded %s of %sDraw a circleDraw a polygonDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchE-mail AddressEdit the textEllipseEnglishEnglish (U.K.)Enter textErase a drawing to the backgroundEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet into a PDF fileExport every sheet to a series of image filesExport preferences to...Export the current sheet to an image fileExport your Whyteboard preferences fileExport your data files as images/PDFsEyedropperFailed to convert file. Ensure GhostScript is installed http://pages.cs.wisc.edu/~ghost/Feedback SentFile %s not foundFile %s not found in the saveFile LocationFilename:Find location...Find...Flood FillFlood fill an areaFolder "%s" does not contain convert.exeFonts and ColorFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHighlight with a transparent penHighlighterHindiHistory PlayerIconsIf you don't save, changes from the last %s will be permanently lost.Ignores the background colorImageImage FilesImageMagick LocationImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Import Preferences From...Import various file typesInput textInsert a noteInsert media and audioInvalid filetype to export as:ItalianItalicJapaneseLicenseLightLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoad in a Whyteboard preferences fileLoaded fileLoading...MediaMedia FilesMove &DownMove &UpMove Shape &DownMove Shape &UpMove Shape DownMove Shape To &BottomMove Shape To &TopMove Shape To BottomMove Shape To TopMove Shape UpMove To &BottomMove To &TopMoves the currently selected shape downMoves the currently selected shape to the bottomMoves the currently selected shape to the topMoves the currently selected shape upNew &WindowNew SheetNext SheetNo Date SavedNo shapes drawnNormalNoteNote: Higher quality takes longer to convertNotesNumber of Recently Closed SheetsNumber of Toolbox Columns:Number of points: %sOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instanceP&references...PDF Cache ViewerPDF ConversionPDF/PS/SVGPage Set&upPage:PagesPaste an Image/TextPaste from your clipboard into a new sheetPaste text or an image from your clipboard into WhyteboardPaste to a &New SheetPath for the image %s not found.PenPicks a color from the selected pixelPlease fill out your email addressPlease provide some feedbackPolygonPortuguesePositionPrefere&ncesPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePrinting ErrorPropertiesQualityQuit WhyteboardRadiusRe&load PreferencesRe&move SheetRecently &Closed SheetsRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationReload your preferences fileRemove cached itemRename sheetRename the current sheetRename this sheet to:Report any bugs or issues with WhyteboardResi&ze Canvas...Resize CanvasRestore sheet "%s"RetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save changes to "%s" before closing?Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSelection Handle SizeSelects this shapeSend &FeedbackSend FeedbackSend feedback directly to Whyteboard's developerSet the background colorSet the foreground colorSet the volumeSet up the page for printingSets the drawing thicknessSha&pesShape Select Shape ViewerShapes at the top of the list are drawn over shapes at the bottomSheetShortcut Key:Show and hide the color gridShow and hide the status barShow and hide the tool previewShow and hide the toolbarShow the color gridShow the title when printingShow the tool previewSkip to a positionSpanishSwap &ColorsSwaps the foreground and background colorsT&ransparentTextThere are no cached items to displayThere is a new version available, %(version)s File: %(filename)s Size: %(filesize)sThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsToggles the selected shape's transparencyTool &PreviewToolbox View:Translate Whyteboard to your languageTranslated byTransparentTransparent Bitmap Select (may draw slowly)TypeUnable to load %s: Unsupported format?Unable to play file %sUndo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView Whyteboard's help documentsView a preview of the page to be printedView all recently closed sheetsView and edit the shapes' drawing orderView and modify Whyteboard's PDF CacheView and replay your drawing historyView information about WhyteboardView the status barView the toolbarWelshWhyteboard Preference FilesWhyteboard doesn't support the filetypeWhyteboard file Whyteboard filesWhyteboard uses ImageMagick to load PDF, SVG and PS files. Please select its installed location.Whyteboard will be translated into %s when restartedWhyteboard will load these files from its cache instead of re-converting themWidth:Written byYou are running the latest version.You have not set any preferencesYour Feedback:Your feedback has been sent, thank you.ZoomZoom in and out of the canvashourhourslanguageminuteminutessecondsecondsProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-09-17 15:13+0000 Last-Translator: Steven Sproat Language-Team: Traditional Chinese MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) "%s"å·²æå£žçš„資料./n 無法載入檔案"%s"save.dataå·²éºå¤±é—œæ–¼æœ¬è»Ÿé«” (&A)新增點(&A)套用(&A)å–æ¶ˆ(&C)清除工作表的塗鴉(&C)關閉(&C)關閉所有工作表(&C)é¡è‰²æ ¼(&C)é¡è‰²(&C)...內容(&C)複製(&C)刪除(&D)刪除形狀(&D)å–æ¶ˆé¸æ“‡å½¢ç‹€(&D)ä¸è¦å„²å­˜(&D)編輯(&E)編輯便æ¢(&E)編輯(&E)...匯出檔案(&E)匯出工作表(&E)匯出(&E)...檔案(&F)全螢幕(&F)幫助(&H)觀看æ“作紀錄(&H)圖åƒ(&I)...匯入檔案(&I)授權(&L)新增工作表(&N)下個工作表(N)確定(&O)開啟(&O)PDFç·©å­˜(&P)...貼上(&P)上一個工作表(&P)列å°(&P)...退出(&Q)å–æ¶ˆå¾©åŽŸ(&R)å·¥ä½œè¡¨é‡æ–°å‘½å(&R)釿–°å‘½å(&R)...回報å•題(&R)儲存(&S)é¸å–(&S)觀看已畫形狀物件(&S)工作表(&S)狀態欄(&S)工具欄(&T)翻譯Whyteboard(&T)復原(&U)復原最後一個關閉的工作表(&U)檢視(&V)é è¦½ç›®å‰çš„工具簡單的白æ¿å’ŒPDF註釋新增新的工作表新增一個多邊形的點所有檔案所有支æ´çš„æª”案發生錯誤ï¼è«‹å›žå ±é€™å€‹å•題阿拉伯文箭頭存æˆPDF(&P)...éŸ³è¨Šæª”æ¡ˆé»žé™£åœ–é¸æ“‡ç²—體製作人員芳å(&R)æ­£åœ¨å–æ¶ˆ...畫布邊緣改變畫布尺寸修改åå¥½è¨­å®šæª¢æŸ¥åŠæ›´æ–°(&U)...ç¹é«”ä¸­æ–‡é¸æ“‡ä½ è‡ªå·±å®šè¨‚çš„é¡è‰²é¸å–語言é¸å–目錄é¸å–多媒體檔案圓形清除全部工作表(&A)清除工作表(&S)清除全部工作表上的塗鴉(&D)清除全部工作表清除全部工作表上的塗鴉(ä¿ç•™åœ–片)清除目å‰å·¥ä½œè¡¨ä¸Šçš„å¡—é´‰(ä¿ç•™åœ–片)清除目å‰å·¥ä½œè¡¨é—œé–‰æ‰€æœ‰å·¥ä½œè¡¨é—œé–‰æ¯ä¸€å€‹å·¥ä½œè¡¨é—œé–‰ç›®å‰å·¥ä½œè¡¨é¡è‰²é€£æŽ¥åˆ°ä¼ºæœå™¨ä¸­...轉æ›å¤±æ•—轉æ›å“質轉æ›ä¸­...複製點陣圖é¸å–複製點陣圖é¸å–範åœç„¡æ³•連接伺æœå™¨ã€‚無法連接到伺æœå™¨é–‹ç™¼äººå“¡æ·å…‹èªžæ—¥æœŸç·©å­˜ä¸é¸å–(&S)é è¨­çš„畫布高度é è¨­çš„畫布寬度é è¨­å­—體刪除形狀刪除目å‰é¸å–çš„å½¢ç‹€å–æ¶ˆç›®å‰é¸å–çš„å½¢ç‹€å–æ¶ˆé¸å–形狀下載中 %s of %s繪製圖形繪製多角形繪製矩形繪製圓角方形繪製直線繪製箭號繪製橢圓形使用筆刷è·è˜­èªžé›»å­éƒµä»¶åœ°å€ç·¨è¼¯æ–‡å­—橢圓形英語英文 (U.K.)輸入文字擦掉背景上的塗鴉橡皮擦錯誤報告儲存檔案錯誤匯出所有工作表(&A)...匯出錯誤匯出資料到...匯出æ¯å€‹å·¥ä½œè¡¨åˆ°ä¸€å€‹PDF檔匯出æ¯å€‹å·¥ä½œè¡¨åˆ°å¤šå¼µåœ–片匯出喜好到...匯出目å‰å·¥ä½œè¡¨åˆ°åœ–片匯出您的Whyteboard喜好設定匯出你的資料到圖片/PDFså–è‰²ç„¡æ³•è½‰æ›æ–‡ä»¶. 請碓èªGhostScriptå·²å®‰è£ http://pages.cs.wisc.edu/~ghost/發é€å›žé¥‹æ‰¾ä¸åˆ° %s 檔案在儲存時找ä¸åˆ° %s 檔案檔案ä½ç½®æª”案å稱:尋找ä½ç½®...æœå°‹...填滿é¡è‰²åœ¨å€åŸŸä¸­å¡«æ»¿è³‡æ–™å¤¾ "%s" ä¸åŒ…å«convert.exe字體與é¡è‰²æ³•語加利西亞語一般德語到下一個工作表到å‰ä¸€å€‹å·¥ä½œè¡¨é«˜åº¦ï¼šå¹«åŠ©æª”æ¡ˆä¸å­˜åœ¨ï¼Œæ˜¯å¦ä¸‹åœ¨ï¼Ÿé«˜æœ€é«˜é€æ˜Žç­†çš„高亮度標示螢光筆å°åœ°èªžæ­·å²ç´€éŒ„播放器圖示如果ä¸å„²å­˜ï¼Œå¾ž %s 之後的改變都會消失忽略背景é¡è‰²åœ–片圖片檔案ImageMagickä½ç½®ImageMagick通知找ä¸åˆ°ImageMagick,在它安è£å‰ï¼Œæ‚¨å°‡æ²’辦法載入PDFåŠPS檔案從...匯入å好設定匯入å„種文件類型輸入文字æ’å…¥ä¾¿æ¢æ’入多媒體åŠéŸ³æ¨‚檔案將無效的檔案類型匯出為:義大利語斜體日語授權許å¯äº®ç›´ç·šè¼‰å…¥Whyteboard儲存檔案,圖片或轉æ›PDF/PS文件載入Whyteboardå好設定檔案載入文件讀å–中...多媒體多媒體檔案下移(&D)上移(&U)下移形狀(&D)上移形狀(&U)下移形狀移動形狀到底部(&B)移動形狀到頂端(&T)移動形狀到頂端移動形狀到頂端上移形狀移動到底部(&B)移動到頂端(&T)下移目å‰é¸å–的形狀移動當å‰é¸å–形狀到底部移動當å‰é¸å–形狀到頂部上移當å‰é¸å–形狀新增視窗(&W)新工作表下個工作表沒有資料儲存沒有繪製形狀正常註記注æ„:以高å“質轉æ›éœ€è¦è¼ƒé•·æ™‚間註記最近關閉工作表的數é‡å·¥å…·ç®±æ¬„ä½çš„æ•¸é‡é»žçš„æ•¸é‡ï¼š%s開啟最近的(&R)開啟檔案開啟檔案...開啟新的Whyteboard範本å好設定...PDF緩存檢視器PDF轉æ›PDF/PS/SVGé é¢è¨­å®š(&U)é :é é¢è²¼ä¸Šåœ–片/文字從剪貼簿貼上到新工作表從剪貼簿貼上文字或圖片貼到新工作表(&N)圖片 %s 路徑找ä¸åˆ°ç­†å¾žå·±é¸çš„åƒç´ ä¸­æŒ‘é¸é¡è‰²è«‹å¡«å¯«æ‚¨çš„é›»å­éƒµä»¶åœ°å€è«‹æä¾›ä¸€äº›å›žé¥‹æ„見多邊形葡è„牙文ä½ç½®å好設定(&N)å好設定上個工作表é è¦½åˆ—å°(&V)é è¦½åˆ—å°åˆ—å°ç›®å‰çš„é é¢å…§å®¹åˆ—å°éŒ¯èª¤å±¬æ€§å“質離開WhyteboardåŠå¾‘釿–°è¼‰å…¥å好設定(&L)移除工作表(&M)最近關閉的工作表(&C)最近開啟的檔案方形é‡åšæœ€å¾Œä¸€å€‹å¾©åŽŸçš„å‹•ä½œé‡åšæœ€å¾Œä¸€å€‹å¾©åŽŸçš„æ“ä½œé‡æ–°è¼‰å…¥åå¥½è¨­å®šæª”æ¡ˆåˆªé™¤ç·©å­˜é …ç›®é‡æ–°å‘½å工作表更改目å‰çš„工作表的åç¨±å°‡å·¥ä½œè¡¨é‡æ–°å‘½å為:回報錯誤或å•題調整畫布尺寸(&Z)...調整畫布尺寸還原工作表 "%s"é‡è©¦åœ“角矩形俄文å¦å­˜æ–°æª”(&A)...儲存繪圖儲存檔案?儲存Whyteboard為...在關閉之å‰å°‡æ”¹è®Šå„²å­˜åˆ° "%s"?儲存Whyteboard資料å¦å­˜æ–°çš„Whyteboard資料檔案儲存中...尋找Whyteboardæ›´æ–°é¸æ“‡å­—體鏿“‡ä¸€å€‹çŸ©å½¢å€åŸŸè¤‡è£½ç‚ºé»žé™£åœ–移動åŠé‡è¨­é¸å–的形狀的大å°é¸æ“‡æ‰‹æŸ„尺寸é¸å–此形狀é€å‡ºå›žé¥‹(&F)é€å‡ºå›žé¥‹ç›´æŽ¥ç™¼é€å›žé¥‹çµ¦Whyteboard開發者設定背景é¡è‰²è¨­å®šå‰æ™¯é¡è‰²è¨­ç½®éŸ³é‡åˆ—å°æ­¤é è¨­ç½®ç¹ªåœ–厚度形狀(&P)é¸å–形狀 形狀檢視器列表中最頂端的形狀繪製了最底端的形狀工作表快æ·éµï¼šé¡¥ç¤ºåŠéš±è—é¡è‰²æ ¼é¡¯ç¤ºå’Œéš±è—狀態欄顯示和隱è—工具é è¦½é¡¯ç¤ºå’Œéš±è—工具列顯示é¡è‰²æ ¼ç•¶å°åˆ—時顯示標題顯示工具é è¦½è·³è½‰åˆ°å®šä½è¥¿ç­ç‰™èªžé¡è‰²äº’æ›(&C)互æ›å‰æ™¯åŠèƒŒæ™¯é¡è‰²é€æ˜Ž(&R)文字有沒有緩存的項目顯示有新的版本å¯å–å¾—, %(version)s 檔å: %(filename)s 檔案大å°: %(filesize)s列å°å‡ºç¾å•題。 或許您目å‰çš„å°è¡¨æ©Ÿè¨­å®šéŒ¯èª¤?厚度厚度:ç¸®åœ–åˆ‡æ›æ‰€é¸å½¢ç‹€çš„逿˜Žåº¦å·¥å…·é è¦½(&P)工具箱檢視:將Whyteboardè¨­å®šç‚ºè‡ªå·±çš„èªžè¨€ç¿»è­¯é€æ˜Žåº¦é€æ˜Žåœ–é¸å–(繪圖å¯èƒ½è¼ƒæ…¢ï¼‰é¡žåž‹ç„¡æ³•載入 %s: 䏿”¯æ´çš„æ ¼å¼?ä¸èƒ½æ’­æ”¾æª”案 %s復原上一個動作復原最後關閉的工作表復原上一個æ“作無標題更新更新正在更新縮圖視訊檔案檢視全螢幕模å¼è§€çœ‹whyteboard說明文件列å°é è¦½æª¢è¦–æ‰€æœ‰æœ€è¿‘é—œé–‰çš„å·¥ä½œè¡¨æª¢è¦–å’Œç·¨è¼¯å½¢ç‹€çš„ç¹ªè£½é †åºæª¢è¦–和修改Whyteboardçš„PDFç·©å­˜æª¢è¦–å’Œé‡æ’­ç¹ªè£½çš„æ­·å²æª¢è¦–Whyteboard資訊查看狀態欄檢視工具欄å¨çˆ¾æ–¯èªžWhyteboardå好檔案Whyteboard䏿”¯æ´é€™å€‹æª”案類型Whyteboard檔案 Whyteboard檔案Whyteboard使用ImageMagick來載入PDF,SVGåŠPSæª”æ¡ˆï¼Žè«‹é¸æ“‡å®ƒçš„安è£è·¯å¾‘Whyteboard釿–°å•Ÿå‹•å¾Œï¼Œå°‡æœƒè¢«ç¿»è­¯æˆ %sWhyteboardå°‡å¾žè‡ªå·±çš„ç·©å­˜ä¾†è¼‰å…¥é€™äº›æ–‡ä»¶ï¼Œè€Œä¸æ˜¯é‡æ–°å°‡å®ƒå€‘轉æ›å¯¬åº¦ï¼šç¨‹å¼ç·¨å¯«æ­¤ç‰ˆæœ¬å·²æ˜¯æœ€æ–°ç‰ˆ.你沒有設定任何å好設定你的回饋:你的回饋已é€å‡ºï¼Œæ„Ÿè¬ä½ ï¼Žç¸®æ”¾ç¸®æ”¾ç•«å¸ƒå°æ™‚å°æ™‚語言分é˜åˆ†é˜ç§’ç§’whyteboard-0.41.1/locale/cy/LC_MESSAGES/whyteboard.mo0000777000175000017500000004703411444706707021200 0ustar stevesteveÞ•C4 ¯L" ,3BIQi p |†Œ ”¢ ²¾ ÄÒ Ûè ù  0 : G R^bkr ‚Œ’˜ ©´ÆÌÔå íù6<%[‘ ±'»ãê ð û   #1Iaw’¨»ÏÖ èõ("1K}•­³ËÝ ñÿ 6 S Y c y Ž #œ &À ç ü  !!#*!N! c!q!„!ž!¤! ³!Á!É! Ñ!!Ü!þ! "")" ?"L""^"-"¯")È"'ò"%# @# K#Y#k# ‰#“#¤# ¬#·#Ê#Ú#á#ê#ò#ù#$'$3/$c$h$p$v$…$‹$¨$ ®$º$Ï$]è$F%a% {% †%”%«%Ê%Ò%Û%Bà%%#& I& U&`& f& r&}&†&—&¦&¼&Ï& ß&'ì&0'-E'%s' ™' ¥' ¯'º'Ê'Ñ',Ö'( (*(E( Z( g( s(€( (°( ¿( Ê(Ö(Ü(â(*ö(:!)\) r)“)%—)"½)à) è)ó) ü) **$* 3*A*X* g*r*z*Š*‘* ¥*³* É*Ó*ï*+ ++8+Q+)g+‘+ £+±+ ·+Ä+ Ì+ Ø+ å+ð+,&, F, P, q,-},$«,Ð,æ,ù,0-9-R-k-z-—-²- º- È-AÕ-. .+.H.e.„.ž.².Ï.å. í.*ú. %/2/P7/ ˆ/ ’/ /)¨/ Ò/ à/%î/ 0+ 0L0&Q0x00¨0À0É0Ð0Ø0 ì0ø0#ý0 !1(B1'k1$“1!¸1Ú1î1ÿ12'!2I2Z2ak24Í23# 3 -3N3]3b3€3…3‹3”3›3£3ª3š²3#M5 q5{5Ž5•5ž5·5 ¼5Ç5Ð5×5 Þ5ê5ù5 6 6 6*6:6 I6V6 ]6j6s6 ‚66 ¡6 ¯6»6Á6Ê6Ñ6 á6ë6 ó6þ6 77,727 97 G7 Q7 ]7 h7 r7|7–77²7Ñ7æ7 838B8I8O8 X8 e8 r8 €8Ž8¤8¹8Ï8è8ú89"9(9 ;9I9d9!v9˜9¶9Ð9å9ê9þ9 ::&: A:O:i: o:y: ‰: —: ¥:#Æ:ê: ú:;;!(;J;_;o;„; —;£;´;Ä;Í; Õ;ã;ý;<*<?<S<e<#{<!Ÿ<Á< à<)ë<"=8=J=[=o= Œ=—= «= ¶=Ã=Ö=æ=ï= ÷=> >!>:>HB>‹>‘>—>>­>µ>Î>Ó>ã>ø>e?t?Œ? ¨? µ?¿?Ô? î? ø?@8 @#E@i@ @Œ@”@ ¥@ ³@Á@Ñ@ä@ü@A#A2AEA]AsA‡A ˜A ¦A´AÍAÕA1ÛA BB4BRBgB vB B!B ±B ¼B ÆBÑBãB ìB÷B/ C8:CsCC®C½CÚC ùC DD D !D,DFIFaF-{F ©F&´F ÛF(çF(G9G KG XG"dG‡GœG°G¿GÛGîG öG HGHWH]HtHH!­HÏHíHI I9IAI'QIyI‚IG‰IÑI×I ÞI#éI JJ2JRJ,ZJ‡J'ŒJ´J#ÎJòJ K K K,KDKSK!YK!{K%KÃKàKùKL+L=LEL4dL™L«Lh¿L:(McM!iM+‹M ·MÂM#ÊMîMòMøMþMN NNByt(%¦@OëÚ8 qcW>‘Ö~ú3vÆzì*&6’¢ˆXѨ9uÜ(…؇®†Ž²ù0.xß•Ï-m™û:Á ÀSQ“¹µÙbJŠM£9‚$±dåœÈ`ñ,C3ó¿ÝR>î7›)Õ6 l$%4ºäÉ5©2þG< " k/8Ä »!=?T/šP˜Â]5¾p*ï'0âZU"«­{Ì𠉞à -jL#s?Be Eް΋æÓò +@^ü¸1Ã:fHoôY,¤.)¶ÒŒ½Å}ÊÔA|&öN³×¼ø÷–Çõã·=_ ÿÐh”áͧ;!D1K#+é„7—C2€¡;¯w'<¥Ëè¬[a IrÛêVªŸ4\ý´ƒAinFgçí "%s" is missing the file save.data&About&Add New Point&Apply&Cancel&Clear Sheets' Drawings&Close&Color Grid&Contents&Copy&Delete&Delete Shape&Deselect Shape&Don't Save&Edit&Edit Note...&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&New Sheet&Next Sheet&OK&Open...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Shape Viewer...&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAdd a new sheetAdds a new point to the PolygonAll filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectCancelling...Canvas BorderChange the canvas' sizeChange your preferencesCheck for &Updates...Choose Your Custom Colors:Choose Your Language:Choose a directoryChoose a media fileCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose the current sheetColorConnecting to server...Conversion FailedConversion Quality:Converting...Copy a Bitmap SelectionCopy a Bitmap Selection regionCould not connect to server.CzechDe&selectDefault Canvas HeightDefault Canvas WidthDefault Font:Delete the currently selected shapeDeselects the currently selected shapeDeselects this shapeDraw a circleDraw a polygonDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchE-mail AddressEdit the textEllipseEnglishEnter textErase a drawing to the backgroundEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet into a PDF fileExport every sheet to a series of image filesExport preferences to...Export the current sheet to an image fileExport your Whyteboard preferences fileExport your data files as images/PDFsEyedropperFeedback SentFile %s not foundFile %s not found in the saveFilename:Find location...Find...Flood FillFlood fill an areaFonts and ColorFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHindiHistory PlayerIconsIgnores the background colorImageImage FilesImageMagick LocationImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Import Preferences From...Import various file typesInput textInsert a noteInsert media and audioInvalid filetype to export as:ItalianJapaneseLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoad in a Whyteboard preferences fileLoaded fileLoading...MediaMedia FilesMove &DownMove &UpMove Shape &DownMove Shape &UpMove Shape To &BottomMove Shape To &TopMove To &BottomMove To &TopMoves the currently selected shape downMoves the currently selected shape to the bottomMoves the currently selected shape to the topMoves the currently selected shape upNew &WindowNew SheetNext SheetNo shapes drawnNormalNoteNote: Higher quality takes longer to convertNotesNumber of Recently Closed SheetsNumber of Toolbox Columns:Number of points: %sOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instanceP&references...PDF ConversionPDF/PS/SVGPage Set&upPage:PagesPaste an Image/TextPaste from your clipboard into a new sheetPaste text or an image from your clipboard into WhyteboardPaste to a &New SheetPath for the image %s not found.PenPicks a color from the selected pixelPlease fill out your email addressPolygonPortuguesePositionPrefere&ncesPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePrinting ErrorPropertiesQualityQuit WhyteboardRadiusRe&load PreferencesRe&move SheetRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationReload your preferences fileRename sheetRename the current sheetRename this sheet to:Report any bugs or issues with WhyteboardResi&ze Canvas...Resize CanvasRetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSelection Handle SizeSelects this shapeSend &FeedbackSend feedback directly to Whyteboard's developerSet the background colorSet the foreground colorSet the volumeSet up the page for printingSets the drawing thicknessSha&pesShape Select Shape ViewerShapes at the top of the list are drawn over shapes at the bottomSheetShortcut Key:Show and hide the color gridShow and hide the status barShow and hide the tool previewShow and hide the toolbarShow the color gridShow the title when printingShow the tool previewSpanishSwap &ColorsSwaps the foreground and background colorsT&ransparentTextThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsToggles the selected shape's transparencyTool &PreviewToolbox View:Translate Whyteboard to your languageTransparentTransparent Bitmap Select (may draw slowly)TypeUnable to load %s: Unsupported format?Undo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView Whyteboard's help documentsView a preview of the page to be printedView and edit the shapes' drawing orderView and replay your drawing historyView information about WhyteboardView the status barView the toolbarWelshWhyteboard Preference FilesWhyteboard doesn't support the filetypeWhyteboard file Whyteboard filesWhyteboard uses ImageMagick to load PDF, SVG and PS files. Please select its installed location.Whyteboard will be translated into %s when restartedWidth:You are running the latest version.You have not set any preferencesYour Feedback:ZoomZoom in and out of the canvashourhourslanguageminuteminutessecondsecondsProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-04-25 22:34+0000 Last-Translator: Steven Sproat Language-Team: Welsh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) Mae "%s" yn colli y ffeil save.data&Ynghylch&Adio Pwynt Newydd&Gosod&DiddymuCliriwch lliniau'r dalen&CauGrid &Lliw&Cynnwys&Copio&Dileu&Dileu Siap&Di-dewis Siap&Peidio â Chadw&GolyguGolygu nodyn&Golygu...&Allforio FfeilAllforio Dalen&Allforio...&Ffeil&Sgrîn Lawn&CymorthGwyliwr &Hanes&Delwedd ...&Mewnforio FfeilDalen &NewyddDalen Nesaf&Iawn&Agor...&GludoDalen Blaenorol&Argraffu&Gadael&Ail-wneud&Ail-enwi DalenAil-&enwi...Adroddy &Problem&CadwD&ewisGolygwr &Siap&DalennauBar &Statws&Bar offerCyfieithu&DadwneudDigwneud Cau y Dalen Olaf&GolwgRhagolwg o'ch teclynSiml bwrdd-gwyn a nodiadwr PDFAdiwch darlen newyddAdio pwynt newydd i'r poligonPob ffeilDigwyddodd gwall - addroddiwch o, os gwelwch yn ddaArabegSaethFel& PDFFfeiliau swnDewis BitmapYn Diddymu...Bordwr CanfasNewid maint yr canfasNewid eich hoffterauChwilio am &DiweddaruDewis Eich LLiwiau ArferDewich Eich IaithDewiswch gyfeiriadurDewis feil cyfrwngCylchClirio &Pob DarlenClirio darlenClirio &Lluniau Pob DarlenClirio pob darlenCliriwch pob darlen, cadw lluniauCliriwch darlen, cadw lluniauCliriwch y darlen cerryntCau y darlen cerryntLliwCysylltu i'r serfwrMethu trosi ffeilAnsawdd TroiTrosi...Copiwch dewisiad fel bitmaCopiwch ardalMethu cysylltu a'r serfwrTsiec&Di-dewisUchder y CanfasLled y CanfasFFont Diffyg:Dilau y siap sydd wedi ei dewisoDi-ddewis y siap sydd wedi ei dewisDi-dewis y siapArlunio cylchArlunio poligonArlunio rheonglArlunio rheongl gyda ymyllau crwnArlunio llinell sythDarllunio saethDarlunio siap hirgwnDarlunio gyda brwsIseldiraiddCyfeiriad E-bostGolygu y testynHirgylchSaesnegMynedi testunDileu yr llun i'r cefndirRwbiwrAdroddiad GwallRoedd camgymeriad gyda safio y ffeil ddataAllforio &Pob DalenGwall by AllforiaAllforio'r ddata i...Allforio pob dalen mewn i ffeil PDFAllforio pop dalen i ffeliau llunAllforio eich hoffiannau i....Allforio yAllforio eich ffeil hoffiannau WhyteboardAllforia eich ddata fel darlun/PDFChwistrell llygadSylw wedi' ArfonHeb ganfod ffeil %sNid oedd ffeil %s mewn y safEnw Ffeil:Chwilio am safle...Chwilio,,,Llenwi LlifoLlenwi Llifo ardalLLiwiau a FfontFfrangegGalisegCyffredinolAlmaenegMynd i'r dalen nesafMynd i'r dalen blaenorolUchder:Nid methu darganfod ffeiliau cymorth. Ydych chi eisiau lawr-llwytho nhw?UchelUchafHindiChwaraewr HanesEiconauAnwybyddu y lliw cefndirLlunFfeiliau DarlunLleoliad ImageMagickHysbysiad ImageMagickNid oedd yn gallu ffeindio ImageMagick. Ni fyddwch yn gallu agor feiliau PDF neu PS nes mae'n sefylduAllforio hoffterau o...Mewnforio ffeiliau gwahanolGosod testunGosod NodGosod cyfrng ac sainFfeil anghywir i allforioEidalaiddSiapanaëegLlinellLlwytho ffeil Whyteboard, llun neu troedigo ffeil PDF/PSLLwytho ffeil hoffiannau WhyteboardFfeil sydd wedi' llythoLlwytho...CyfrwngFfeiliau cyfrwngSymud i &LawrSymud i &FynySymud Siap LawrSymud Siap I &FynySymud Siap I'r &GwaelodSymud Siap I'r &TopSymud i'r &GwaelodSymud i'r &TopSymyd y siap llawrSymyd y siap 'r gwaelodSymyd y siap i'r copaSymyd y siap i fynu&Ffenestr NewyddDarlen NewyddTudalen NesafDim siapau wedi' arleioArferolNodynNod: Mae ansawdd uch yn cymrud mwy amswer i trosiNodiadauNifer o Diweddari Gau DalenauNifer o Colofnau Bocs OfferynNifer o pwyntiau: %sAgor &DiweddarAgor FfeilAgor ffeil...Agor enghraifft Whyteboard newyffHoffte&rauTrosi PDFPDF/PS/SVGGosodiad T&udalenTudalen:TudalennauPastio Testun/LlunPastio llun o'ch bwrdd clip mewn i dalen newyddPastio testun neu llun o'ch bwrrd clip mewn i WhyteboardPastio Llun i Dalen &NewyddNid oedd yr darlun %s yn y safPen-ysgrifennuNewid llis o'r pixel dewisodTeipiwch eich cyfeiriad e-bostAmlochrynPortiwgalegSafle&DewisiadauHoffiannauDalen FlaenorolRha&golwg ArgraffuRhagolwg ArgraffuArgraffu'r dudalen honProblem argraffuPriodweddauAnsawddCau WhyteboardRadiws&Ail-llwytho HoffterauGwyriad DalenFfeiliau wedi' agor yn diweddarPetryalAil-gwneud y last peth a'i dad-gwneudAil-gwneud y last peth a'i dad-gwneudAil-llywtho eich ffeil hoffterauAil-enwi dalenAil-enwi y dalen cerryntAil-enwi y dalen hon i:Adroddi ynrhyw problemau gyda WhytebpardNewid &Maint CanfasNewid maint yr canfasAilgeisioPendrongl CylchRwsiaiddCadw &Fel...Safio DarlunCadw Feil?Cadw Whyteboard fel....Cadwch y ddata WhyteboardCadwch y ddata Whyteboard mewn i ffeil newyddYn cadw...Chwilio am ddiweddariadau i WhyteboardDewis FfontDewisio ardal rheongl i copio fel bitmapDewiswch siap i symyd neu newid ei maintMaint carn dewisoDewis y siap&Arfon SylwArfon sylw i datblygudd WhyteboardDewid y lliw cefndirDewis lliw blaendirNewid y cyfrolGosod y tudalen am argraffuNewid trwch y llunSia&pauDewis Siap Golygwr SiapMae siapau ar top y list yn cael ei arlunio o dros siapau are y gwaelodDalenAllwedd Llwybr Llygad:Dangos a cuddio y grid lliwDangos a cuddio y bar statwsDangos a cuddio y rhagolwg teclynDangos a cuddio y bar offerynDangos y grid lliwDangos yr teitl pryd yn printioDangos y rhagolwg teclynSbaenegSwapio &LLiwiauSwapiwch y lliwiau blaendir a'r cefndir&TryloywTestunRoedd problem gyda argraffu Ydych eich argraffwr wedi ei gosod yn iawn?TrwchTrwch:RhagolygonNewid tryloyw siap sydd wedi' dewis&Rhagolwg TeclynGolwg Bocs Offeryn:Cyfieithu Whyteboard i'ch iaithTryloywBitmap Transparent (efallai arlunio yn araf)MathMethu llywddo %s - fformat di-ymgunnal?Dadwneud y gweithred olafDadwneud yr darlen wedi cau yn olafDadwneud y gweithred olafDideitlDiweddaruDiweddariadauDiweddariadau Hoel-bawdFfeiliau fideoGolwgGweld Whyteboard mewn llewn sgrinGewld documentiau help WhyteboardGweld rhagolwg o'r tudalen i argraffuGweld a newid trefn y siapauGweld eich hanes arlunioGweld gwybodaeth am WhyteboardGweld y bar statwsGweld y bar offerCymraegFfeiliau Hoffiannau WhyteboardDydi Whyteboard ddim yn gallu agor y maeth ffeil ymaFfeil Whyteboard Ffeiliau WhyteboardMae Whyteboard yn defnyddio ImageMagick i llwytho ffeiliau PDF, SVG ac PS. Dewiswch ble mae e yn sefydluBydd Whyteboard yn ei cyfieuthu mewn i %s pryd ail-dechrayLled:Rydych yn rhedeg y versiwn newyddDydych chi ddim wedi newid unrhyw hoffterauEich Sylw:ChwyddoZoomiwch i fewn ac allan o'r canfasawroriauiaithmunudmunudaueiliadeiliadauwhyteboard-0.41.1/locale/es/LC_MESSAGES/whyteboard.mo0000777000175000017500000005325611444706710021171 0ustar stevesteveÞ•T¼ É\€2"´×Þíôü - 9 CMS [i y… ‹™ ¢¯ ÀË ÑÞä ÷   ".2 ;IP `jpv ‡’¤ª²Ã Ë×àöü%9_o ™'®ÖÝ ã î ú   $ 2 J b x Ž © ¿ Ò æ í ÿ  !(!(9!1b!”!¬!½!Ï!ç!í!"" +"9"Q"p"R"à"è" î" ú"## /# =##J#&n#•#ª# ¾#Ì#Û##ì#$ %$3$F$`$f$ u$ƒ$‹$“$ ¢$!­$Ï$ Ö$ã$ú$ %%"/%-R%€%)™%'Ã%%ë% &X& u&ƒ&•& ³& Á&Ë&Ü& ä&ï&('+';'B'K'S'Z'o'ˆ'3'Ä'É' Ñ' ò'þ'(((6( <(H(](]v(Ô(ï( ) )")9)X)`)g)p)x)B})%À) æ) ò)ý) * **#*4*C*S*i*|*Ž* ž*'«*0Ó*-+%2+ X+ d+ n+ y+‡+—+ž+,£+Ð+ Ö+÷+, ', 4, @,M,m,}, Œ, —,£,©,¯,*Ã,:î,)- ?-`-%d-"Š-­- µ-À- É- Ö-â-ñ- ..%. 4.?.G.W.^. r.€.˜. ®.¸.Ô.ó. //6/)L/ v/„/ Š/—/ Ÿ/ «/ ¸/Ã/Ù/&ò/ 0 #0 D0-P0$~0£0¹0 Ì0Ú0ó011 :1 H1U1 [1i1†1 1½1Ð1 Ø1 å1ò1P÷1 H2 R2 ]2 h2 v2%„2 ª2 ¸2Ä2É2Þ2ù233!3)3 =3I3#N3 r3(“3'¼3$ä3! 4+4?4P4V4'r4š4«4a¼445S5 Z5#e5 ‰5ª5¯5Í5Ò5Ø5á5è5ð5÷5œÿ5;œ7&Ø7 ÿ7 88 '818J8R8j8 |8 †8’8š8 ¢8°8 Æ8Ò8Ú8 ê8õ89 9&9/9B9I9 _9j9 |9 †9 ’9 9 ©9³9Å9Ì9 Û9è9ï9ø9 ::,: 5:B:T:[:l:ƒ: ˜:¢:À:%Å:'ë:;"*;M;`;'~;¦;¯; ¶;À;Ó;ì; ô; ÿ; <<;<T<r<!†<¨<¹<Ò<ê<ó< =#=>=9V=4=Å=Ü=ó= > >&>C>V>n>"~>.¡>%Ð>Cö> :?D?J?Y?i?…?¢?»?%Ê?0ð?!@:@K@_@t@/‹@»@Ó@å@ÿ@ A!'AIAYA`AhA ~A‹A£A²A ÅAæABB+(B4TB‰B.¤B1ÓB(C.Cp5C¦C»C%ÔCúCD'D 7DDDMD'_D‡DžD§D¯D·D¿DÖDìD?ôD4E 9E%FElEuE|E”E›E´E»EÎEèEYF_F {FœF­F¿F+ÖFG GGG%GT,G/G±G ÁGÍGÔG èG õGHH+HDHZHqH ‹H˜H6©H=àH%I7DI|I ‹I –I£I¼IÕIÜIMáI/J'5J-]J‹J¡J±JÂJ&ÓJúJ K K(KCKLKUK)jK=”KÒK0êKL%!L,GL tL ~L ‰L “L ¡L ®L¼LÓLñL M M,M4MHMNM eMrMM °M#¼M&àM#N+N#:N^N4vN«N ÁNÌNãNèNùNOO9O/UO …O&’O¹OAÑO0P#DPhP~PP©P"ºPÝPùPQ Q%Q$>Q'cQ$‹Q°QÇQÐQ áQïQoõQeRlR tRRR¼R ÜR êR÷RüR S9S XS dSoSS—SªS+®S)ÚS1T-6T(dT!T¯TÃTÜT%ãT( U2UFUvZU1ÑUV V%V>V\VaV|VV‡VŽV•VV¥Vêu%Èør(÷ƆmÚB·MŠ:¹&?JÕbòU ’ D<©t5…(ÊàW,@Ц&Ý$If3-éŒp¢ý›Ô"žŽ;Ù²QPänˆ3á°®8ÅßÂÛEÞ h“˜¸5NR>ׄF•ï þSBJ9.µÉkŸ–çsìº?À=¼T€”/_â ~[—Gxñy. ÁÜã,QK @o'lAL 0C0í™=Ò´æ`#¿SY4ÇH¤Ñ‘!ùôKqzeZj7v4/)* Ë)‡ðèÓO¯ªi¬ÎFƒM}c^ÍCÌšîÃOLÏE £+PGR<{­:‚!'»³66§"A9D8ë*g2‰$>w±½döóú;\#ÿ2¨N«Ä 1%IT¶Vå‹ a]HÖœõ7¾|Ø¡ ¥1ûü-+X"%s" has corrupt data. This file cannot be loaded."%s" is missing the file save.data&About&Add New Point&Apply&Cancel&Clear Sheets' Drawings&Close&Close All Sheets&Color Grid&Color...&Contents&Copy&Delete&Delete Shape&Deselect Shape&Don't Save&Edit&Edit Note...&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&License&New Sheet&Next Sheet&OK&Open...&PDF Cache...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Shape Viewer...&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAdd a new sheetAdds a new point to the PolygonAll filesAll suppported filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectBoldC&reditsCancelling...Canvas BorderChange the canvas' sizeChange your preferencesCheck for &Updates...Chinese (Traditional)Choose Your Custom Colors:Choose Your Language:Choose a directoryChoose a media fileCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose All SheetsClose every sheetClose the current sheetColorConnecting to server...Conversion FailedConversion Quality:Converting...Copy a Bitmap SelectionCopy a Bitmap Selection regionCould not connect to server.Could not connect to server. Check your Internet connection and firewall settingsCreditsCzechDate CachedDe&selectDefault Canvas HeightDefault Canvas WidthDefault Font:Delete ShapeDelete the currently selected shapeDeselects the currently selected shapeDeselects this shapeDownloaded %s of %sDraw a circleDraw a polygonDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchE-mail AddressEdit the textEllipseEnglishEnglish (U.K.)Enter textErase a drawing to the backgroundEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet into a PDF fileExport every sheet to a series of image filesExport preferences to...Export the current sheet to an image fileExport your Whyteboard preferences fileExport your data files as images/PDFsEyedropperFailed to convert file. Ensure GhostScript is installed http://pages.cs.wisc.edu/~ghost/Feedback SentFile %s not foundFile %s not found in the saveFile LocationFilename:Find location...Find...Flood FillFlood fill an areaFolder "%s" does not contain convert.exeFonts and ColorFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHighlight with a transparent penHighlighterHindiHistory PlayerIconsIgnores the background colorImageImage FilesImageMagick LocationImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Import Preferences From...Import various file typesInput textInsert a noteInsert media and audioInvalid filetype to export as:ItalianItalicJapaneseLicenseLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoad in a Whyteboard preferences fileLoaded fileLoading...MediaMedia FilesMove &DownMove &UpMove Shape &DownMove Shape &UpMove Shape DownMove Shape To &BottomMove Shape To &TopMove Shape To TopMove To &BottomMove To &TopMoves the currently selected shape downMoves the currently selected shape to the bottomMoves the currently selected shape to the topMoves the currently selected shape upNew &WindowNew SheetNext SheetNo Date SavedNo shapes drawnNormalNoteNote: Higher quality takes longer to convertNotesNumber of Recently Closed SheetsNumber of Toolbox Columns:Number of points: %sOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instanceP&references...PDF ConversionPDF/PS/SVGPage Set&upPage:PagesPaste an Image/TextPaste from your clipboard into a new sheetPaste text or an image from your clipboard into WhyteboardPaste to a &New SheetPath for the image %s not found.PenPicks a color from the selected pixelPlease fill out your email addressPolygonPortuguesePositionPrefere&ncesPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePrinting ErrorPropertiesQualityQuit WhyteboardRadiusRe&load PreferencesRe&move SheetRecently &Closed SheetsRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationReload your preferences fileRename sheetRename the current sheetRename this sheet to:Report any bugs or issues with WhyteboardResize CanvasRetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSelection Handle SizeSelects this shapeSend FeedbackSet the background colorSet the volumeSet up the page for printingSets the drawing thicknessShape Select Shape ViewerSheetShortcut Key:Show and hide the status barShow and hide the toolbarShow the title when printingSkip to a positionSpanishSwap &ColorsT&ransparentTextThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsTool &PreviewToolbox View:Translate Whyteboard to your languageTranslated byTransparentTypeUndo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView Whyteboard's help documentsView a preview of the page to be printedView and edit the shapes' drawing orderView and replay your drawing historyView information about WhyteboardView the status barView the toolbarWelshWhyteboard Preference FilesWhyteboard doesn't support the filetypeWhyteboard file Whyteboard filesWhyteboard uses ImageMagick to load PDF, SVG and PS files. Please select its installed location.Whyteboard will be translated into %s when restartedWidth:Written byYou are running the latest version.You have not set any preferencesZoomZoom in and out of the canvashourhourslanguageminuteminutessecondsecondsProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-09-17 15:27+0000 Last-Translator: Steven Sproat Language-Team: Spanish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) "%s" tiene datos corruptos. Este archivo no puede cargarse."%s" no encuentra el archivo save.data&Acerca de&Añade Nuevo Punto&Aplicar&Cancelar&Limpiar dibujos de hoja&Cerrar&Cerrar todas las hojas&Rejilla de Color&Color...&Contenidos&Copiar&Borrar&Borrar forma&Deseleccionar figura&No guardar&Editar&Editar nota...&Editar...&Exportar archivo&Exportar hoja...&Exportar...&Archivo&Pantalla completa&Ayuda&Visor de historia...&Imagen...&Importar Archivo&Licencia&Nueva HojaPróxima Hoja&Aceptar&Abrir...Caché de &PDF...&Pegar&Hoja Anterior&Imprimir...&Salir&Rehacer&Renombrar hoja&Renombrar...&Reportar un problema&Guardar&Seleccionar&Visor de Figuras&Hojas&Barra de estado&Barra de herramientas&Traducir Whyteboard&Deshacer&Deshacer Ultima Hoja Cerrada&VerVista previa de tu herramienta actualUna sencilla pizarra y anotador de PDFsAñadir una nueva hojaAñade un nuevo punto al PolígonoTodos los archivosTodos los archivos soportadosOcurrió un error - informelo por favorArábigoFlechaComo &PDFArchivos de sonidoSeleccionar mapa de bitsNegritaC&réditosCancelando...Borde del lienzoCambia el tamaño del lienzoCambiar sus preferenciasComprobar &actualizaciones...Chino (tradicional)Elija sus colores personalizados:Elija su idioma:Seleccione un directorioElija archivo de mediosCírculoLimpiar &todas las hojasLimpiar &hojaLimpiar &dibujos de todas las hojasLimpiar todas las hojasLimpiar &dibujos de todas las hojas (conservar imágenes)Limpiar dibujos de hoja actual (conservar imágenes)Limpiar la hoja actualCerrar todas las hojasCerrar todas las hojasCerrar la hoja actualColorConectándose al servidor...Conversión FallóCalidad de conversión:Convirtiendo...Copiar una selección mapa de bitsCopiar una región seleccionada a mapa de bitsNo se puede conectar con el servidor.No pudo conectar al servidor. Comprueba la conexión y el firewallCréditosChecoFecha guardadaDes&seleccionarAlto del lienzo por defectoAncho del lienzo por defectoTipografía por defecto:Eliminar formaEliminar la actual forma seleccionadaDeselecciona la forma presentemente seleccionadaDeseleccionar esta formaBajados %s de %sDibujar un círculoDibujar un polígonoDibujar un rectánguloDibujar un rectángulo con esquinas redondeadasDibujar una linea rectaDibuja una flechaDibujar una forma ovaladaDibuja trazos con un pincelNeerlandésDirección de correo electrónicoEditar el textoElipseInglésInglés (Reino Unido)Introd textoBorrar dibujo del fondoGoma de borrarInforme de erroresError guardando datos de archivoExportar &todas las hojas...Error al exportarExportar data a...Exportar cualquier estilo en el archivo PDFExportar cada hoja a una serie de archivos de imagenExportar preferencias a...Exportar la hoja actual a un archivo de imagenExportar tu archivo de preferencias de WhyteboardExporta tus archivos como imágenes/PDFsPipetaFalló la conversión de imágenes. Asegúrate que GhostScript está instalado. http://pages.cs.wisc.edu/~ghost/Información enviadaArchivo %s no encontradoArchivo %s no encontrado en guardadosLocalización de archivosNombre de Archivo:Buscar lugar...Encontrar...RellenarRellenar en áreaLa carpeta "%s" no contiene convert.exeTipografías y coloresFrancésGallegoGeneralAlemánIr a la siguiente hojaIr a la hoja anteriorAltura:No se encontraron los archivos de ayuda, ¿quiere descargarlos?AltaLa más altaMarcar con un bolígrafo transparenteMarcadorHindúReproductor de historiaIconosIgnora el color de fondoImagenArchivos de imagenUbicación de ImageMagickNotificación de ImageMagickNo se ha encontrado ImageMagick. No podrá cargar archivos PDF y PS hasta que se instale.Importar preferencias de...Importar varios tipos de archivoTexto de entradaInsertar una notaInserte medios y audioTipo de archivo no válido a exportar como:ItalianoCursivaJaponésLicenciaLíneaCargue un archivo guardado de Whyteboard, una imagen o convierta un documento PDF/PSCargar en archivo de preferencias de WhyteboardArchivo cargadoCargando...MediosArchivos multimediaMover &AbajoMover &ArribaMover forma &abajoMover figura &ArribaMover figura hacia abajoMover forma al &fondoMover forma al fren&teMover figura al principioMover &abajoMover al fren&teMueve la figura seleccionada presentemente hacia abajoMueve la figura seleccionada actualmente lo mas abajo posibleMueve la forma seleccionada al frenteMueve la figura seleccionada presentemente hacia arribaNueva &VentanaHoja nuevaProxima hojaNo se han guardado datosNo hay figuras dibujadasNormalNotaNota: una calidad más alta tardará más tiempo en realizarse la conversiónNotasNúmero de hojas recientemente cerradasNúmero de columnas de cajas de herramientas:Número de puntos: %sAbrir &RecienteAbrir un ArchivoAbrir archivo…Abre una nueva instancia de WhyteboardP&referencias...Conversión a PDFPDF/PS/SVGConfiguración de Pá&ginaPágina:PáginasPegar imagen o textoPega de su portapapeles en una nueva hojaPega un texto o una imagen desde su portapapeles a WhyteboardPegar a una &nueva hojaNo se ha encontrado el parche para la imagen %s.PlumaToma el color del píxel seleccionadoRellene su dirección de correo electrónicoPolígonoPortuguésPosiciónPrefere&nciasPreferenciasHoja anteriorImprimir vista pre&viaVista previa de la impresiónImprimir la página actualError de impresiónPropiedadesCalidadSalir de WhyteboardRadio&Recargar preferenciasRemover hojaHojas &cerradas recientementeArchivos abiertos recientementeRectánguloRehacer la última acción deshechaRehacer la última operación deshechaRecargar su archivo de preferenciasRenombrar hojaCambiar el nombre de la hoja activaRenombarar esta hoja a:Repoartar cualquier error o problemas con WhyteboardRedimension el LienzoReintentarRectángulo redondeadoRusoGu&ardar como...Guardar dibujo¿Guardar el archivo?Guardar Whyteboard como...Guardar datos de WhyteboardGuardar datos de Whyteboard en un nuevo archivoGuardando...Buscar actualizaciones para WhyteboardSeleccionar tipografíaSeleccionar una región rectangular para copiar como mapa de bitsSeleccione una forma a mover o a redimensionarlaTamaño de manillares de selecciónSelecciona esta formaEnviar ComentarioElegir el color de fondoAjuste el volumeConfigurar página para impresiónAsigna el grosor del dibujoSelección de formas Visor de FigurasHojaTecla de acceso directo:Mostrar y ocultar la barra de estadoMostrar y ocultar barra de herramientasMostrar el título cuando se imprimeSaltar a una posiciónEspañolCamviar %coloresT&ransparenteTextoSe ha detectado un problema en la impresión. ¿Quizás su impresora actual no está correctamente configurada?GrosorGrosor:MiniaturasVista &previa de herramientasVista de caja de herramientas:Traduzca Whyteboard a su idiomaTraducido porTransparenteTipoDeshacer la última acciónDeshacer la última hoja cerradaDeshacer la última operaciónSin títuloActualizarActualizacionesActualizando miniaturasArchivos de VídeoVerVer Whyteboard en modo de pantalla completaVer documentación de ayuda de WhyteboardVer una vista preliminar de la página a imprimirVer y editar el orden de dibujo de las formasVer y reproducir su historial de dibujosVer información sobre WhyteboardVer barra de estadoVer caja de herramientasGalésArchivos de preferencia de WhyteboardWhyteboard no soporta el tipo de archivoArchivo Whyteboard Archivos WhyteboardWhyteboard usa ImageMagick para cargar archivos PDF, SVG y PS. Por favor seleccione la localización de instalación.Whyteboard se traducirá en %s cuando se reinicieAnchura:Escrito porEstá ejecutando la última versión.No ha fijado sus preferenciasZoomAcercar y alejar el lienzohorahorasidiomaminutominutossegundosegundoswhyteboard-0.41.1/locale/de/LC_MESSAGES/whyteboard.mo0000777000175000017500000006160511444706712021151 0ustar stevesteveÞ•wÔ ÷Œh2i"œ¿ÆÕÜòú  + 7 A K Q Y g w ƒ ‰ —   ­ ¾ É Ï Ü â õ ÿ  ! ! !,!0! 9!G!N! ^!h!n!t! …!!¢!¨!°!Á! É!Õ!Þ!ô!ú!""%7"]"n"~" ž"¨"'½"å"ì" ò" ý" ### %# 3#A#,Y#!†#¨#À#Ö#ì#$$0$D$K$ ]$j$†$(—$1À$ò$ %%-%E%K%c%u% ‰%—%¯%Î%Rë%>&F& L& X&b&x& & ›&#¨&&Ì&ó&' '*'9'#J'n' ƒ'‘'¤'¾'Ä' Ó'á'é'ñ' (! (-( 4(A(X( n({("(-°(Þ()÷('!)%I) o)Xz) Ó)á)ó) * *)*:* B*M*(`*‰*™* *©*±*¸*Í*æ*3î*"+'+ /+ P+\+b+q+Ew+½+Ú+ à+ì+,],x,“, ­, ¸,Æ,Ý,ü,- ---"-B'-%j- - œ-§- ­- ¹-Ä-Í-Þ-í-ý-.&.;. M.[. k.'x.0 .-Ñ.%ÿ. %/ 1/ ;/ F/T/d/k/,p// £/Ä/ß/ ô/ 0 00:0J0[0 j0 u00‡00*¡0:Ì01 1>1%B1"h1‹1¨1 °1»1 Ä1 Ñ1Ý1ì1 û1 2 2 /2:2B2R2Y2 m2{2“2 ©2³2Ï2î2 3 3+3D3)Z3„3 –3¤3·3 ½3Ê3 Ò3 Þ3 ë3ö3$ 414&J4 q4 {4 œ4-¨4$Ö4û45$5 350A5r5‹5¤5³5Ð5ë5 ó5 6A6P6 V6d66ž6½6×6ë67717 97*F7 q7~7$ƒ7S¨7Pü7 M8 W8 b8)m8 —8 ¥8%³8 Ù8 ç8+ó89&$9K9b9w9’9ª9³9º9Â9 Ö9â9#ç9 :(,:U:'u:&:$Ä:!é: ;;0;6;'R;z;‹;aœ;4þ;M3<< ˆ<#“< ·<Ø<'ç<==2=7===F=M=U=\=“d=:ø>3?R?Y? q?{? ’?? ±?½? Ô? á?ì? ô? þ?@@*@ ;@G@[@j@|@@Ÿ@ ¦@°@·@É@Ï@â@ ê@õ@A A A $A/A @A LA WAdAyA$ˆA ­A ¸AÄAàA èAöAB %B#3B WB%aB:‡BÂBÓBíB CC*5C`CiC oC {CˆCšC ŸC ¬C·C"ÏC8òC-+DYDoD‡D$¡DÆDÙDðDEE 'E#4EXEGkEL³EFF-FDF^F*dFF¬FÄFÓFëF.üFm+G ™G ¥G ±G¾G!ÐG"òGH(H;HVHkH€H˜H­HÂH&ØHÿHI$I6I HIUIeIuI}I†I –I¤I ÁI ÍI&ÛIJ J(J(BJ4kJ! J,ÂJ#ïJ%K9K[AKK³K)ËK õK L L &L 0L:L.ML|L ”L ¡L «L¶L¾LØLóL8úL3M9M#BM fMqMwM‰MW‘MéMN NN-N~JN ÉNêN OO )O,JO wOƒO ŠO”O›O¢OK¨O+ôO P1PAP HPVP^PdPP˜PµPÒP!ðP Q3QOQgQ2~Q/±Q/áQ1RCR RR]RlR!ƒR¥R¬R?²RòR,úR!'SIS`S sSS$’S·SÆSÛS ìS÷STTT13T@eT¦T(ÃTìT)òT%U5BUxU €UŽU—U¦U·UÇU ÖUäUûU V V'V:VAVYVjV‰V¦V4¯V äVðV W(W;WVW)rWœW"¹WÜWøWXX'X1g±÷“ÔS¢N@Jve• Þ9dKL_jy¿Fû´ò £">×ôqß¡\OÜ3|X, ~gRð2r®Ç :þ¦H0Ò:'YÐ,#!G^Á»(Ú‰üÙŠ6øµ*E†;ª5ÈÉkP&úño0M Z’ìfuïãh­ÖÕ<%+èOv!Laä²zƒw”¨iý)PBt›ÊWå‹ $)4RÑÿb<‡=5¸žoZ/©Ââ—æ¤DáG Dˆçõ éTf¾„`ó]Q{söN8=ëíËIdVXÎê24Ã`8nHišA[6Ï}ASœrÍc+ÄBWÆ"%s" has corrupt data. This file cannot be loaded."%s" is missing the file save.data&About&Add New Point&Apply&Background &Color...&Cancel&Clear Sheets' Drawings&Close&Close All Sheets&Color Grid&Color...&Contents&Copy&Delete&Delete Shape&Deselect Shape&Don't Save&Edit&Edit Note...&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&License&New Sheet&Next Sheet&OK&Open...&PDF Cache...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Shape Viewer...&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAbout WhyteboardAdd a new sheetAdds a new point to the PolygonAll filesAll suppported filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectBoldC&reditsCancelling...Canvas BorderChange the canvas' sizeChange the selected shape's background colorChange the selected shape's colorChange your preferencesCheck for &Updates...Chinese (Traditional)Choose Your Custom Colors:Choose Your Language:Choose a directoryChoose a media fileCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose All SheetsClose every sheetClose the current sheetColorConnecting to server...Conversion FailedConversion Quality:Converting...Copy a Bitmap SelectionCopy a Bitmap Selection regionCould not connect to server.Could not connect to server. Check your Internet connection and firewall settingsCreditsCzechDate CachedDe&selectDefault Canvas HeightDefault Canvas WidthDefault Font:Delete ShapeDelete the currently selected shapeDeselects the currently selected shapeDeselects this shapeDownloaded %s of %sDraw a circleDraw a polygonDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchE-mail AddressEdit the textEllipseEnglishEnglish (U.K.)Enter textErase a drawing to the backgroundEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet into a PDF fileExport every sheet to a series of image filesExport preferences to...Export the current sheet to an image fileExport your Whyteboard preferences fileExport your data files as images/PDFsEyedropperFailed to convert file. Ensure GhostScript is installed http://pages.cs.wisc.edu/~ghost/Feedback SentFile %s not foundFile %s not found in the saveFile LocationFilename:Find location...Find...Flood FillFlood fill an areaFolder "%s" does not contain convert.exeFonts and ColorFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHighlight with a transparent penHighlighterHindiHistory PlayerIconsIf you don't save, changes from the last %s will be permanently lost.Ignores the background colorImageImage FilesImageMagick LocationImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Import Preferences From...Import various file typesInput textInsert a noteInsert media and audioInvalid filetype to export as:ItalianItalicJapaneseLicenseLightLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoad in a Whyteboard preferences fileLoaded fileLoading...MediaMedia FilesMove &DownMove &UpMove Shape &DownMove Shape &UpMove Shape DownMove Shape To &BottomMove Shape To &TopMove Shape To BottomMove Shape To TopMove Shape UpMove To &BottomMove To &TopMoves the currently selected shape downMoves the currently selected shape to the bottomMoves the currently selected shape to the topMoves the currently selected shape upNew &WindowNew SheetNext SheetNo Date SavedNo shapes drawnNormalNoteNote: Higher quality takes longer to convertNotesNumber of Recently Closed SheetsNumber of Toolbox Columns:Number of points: %sOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instanceP&references...PDF Cache ViewerPDF ConversionPDF/PS/SVGPage Set&upPage:PagesPaste an Image/TextPaste from your clipboard into a new sheetPaste text or an image from your clipboard into WhyteboardPaste to a &New SheetPath for the image %s not found.PenPicks a color from the selected pixelPlease fill out your email addressPlease provide some feedbackPolygonPortuguesePositionPrefere&ncesPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePrinting ErrorPropertiesQualityQuit WhyteboardRadiusRe&load PreferencesRe&move SheetRecently &Closed SheetsRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationReload your preferences fileRemove cached itemRename sheetRename the current sheetRename this sheet to:Report any bugs or issues with WhyteboardResi&ze Canvas...Resize CanvasRestore sheet "%s"RetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save changes to "%s" before closing?Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSelection Handle SizeSelects this shapeSend &FeedbackSend FeedbackSend feedback directly to Whyteboard's developerSet the background colorSet the foreground colorSet the volumeSet up the page for printingSets the drawing thicknessSha&pesShape Select Shape ViewerShapes at the top of the list are drawn over shapes at the bottomSheetShortcut Key:Show and hide the color gridShow and hide the status barShow and hide the tool previewShow and hide the toolbarShow the color gridShow the title when printingShow the tool previewSkip to a positionSpanishSwap &ColorsSwaps the foreground and background colorsT&ransparentTextThere are no cached items to displayThere is a new version available, %(version)s File: %(filename)s Size: %(filesize)sThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsToggles the selected shape's transparencyTool &PreviewToolbox View:Translate Whyteboard to your languageTranslated byTransparentTransparent Bitmap Select (may draw slowly)TypeUnable to load %s: Unsupported format?Unable to play file %sUndo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView Whyteboard's help documentsView a preview of the page to be printedView all recently closed sheetsView and edit the shapes' drawing orderView and modify Whyteboard's PDF CacheView and replay your drawing historyView information about WhyteboardView the status barView the toolbarWelshWhyteboard Preference FilesWhyteboard doesn't support the filetypeWhyteboard file Whyteboard filesWhyteboard uses ImageMagick to load PDF, SVG and PS files. Please select its installed location.Whyteboard will be translated into %s when restartedWhyteboard will load these files from its cache instead of re-converting themWidth:Written byYou are running the latest version.You have not set any preferencesYour Feedback:Your feedback has been sent, thank you.ZoomZoom in and out of the canvashourhourslanguageminuteminutessecondsecondsProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-09-17 15:20+0000 Last-Translator: David Language-Team: German MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) "%s" ist beschädigt. Die Datei kann nicht geladen werden."%s" fehlt die Datei save.data&ÜberNeuen Punkt hinzufügenAn&wenden&Hintergrund &Farbe...A&bbrechen&Von vorne anfangenSchl&ießenAlle Seiten schließen&Farbpalette&Farbe ...&Inhalt&Kopieren&LöschenObjekt löschenAuswahl abbrechen&Nicht speichern&BearbeitenNotiz bearbeiten...&Bearbeiten...Datei exportieren&Exportieren Seite&Exportieren...&Datei&Vollbild&Hilfe&Verlauf anzeigen&Bild&Datei importieren&LizenzNeue Seite&Nächstes Seite&OK&Öffnen...&PDF Cache...&Einfügen&Vorherige Seite&Drucken...&Verlassen&Wiederholen&Seite umbenennen...&Umbenennen...Einen Fehler in der Anwendung melden&SpeichernAu&swählenObjektreihenfolge festlegen&Seiten&Statusleiste&WerkzeugleisteDiese Anwendung übersetzen...&RückgängigGeschlossene Seite wiederherstellen&AnzeigenEine Vorschau des aktuellen WerkzeugsEine einfache Whiteboard- und PDF-Kommentierungs-AnwendungÜber WhyteboardEine neue Seite anhängenNeuen Punkt zu Objekt hinzügenAlle DateienAlle unterstützen DateienEin Fehler trat auf - bitte melden Sie ihnArabischPfeilAls &PDF...AudiodateienBitmap auswählenFett&MitwirkendeAbbruch...Rand der ArbeitsflächeGröße der Arbeitsfläche ändernDie Hintergrundfarbe der ausgewählten Zeichnung ändernDie Farbe der ausgewählten Zeichnung ändernEinstellungen ändernNach &Updates suchen...Chinesisch (Traditionell)Benutzerdefinierte Farben auswählenSprache auswählenVerzeichnis auswählenMultimediadatei auswählen...KreisAlle Seiten leerenSeite leerenAlle Seiten und &Zeichnen abklärenAlle Seiten leerenAlle Zeichnungen (auf allen Seiten) entfernen (Bilder bleiben erhalten)Alle Zeichnungen auf der aktuellen Seite entfernen (Bilder bleiben erhalten)Aktuelle Seite leerenAlle Seiten schließenAlle Seiten schließenAktuelle Seite schließenFarbeVerbindung zum Server wird hergestellt ...Konvertierung fehlgeschlagenKonvertierungsqualitätKonvertiere...Bitmap-Auswahl kopierenAuswahl kopierenKonnte keine Verbindung zum Server herstellen.Konnte keine Verbindung zum Server herstellen. Prüfen Sie die Internetverbindung und Firewall EinstellungenMitwirkendeTschechischDatum CachedAuswahl abbrechenStandard-Höhe der ArbeitsflächeStandard-Breite der ArbeitsflächeStandardschriftartZeichnung löschenMarkiertes Objekt löschenMarkierung entfernenMarkierung entfernen%s von %s runtergeladenEinen Kreis zeichnenEin Polygon zeichnenEin Rechteck zeichnenEin Rechteck mit runden Ecken zeichnenGerade Linie zeichnenPfeil zeichnenEin Oval zeichnenFreihand zeichnenHolländischE-Mail AddresseText bearbeitenEllipseEnglischEnglisch (U.K.)Text eingebenBereich löschen/überdeckenRadiergummiFehlerberichtDatei konnte nicht gespeichert werden.Alle Seiten exportierenExport-FehlerDaten exportieren nach...Jede Seite in eine PDF-Datei exportierenExportiere jede Seite zu einer Reihe aus BilddateienEinstellungen exportieren nach...Exportiere aktuelle Seite zu einer BilddateiExportiere Whyteboard-EinstellungenExportiere deine Daten in Bilder/PDFsPipetteKonvertierung fehlgeschlagen. Ist GhostScript installiert? http://pages.cs.wisc.edu/~ghost/Rückmeldung gesendetDatei %s nicht gefundenDie Datei %s konnte nicht gefunden werdenSpeicherortDateiname:Verzeichnis auswählen...Suchen...FarbeimerFläche ausfüllenIm Ordner "%s" befindet sich keine convert.exeSchriftarten und FarbenFranzösischGalizisch&AllgemeinDeutschZur nächsten Seite gehenZur vorherigen Seite gehenHöhe:Hilfe-Datei nicht gefunden, möchten Sie sie downloaden?HöheHöchsteMit einem farblosen Stift aufhellenTextmarkerHindiVerlauf abspielenSymboleWenn sie die Änderungen der letzten %s nicht speichern, gehen sie für immer verloren.Ignoriert die HintergrundfarbeBildBilddateienOrt von ImageMagickImageMagick BenachrichtigungImageMagick wurde nicht gefunden. Es wird nicht möglcih sein PDF- oder Postscript-Dateien zu laden, bevor es installiert ist.Einstellungen importieren aus...Beliebigen Dateityp importierenText einfügenNotiz einfügenAudio- oder Videodatei einfügenUngültiger Dateityp um zu importieren nach:ItalienischKursivJapanischLizenzLeichtLinieEine Whyteboard-Datei oder Bild öffnen, oder ein PDF/PS Dokument umwandelnIn einer Whyteboard Einstellungsdatei ladenGeöffnete DateiWird geladen...MedienMediendateienR&unterH&ochRahmen nach &unten bewegenFigur nach &oben bewegenZeichnung nach unten bewegenFigur zur &Oberseite bewegenFigur zur &Unterseite bewegenZeichnung ganz nach unten bewegenZeichnung ganz nach oben bewegenZeichnung nach oben bewegenNach &unten verschiebenNach &oben verschiebenBewegt die ausgewählte Form eine Ebene nach untenBewegt die ausgewählte Form in den HintergrundBewegt die ausgewählte Form in den VordergrundBewegt die ausgewählte Form eine Ebene nach obenNeues &FensterNeue SeiteNächste SeiteKein Datum gespeichertEs wurden keine Formen gezeichnetNormalNotizAnmerkung: Bei höherer Qualität dauert die Umwandlung längerNotizenAnzahl der kürzlich geschlossenen DokumenteAnzahl Spalten im Werkzeugkasten:Anzahl von Punkten: %s&Zuletzt geöffnetDatei öffnenDatei öffnen...Öffnet eine neue Whyteboard InstanzE&instellungenPDF-Cache BetrachterIn PDF umwandelnPDF/PS/SVGSeite EinrichtenSeite:SeitenEin Bild oder Text einfügenDie Zwischenablage in ein neues Dokument kopierenText oder ein Bild aus der Zwischenablage in Whyteboard kopierenEinfügen in &neues DokumentDer Dateipfad zu %s wurde nicht gefundenStiftNimmt die Farbe des gewählten Pixels aufBitte geben Sie Ihre Email-Adresse anBitte unterstützen Sie Whyteboard mit RückmeldungenPolygonPortugiesischPositionEi&nstellungenVoreinstellungenVorherige SeiteDruck&vorschauDruckvorschauAktuelle Seite druckenFehler beim DruckenEigenschaftenQualitätBeended WhyteboardRadiusEinstellungen neu&ladenBlatt ent&fernenKürzlich &geschlossene SeitenKürzlich geöffnete DateienRechteckLetzte rückgängig gemachte Aktion wiederherstellenWiederholenEinstellungsdatei neu ladenGecachtes Element entfernenTabelle umbenennenAktuelles Blatt umbenennenDieses Blatt umbenennen in:Fehler oder Probleme in Whyteboard meldenGröße der Leinwand ändernGröße der Zeichenfläche ändernSeite "%s" wiederherstellenWiederaufnehmenAbgerundetes RechteckRussischSpeichern &Unter ...Speichern ZeichnungSoll die Datei gespeichert werden?Speichern Whyteboard Unter ...Veränderungen in "%s" vor dem Beenden speichern?Whyteboard-Daten speichernWhyteboard-Daten in neuer Datei speichernSpeichern...Nach Software-Aktualisierungen für Whyteboard suchenSchriftart auswählenMarkiere ein Rechteck um es als Bitmap zu kopierenMarkiere eine Form um zu bewegen und zu skalierenGröße des AuswahlgriffsWählt diese Form&Feedback sendenFeedback sendenVorschlag an die Whyteboard Entwickler sendenHintergrundfarbe auswählenVordergrundfarbe wählenVolumen wählenRichte diese Seite für Druck einDie dicke des Zeichengeräts einstellenFo&rmenAuswahl nach Form Formen ansehenFormen am Anfang der Liste überlagern Formen, die weiter unten stehenBlattTastenkombination:Die Farbpalette ein- und ausblendenStatusleiste ein- und ausblendenDie Werkzeug-Vorschau ein- und ausblendenDie Werkzeugleiste ein- und ausblendenDie Farbpalette anzeigenTitel während des Drucks anzeigenDie Werkzeug-Vorschau anzeigenZu einer Position springenSpanischFarben &vertauschenVertauscht die Vorder- und Hintergrundfarbe&FarblosTextEs gibt keine gecachten Elemente zum AnzeigenEs gibt eine neue Version, %(version)s Datei: %(filename)s Größe: %(filesize)sEs gab ein Problem beim Drucken Vielleicht ist der aktuelle Drucker nicht richtig konfiguriertDickeDicke:MiniaturansichtPasst die Deckkraft der ausgewählten Form anWerkzeug &VorschauAnsicht des Werkzeugkasten:Diese Anwendung übersetzen...Übersetzung vonTransparentTransparente Bitmap-Auswahl (verlangsamt das Zeichnen)Typ%s kann nicht geladen werden: Nicht unterstütztes Dateiformat?Datei %s kann nicht abgespielt werdenRückgängigDas zuletzt geschlossene Dokument WiederherstellenDie letzte Operation rückgängig machenUnbenanntAktualisierenAktualisierungenVorschaubilder werden aktualisiertVideodateienAnsichtVollbild-Modus startenWhyteboards Hilfe aufrufenDruckvorschauAlle kürzlich geschlossenen Seiten betrachtenZeichenreihenfolge der Formen angucken und ändernWhyteboards Cache betrachten und bearbeitenZeichenverlauf angucken und erneut abspielenInformationen über Whyteboard anzeigenStatusleiste zeigenWerkzeugleiste zeigenWalisischWhyteboard EinstellungsdateienWhyteboard unterstützt den Dateityp nichtWhyteboard-Datei Whyteboard-DateienWhyteboard benutzt ImageMagick um PDF-, SVG- und PS-Daten zu laden. Bitte wähle das Verzeichnis aus, in dem es installiert ist.Whyteboard wird in %s übersetzt nachdem es neu gestartet wurdeWhyteboard wird diese Datein aus dem Cache laden, anstatt sie erneut zu konvertiren.Breite:Programm vonSie benutzen die neueste Version.Sie haben keine Einstellungen vorgenommenIhr Feedback:Ihr feedback wurde versandt, Vielen Dank.ZoomArbeitsfläche zoomenStundeStundenSpracheMinuteMinutenSekundeSekundenwhyteboard-0.41.1/locale/it/LC_MESSAGES/whyteboard.mo0000777000175000017500000005726311444706711021201 0ustar stevesteveÞ•gT ߌ2 "<_fu|„œ £ ¯ ¹ÃÉ Ñß ïû  % 6A GTZ m w„  ˜¤¨ ±¿Æ Öàæì ý   ( 9 A M V l r Š  %¯ Õ å !!'$!L!S! Y! d! p!~!ƒ! Œ! š!¨!À!Ø!î!""5"H"\"c" u"‚"ž"(¯"1Ø" #"#:#@#X#j# ~#Œ#¤#Ã#Rà#3$;$ A$ M$W$m$ ‚$#$&´$Û$ ð$þ$ %#%B% W%e%x%’%˜% §%µ%½%Å% Ô%!ß%& &&,& B&O&"a&-„&²&)Ë&'õ&%' C'XN' §'µ'Ç' å' ó'ý'( (!((4(](m(t(}(…(Œ(¡(º(3Â(ö(û( ) $)0)6)E)EK)‘)®) ´)À)Õ)]î)L*g* * Œ*š*±*Ð*Ø*ß*è*ð*Bõ*%8+ ^+ j+u+ {+ ‡+’+›+¬+»+Ñ+ä+ ô+',0),-Z,%ˆ, ®, º, Ä, Ï,Ý,í,ô,,ù,&- ,-M-h- }- Š- –-£-Ã-Ó-ä- ó- þ- ...**.:U.. ¦.Ç.%Ë."ñ./1/ 9/D/ M/ Z/f/u/ „/’/©/ ¸/Ã/Ë/Û/â/ ö/00 20<0X0w0”0 §0´0Í0)ã0 1 1-1 31@1 H1 T1 a1l1$‚1§1&À1 ç1 ñ1 2-2$L2q2‡2š2 ©20·2è233)3F3a3 i3 w3A„3Æ3 Ì3Ú3÷3434M4a4~4”4§4 ¯4*¼4 ç4ô4$ù4P5 o5 y5 „5)5 ¹5 Ç5%Õ5 û5 6+6A6&F6m6„6™6´6Ì6Õ6Ü6ä6 ø67# 7 -7(N7w7'—7&¿7$æ7! 8-8A8R8X8't8œ8­8a¾84 9MU9£9 ª9#µ9 Ù9ú9' :1:6:T:Y:_:h:o:w:~:œ†:;#<"_<‚<ˆ<¡<ª<³<Ò<Ú< ô<ÿ<= ==&= 9= F=P= _=l=u= …=‘=—=§=­= Ì= Ù=ç= ð=þ=>> >,>5> H>S> Y> e> r>>”> ›>¦>¹>Á>Ñ>è>ü>%? +?%7?6]?”?#¥? É?Ö?3î?"@(@ 0@ >@I@ Y@c@s@ Œ@#™@½@Ô@ð@AA1A!HAjArAŠA+¥A5ÑAABEIB-B½B×B!ÞBCC2CACaC"C`¢CDDD (D5D!UDwD‡D¤D!ÂDäDýDE)'EQEiE}E‘E§E°EÀEÒEÚEâEøE FF F0FGF`FwF †F>§FæF+ùF-%G"SG vGiGëGH0HMH_HnHtH }H‰H'˜HÀHÓHÜHäHíHõHI+I;3IoItI#|I  I®I´IÏIRÕI(JCJ LJ$ZJJw”J K'(KPKeK yK$šK¿KÈK ÐKÛKãKIéK'3L [LiLxLŽL ŸL ­L »L ÈLÕLéLÿLM +M(LM*uM! MÂM ÒMßMñMN-N5NO:NŠN#N1³NåN ùN O O"OAOPOoO OŠOšO¢O©O#ÂO4æOP,4PaP"gP.ŠP"¹PÜP åP ðP úP QQ#Q8QLQfQ zQ…QŽQ¡Q¨Q½QÍQ çQ ôQÿQ%RŽSÍS+èST4#TXTBlT1¯T!áTUU&U"5UXU"tU—U¨U¿UÝUäUüUJV\V cV2qV#¤V2ÈV(ûV)$WNW)lW–W­W¶W,ÈW õWX3Xj:œÂ1´P^Áöhz‰0µÇ! gRw€&†ýU Ÿ˜dI1xm@|øÞF·õ]‹À% ükúÝKTfRU5 O ?p A,©jë?Z^þ ~å3¤ ¥ºPge§ä[2ž 64;`—âH2DŽN<ÿ3ŒXelQÌãÉ\ÒdÙB£Ãc¸Lès’.=-±ØóšÔÑ[²{$0c!TG¶KB7Ï.ôÖùˆi¡Y‡”÷V+×ÛL_aéêÜA 8FZ/½X'ûíG…ÆyOƒìr™¼‚n­ È ð°Y)Jàï8"î`]ËÎ »¦‘6Ú›@5¾QßMN-aS*EÍÊfШ«,ò\(C“<Š:+b®S)–_¯³V7t>49„¿ª%}Içq9ÄÓo/="•Mu¬JCbvED;W"%s" has corrupt data. This file cannot be loaded."%s" is missing the file save.data&About&Add New Point&Apply&Cancel&Clear Sheets' Drawings&Close&Color Grid&Color...&Contents&Copy&Delete&Delete Shape&Deselect Shape&Don't Save&Edit&Edit Note...&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&License&New Sheet&Next Sheet&OK&Open...&PDF Cache...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Shape Viewer...&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAdd a new sheetAdds a new point to the PolygonAll filesAll suppported filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectBoldC&reditsCancelling...Canvas BorderChange the canvas' sizeChange your preferencesCheck for &Updates...Chinese (Traditional)Choose Your Custom Colors:Choose Your Language:Choose a directoryChoose a media fileCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose the current sheetColorConnecting to server...Conversion FailedConversion Quality:Converting...Copy a Bitmap SelectionCopy a Bitmap Selection regionCould not connect to server.Could not connect to server. Check your Internet connection and firewall settingsCreditsCzechDate CachedDe&selectDefault Canvas HeightDefault Canvas WidthDefault Font:Delete the currently selected shapeDeselects the currently selected shapeDeselects this shapeDraw a circleDraw a polygonDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchE-mail AddressEdit the textEllipseEnglishEnglish (U.K.)Enter textErase a drawing to the backgroundEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet into a PDF fileExport every sheet to a series of image filesExport preferences to...Export the current sheet to an image fileExport your Whyteboard preferences fileExport your data files as images/PDFsEyedropperFailed to convert file. Ensure GhostScript is installed http://pages.cs.wisc.edu/~ghost/Feedback SentFile %s not foundFile %s not found in the saveFile LocationFilename:Find location...Find...Flood FillFlood fill an areaFolder "%s" does not contain convert.exeFonts and ColorFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHighlight with a transparent penHighlighterHindiHistory PlayerIconsIf you don't save, changes from the last %s will be permanently lost.Ignores the background colorImageImage FilesImageMagick LocationImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Import Preferences From...Import various file typesInput textInsert a noteInsert media and audioInvalid filetype to export as:ItalianItalicJapaneseLicenseLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoad in a Whyteboard preferences fileLoaded fileLoading...MediaMedia FilesMove &DownMove &UpMove Shape &DownMove Shape &UpMove Shape To &BottomMove Shape To &TopMove To &BottomMove To &TopMoves the currently selected shape downMoves the currently selected shape to the bottomMoves the currently selected shape to the topMoves the currently selected shape upNew &WindowNew SheetNext SheetNo Date SavedNo shapes drawnNormalNoteNote: Higher quality takes longer to convertNotesNumber of Recently Closed SheetsNumber of Toolbox Columns:Number of points: %sOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instanceP&references...PDF Cache ViewerPDF ConversionPDF/PS/SVGPage Set&upPage:PagesPaste an Image/TextPaste from your clipboard into a new sheetPaste text or an image from your clipboard into WhyteboardPaste to a &New SheetPath for the image %s not found.PenPicks a color from the selected pixelPlease fill out your email addressPlease provide some feedbackPolygonPortuguesePositionPrefere&ncesPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePrinting ErrorPropertiesQualityQuit WhyteboardRadiusRe&load PreferencesRe&move SheetRecently &Closed SheetsRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationReload your preferences fileRemove cached itemRename sheetRename the current sheetRename this sheet to:Report any bugs or issues with WhyteboardResi&ze Canvas...Resize CanvasRetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save changes to "%s" before closing?Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSelection Handle SizeSelects this shapeSend &FeedbackSend FeedbackSend feedback directly to Whyteboard's developerSet the background colorSet the foreground colorSet the volumeSet up the page for printingSets the drawing thicknessSha&pesShape Select Shape ViewerShapes at the top of the list are drawn over shapes at the bottomSheetShortcut Key:Show and hide the color gridShow and hide the status barShow and hide the tool previewShow and hide the toolbarShow the color gridShow the title when printingShow the tool previewSkip to a positionSpanishSwap &ColorsSwaps the foreground and background colorsT&ransparentTextThere are no cached items to displayThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsToggles the selected shape's transparencyTool &PreviewToolbox View:Translate Whyteboard to your languageTranslated byTransparentTransparent Bitmap Select (may draw slowly)TypeUnable to load %s: Unsupported format?Unable to play file %sUndo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView Whyteboard's help documentsView a preview of the page to be printedView all recently closed sheetsView and edit the shapes' drawing orderView and modify Whyteboard's PDF CacheView and replay your drawing historyView information about WhyteboardView the status barView the toolbarWelshWhyteboard Preference FilesWhyteboard doesn't support the filetypeWhyteboard file Whyteboard filesWhyteboard uses ImageMagick to load PDF, SVG and PS files. Please select its installed location.Whyteboard will be translated into %s when restartedWhyteboard will load these files from its cache instead of re-converting themWidth:Written byYou are running the latest version.You have not set any preferencesYour Feedback:Your feedback has been sent, thank you.ZoomZoom in and out of the canvashourhourslanguageminuteminutessecondsecondsProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-09-17 15:23+0000 Last-Translator: Steven Sproat Language-Team: Italian MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) %s contiene dati corrotti. Il file non può essere caricato"%s" il file save.data è mancante&Info&Aggiungi un Nuovo Punto&Applica&AnnullaCancella &Tutte le Annotazioni&Chiudi&Griglia Selezione Colore&Colore...&Guida&Copia&EliminaCancella &FormaAnnu&lla Selezione&Non salvare&ModificaModifica &Nota&Modifica...&EsportaEsporta &Pagina&Esporta...&FileSchermo &Intero&HelpVisualizzatore di Se&quenza...&Immagine...&Importa File&Licenza&Nuova PaginaPagina &Successiva&OK&Apri...&PDF Cache...&IncollaPagina &PrecedenteS&tampa...&Esci&RipristinaRino&mina...&Rinomina...Segnala un &Problema&Salva&Seleziona&Ordina Oggetti...Pa&gineBarra di &Stato&Barra degli Strumenti&Traduci Whyteboard&AnnullaRipristina L'&ultima Pagina Eliminata&Visualizzaanterpima dello strumento selezionatoUna semplice lavagna interattiva -con annotazione PDF-A&ggiungi PaginaAggiunge un nuovo punto al poligonoTutti i FileTutti i file supportatiSi è verificato un errore - si prega di segnalarloAraboFrecciaCome &PDF ...File audioSelez. ritaglioGrassetto&RiconoscimentiAnnullamento in corso...Bordi paginaModifica le dimensioni della paginaModifica le preferenzeControlla &Aggiornamenti...Cinese (Tradizionale)Colore personalizzatoScelta della lingua:Scegliere una cartellaSeleziona un inserto multimedialeCerchioA&zzera Tutte le PagineAzzera la Pagina &CorrenteCancella le &Annotazioni da Tutte le PagineRipulisci tutte le pagine (ripristina il solo sfondo)Cancella le annotazioni da tutte le pagine (mantieni le immagini)Cancella le annotazioni dal foglio corrente (ma mantiene le immagini)Azzera la pagina corrente (riporta lo sfondo)Chiudi la pagina correnteColoreConnessione al server in corso...Conversione file fallitaQualità di conversione:Conversione...Copia una selezione come bitmapCopia bitmap di una selezioneImpossibile connettersi al server.Connessione al server impossibile. Controllare la connessione internet e i settaggi del firewallRiconoscimentiCecoData memorizzata&DeselezionaAltezza di default della paginaLarghezza di default della paginaFont di defaultElimina la forma selezionataannulla la selezione correnteAnnulla la selezione dell'oggettoTraccia un circonferenzaTraccia un poligonoDisegna un rettangoloDisegna un rettangolo (bordi arrotondati)Traccia una linea rettaDisegna una frecciaTraccia una ellisseDisegna a mano liberaOlandeseIndirizzo eMailModifica il testoEllisseIngleseInglese (Regno Unito)Immettere il testoCancella tuttoGommaNotifica erroreErrore nel salvataggioEsporta &Tutte le PagineErrore di esportazioneEsporta dati aEsportazione in PDF delle pagineEsportazione seriale di tutte le pagine come serie di immaginiEsporta preferenzeEsporta il foglio selezionato come immagineEsporta il file delle preerenze di WhyteboardEsporta i dati come immagine o pdfSel.coloreFallita conversione del file. Assicurarsi che Ghostscript sia installato http://pages.cs.wisc.edu/~ghost/il feedback è stato inviatoFile %s non trovatoil file %s non e' disponibile per il salvataggioPercorso del fileNome del file:TrovaTrova...RiempimentoRiempi un'areaLa cartella %s non contiene convert.exeCaratteri e coloriFranceseGallegoGeneraleTedescoVai alla pagina successivaVai alla pagina precedenteAltezzaFile guida non disponibile. Si vuole procedere al download?AltaMassimaEvidenzia con un tratto trasparenteEvidenziatoreHindiVisualizzatore di sequenzeIconeSe non si procede con il salvataggio, le modifiche dall'ultimo %s andranno perduteIgnora il colore di sfondoImmagineFile immaginepersorso dell'eseguibile ImagemagickNotifica ImageMagickImageMagick non risulta presente sul sistema. Questo non permette di caricare file PDF e PS fino alla sua installazioneImportare preferenze da ...Importazione di diversi formati di fileInserimento di testoInserimento di notaInserisci elemento video o audioEsportazione di formati errati come:ItalianoItalicoGiapponeseLicenzaLineaCarica un file whyteboard salvato, un'immagine, o converti un file PDF/PSCarica in file di preferenze WhyteboardFile caricatoCaricamento...Elemento multimedialeFile audio-videoSposta &SottoSposta S&opraPorta &SottoPorta S&opraPorta S&ulla SfondoPorta in &Primo PianoSposta Sullo S&fondoSposta in &Primo PianoPorta sotto la forma selezionataPorta l'oggetto selezionato sullo sfondoPorta l'oggetto selezionato in primo pianoPorta sopra l'oggetto selezionatoNuova &FinestraNuova paginaPagina SuccessivaLa data non è stata salvataSulla pagina non ci sono formeNormaleNotaAttenzione: la qualità più elevata impone tempi più lunghi nella conversioneNotePagine chiuse di cui tenere memoriaNumero delle colonne per la barra degli strumentiNumero di punti: %sApri &RecentiApri fileApri file...Apre una nuova sessione WhyteboardP&referenze...Visualizzatore della Cache PDFConversione PDFPDF/PS/SVG&Imposta PaginaPagina:PagineIncolla testo o immagineIncolla appunti su una nuova paginaIncolla su Whyteboard testo o immagine dagli appuntiIncolla su &Nuova PaginaIl percorso dell'immagine %s non e' correttoPennaScelta di un colore personalizzatoInserire per favore il proprio indirizzo emailSei invitato a fornire un feedbackPoligonoPortoghesePosizionePreferen&zePreferenzePagina PrecedenteAntepri&ma di StampaAnteprima di stampaStampa la pagina correnteErrire nella stampaProprietàQualitàEsci da WhyteboardRaggioRi&carica PreferenzeRimuo&vi PaginaPagine chiuse di &recenteFile recentiRettangoloRipeti l'ultima azioneRipristina l'ultima azione cancellataRicarica preferenze da fileRimuovi gli oggetti dalla cacheRinomina paginaRinomina la pagina correnteRinomina la pagina come:Rileva errori e malfunzionamenti di WhyteboardRi&dimensiona la PaginaRidimensiona la pagina...RiprovaRett. (arrot.)RussoSalva con &Nome...Salva le annotazioniSalvare il file?Salva Whyteboard con nomeSalvare le modifiche apportate a %s prima di chiudere il file?Slava i dati di WhyteboardSalva i dati di Whyteboard in un nuovo fileSalvataggio...Controlla la presenza di aggiornamenti del programmaSeleziona carattereSeleziona una sezione rettangolare da copiare bitmap negli appuntiSeleziona una forma per muoverla o ridiensionarlaSpessore dei simboli di selezioneSeleziona la formaInvia &FeedbackInvia feedbackInvia feedback al team di sviluppoImposta il colore di sfondoDefinisci il colore in primo pianoRegola il volumeImpostazioni di stampaScegli lo spessore del trattoFo&rmeSelezione di una forma Gestione delle formeLe forme più in alto nella lista vengono poste sopra le forme sottostantiPaginaTasti rapidi:Mostra/Nascondi la griglia di selezione dei coloreMostra / Nascondi la barra di statoMostra/Nascondi la griglia di selezione dei coloreMostra/nascondi la barra degli strumentiMostra la griglia di selezione dei coloreMostra il titolo nella stampaMostra la griglia di selezione dei colorePassa ad una posizioneSpagnolo&Inverti i ColoriInverte i colori di sfondo e di tracciamentoTra&sparenteTestoNon ci sono oggetti in cache per la visualizzazioneC'è stato un problema con la stampa. Possibile che la stampante corrente non sia impostata correttamente?SpessoreSpessore:MiniatureApplica/togli la trasparenza alla forma selezionataStrumento &AnteprimaVista della barra degli strumentiTraduci Whyteboard nella tua linguaTradotto daTrasparenteSelezione bitmap trasparente (può comportare rallentamenti)Tipo:Impossibile caricare %s: probabile formato sconosciuto o non supportatoLa visualizzazione del file %s è impossibileAnnullla l'ultima azioneRipristina l'ultima pagina eliminataAnnulla l'ultima operazioneSenza titoloAggiornaAggiornamentiAggiornamento miniatureFile videoVisualizzaVista a pieno schermoAccedi alla documentazione di WhyteboardAnteprima di stampa della pagina correnteVista di tutte le pagine recenti chiuseVedi e modifica l'ordine delle forme presenti sulla paginaVisualizza e modifica la cache PDF di WhyteboardVista in sequenza delle annotazioni fatteInformazioni sul programmaVisualizza la barra di statoVisualizza la barra degli strumentiGalleseFile delle preferenze WhyteboardWhyteboard non supporta questo formatoFile di Whyteboard File di WhyteboardWhyteboard utilizza ImageMagick per caricare file PDF, SVG e PS. Selezionare il percorso del file eseguibile.Whyteboard sarà tradotto in %s al prossimo riavvioWhyteboard procederà a caricare i file dalla propria cache anziché procedere con la riconversioneAmpiezza:Scritto daE' in uso la versione più aggiornataNon hai specificato alcuna preferenzaIl tuo feedbackIl vuostro commento è stato inviato. Grazie,ZoomZoom sulla paginaoraorelinguaminutominutisecondosecondiwhyteboard-0.41.1/locale/nl/LC_MESSAGES/whyteboard.mo0000777000175000017500000003066311444706707021176 0ustar stevesteveÞ•Ó´L °"±ÔÛâê   ) :E KX^ q { ˆ “Ÿ£¬³ ÃÍÓÙ êõ  )2HNfl%‹± ÁËÒ Ø ä ò.I_ry ‹˜´(Å1î 8PVn ‚¯Ì Ò àî#ÿ# 8FYsy ‰” ›¨¿ Õâ-ô)" L W eo€ ˆ“£ª³»Â×ð3ø,19?NT Zf] Ý èö&B+ ny  ‹– Ÿ « µÀÇÌ Ò ó  9 HT*Z…›%ŸÅ ÍØ áíü  0;CSZ pz– µÂÛ ñÿ   & 3>T&m ” ž ¿-Ë$ù 9GMj„ŒP‘ â ì ÷% (49NiŠ‘™ ­¹#¾$â!)'/Wh#o“˜š¡'º¼« =ŠÉ­ &¨¯¡TšŽI§a;±_Ȇµ1{S° £®lr!“ "%s" is missing the file save.data&About&Apply&Cancel&Clear Sheets' Drawings&Contents&Copy&Delete&Edit&Edit...&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&New Sheet&Next Sheet&OK&Open...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAdd a new sheetAll filesArabicArrowAudio FilesBitmap SelectCancelling...Change your preferencesCheck for &Updates...Choose Your Custom Colors:Choose Your Language:Choose a directoryCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose the current sheetColorConnecting to server...Conversion Quality:Converting...Copy a Bitmap Selection regionCould not connect to server.CzechDefault Font:Draw a circleDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchEllipseEnglishEnter textEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet to a series of image filesExport the current sheet to an image fileEyedropperFile LocationFilename:Find location...Find...Flood FillFonts and ColorFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHindiHistory PlayerIconsImageImage FilesImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Input textInsert a noteInvalid filetype to export as:ItalianJapaneseLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoading...MediaMedia FilesMove &DownMove &UpNew &WindowNew SheetNext SheetNormalNoteNotesNumber of Recently Closed SheetsOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instancePDF ConversionPage Set&upPage:Paste from your clipboard into a new sheetPaste to a &New SheetPenPicks a color from the selected pixelPolygonPortuguesePositionPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePropertiesQualityQuit WhyteboardRadiusRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationRename sheetRename the current sheetRename this sheet to:Resize CanvasRetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSets the drawing thicknessShape Select SheetShow and hide the status barShow and hide the toolbarSpanishTextThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsTranslate Whyteboard to your languageTransparentTypeUndo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView and replay your drawing historyView information about WhyteboardWelshWhyteboard doesn't support the filetypeWhyteboard file Width:You are running the latest version.ZoomlanguageProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-04-06 22:56+0000 Last-Translator: Steven Sproat Language-Team: Dutch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) "%s" ontbreekt in het bestand save.data&Over&Toepassen&Annuleren&Wis alle aantekeningen van dit blad&Inhoud&KopiërenVerwij&deren&BewerkenBe&werken...&Exporteer Blad...&Exporteren...&Bestand&Volledig Scherm&Help&Bekijk Geschiedenis...&Afbeelding...&Importeer Bestand&Nieuw Blad&Volgend Blad&OK&Openen...&Plakken&Vorig BladAf&drukken...&Afsluiten&Opnieuw&Hernoem Blad...He&rnoemen...&Meld een Probleem&Opslaan&Selecteren&Bladen&Statusbalk&Werkbalk&Vertaal Whyteboard&Ongedaan maken&Terughalen Laatstgesloten Blad&BeeldEen voorbeeld van uw huidige gereedschapEen simpele whiteboard en PDF annotatorVoeg een nieuw blad toeAlle bestandenArabischPijlGeluidsbestandenSelecteer BitmapAnnuleren...Uw voorkeuren wijzigenControleer op &Updates...Kies uw eigen kleurenKies uw taalKies een mapCirkel&Alle Bladen LeegmakenMaak blad &leegWis Alle &BladtekeningenMaak alle bladen leegVerwijder alle bladtekeningen (behoud afbeeldingen)Verwijder alle bladtekeningen op het huidige blad (behoud afbeeldingen)Maak het huidige blad leegSluit het huidige bladKleurMaakt verbinding met de server...ConversiekwaliteitConverteren...Kopieer de Bitmap-selectieKon geen verbinding met de server maken.TsjechischStandaard lettertype:Teken een cirkelTeken een rechthoekTeken een rechthoek met afgeronde hoekenTeken een rechte lijnTeken een pijlTeken een ovaalTeken stroken met een kwastNederlandsEllipsEngelsTekst invoerenGumFoutenrapportFout bij opslaan van bestandExporteer &Alle Bladen...ExportfoutExporteer data naar...Exporteer elk blad naar een series van afbeeldingenExporteer het huidige blad naar een afbeeldingsbestandPipetBestandslocatieBestandsnaam:Vind locatie...Zoeken...VloedvullingLettertypen en kleurFransGaliciaansAlgemeenDuitsGa naar het volgende bladGa naar het vorige bladHoogte:Helpbestanden niet gevonden; wilt u ze downloaden?HoogHoogsteHindoestaansGeschiedenis AfspelerPictogrammenAfbeeldingAfbeeldingbestandenImageMagick NotificatieImageMagick is niet op uw systeem gevonden. Het is onmogelijk om PDF en PS bestanden te laden totdat het programma geïnstalleerd is.TekstinvoerVoeg een notitie toeOngeldig bestandstype om te exporteren als:ItaliaansJapansLijnLaad een opgeslagen Whyteboard bestand, een afbeelding of converteer een PDF/PS documentBezig met laden...MediaMediabestandenOmlaag ve&rplaatsenOmhoog ver&plaatsenNieuw &VensterNieuw BladVolgend BladNormaalNotitieNotitiesAantal recent gesloten bladenOpen &RecenteEen bestand openenBestand openen...Opent een nieuwe Whyteboard instantiePDF ConversiePagina-&instellingPagina:Plak van uw klembord in een nieuw bladPlak in een &Nieuw BladPenKies de kleur van de geselecteerde pixelVeelhoekPortugeesPositieVoorkeurenVorig BladAfdruk &VoorbeeldAfdruk VoorbeeldDruk de huidige pagina afEigenschappenKwaliteitWhyteboard afsluitenStraalRecent geopende bestandenRechthoekLaatst ongedaan gemaakte actie opnieuw uitvoerenMaak de laatste actie ongedaanBlad hernoemenHet huidige blad hernoemenDit blad hernoemen naar:Herschaal CanvasOpnieuw proberenAfgeronde rechthoekRussischOpslaan &Als...Sla tekening opBestand Opslaan?Sla Whyteboard op als...Sla de Whyteboard data opSla de Whyteboard data op in een nieuw bestandOpslaan...Zoek naar updates voor WhyteboardSelecteer LettertypeMaak rechthoekige selectie om te kopieren als BitmapSelecteer een vorm om te bewegen en te herschalenBepaalt de lijndikteVorm Selecteren BladVerberg en toon de status balkVerberg en toon de werkbalkSpaansTekstEr was een probleem tijdens het printen. Misschien is uw huidige printer niet goed ingesteld?DikteDikte:ThumbnailsVertaal Whyteboard naar uw taalTransparantTypeLaatste actie ongedaan makenBlad sluiten ongedaan makenDe laatste bewerking ongedaan makenNaamloosBijwerkenUpdatesUpdaten van Thumbnails...VideobestandenWeergaveLaat Whyteboard in volledige scherm modus zienLaat de geschiedenis van deze tekening zienBekijk informatie over WhyteboardWelshWhyteboard ondersteunt het bestandstype nietWhyteboard bestand Breedte:U gebruikt momenteel de meest recente versie.Zoomentaalwhyteboard-0.41.1/locale/cs/LC_MESSAGES/whyteboard.mo0000777000175000017500000001414711444706713021166 0ustar stevesteveÞ•t¼\Ð Ñ Ø ç î ö ý    ! ' 5 > D Q W j t  … Ž • Ÿ ¥ « ¶ È Î Ö â ë    , L V 'k “ š   « · Å Ó ë   * 1 7 O ] z € ‘ ¦ ¹ ¿ Ç Ï Ú á ì ó û      % . 3 > J T [ ` f r  ‹ ‘ • ¡ ° ¾ Õ ß ø    #/5=PB “ ž©ÁÊÑÙí#ò!8L]'c‹œ£ AM `j s} „  › §± ÇÓÛ ìø 4 8F O \fn“œ¤¶Ëâ éõ&;L#j Ž™  ­ ¾ ÌÙò#5:C_p•¨ ¾ ÌÙ à ì÷ü &/ 7C I T`h x „  𠤮¿Ó èò ÷- LWsŒ•¦ ¶ÄÉØCÝ !-6 P [ ht+–Ââü (!J ]`ms!YW, .'d^:"aLjnC=I(t\k#*R;P%TJO@AK- 4 N[U<2]_6c1befqH39VroM0X&F QBi8Eh/g 5) pZ7G+lS$?D>&About&Add New Point&Apply&Cancel&Close&Contents&Copy&Delete&Don't Save&Edit&Edit Note...&Edit...&File&Full Screen&Help&History Viewer...&Image...&Import File&OK&Open...&Paste&Print...&Quit&Redo&Rename...&Report a Problem&Save&Select&Status Bar&Toolbar&Translate Whyteboard&Undo&ViewA preview of your current toolAdds a new point to the PolygonAll filesAll suppported filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectCancelling...Change your preferencesCheck for &Updates...Choose Your Language:Choose a directoryCircleColorConnecting to server...Converting...Could not connect to server.CzechDraw a rectangleDraw a straight lineDraw an oval shapeDutchEllipseEnglishEnter textEraserEyedropperFrenchGeneralGermanHeight:HighHighestIconsItalianJapaneseLineLoading...New &WindowNew SheetNormalNoteNotesOpen a FileOpen file...Page Set&upPage:PenPreferencesPrint Pre&viewPrint PreviewPrint the current pageRectangleRename the current sheetResize CanvasRetrySave &As...Save File?Select FontSheetSpanishTextThere was a problem printing. Perhaps your current printer is not set correctly?Thickness:ThumbnailsUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsViewView Whyteboard in full-screen modeView information about WhyteboardView the status barView the toolbarWelshWhyteboard doesn't support the filetypeWhyteboard file Width:Project-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-04-26 18:08+0000 Last-Translator: Lukáš Machyán Language-Team: Czech MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) &O aplikaciPÅ™id&at nový bod&Použít&ZruÅ¡it&ZavřítO&bsah&KopírovatO&dstranitNeuklá&dat&Editovat&Upravit poznámku...&Upravit...&Soubor&Celá obrazovka&NápovÄ›da&Zobrazení historie...&Obrázek...&Importování souboru&OK&Otevřít...&Vložit&Tisknout...&UkonÄit&VpÅ™ed&PÅ™ejmenovat...&Nahlásit problém&Uložit&Vybrat&Stavový řádek&Nástrojová liÅ¡ta&PÅ™eložit Whyteboard&ZpÄ›t&ZobrazeníNáhled aktuálního nástrojePÅ™idává nový bod k mnohoúhelníkuVÅ¡echny souboryVÅ¡echny podporované souboryNastala chyba - nahlaste ji prosímArabÅ¡tinaÅ ipkaJako &PDF...Zvukové souboryVybrat bitmapRuší se...ZmÄ›na svého nastaveníKontrolovat &Aktualizace...Vyberte svůj jazyk:Vyberte adresářKruhBarevnýPÅ™ipojování k serveru...PÅ™evádÄ›ní...Nelze se pÅ™ipojit k serveruÄŒeskýKreslit obdélníkKreslit rovnou ÄáruKreslit oválHolandÅ¡tinaElipsaAngliÄtinaVlož textGumaKapátkoFrancouzÅ¡tinaObecnéNÄ›meckýVýška:VysokýNejvyššíIkonyItalÅ¡tinaJaponÅ¡tinaŘádkaNaÄítá se...Nové &oknoNový listNormálníPoznámkaPoznámkyOtevřít souborOtevřít soubor...Na&stavení stránkyStránka:PeroPÅ™edvolbyNáhle&d pÅ™ed tiskemNáhled pÅ™ed tiskemVytisknout aktuální stránkuObdélníkPÅ™ejmenuje aktuální listZmÄ›na velikosti plátnaOpakovatUložit j&ako...Uložit soubor?Vybrat písmoListÅ panÄ›lÅ¡tinaTextNastala chyba bÄ›hem tisku. Je VaÅ¡e tiskárna správnÄ› nastavena?Tloušťka:NáhledyVrátit poslední operaciBez názvuAktualizovatAktualizaceAktualizují se náhledyZobrazitZobrazit Whyteboard v módu celé obrazovkyZobrazit informace o WhyteboardZobrazit stavový řádekZobrazit nástrojové menuVelÅ¡tinaWhyteboard nepodporuje tento typ souboruWhyteboard soubor Šířka:whyteboard-0.41.1/locale/en_GB/LC_MESSAGES/whyteboard.mo0000777000175000017500000004705411444706707021541 0ustar stevesteveÞ•= ¯ì¨"©ÌÓâéÿ  + 5?E M[k q ˆ• ¦± ·ÄÊ Ý ç ô ÿ  /9?E Vasy’ š¦¯ÅËãé%.> ^'h—  ¨ ´  ÐÞ,ö!#E]sޤ·ËÒ äñ (1Gy‘©¯ÇÙ íû 2 O U _ u Š #˜ &¼ ã ø !!#&!J! _!m!€!š! ! ¯!½!Å! Í!!Ø!ú! ""%" ;"H""Z"-}"«")Ä"'î"%# <# G#U# s#}#Ž# –#¡#´#Ä#Ë#Ô#Ü#ã#ø#$3$M$R$ Z$ {$‡$$œ$¢$¿$ Å$Ñ$æ$]ÿ$]%x% ’% %«%Â%á%é%ò%B÷%%:& `& l&w& }& ‰&”&&®&½&Ó&æ& ö&''0+'-\'%Š' °' ¼' Æ'Ñ'á'è',í'( (A(\( q( ~( Š(—(·(Ç( Ö(â(è(*ü(:')b) x)™)%)"Ã)æ)î) ÷) *** .*<*S* b*m*}*„* ˜*¦* ¼*Æ*â*+ +++D+)Z+„+ –+¤+ ª+·+ ¿+ Ë+ Ø+ã+ù+&, 9, C, d,-p,$ž,Ã,Ù,ì, û,0 -:-S-l-{-˜-³- »- É-AÖ-. .,.I.f.….Ÿ.³.Ð.æ. î.*û. &/3/P8/ ‰/ “/ ž/)©/ Ó/ á/%ï/ 0+!0M0&R0y0Ž0©0Á0Ê0Ñ0Ù0 í0ù0#þ0 "1(C1'l1$”1!¹1Û1ï122'"2J2[2al24Î23# 3 .3O3^3c33°Š3";5^5e5t5{5’5š5 ²5 ¿5 Ê5Ô5Ú5 â5ð56 66 6*6 ;6F6 L6Y6_6 r6 |6 ‰6 ”6 6¤6­6´6 Ä6Î6Ô6Ú6 ë6ö6777'7 /7;7D7Z7`7x7~7%7Ã7Ó7 ó7'ý7%8,8 28 =8 I8 W8 e8s8-‹8"¹8Ü8ô8 9&9<9O9c9j9 |9‰9¥9(¶91ß9:):A:H:`:r: †:”:¬:Ë:è: î:ø:; #;#1;&U;|; ‘;Ÿ;®;#¿;ã; ø;<<3<9< H<V<^< f<!q<“< š<§<¾< Ô<á<"ó<-=D=)]='‡=%¯= Õ= à=î= >>'> />:>M>^>e>n>v>}>’>«>3³>ç>ì> ô> ?!?'?6?ISIpI†I ŽI+œI ÈIÕIPÚI +J 5J @J)KJ uJ ƒJ%‘J ·J+ÃJïJ&ôJK0KKKcKlKsK{K K›K# K ÄK(åK'L$6L![L}L‘L¢L¨L'ÄLìLýLaM4pM¥M#¬M ÐMñMNN#N{v(%‚©Qê"8£seY>”Rù3x|ë*&6•¥‰Z!Ñ«9wÛ(†Øˆ5±‡‘Þµø0.zÒ“˜€Ð-oœû:Ä ÃUS–¼¸ÙdL‹O¦9ƒ$´fäŸÉbð,òÂÜTí7ž)Õ6 n$%Ž4½ éÊ5¬2þI< 3 m/8Æ ¾!=?V/ ›Å_Ár*î'0á\W"®°}Íï Š¡ß -lN#u=Dg GݳόÌÓñ +B`ü»1ç:hJqó[,§.ú¹AÀÇ~Ë@&õP¶×)¿÷™ÈôâaÔ ÿãÖj—àΪå;ºF1M#+è…7šE2¤;²y'<¨¯’]cKtÚöX­¢4^ý·„CkpHiæì "%s" is missing the file save.data&About&Add New Point&Apply&Background &Color...&Cancel&Clear Sheets' Drawings&Color Grid&Color...&Contents&Copy&Delete&Delete Shape&Deselect Shape&Edit&Edit Note...&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&New Sheet&Next Sheet&OK&Open...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Shape Viewer...&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAdd a new sheetAdds a new point to the PolygonAll filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectCancelling...Canvas BorderChange the canvas' sizeChange the selected shape's background colorChange the selected shape's colorChange your preferencesCheck for &Updates...Choose Your Custom Colors:Choose Your Language:Choose a directoryChoose a media fileCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose the current sheetColorConnecting to server...Conversion FailedConversion Quality:Converting...Copy a Bitmap SelectionCopy a Bitmap Selection regionCould not connect to server.CzechDe&selectDefault Canvas HeightDefault Canvas WidthDefault Font:Delete the currently selected shapeDeselects the currently selected shapeDeselects this shapeDraw a circleDraw a polygonDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchE-mail AddressEdit the textEllipseEnglishEnter textErase a drawing to the backgroundEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet into a PDF fileExport every sheet to a series of image filesExport preferences to...Export the current sheet to an image fileExport your Whyteboard preferences fileExport your data files as images/PDFsEyedropperFeedback SentFile %s not found in the saveFilename:Find location...Find...Flood FillFlood fill an areaFonts and ColorFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHighlight with a transparent penHighlighterHindiHistory PlayerIconsIgnores the background colorImageImage FilesImageMagick LocationImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Import Preferences From...Import various file typesInput textInsert a noteInsert media and audioInvalid filetype to export as:ItalianJapaneseLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoad in a Whyteboard preferences fileLoaded fileLoading...MediaMedia FilesMove &DownMove &UpMove Shape &DownMove Shape &UpMove Shape To &BottomMove Shape To &TopMove To &BottomMove To &TopMoves the currently selected shape downMoves the currently selected shape to the bottomMoves the currently selected shape to the topMoves the currently selected shape upNew &WindowNew SheetNext SheetNo shapes drawnNormalNoteNote: Higher quality takes longer to convertNotesNumber of Recently Closed SheetsNumber of Toolbox Columns:Number of points: %sOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instanceP&references...PDF ConversionPage Set&upPage:Paste an Image/TextPaste from your clipboard into a new sheetPaste text or an image from your clipboard into WhyteboardPaste to a &New SheetPath for the image %s not found.PenPicks a color from the selected pixelPlease fill out your email addressPolygonPositionPrefere&ncesPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePrinting ErrorPropertiesQuit WhyteboardRadiusRe&load PreferencesRe&move SheetRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationReload your preferences fileRename sheetRename the current sheetRename this sheet to:Report any bugs or issues with WhyteboardResi&ze Canvas...Resize CanvasRetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSelection Handle SizeSelects this shapeSend &FeedbackSend FeedbackSend feedback directly to Whyteboard's developerSet the background colorSet the foreground colorSet the volumeSet up the page for printingSets the drawing thicknessSha&pesShape Select Shape ViewerShapes at the top of the list are drawn over shapes at the bottomSheetShortcut Key:Show and hide the color gridShow and hide the status barShow and hide the tool previewShow and hide the toolbarShow the color gridShow the title when printingShow the tool previewSpanishSwap &ColorsSwaps the foreground and background colorsT&ransparentTextThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsToggles the selected shape's transparencyTool &PreviewToolbox View:Translate Whyteboard to your languageTransparentTransparent Bitmap Select (may draw slowly)TypeUnable to load %s: Unsupported format?Undo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView Whyteboard's help documentsView a preview of the page to be printedView and edit the shapes' drawing orderView and replay your drawing historyView information about WhyteboardView the status barView the toolbarWelshWhyteboard Preference FilesWhyteboard doesn't support the filetypeWhyteboard file Whyteboard filesWhyteboard uses ImageMagick to load PDF, SVG and PS files. Please select its installed location.Whyteboard will be translated into %s when restartedWidth:You are running the latest version.You have not set any preferencesYour Feedback:ZoomZoom in and out of the canvaslanguageProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-07-31 02:19+0000 Last-Translator: Steven Sproat Language-Team: English (United Kingdom) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) "%s" is missing the file save.data&About&Add New Point&Apply&Background &Colour...&Cancel&Clear Sheets' Drawings&Colour Grid&Colour...&Contents&Copy&Delete&Delete Shape&Deselect Shape&Edit&Edit Note...&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&New Sheet&Next Sheet&OK&Open...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Shape Viewer...&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAdd a new sheetAdds a new point to the PolygonAll filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectCancelling…Canvas BorderChange the canvas' sizeChange the selected shape's background colourChange the selected shape's colourChange your preferencesCheck for &Updates...Choose Your Custom Colours:Choose Your Language:Choose a directoryChoose a media fileCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose the current sheetColourConnecting to server...Conversion FailedConversion Quality:Converting...Copy a Bitmap SelectionCopy a Bitmap Selection regionCould not connect to server.CzechDe&selectDefault Canvas HeightDefault Canvas WidthDefault Font:Delete the currently selected shapeDeselects the currently selected shapeDeselects this shapeDraw a circleDraw a polygonDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchE-mail AddressEdit the textEllipseEnglishEnter textErase a drawing to the backgroundEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet into a PDF fileExport every sheet to a series of image filesExport preferences to...Export the current sheet to an image fileExport your Whyteboard preferences fileExport your data files as images/PDFsEyedropperFeedback SentFile %s not found in the saveFilename:Find location...Find...Flood FillFlood fill an areaFonts and ColourFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHighlight with a transparent penHighlighterHindiHistory PlayerIconsIgnores the background colourImageImage FilesImageMagick LocationImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Import Preferences From...Import various file typesInput textInsert a noteInsert media and audioInvalid filetype to export as:ItalianJapaneseLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoad in a Whyteboard preferences fileLoaded fileLoading...MediaMedia FilesMove &DownMove &UpMove Shape &DownMove Shape &UpMove Shape To &BottomMove Shape To &TopMove To &BottomMove To &TopMoves the currently selected shape downMoves the currently selected shape to the bottomMoves the currently selected shape to the topMoves the currently selected shape upNew &WindowNew SheetNext SheetNo shapes drawnNormalNoteNote: Higher quality takes longer to convertNotesNumber of Recently Closed SheetsNumber of Toolbox Columns:Number of points: %sOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instanceP&references...PDF ConversionPage Set&upPage:Paste an Image/TextPaste from your clipboard into a new sheetPaste text or an image from your clipboard into WhyteboardPaste to a &New SheetPath for the image %s not found.PenPicks a colour from the selected pixelPlease fill out your email addressPolygonPositionPrefere&ncesPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePrinting ErrorPropertiesQuit WhyteboardRadiusRe&load PreferencesRe&move SheetRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationReload your preferences fileRename sheetRename the current sheetRename this sheet to:Report any bugs or issues with WhyteboardResi&ze Canvas...Resize CanvasRetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSelection Handle SizeSelects this shapeSend &FeedbackSend FeedbackSend feedback directly to Whyteboard's developerSet the background colourSet the foreground colourSet the volumeSet up the page for printingSets the drawing thicknessSha&pesShape Select Shape ViewerShapes at the top of the list are drawn over shapes at the bottomSheetShortcut Key:Show and hide the colour gridShow and hide the status barShow and hide the tool previewShow and hide the toolbarShow the colour gridShow the title when printingShow the tool previewSpanishSwap &ColoursSwaps the foreground and background coloursT&ransparentTextThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsToggles the selected shape's transparencyTool &PreviewToolbox View:Translate Whyteboard to your languageTransparentTransparent Bitmap Select (may draw slowly)TypeUnable to load %s: Unsupported format?Undo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView Whyteboard's help documentsView a preview of the page to be printedView and edit the shapes' drawing orderView and replay your drawing historyView information about WhyteboardView the status barView the toolbarWelshWhyteboard Preference FilesWhyteboard doesn't support the filetypeWhyteboard file Whyteboard filesWhyteboard uses ImageMagick to load PDF, SVG and PS files. Please select its installed location.Whyteboard will be translated into %s when restartedWidth:You are running the latest version.You have not set any preferencesYour Feedback:ZoomZoom in and out of the canvaslanguagewhyteboard-0.41.1/locale/fr/LC_MESSAGES/whyteboard.mo0000777000175000017500000006004411444706712021164 0ustar stevesteveÞ•gT ߌ2 "<_fu|„œ £ ¯ ¹ÃÉ Ñß ïû  % 6A GTZ m w„  ˜¤¨ ±¿Æ Öàæì ý   ( 9 A M V l r Š  %¯ Õ å !!'$!L!S! Y! d! p!~!ƒ! Œ! š!¨!À!Ø!î!""5"H"\"c" u"‚"ž"(¯"1Ø" #"#:#@#X#j# ~#Œ#¤#Ã#Rà#3$;$ A$ M$W$m$ ‚$#$&´$Û$ ð$þ$ %#%B% W%e%x%’%˜% §%µ%½%Å% Ô%!ß%& &&,& B&O&"a&-„&²&)Ë&'õ&%' C'XN' §'µ'Ç' å' ó'ý'( (!((4(](m(t(}(…(Œ(¡(º(3Â(ö(û( ) $)0)6)E)EK)‘)®) ´)À)Õ)]î)L*g* * Œ*š*±*Ð*Ø*ß*è*ð*Bõ*%8+ ^+ j+u+ {+ ‡+’+›+¬+»+Ñ+ä+ ô+',0),-Z,%ˆ, ®, º, Ä, Ï,Ý,í,ô,,ù,&- ,-M-h- }- Š- –-£-Ã-Ó-ä- ó- þ- ...**.:U.. ¦.Ç.%Ë."ñ./1/ 9/D/ M/ Z/f/u/ „/’/©/ ¸/Ã/Ë/Û/â/ ö/00 20<0X0w0”0 §0´0Í0)ã0 1 1-1 31@1 H1 T1 a1l1$‚1§1&À1 ç1 ñ1 2-2$L2q2‡2š2 ©20·2è233)3F3a3 i3 w3A„3Æ3 Ì3Ú3÷3434M4a4~4”4§4 ¯4*¼4 ç4ô4$ù4P5 o5 y5 „5)5 ¹5 Ç5%Õ5 û5 6+6A6&F6m6„6™6´6Ì6Õ6Ü6ä6 ø67# 7 -7(N7w7'—7&¿7$æ7! 8-8A8R8X8't8œ8­8a¾84 9MU9£9 ª9#µ9 Ù9ú9' :1:6:T:Y:_:h:o:w:~:›†:@"< c< „<< ¡<¬< µ<Ö<Þ< ï<û<= =='=== Q=[= o= z=„= ™=£= ¬=º=À= Ö=à=õ=þ=>!> %> 0>:>B> X>b> j>u> Š>—> ®>»>Ê> à>ê>ù> ? ?'(? P?\?$x??º?Ö?è?.@4@:@B@ J@X@l@ q@{@’@#¦@Ê@å@AA9ARAlA‰AA­A&ÁAèA:B>?B~BšBµB½BÕBðB CC4C%QCjwCâCëC ôCDD1DND aD%‚D¨DÄDÕDèD)üD&E@EUE!nE EE¬E½EÅEÍEçE$÷EF-F>F^F{FF'¬F)ÔFþF"G%?G$eGŠGn’G HH6(H_HvH†H ŸHªHºH.ËHúH II I*I3I PI qI={I¹I ¿IÊI êIõIüIJWJ!tJ–JœJ¬JÆJsßJ%SK!yK›KªK¼K;ÜKL L)L2L:LH@L$‰L®L ¾LÌLÓL äLïL÷L M%M BMcM~M-–M4ÄM1ùM+NIN\NmN~N—N®N¶N:¿N úN&O+OGO]OmOO‘O¯OÁO×O éO ôOPPP$P<DP P#¢PÆP)ÌP'öP2QQQ ZQdQmQ |QŠQŸQ¹QÒQìQ R RR)R0RMRcRR ›R&¥R(ÌR%õRS8SGS`S=|SºSÓS ðSûS TT(T=TWT5tT"ªT:ÍTU*#UNU7gU6ŸU$ÖUûUV%VAEV‡V#¦VÊVÜVøVWW1WNEW”WœW+°W$ÜW3X%5X [X&|X(£XÌXãXìX8Y ;YGY-MYg{Y ãY íY øY1Z5ZPZ%eZ ‹Z —Z@£ZäZ0éZ![<[,X[…[ ¤[¯[ ¾[Ì[ç[ ÷[\#!\1E\+w\,£\3Ð\+](0]Y]s]Ž](–].¿]î]^^8–^SÏ^#_ ,_"7_(Z_ƒ_)–_ À_!Î_ð_ö_ý_` ```æ¢#ñ¹á'WÅÕH*$(&#>:œÂ1´P^Áöhz‰0µÇ! gRw€&†ýU Ÿ˜dI1xm@|øÞF·õ]‹À% ükúÝKTfRU5 O ?p A,©jë?Z^þ ~å3¤ ¥ºPge§ä[2ž 64;`—âH2DŽN<ÿ3ŒXelQÌãÉ\ÒdÙB£Ãc¸Lès’.=-±ØóšÔÑ[²{$0c!TG¶KB7Ï.ôÖùˆi¡Y‡”÷V+×ÛL_aéêÜA 8FZ/½X'ûíG…ÆyOƒìr™¼‚n­ È ð°Y)Jàï8"î`]ËÎ »¦‘6Ú›@5¾QßMN-aS*EÍÊfШ«,ò\(C“<Š:+b®S)–_¯³V7t>49„¿ª%}Içq9ÄÓo/="•Mu¬JCbvED;W"%s" has corrupt data. This file cannot be loaded."%s" is missing the file save.data&About&Add New Point&Apply&Cancel&Clear Sheets' Drawings&Close&Color Grid&Color...&Contents&Copy&Delete&Delete Shape&Deselect Shape&Don't Save&Edit&Edit Note...&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&License&New Sheet&Next Sheet&OK&Open...&PDF Cache...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Shape Viewer...&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAdd a new sheetAdds a new point to the PolygonAll filesAll suppported filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectBoldC&reditsCancelling...Canvas BorderChange the canvas' sizeChange your preferencesCheck for &Updates...Chinese (Traditional)Choose Your Custom Colors:Choose Your Language:Choose a directoryChoose a media fileCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose the current sheetColorConnecting to server...Conversion FailedConversion Quality:Converting...Copy a Bitmap SelectionCopy a Bitmap Selection regionCould not connect to server.Could not connect to server. Check your Internet connection and firewall settingsCreditsCzechDate CachedDe&selectDefault Canvas HeightDefault Canvas WidthDefault Font:Delete the currently selected shapeDeselects the currently selected shapeDeselects this shapeDraw a circleDraw a polygonDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchE-mail AddressEdit the textEllipseEnglishEnglish (U.K.)Enter textErase a drawing to the backgroundEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet into a PDF fileExport every sheet to a series of image filesExport preferences to...Export the current sheet to an image fileExport your Whyteboard preferences fileExport your data files as images/PDFsEyedropperFailed to convert file. Ensure GhostScript is installed http://pages.cs.wisc.edu/~ghost/Feedback SentFile %s not foundFile %s not found in the saveFile LocationFilename:Find location...Find...Flood FillFlood fill an areaFolder "%s" does not contain convert.exeFonts and ColorFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHighlight with a transparent penHighlighterHindiHistory PlayerIconsIf you don't save, changes from the last %s will be permanently lost.Ignores the background colorImageImage FilesImageMagick LocationImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Import Preferences From...Import various file typesInput textInsert a noteInsert media and audioInvalid filetype to export as:ItalianItalicJapaneseLicenseLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoad in a Whyteboard preferences fileLoaded fileLoading...MediaMedia FilesMove &DownMove &UpMove Shape &DownMove Shape &UpMove Shape To &BottomMove Shape To &TopMove To &BottomMove To &TopMoves the currently selected shape downMoves the currently selected shape to the bottomMoves the currently selected shape to the topMoves the currently selected shape upNew &WindowNew SheetNext SheetNo Date SavedNo shapes drawnNormalNoteNote: Higher quality takes longer to convertNotesNumber of Recently Closed SheetsNumber of Toolbox Columns:Number of points: %sOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instanceP&references...PDF Cache ViewerPDF ConversionPDF/PS/SVGPage Set&upPage:PagesPaste an Image/TextPaste from your clipboard into a new sheetPaste text or an image from your clipboard into WhyteboardPaste to a &New SheetPath for the image %s not found.PenPicks a color from the selected pixelPlease fill out your email addressPlease provide some feedbackPolygonPortuguesePositionPrefere&ncesPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePrinting ErrorPropertiesQualityQuit WhyteboardRadiusRe&load PreferencesRe&move SheetRecently &Closed SheetsRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationReload your preferences fileRemove cached itemRename sheetRename the current sheetRename this sheet to:Report any bugs or issues with WhyteboardResi&ze Canvas...Resize CanvasRetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save changes to "%s" before closing?Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSelection Handle SizeSelects this shapeSend &FeedbackSend FeedbackSend feedback directly to Whyteboard's developerSet the background colorSet the foreground colorSet the volumeSet up the page for printingSets the drawing thicknessSha&pesShape Select Shape ViewerShapes at the top of the list are drawn over shapes at the bottomSheetShortcut Key:Show and hide the color gridShow and hide the status barShow and hide the tool previewShow and hide the toolbarShow the color gridShow the title when printingShow the tool previewSkip to a positionSpanishSwap &ColorsSwaps the foreground and background colorsT&ransparentTextThere are no cached items to displayThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsToggles the selected shape's transparencyTool &PreviewToolbox View:Translate Whyteboard to your languageTranslated byTransparentTransparent Bitmap Select (may draw slowly)TypeUnable to load %s: Unsupported format?Unable to play file %sUndo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView Whyteboard's help documentsView a preview of the page to be printedView all recently closed sheetsView and edit the shapes' drawing orderView and modify Whyteboard's PDF CacheView and replay your drawing historyView information about WhyteboardView the status barView the toolbarWelshWhyteboard Preference FilesWhyteboard doesn't support the filetypeWhyteboard file Whyteboard filesWhyteboard uses ImageMagick to load PDF, SVG and PS files. Please select its installed location.Whyteboard will be translated into %s when restartedWhyteboard will load these files from its cache instead of re-converting themWidth:Written byYou are running the latest version.You have not set any preferencesYour Feedback:Your feedback has been sent, thank you.ZoomZoom in and out of the canvashourhourslanguageminuteminutessecondsecondsProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-09-17 15:18+0000 Last-Translator: Steven Sproat Language-Team: French MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) "%s" a des données corrompues Ce fichier ne peut être chargé.save.data est manquant pour "%s"&À propos&Ajouter un point&Appliquer&AnnulerEffacer les dessins de feuilles.&FermerGrille &colorée&Couleur...&Contenu&Copier&SupprimerSupprimer formeDéselectionner forme&Ne pas sauvegarder&Modifier&Editer une note...&Editer...&Exporter&Exporter la feuille&Exporter&Fichier&Plein écran&AideVisionneuse d'archive&Image...&Importer le fichier&LicenceNouvelle feuilleProchaine feuille&OK&Ouvrir...Cache PDF&Coller&Feuille précédante&Imprimer&Fermer&Répéter&Renommer la feuille&Renommer...&Signaler un problème&Sauvegarder&Sélectionner&Visionneuse de forme&Feuilles&Barre d'état&Barre d'outils&Traduire Whyteboard&Défaire&Défaire la feuille fermée en dernier&VisualiserUn aperçu de l'outil actifUn simple whiteboard et annoteur PDFAjouter une nouvelle feuilleAjoute un point au polygoneTous les fichiersTous les fichiers supportésUne erreur est survenue - veuillez la reporterArabeFlèche&PDF...Fichier AudioSelectionner BitmapGrasC&réditsAnnulation en cours...Bordure de la toileModifier les dimensions de la toileModifier vos préférencesVérifier pour &mises à jourChinois (Traditionnel)Choisir Votre Couleurs CoutumeChoisissez votre langageChoisissez un répertoireChoisissez un fichier médiaCercleEffacer &Toutes les FeuillesEffacer &La FeuilleEffacer toutes les dessins de feuillesEffacer toutes les feuillesEffacer toutes les dessins de feuilles (garder les images)Effacer les dessins de la feuille actuelle (garder les images)Effacer la feuille actuelleFermer la feuille actuelleCouleurConnexion au serveur…La conversion a échouiéeQualité de la conversionConversion...Copier une selection BitmapCopier une sélection bitmapImpossible de se connecter au serveurImpossible de se connecter au serveur. Vérifiez votre connexion internet et vos paramètres de pare-feu.CréditsTchèqueDate en cacheDé&sélectionnerHauteur de toile par défautLargeur de toile par défautPolice par défautSupprimer la forme selectionnéeDéselectionne la forme selectionnéeDésélectionne cette formeTracer un cercleTracer un polygoneTracer un rectangleTracer un rectangle aux arêtes courbéesDessiner une ligne droiteDessiner une flècheDessiner une forme ovaleDessine des contours à la brosseNéerlandaisAdresse e-mailÉditer le texteEllipseAnglaisAnglais (Grande-Bretagne)Entrer le texteEffacer un dessin à l'arrière-planGomme à effacerRapport d'erreurErreur de sauvegarde du fichierExporter toutes les feuillesErreur d'exportationExporter les données en...Exporter chaque feuille en fichiers PDFExporter chaque fuille en série d'imagesExporter les préférences...Exporter la fuille active en imageExporter vos préférences WhyteboardExporter vos données en Images/PDFsPipetteLa conversion du fichier a failli. Assurez vous que GhostScript est installé http://pages.cs.wisc.edu/~ghost/Rapport émisFichier "%s" non trouvéLe fichier %s n'a pas été trouvé dans la sauvegardeEmplacement du fichierNom du fichier:Trouver l'emplacement...Trouver...Pot de peintureRemplir une zoneLe repertoire "%s" ne contient pas convert.exePolices et couleurFrançaisGalicienGénéralAllemandAller à la feuille suivanteAller à la feuille précédenteHauteur :Fichiers d'aide non trouvés. Voulez-vous les télécharger ?HautePlus hauteSurligner au crayon transparentSurligneurHindîHistorique des lecturesIcônesSi vous ne sauvegardez pas, les derniers %s changements seront définitivement perdus.Ignore la couleur d'arrière-planImageFichiers imagesEmplacement d'ImageMagickNotification ImagemagickImageMagick n'a pas été trouvé. Vous ne pourrez charger des fichiers PDF ou PS jusqu'à ce qu'il soit installé.Importer les préférences depuis ...Importer divers types de fichiersTexte d'entréInsérer une noteInsérer un media et de l'audioLe type de fichier est invalide pour exporter en tant que :ItalienItaliqueJaponaisLicenceTraitCharge un fichier de sauvegarde, une image ou convertir un fichierPDF/PSCharger une fichier de préférencesFichier chargéChargement...MédiaFichiers médias&Descendre&Monter&Descendre la forme&Monter la formeDescendre la forme à l'arrière-planMonter la forme au &premier-planMettre à l'&Arrière-planMettre au &premier-planDéplacer la forme sélectionnée vers le basDéplacer la forme sélectionnée à l'arrière-planDéplacer la forme sélectionnée au premier-planRemonte la forme sélectionneNouvelle &fenêtreNouvelle feuilleFeuille suivanteAucune date sauvegardéeAucune forme dessinéeNormaleRemarqueNote : Une meilleure qualité est plus longue à convertirRemarquesNombre de feuilles récemment ferméesNombre de colonnes d'outilsNombre de points : %sOuvrir &RécentOuvrir un fichierOuvrir fichier...Ouvre une instance WhyteboardPréféren&ces...Cache des vues en PDFConversion en PDFPDF/PS/SVGMise en &pagePage:PagesColle une image/texteColle dans une nouvelle feuilleColle du texte ou une image du presse-papier vers WhyteboardColle dans une &nouvelle feuilleChemin pour l'image %s non trouvé.StyloCapture la couleur du pixel sélectionnéMerci de renseigner votre adresse emailMerci de nous faire parvenir quelques commentairesPolygonePortugaisPositionPréfére&ncesPréférencesFeuille précédenteAperçu a&vant impressionAperçu avant impressionImprimer la page couranteErreur d'impressionPropriétésQualitéQuitter WhyteboardRadiusRe&charger les PréférencesSuppri&mer la feuilleFeuilles fermées recemmentFichiers ouverts récemmentRectangleRefaire la dernière l'action annuléeRefaire la dernière opération annuléeRecharge votre fichier de preferencesElements supprimés du cacheRenomm FeuilleRenomm la feuille activeRenommer cette feuille en :Rapporter un bug ou une fermeture intempestive avec WyteboardRedimensionner le canvasChanger la taille du canevasRéessayerRectangle arrondiRusse&Enregistrer sous...Enregistre le dessinEnregistrer le fichier ?Enregistre Whyteboard en...Sauvegarder les changements de "%s" avant de fermer ?Enregistre les données WhyteboardEnregistre les données Whyteboard dans un nouveau fichierEnregistrement en cours...Rechercher des mises à jour de WhyteboardSélectionner une policeSélectionne une zone rectangulaire à copier en bitmapSélectionne une zone à déplacer et la redimensionnePoignée de séléction de la tailleChoisir cette formeEnvoi de commentairesEnvoyer un retour d'informationEnvoyer des commentaires directement aux developeurs de WyteboardParamètrer la couleur de fondDéfinir la couleur de premier planRégler le volumeRégler la page à imprimerRégler la finesse du dessinFormesSelecteur de formes Visioneur de formesLes formes du haut de la liste sont dessinés par dessus les formes du dessousFeuilleRoaccourcis clavierAfficher ou masquer la pallette de couleursAfficher ou masquer la barre d'étatAfficher ou masquer les outils de prévisualisationAfficher ou masquer la barre d'outilsAfficher la pallette de couleursAfficher le titre lors de l'impressionAfficher les outils de prévisualisationPasser à une positionEspagnolEchanger les couleursEchanger les couleurs de premier plan et d'arrière planTransparentTexteIl n'y a pas d'items mis en cache à afficherL'impression a rencontré un problème. Votre imprimante n'est peut-être pas configurée correctement?EpaisseurEpaisseur:MiniaturesChanger la transparence de la forme sélectionnéOutils & prévisualisationBarre d'outil de vueTraduire Whyteboard dans votre langueTraduit parTransparentBitmap transparent séléctionné (peut dessiner plus lentement)TypeIncapable de charger %s : format non supporté ?Impossible de jouer le fichier %sAnnuler la dernière actionAnnuler la fermeture de la dernière feuilleAnnule la dernière opérationSans titreMettre à jourMises à jourMise à jour des vignettesFichiers VidéoAffichageVoir whyteboard en plein écranVoir la documentation de WhyteboardVoir une prévisualisation de la page à imprimerVoir toutes les feuilles fermées recemmentVoir et éditer l'ordre de dessin des formesAfficher et modifier le cache des PDF de WhyteboardVoir et refaire votre historique de dessinsAfficher des informations sur WhyteboardAfficher la barre d'étatAfficher la barre d'outilsGalloisPréférences des fichiers de WhyteboardWhyteboard ne supporte pas ce type de fichiersFichier Whyteboard Fichiers WhyteboardWhyteboard utilise ImageMagick pour charger des fichiers PDF, SVG et PS. Merci de sélectionner son emplacement d'installation.Whyteboard sera traduis en %s quand il aura re-démmaréWhyteboard va charger ces fichiers depuis son cache à la place de les re-convertirLargeur:Écrit parVous utilisez la dernière versionVous n'avez pas défini de préférencesVotre commentaire:Votre commentaire a été envoyé, merci.Faire un zoomzoomer et dé-zoomer sur la toileheureheureslangueminuteminutessecondesecondeswhyteboard-0.41.1/locale/hi/LC_MESSAGES/whyteboard.mo0000777000175000017500000001020611444706713021151 0ustar stevesteveÞ•D<a\àáè ðú  )-6 =GMS Yentz’™ ±¿ÅËÓ Ú çòù#+4 9 DPW\ b n{ …‘  ® ÅÏ Õ áíó û !).4š;"Ö ù  6 H ` 'r š © » Ê â ý  3 N 'f $Ž ³ Ò 5ç  D' l Š — ¡ À (Ê ó   5 E X e x ˆ › ® $» à ÿ +A[u ˆ’.«.Ú5 ?*X$ƒ"¨ ËÕëþ-@Scs1 %C0 #=@.>A,&9 465<$()'; *8B"D?-/!+32: 7&About&Cancel&Contents&Copy&Edit&File&Full Screen&Help&Image...&OK&Open...&Paste&Print...&Quit&Redo&Save&Status Bar&Toolbar&Undo&ViewChange your preferencesCircleConnecting to server...Converting...CzechDutchEllipseEraserError ReportEyedropperFrenchGeneralGermanHeight:HighHighestIconsItalianJapaneseLineLoading...New &WindowNormalNoteNotesOpen a FileOpen file...Page:PenPreferencesPrint Pre&viewPrint PreviewPrint the current pageRectangleRetrySave &As...Select FontSheetSpanishThickness:ThumbnailsUntitledUpdateUpdatesViewWelshWidth:Project-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2009-10-22 05:00+0000 Last-Translator: Steven Sproat Language-Team: Hindi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) के बारे में (&A)रदà¥à¤¦ करें (&C)विषयसूची (&C)नक़ल (&C)संपादन (&E)फाइल (&F)पूरा सà¥à¤•à¥à¤°à¥€à¤¨ (&F)मदद (&H)छवि... (&I)ठीक (&O)खोलें... (&O)चिपकाà¤à¤ (&P)छापें...(&P)बाहर जाà¤à¤ (&Q)दोहराà¤à¤ (&R)सहेजें (&S)सà¥à¤¥à¤¿à¤¤à¤¿ पटà¥à¤Ÿà¥€ (&S)औज़ार-पटà¥à¤Ÿà¥€ (&T)पहले जैसा (&U)देखें (&V)अपनी वरीयताà¤à¤ बदलेंवृतसरà¥à¤µà¤° से कनेकà¥à¤Ÿ हो रहा है...बदल रहा है...चेकà¥à¤¡à¤šà¥à¤¦à¥€à¤°à¥à¤˜à¤µà¥ƒà¤¤à¥à¤¤à¤°à¤¬à¤°à¤¤à¥à¤°à¥à¤Ÿà¤¿ रिपोरà¥à¤Ÿà¤†à¤‡à¤¡à¥à¤°à¥‰à¤ªà¤°à¤«à¥à¤°à¥‡à¤‚चसामानà¥à¤¯à¤œà¤°à¥à¤®à¤¨à¤Šà¤à¤šà¤¾à¤ˆà¤ƒà¤‰à¤šà¥à¤šà¤‰à¤šà¥à¤šà¤¤à¤®à¤šà¤¿à¤¹à¥à¤¨à¤‡à¤¤à¤¾à¤²à¤µà¥€à¤œà¤¾à¤ªà¤¾à¤¨à¥€à¤°à¥‡à¤–ालोड कर रहा है...नया विंडो (&W)सामानà¥à¤¯à¤Ÿà¤¿à¤ªà¥à¤ªà¤£à¥€à¤Ÿà¤¿à¤ªà¥à¤ªà¤£à¥€à¤«à¤¾à¤ˆà¤² खोलोफाईल खोलोपृषà¥à¤ à¤ƒà¤ªà¥‡à¤¨à¤µà¤°à¥€à¤¯à¤¤à¤¾à¤à¤‚छपाई पूरà¥à¤µà¤¾à¤µà¤²à¥‹à¤•नछपाई पूरà¥à¤µà¤¾à¤µà¤²à¥‹à¤•नवरà¥à¤¤à¤®à¤¾à¤¨ पृषà¥à¤  छापेंचतà¥à¤°à¥à¤­à¥à¤œà¤ªà¥à¤¨: पà¥à¤°à¤¯à¤¾à¤¸ करेंà¤à¤¸à¥‡ सहेजें... (&A)फ़ॉनà¥à¤Ÿ चà¥à¤¨à¥‡à¤‚शीटसà¥à¤ªà¥…निशमोटाईःथंबनेलशीरà¥à¤·à¤•हीनअदà¥à¤¯à¤¤à¤¨à¤…दà¥à¤¯à¤¤à¤¨à¤¦à¥ƒà¤¶à¥à¤¯à¤µà¥‡à¤²à¥à¤¶à¤šà¥Œà¤¡à¤¾à¤ˆà¤ƒwhyteboard-0.41.1/locale/ja/LC_MESSAGES/whyteboard.mo0000777000175000017500000002073711444706714021156 0ustar stevesteveÞ•‰d¿¬¨ © ° · ¿ Æ Ð Ö Þ ê ð ù   " ( 5 ; E R ] i m v } —  £ ´ ¿ Ñ × ß ç ó ü   0 6 F P 'e  ” š ¦ ´ Ì â ý &- ?L]u“« ¿ÍRê=CY n| ¢°¶ ÅÓÛ ãî õ /<N'gXè ú ")2:A3I}‚Š–ž £ ® ºÄËÐ Ö ãïõ ù " 9C \j p | ‡“™¡P¦ ÷  %.5=BHžOî  ' 5 @ NY m x%†%¬Òìý  %"3Vm …“¤ ¸ ÆÑ"â 6 A LZtˆ .± à!ë -&9TŽž¥¸Î%ç! $/Tp"t—±Ðï 1 A<N€‹ !;Zsƒ“£³Ê àêñ E*1p¢!¾à2ÿœ2(Ïø!7GW ^kQsÅÌÓ æó :MT [e~ ’¤·Ñ!ç $ !5 W a ‚ ˜ ® ¸ È mÕ C!K!4[!!—!ª! ½!Ç!Ú!7#O0A~!WJ:<UQyS}8-N1X€ Y,ƒki[%p.KEgI ˆP`H{32a+xe$sfRht_D&@*Z…wcLBr9^/F'?†(‡‚b„= 6q4dT Mm\]jG5"o>‰; CuV)vl|nz&About&Apply&Cancel&Close&Contents&Copy&Delete&Don't Save&Edit&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&Image...&Import File&New Sheet&Next Sheet&OK&Open...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewAdd a new sheetAll filesAll suppported filesAn error has occured - please report itArabicArrowAudio FilesCancelling...Change your preferencesCheck for &Updates...Choose Your Custom Colors:Choose Your Language:Choose a directoryCircleClear &All SheetsClear &SheetClear all sheetsClear the current sheetClose the current sheetColorConnecting to server...Conversion Quality:Converting...Could not connect to server.Could not connect to server. Check your Internet connection and firewall settingsCzechDefault Canvas HeightDefault Canvas WidthDefault Font:Draw a rectangleDraw a straight lineDraw an arrowDutchE-mail AddressEdit the textEllipseEnglishEnter textEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export preferences to...Export your Whyteboard preferences fileFailed to convert file. Ensure GhostScript is installed http://pages.cs.wisc.edu/~ghost/File %s not foundFile LocationFilename:Fonts and ColorFrenchGalicianGeneralGermanHeight:Help files not found, do you want to download them?HighHighestHindiIconsItalianLineLoading...New &WindowNew SheetNormalNoteNotesOpen file...Page Set&upPage:PenPreferencesPrint Pre&viewPrint PreviewPrint the current pageRectangleRename the current sheetResize CanvasRetrySave &As...Save File?Select FontSheetSpanishTextThere was a problem printing. Perhaps your current printer is not set correctly?Thickness:ThumbnailsUndo the last operationUntitledUpdateUpdatesViewWelshWidth:Project-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-04-18 04:57+0000 Last-Translator: Hiroshi Tagawa Language-Team: Japanese MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) ãƒãƒ¼ã‚¸ãƒ§ãƒ³æƒ…å ±(&A)é©ç”¨(&A)キャンセル(&C)é–‰ã˜ã‚‹(&C)目次(&C)コピー(&C)削除(&D)ä¿å­˜ã—ãªã„(&D)編集(&E)編集(&E)...ファイルをエクスãƒãƒ¼ãƒˆ(&E)シートをエクスãƒãƒ¼ãƒˆ(&E)...エクスãƒãƒ¼ãƒˆ(&E)...ファイル(&F)フルスクリーン(&F)ヘルプ(&H)ç”»åƒ(&I)...ファイルをインãƒãƒ¼ãƒˆ(&I)æ–°ã—ã„シート(&N)次ã®ã‚·ãƒ¼ãƒˆ(&N)&OKé–‹ã(&O)...貼り付ã‘(&P)å‰ã®ã‚·ãƒ¼ãƒˆ(&P)å°åˆ·(&P)...終了(&Q)やり直ã—(&R)シートã®åå‰ã‚’変更(&R)...åå‰ã‚’変更(&R)...å•題を報告ã™ã‚‹(&R)ä¿å­˜(&S)é¸æŠž(&S)シート(&S)ステータスãƒãƒ¼(&S)ツールãƒãƒ¼(&T)Whyteboardを翻訳(&T)å…ƒã«æˆ»ã™(&U)最後ã«é–‰ã˜ãŸã‚·ãƒ¼ãƒˆã‚’å…ƒã«æˆ»ã™(&U)表示(&V)æ–°ã—ã„シートを追加ã™ã‚‹ã™ã¹ã¦ã®ãƒ•ァイルã™ã¹ã¦ã®ã‚µãƒãƒ¼ãƒˆã•れãŸãƒ•ァイルエラーãŒç™ºç”Ÿã—ã¾ã—㟠- 報告ã—ã¦ãã ã•ã„アラビア語矢å°éŸ³å£°ãƒ•ァイルキャンセル中...設定を変更ã—ã¾ã™ã‚¢ãƒƒãƒ—デートをãƒã‚§ãƒƒã‚¯(&U)ã‚«ã‚¹ã‚¿ãƒ ã‚«ãƒ©ãƒ¼ã‚’é¸æŠžï¼šè¨€èªžã‚’é¸æŠžã—ã¦ãã ã•ã„:ディレクトリã®é¸æŠžå††å…¨ã¦ã®ã‚·ãƒ¼ãƒˆã‚’クリア(&A)シートをクリア(&S)å…¨ã¦ã®ã‚·ãƒ¼ãƒˆã‚’クリアç¾åœ¨ã®ã‚·ãƒ¼ãƒˆã‚’クリアç¾åœ¨ã®ã‚·ãƒ¼ãƒˆã‚’é–‰ã˜ã‚‹ã‚«ãƒ©ãƒ¼ã‚µãƒ¼ãƒã«æŽ¥ç¶šä¸­...変æ›ã®è³ªï¼šå¤‰æ›ä¸­...サーãƒã«æŽ¥ç¶šã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—ãŸã€‚サーãƒã«æŽ¥ç¶šã§ãã¾ã›ã‚“ インターãƒãƒƒãƒˆæŽ¥ç¶šã¨ãƒ•ァイアウォール設定をãƒã‚§ãƒƒã‚¯ã—ã¦ãã ã•ã„ãƒã‚§ã‚³èªžæ¨™æº–ã®ã‚­ãƒ£ãƒ³ãƒã‚¹ã®é«˜ã•標準ã®ã‚­ãƒ£ãƒ³ãƒã‚¹ã®å¹…標準ã®ãƒ•ォント:矩形をæãç›´ç·šã‚’æã矢å°ã‚’æãオランダ語Eメールアドレステキストを編集楕円形英語テキストを入力消ã—ゴムエラーレãƒãƒ¼ãƒˆãƒ•ァイルデータをä¿å­˜ã™ã‚‹éš›ã«ã‚¨ãƒ©ãƒ¼ãŒç”Ÿã˜ã¾ã—ãŸã™ã¹ã¦ã®ã‚·ãƒ¼ãƒˆã‚’エクスãƒãƒ¼ãƒˆ(&A)...エクスãƒãƒ¼ãƒˆã‚¨ãƒ©ãƒ¼ãƒ‡ãƒ¼ã‚¿ã‚’エクスãƒãƒ¼ãƒˆ...設定をエクスãƒãƒ¼ãƒˆ...Whyteboard 設定ファイルをエクスãƒãƒ¼ãƒˆãƒ•ァイルã®å¤‰æ›ã«å¤±æ•—ã—ã¾ã—ãŸã€‚GhostScript ãŒã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•れã¦ã„ã‚‹ã‹ç¢ºèªã—ã¦ãã ã•ã„。 http://pages.cs.wisc.edu/~ghost/ファイル %s ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ファイルã®å ´æ‰€ãƒ•ァイルå:フォントã¨é…色フランス語ガリシア語全般ドイツ語高ã•:ヘルプファイルãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。ダウンロードã—ã¾ã™ã‹ï¼Ÿé«˜ã„最高ヒンディー語アイコンイタリア語直線読ã¿è¾¼ã¿ä¸­...æ–°ã—ã„ウィンドウ(&W)æ–°è¦ã®ã‚·ãƒ¼ãƒˆé€šå¸¸ãƒ¡ãƒ¢ãƒŽãƒ¼ãƒˆãƒ•ァイルを開ã...ページ設定(&u)ページ:ペンユーザー設定å°åˆ·ãƒ—レビュー(&v)å°åˆ·ãƒ—レビューã“ã®ãƒšãƒ¼ã‚¸ã‚’å°åˆ·ã—ã¾ã™çŸ©å½¢ä¸­ã®ã‚·ãƒ¼ãƒˆåを変更ã—ã¾ã™ã‚­ãƒ£ãƒ³ãƒã‚¹ã®ã‚µã‚¤ã‚ºå¤‰æ›´å†è©¦è¡Œåå‰ã‚’付ã‘ã¦ä¿å­˜(&A) ...ä¿å­˜ã—ã¾ã™ã‹ï¼Ÿãƒ•ã‚©ãƒ³ãƒˆã‚’é¸æŠžã‚·ãƒ¼ãƒˆã‚¹ãƒšã‚¤ãƒ³èªžãƒ†ã‚­ã‚¹ãƒˆå°åˆ·ä¸­ã«å•題ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ ç¾åœ¨ã®ãƒ—ãƒªãƒ³ã‚¿ãƒ¼ã¯æ­£ã—ã設定ã•れã¦ã„ã¾ã™ã‹ï¼ŸåŽšã•:サムãƒã‚¤ãƒ«ç›´å‰ã®æ“作をå–り消ã—ã¦1段階戻りã¾ã™ç„¡é¡Œã‚¢ãƒƒãƒ—デートアップデートビューウェールズ語幅:whyteboard-0.41.1/locale/pt/LC_MESSAGES/whyteboard.mo0000777000175000017500000006045311444706713021205 0ustar stevesteveÞ•s´ óL2"Lov…Œ¢ªÂÉ Û ç ñû  ' 3 9 G P ] n y  Œ ’ ¥ ¯ ¼ Å Ð Ü à é ÷ þ !!!$! 5!@!R!X!`!q! y!…!Ž!¤!ª!Â!È!%ç! "" ="G"'\"„"‹" ‘" œ" ¨"¶"»" Ä" Ò"à",ø"!%#G#_#u#‹#¦#¼#Ï#ã#ê# ü# $%$(6$1_$‘$©$º$Ì$ä$ê$%% (%6%N%m%RŠ%Ý%å% ë% ÷%&& ,& :&#G&&k&’&§& »&É&Ø&#é& ' "'0'C']'c' r'€'ˆ'' Ÿ'!ª'Ì' Ó'à'÷' ((",(-O(}()–('À(%è( )X) r)€)’) °) ¾)È)Ù) á)ì)(ÿ)(*8*?*H*P*W*l*…*3*Á*Æ* Î* ï*û*++E+\+y+ +‹+ +]¹+,2, L, W,e,|,„,‹,”,œ,¢,B§,%ê, - -'- -- 9-D-M-^-m-}-“-¦-»- Í-Û- ë-'ø-0 .-Q.%. ¥. ±. ». Æ.Ô.ä.ë.,ð./ #/D/_/ t/ / /š/º/Ê/Û/ ê/ õ/00 0*!0:L0‡0 0¾0%Â0"è0 1(1 01;1 D1 Q1]1l1 {1‰1 1 ¯1º1Â1Ò1Ù1 í1û12 )232O2n2‹2 ž2«2Ä2)Ú23 3$373 =3J3 R3 ^3 k3v3$Œ3±3&Ê3 ñ3 û3 4-(4$V4{4Ž4 40«4Ü4õ455:5U5 ]5 k5Ax5º5 À5Î5ë56'6A6U6r6ˆ6›6 £6*°6 Û6è6$í6S7Pf7 ·7 Á7 Ì7)×7 8 8%8 C8 Q8]8&b8‰8 8µ8Ð8è8ñ8ø89 9 9#%9 I9(j9“9'³9&Û9$:!':I:]:n:t:':¸:É:aÚ:4<;Mq;¿; Æ;#Ñ; õ;<'%<M<R<p<u<{<„<‹<“<š<Ø¢<E{>%Á>ç>ï>?? ?(?A?I?a?p? x?ƒ?‹?“?¢? ´?Á?É? Ù?ä?÷? @ @!@1@8@ K@V@ i@ s@@@ “@ @«@²@ Â@Ï@Õ@Þ@ ñ@þ@A A)ABAJA[AqA †A"A³A#¸A$ÜAB#B:BMB&kB’B™B žB«B¼BÐB ØB ãBñBC-C$HCmC‹CªC ÁCâCôC D*D3D KD"YD|D1“D3ÅDùDE'E9EOESEjE}E–E¥E%ÄE'êEhF {F…F‹F ¢F­FÇFâF üF G'GFG[GpG…G›G/³GãGüGH'H >HHH\HkHrH zH ˆH!–H¸HÁHÕHíH II6I*OIzI'™I0ÁI0òI #Jn/JžJ!µJ×JóJ KK 9K EKOK&cKŠKžK§K®K´K¼KÕKîK<öK3L 8LBL [LfLlL…LRLàL÷LþLM#Mc=M¡M$¾M ãM ñMþMNN(N1N:N@NKFN1’NÄN ×NåNëNOO$O=OUOmO‡O¡OºOÓOêOûO( P)6P)`P'ŠP ²P ¿PÊPÙPîPQQ;QOQ'UQ*}Q¨Q¾QÍQßQ&ñQR*RGR YRdRRˆR‘R4¨RCÝR!S)8SbS!iS#‹S(¯S ØS âS íS÷S TT#T7TJTdT wT „TŽT¡T¦TÀTÏTíT U"U%;U*aUŒU¢U±UÉU/ãUV#V6VMV^VvV|VŒVV°V4ÊVÿV,W GW+SWW)šW.ÄWóW X!X86XoX†XžX¯XÌXêXóX Y=!Y_YeY!vY#˜Y,¼Y(éYZ*ZFZfZ|Z …Z“Z ¥Z³Z¹ZTÙZK.[ z[ „[ [/š[Ê[á[&÷[ \ ,\9\/>\n\‡\!¢\Ä\ â\ î\ù\ ]!]5]"9](\])…])¯]+Ù]-^&3^$Z^^•^°^%·^'Ý^__u/_-¥_]Ó_1` :`'F`n`Œ`/£` Ó`ß`ù`þ`a aaa"aÔ%Kã[øLês2Þ•zAŒŠG0g1*‘ÖßlBËÐýÙQ[XYEûÑ_R"زev¹Kúª)­õâ5ñ|ÊwH i#>µÎ.}k–àˆ(¨3-Ò)I=6 Xo’AÅ é<ù?,ð !OP̆¼讂&Blhq$-ò0ƒu—(Da„S'£^MFï´y?Vn¬GŽì»Û~‰N hÜ]*/#8Ÿ˜9%>eîQpÃt¥ S ‡{L¿æ`ô\œ™V€÷ZÍp Ú$Wöm6ji‹,J“`š3jo¢¯ÂÁó¤ÝZ=Oc^À<ÿbaqJnPDÏT@…d54Õº;°¶2Hr \Ç.©CÉ«b+U @Cg9:Mþdá]הƧ+WUfå › 81ëÓ&³±kYFIÈcs¡r·äfx¾ü_T;4 ¦R½'¸7/!mN:íç"7EžÄ"%s" has corrupt data. This file cannot be loaded."%s" is missing the file save.data&About&Add New Point&Apply&Background &Color...&Cancel&Clear Sheets' Drawings&Close&Close All Sheets&Color Grid&Color...&Contents&Copy&Delete&Delete Shape&Deselect Shape&Don't Save&Edit&Edit Note...&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&License&New Sheet&Next Sheet&OK&Open...&PDF Cache...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Shape Viewer...&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAdd a new sheetAdds a new point to the PolygonAll filesAll suppported filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectBoldC&reditsCancelling...Canvas BorderChange the canvas' sizeChange the selected shape's background colorChange the selected shape's colorChange your preferencesCheck for &Updates...Chinese (Traditional)Choose Your Custom Colors:Choose Your Language:Choose a directoryChoose a media fileCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose All SheetsClose every sheetClose the current sheetColorConnecting to server...Conversion FailedConversion Quality:Converting...Copy a Bitmap SelectionCopy a Bitmap Selection regionCould not connect to server.Could not connect to server. Check your Internet connection and firewall settingsCreditsCzechDate CachedDe&selectDefault Canvas HeightDefault Canvas WidthDefault Font:Delete ShapeDelete the currently selected shapeDeselects the currently selected shapeDeselects this shapeDownloaded %s of %sDraw a circleDraw a polygonDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchE-mail AddressEdit the textEllipseEnglishEnglish (U.K.)Enter textErase a drawing to the backgroundEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet into a PDF fileExport every sheet to a series of image filesExport preferences to...Export the current sheet to an image fileExport your Whyteboard preferences fileExport your data files as images/PDFsEyedropperFailed to convert file. Ensure GhostScript is installed http://pages.cs.wisc.edu/~ghost/Feedback SentFile %s not foundFile %s not found in the saveFile LocationFilename:Find location...Find...Flood FillFlood fill an areaFolder "%s" does not contain convert.exeFonts and ColorFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHighlight with a transparent penHighlighterHindiHistory PlayerIconsIf you don't save, changes from the last %s will be permanently lost.Ignores the background colorImageImage FilesImageMagick LocationImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Import Preferences From...Import various file typesInput textInsert a noteInsert media and audioItalianItalicJapaneseLicenseLightLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoad in a Whyteboard preferences fileLoaded fileLoading...MediaMedia FilesMove &DownMove &UpMove Shape &DownMove Shape &UpMove Shape DownMove Shape To &BottomMove Shape To &TopMove Shape To BottomMove Shape To TopMove Shape UpMove To &BottomMove To &TopMoves the currently selected shape downMoves the currently selected shape to the bottomMoves the currently selected shape to the topMoves the currently selected shape upNew &WindowNew SheetNext SheetNo Date SavedNo shapes drawnNormalNoteNote: Higher quality takes longer to convertNotesNumber of Recently Closed SheetsNumber of Toolbox Columns:Number of points: %sOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instanceP&references...PDF Cache ViewerPDF ConversionPDF/PS/SVGPage Set&upPage:PagesPaste an Image/TextPaste from your clipboard into a new sheetPaste text or an image from your clipboard into WhyteboardPaste to a &New SheetPath for the image %s not found.PenPicks a color from the selected pixelPlease fill out your email addressPlease provide some feedbackPolygonPortuguesePositionPrefere&ncesPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePrinting ErrorPropertiesQualityQuit WhyteboardRadiusRe&load PreferencesRe&move SheetRecently &Closed SheetsRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationReload your preferences fileRemove cached itemRename sheetRename the current sheetRename this sheet to:Report any bugs or issues with WhyteboardResi&ze Canvas...Resize CanvasRestore sheet "%s"RetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save changes to "%s" before closing?Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSelects this shapeSend &FeedbackSend FeedbackSend feedback directly to Whyteboard's developerSet the background colorSet the foreground colorSet the volumeSet up the page for printingSets the drawing thicknessSha&pesShape Select Shape ViewerShapes at the top of the list are drawn over shapes at the bottomSheetShortcut Key:Show and hide the color gridShow and hide the status barShow and hide the tool previewShow and hide the toolbarShow the color gridShow the title when printingShow the tool previewSkip to a positionSpanishSwap &ColorsSwaps the foreground and background colorsT&ransparentTextThere are no cached items to displayThere is a new version available, %(version)s File: %(filename)s Size: %(filesize)sThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsToggles the selected shape's transparencyTool &PreviewToolbox View:Translate Whyteboard to your languageTranslated byTransparentTypeUnable to load %s: Unsupported format?Unable to play file %sUndo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView Whyteboard's help documentsView a preview of the page to be printedView all recently closed sheetsView and edit the shapes' drawing orderView and modify Whyteboard's PDF CacheView and replay your drawing historyView information about WhyteboardView the status barView the toolbarWelshWhyteboard Preference FilesWhyteboard doesn't support the filetypeWhyteboard file Whyteboard filesWhyteboard uses ImageMagick to load PDF, SVG and PS files. Please select its installed location.Whyteboard will be translated into %s when restartedWhyteboard will load these files from its cache instead of re-converting themWidth:Written byYou are running the latest version.You have not set any preferencesYour Feedback:Your feedback has been sent, thank you.ZoomZoom in and out of the canvashourhourslanguageminuteminutessecondsecondsProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-09-17 15:11+0000 Last-Translator: Steven Sproat Language-Team: Portuguese MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) X-Poedit-Country: PORTUGAL X-Poedit-Language: Portuguese "%s" tem os dados danificados. Este ficheiro não pode ser carregado."%s" não possui o ficheiro save.data&Acerca&Adicionar Novo Ponto&Aplicar&Fundo e Cor...&CancelarLimpar Desenhos da Folha&FecharF&echar Todas as FolhasCor da &Grelha&cor...&Conteúdo&Copiar&Apagar&Apagar Figura&Desmarcar Figura&Não Gravar&Editar&Editar Nota...&Editar...&Exportar Ficheiro&Exportar Folha...&Exportar...&Ficheiro&Ecrã CompletoA&judaVer &Histórico...&Imagem...&Importar Ficheiro&Licença&Nova FolhaFolha &Seguinte&OK&Abrir...&PDF Cache...C&olarFolha &AnteriorIm&primir...&Sair&Refazer&Renomear Folha...&Renomear...&Reportar um Problema&Gravar&Seleccionar&Visualizador de FigurasFol&hasBarra de &EstadoBarra de &Ferramentas&Traduzir Whyteboard&DesfazerRec&uperar a Última Folha Fechada&VerUma antevisão da ferramenta actualUm simples quadro e anotador de PDFsAdicionar nova folhaAdiciona um novo ponto ao polígonoTodos os ficheirosTodos os ficheiros suportadosOcorreu um erro - Por favor, reporte-oÃrabeSetaComo &PDF...Ficheiros ÃudioSeleccionado BitmapNegritoC&réditosA Cancelar...Limite da TelaAlterar o tamanho da telaAlterar a cor de fundo da figura seleccionadaAlterar a cor da figura seleccionadaAlterar as suas preferências&Verificar por ActualizaçõesMandarim (Tradicional)Escolha as Cores Personalizadas:Escolha o Idioma:Escolha um directórioEscolha o ficheiro multimédiaCírculoLimpar &Todas as FolhasLimpar &FolhaLimpar Todos os &Desenhos da FolhaLimpar todas as folhasLimpar todos os desenhos da folha (mater imagens)Limpar os desenhos da folha actual (manter imagens)Limpara a folha actualFechar Todas as FolhasFechar cada folhaFechar a folha actualCorA ligar ao servidor...Falha ao ConverterQualidade da Conversão:A converter...Copiar uma Selecção "Bitmap"Copiar a área da Selecção "Bitmap"Não é possível ligar-se ao servidor.Não foi possível ligar ao servidor. Verifique a ligação à Internet e as definições da "firewall"CréditosChecoData Colocada em CacheDe&smarcarAltura da Tela (Omissão)Largura da Tela (Omissão)Tipo de Letra (Omissão):Apagar FiguraApagar a figura seleccionadaDesmarca a figura seleccionadaDesmarca esta figuraTransferido %s de %sDesenhar um círculoDesenhar um polígonoDesenhar um rectânguloDesenhar um rectângulo com cantos arredondadosDesenhar uma linha rectaDesenhar uma setaDesenhar uma figura ovalDesenhar com um pincelHolandêsEndereço de E-mailEditar o textoElipseInglêsInglês (U.K)Inserir textoApagar desenho (total ou parcial)BorrachaRelatório de ErrosErro ao gravar os dadosExportar &Todas as Folhas...Erro de ExportaçãoExportar dados para...Exportar folhas para PDFExportar folhas para uma série de imagensExportar preferências para...Exportar a folha actual para uma imagemExportar o ficheiro das preferências WhyteboardExportar os ficheiros de dados para Imagens/PDFsConta-gotasFalha ao converter o ficheiro. Certifique-se que tem o GhostScript instalado. http://pages.cs.wisc.edu/~ghost/Envio de InformaçõesO ficheiro %s não foi encontradoFicheiro %s não encontradoLocalização do FicheiroNome do ficheiro:Procurar localização...Procurar...PreencherPreencher uma áreaA pasta "%s" não possui o convert.exeTipo de Letra e CorFrancêsGalegoGeralAlemãoIr para a folha seguinteIr para a folha anteriorAltura:Ficheiros de ajuda não encontrados. Pretende transferi-los?AltaMais AltaRealçar com um marcadorRealçadorHindiReprodutor de HistóricoÃconesSe não gravar, as alterações dos últimos %s serão irremediavelmente perdidas.Ignorar a cor de fundoImagemImagensLocalização do ImageMagickNotificação ImageMagickO ImageMagick não foi encontrado. Não será capaz de carregar ficheiros PDF ou PS sem o programa.Importar Preferências De...Importar diversos tipos de ficheirosInserir textoInserir notaInserir vídeo e áudioItalianoItálicoJaponêsLicençaClaroLinhaCarregar um ficheiro Whyteboard, uma imagem ou converta um documento PDF/PSCarregar num ficheiro de preferências WhyteboardFicheiro carregadoA carregar...MediaFicheiros MultimédiaMover para &BaixoMover para &CimaMover Figura Para &BaixoMover Figura Para &CimaMover Figura Para BaixoMover Figura Para a Ba&seMover Figura Para o To&poMover Figura Para a BaseMover Figura Para o TopoMover Figura Para CimaMover Para &CimaMover Para &BaixoDesloca para baixo a figura seleccionadaDesloca a figura seleccionada para a baseDesloca a figura seleccionada para o topoDesloca para cima a figura seleccionadaNova &JanelaNova FolhaFolha SeguinteNenhuam Data GravadaNenhuma figura desenhadaNormalNotaAtenção: Uma melhor qualidade leva mais tempo a converterNotasNúmero de Folhas Recentemente FechadasNúmero de Colunas da Caixa de FerramentasNúmero de pontos: %sAbrir &RecenteAbrir um FicheiroAbrir ficheiro...Abre uma nova instância do WhyteboardP&referências...Visualizador da Cache de PDFConversão de PDFPDF/PS/SVGConfig&uração de PáginaPágina:PáginasColar uma Imagem/TextoColar da área de transferência para uma nova folhaColar uma imagem/texto da área de transferência para o WhyteboardColar Numa &Nova FolhaCaminho para a imagem %s não encontrado.CanetaObter a cor do pixel seleccionadoPor favor, preencha o seu endereçoPor favor, indique algumas informaçõesPolígonoPortuguêsPosiçãoPreferê&nciasPreferênciasFolha AnteriorAnte&ver ImpressãoAntever ImpressãoImprimir a página actualErro de ImpressãoPropriedadesQualidadeSair do WhyteboardRaioRecarre&gar PreferênciasRe&mover FolhaFolhas Recentemente Fec&hadasFicheiros Abertos RecentementeRectânguloRefazer a Última Acção DesfeitaRefazer a última operação desfeitaRecarregar o seu ficheiro de preferênciasRemover item em cacheRenomear folhaRenomear a folha actualRenomear esta folha para:Reporte quaisquer erros relativas ao WhyteboardRea&justar TelaRedimensionar TelaRestaurar a folha "%s"Tentar NovamenteRectângulo ArredondadoRussoGravar &Como...Gravar o DesenhoGravar o Ficheiro?Gravar Whyteboard Como...Quer gravar as alterações em "%s" antes de fechar?Gravar os dados WhyteboardGravar os dados Whyteboard num novo ficheiroGravando...Verificar por actualizações do WhyteboardSeleccione o Tipo de LetraSeleccione a região a copiar como bitmapSeleccione a figura para mover e redimensionarSelecciona esta ficgura&Enviar InformaçõesEnviar InformaçõesEnviar as informações para o programador do WhyteboardDefinir a cor de fundoDefinir a cor principalDefinir o volumeDefinir a página a imprimirDefine a espessura do desenhoFig&urasSelecção de Figura Visualizador de FigurasAs figuras no topo da lista sobrepõem-se ás figuras da baseFolhaTecla de Atalho:Mostrar e ocultar a cor da grelhaMostrar e ocultar a barra de estadoMostrar e ocultar a ferramenta de antevisãoMostrar e ocultar a barra de ferramentasMostrar a cor da grelhaMostrar título ao imprimirMostrar a ferramenta antevisãoIr para uma posiçãoEspanholT&rocar CoresInverter as cores&TransparenteTextoNão existem itens para mostrarEstá disponível a versão %(version)s Ficheiro: %(filename)s Tamanho: %(filesize)sHouve um problema com a impressão. A sua impressora está bem configurada?EspessuraEspessura:MiniaturasAlterna a transparência da figura seleccionadaFerramenta &AntevisãoCaixa de Ferramentas:Traduza o Whyteboard para o seu idiomaTraduzido porTransparenteTipoIncapaz de carregar %s. Formato não suportado?Incapaz de reproduzir %sDesfazer a Última AcçãoRecuperar a última folha fechadaDesfazer a última operaçãoSem títuloActualizarActualizaçõesA Actualizar MiniaturasFicheiros de VídeoVerVer o Whyteboard em ecrã completover os documentos de ajuda do WhyteboardVisualizar a página que vai ser impressaVer todas as folhas fechadas recentementeVer e editar a ordem do desenho das figurasVer e modificar a cache de PDFs do WhyteboardVer e repetir o histórico de desenhosVer informações sobre o WhyteboardVer a barra de estadoVer a barra de ferramentasGalêsFicheiros de Preferências WhiteboardO Whyteboard não suporta este ficheiroFicheiro Whyteboard Ficheiros WhyteboardO Whyteboard utiliza o ImageMagick para abrir ficheiros PDF, SVG e PS. Por favor, indique o directório do programa.O Whyteboard será reiniciado com o idioma %sO Whyteboard vai abrir os ficheiros colocados em cache para não efectuar uma nova conversãoLargura:Escrito porJá está a executar a última versão.Não definiu as preferênciasAs Suas Informações:As suas informações foram enviadas, obrigado.AmpliaçãoAmpliar ou reduzir a telahorahorasidiomaminutominutossegundosegundoswhyteboard-0.41.1/locale/ru/LC_MESSAGES/whyteboard.mo0000777000175000017500000006337511444706710021213 0ustar stevesteveÞ•5Ä £løù' . 8BH P^ nz €Ž —¤ µÀ ÆÓÙ ì ö  #'07 GQW] ny‹‘¢ ª¶¿ÕÛóù%>N nx'µ¼ Â Í Ùç ð þ $<Rhƒ™¬ÀÇ Ùæ(1<n†ž¤¼Î âð'RD—³ È#Ö&ú ! / > #O s ˆ – © à É Ø æ î ö !!!2! 9!F!]! s!€!"’!-µ!ã!)ü!'&"%N" t"X"Ø" ê" ø"## #&#(9#b#r#y#‚#Š#‘#¦#¿#3Ç#û#$ $ )$5$;$J$EP$–$³$ ¹$Å$Ú$]ó$Q%l% †% ‘%Ÿ%¶%Õ%Ý%ä%í%Bò%%5& [& g&r& x&„&•&¤&º&'Í&0õ&-&'%T' z' †' '›'«'²',·'ä' ê' ( ( -( 9(F(f(v( …( (œ(¢(*¶(:á() 2)S)%W)"}) )½) Å)Ð) Ù) æ)ò)* **5* D*O*W*g* n*|* ’*œ*¸*×* ô*++)0+Z+ l+z+ €++ •+ ¡+ ®+¹+$Ï+ô+& , 4, >, _,-k,$™,¾,Ô,ç,--(-E-`- h- v-Aƒ-Å- Ë-Ù-ö-.-.J.*R. }.Š.P. à. ê. õ. /%/ 4/ B/+N/z/&/¦/½/Ò/í/0000 10=0#B0(f0'0$·0!Ü0þ01#1)1'E1m1~1a14ñ1&2#-2Q2V2t2œ}2414 E40S4„4 ”4¡4·4Í4Ý41ú4,5E5+W5ƒ5&˜5)¿5é5 ü56 6)06Z6$u6š6¬6Á6Þ6â6õ67&7 779C7'}7¥7%Ä7ê7þ7 8 )8&J8q88F¢8é8Gû8BC9$†9D«9ð9.:M1:::Ÿ:®:Ê:å:;;(+;,T;:;-¼;%ê;#<4<$P<u<#Š<®<;É<"=d(=h=(ö=&>F>+O>){>.¥>Ô>@ô>@5??v?³¶?j@/y@/©@#Ù@0ý@C.A)rA/œA/ÌAFüA,CB#pB!”B/¶BæBýB%C ?CLC3aC•C5¯C åCòC:D2LDD/D8ÍDdE%kEW‘EBéEV,FƒF–’F!)G#KGoG&‚G ©G·G.×G3H:HUHlH ƒHŽH/ŸH3ÏH IPIbIqI2‹I ¾I ËI)ÖI J— J*¥JÐJ!çJ$ K.K‹MK5ÙKCLSLmL5LBÃLM M*M ;MzFM7ÁMùMN-NDN&ZN(N8ªN:ãN9OKXOM¤O;òO.PCPWP&sPšP©P‹¸PDQASQ•Q°QÍQåQ-R.R ER fR$qR–R2¨R_ÛRf;S*¢S8ÍST<T\LTJ©TôTU*U=UQUdU‚U¡U,¿UìUVV(V CVPV#hVŒVS§VSûV4OW#„W2¨W0ÛWG X-TX.‚X!±XÓXéXøX!Y8YUYJrY*½YAèY*Z*BZmZp‡ZgøZ.`[[&®[;Õ[%\77\4o\ ¤\±\Ê\sæ\Z] c]<„]LÁ]R^7a^™^P¬^ý^ _Ÿ_¾_Í_Ý_÷_2`B`Q`if`Ð`Y×`:1a4laE¡a4çab1bFb0[bŒb¢bF³bAúbSí‰jÿÜ¢Ò‹ÎxC-]-’³~gT¥½žØY*¸«©únÁß·“mkQ Ù/ˆŽþ v{âbìû¹E€Š'@#H&About&Apply&Cancel&Clear Sheets' Drawings&Close&Color...&Contents&Copy&Delete&Delete Shape&Deselect Shape&Don't Save&Edit&Edit Note...&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&License&New Sheet&Next Sheet&OK&Open...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Shape Viewer...&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAdd a new sheetAdds a new point to the PolygonAll filesAll suppported filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectC&reditsCancelling...Canvas BorderChange the canvas' sizeChange your preferencesCheck for &Updates...Chinese (Traditional)Choose Your Custom Colors:Choose Your Language:Choose a directoryChoose a media fileCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose the current sheetColorConnecting to server...Conversion FailedConversion Quality:Converting...Copy a Bitmap SelectionCopy a Bitmap Selection regionCould not connect to server.Could not connect to server. Check your Internet connection and firewall settingsCzechDefault Canvas HeightDefault Canvas WidthDefault Font:Delete the currently selected shapeDeselects the currently selected shapeDraw a circleDraw a polygonDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchE-mail AddressEdit the textEllipseEnglishEnglish (U.K.)Enter textErase a drawing to the backgroundEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet into a PDF fileExport every sheet to a series of image filesExport preferences to...Export the current sheet to an image fileExport your Whyteboard preferences fileExport your data files as images/PDFsEyedropperFailed to convert file. Ensure GhostScript is installed http://pages.cs.wisc.edu/~ghost/File %s not foundFile LocationFilename:Find location...Find...Flood FillFlood fill an areaFolder "%s" does not contain convert.exeFonts and ColorFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHighlight with a transparent penHighlighterHindiHistory PlayerIconsIf you don't save, changes from the last %s will be permanently lost.Ignores the background colorImageImage FilesImageMagick LocationImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Import Preferences From...Import various file typesInput textInsert a noteInsert media and audioInvalid filetype to export as:ItalianItalicJapaneseLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoad in a Whyteboard preferences fileLoaded fileLoading...MediaMedia FilesMove Shape &DownMove Shape &UpMove Shape To &BottomMove Shape To &TopMoves the currently selected shape downMoves the currently selected shape to the bottomMoves the currently selected shape to the topMoves the currently selected shape upNew &WindowNew SheetNext SheetNo shapes drawnNormalNoteNote: Higher quality takes longer to convertNotesNumber of Recently Closed SheetsNumber of points: %sOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instanceP&references...PDF ConversionPDF/PS/SVGPage Set&upPage:Paste an Image/TextPaste from your clipboard into a new sheetPaste text or an image from your clipboard into WhyteboardPaste to a &New SheetPath for the image %s not found.PenPicks a color from the selected pixelPlease fill out your email addressPlease provide some feedbackPolygonPortuguesePositionPrefere&ncesPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePrinting ErrorPropertiesQualityQuit WhyteboardRadiusRe&move SheetRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationReload your preferences fileRename sheetRename the current sheetRename this sheet to:Report any bugs or issues with WhyteboardResi&ze Canvas...Resize CanvasRetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save changes to "%s" before closing?Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSelection Handle SizeSelects this shapeSet the background colorSet the foreground colorSet the volumeSet up the page for printingSets the drawing thicknessSha&pesShape Select Shape ViewerShapes at the top of the list are drawn over shapes at the bottomSheetShortcut Key:Show and hide the color gridShow and hide the status barShow and hide the toolbarShow the title when printingSpanishSwaps the foreground and background colorsT&ransparentTextThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsToolbox View:Translate Whyteboard to your languageTranslated byTransparentTransparent Bitmap Select (may draw slowly)TypeUnable to load %s: Unsupported format?Unable to play file %sUndo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView a preview of the page to be printedView and edit the shapes' drawing orderView and replay your drawing historyView information about WhyteboardView the status barView the toolbarWelshWhyteboard Preference FilesWhyteboard doesn't support the filetypeWhyteboard file Whyteboard filesWhyteboard uses ImageMagick to load PDF, SVG and PS files. Please select its installed location.Whyteboard will be translated into %s when restartedWidth:You are running the latest version.ZoomZoom in and out of the canvaslanguageProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-09-17 15:39+0000 Last-Translator: Steven Sproat Language-Team: Russian MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) &О программе&ПрименитьО&тмена&ОчиÑтить риÑунки на лиÑте&Закрыть&Цвет...&Содержание&Копировать&Удалить&Удалить фигуру&Отменить выделение фигуры&Ðе ÑохранÑть&Изменить&Редактировать запиÑÑŒ...&Изменить...&ЭкÑпортировать файл&ЭкÑпортировать лиÑÑ‚...&ЭкÑпорт...&Файл&Во веÑÑŒ Ñкран&Справка&ПроÑмотреть иÑторию...&Изображение...&Импортировать файл&ЛицензиÑ&Ðовый лиÑÑ‚&Следующий лиÑÑ‚&OK&Открыть...&Ð’Ñтавить&Предыдущий лиÑÑ‚&Печать...&Выйти&Повторить отменённое дейÑтвие&Переименовать лиÑÑ‚...&Переименовать...&Сообщить о проблеме&СохранитьПроÑмотр фигур...&ЛиÑты&Строка ÑоÑтоÑниÑ&Панель инÑтрументов&ПеревеÑти Whyteboard&Откатить&ВоÑÑтановить поÑледний закрытый лиÑÑ‚&ПроÑмотрПредпроÑмотр Ð´Ð»Ñ Ñ‚ÐµÐºÑƒÑ‰ÐµÐ³Ð¾ инÑтрументаПроÑÑ‚Ð°Ñ Ð±ÐµÐ»Ð°Ñ Ð´Ð¾Ñка и PDF комментаторДобавить новый лиÑтДобавлÑет новый угол в многоугольникВÑе файлыВÑе поддерживаемые файлыОбнаружена ошибка - проÑьба Ñообщить о нейÐрабÑкийСтрелкаКак &PDF...Звуковые файлыВыделить карт.&БлагодарноÑтиОтмена...Граница лиÑтаИзменить размер доÑкиИзменить ваши уÑтановкиПроверить наличие &обновлений...КитайÑкий (Традиционный)Выберите Ñвои цвета:Выберите Ñвой Ñзык:Выберите папкуВыберите медиа-файлОкружноÑтьОчиÑтить &вÑе лиÑтыОчиÑтить &лиÑтОчиÑтить &риÑунки на вÑех лиÑтахОчиÑтить вÑе лиÑтыОчиÑтить риÑунки на вÑех лиÑтах (оÑтавлÑÑ Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ðµ)ОчиÑтить риÑунок на текущем лиÑте (оÑтавлÑÑ Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ðµ)ОчиÑтить текущий лиÑтЗакрыть текущий лиÑтЦветПодключение к Ñерверу...Ошибка преобразованиÑКачеÑтво преобразованиÑ:Преобразование...Копировать выделенное изображениеКопировать выделенное изображениеÐе удалоÑÑŒ подключитьÑÑ Ðº Ñерверу.Ðе уудалоÑÑŒ подключитьÑÑ Ðº Ñерверу. Проверьте подключение к интернету и наÑтройки cетевой защитыЧешÑкийВыÑота лиÑта по умолчаниюШирина лиÑта по умолчаниюШрифт по умолчанию:Удалить выделенную фигуруОтменÑет выделение выбранной фигурыÐариÑовать окружноÑтьÐариÑовать многоугольникÐариÑовать прÑмоугольникÐариÑовать Ñкруглённый прÑмоугольникÐариÑовать прÑмую линиюÐариÑовать ÑтрелкуÐариÑовать ÑллипÑÐариÑовать линию киÑтью...ГолландÑкаÑÐÐ´Ñ€ÐµÑ Ñл. почтыРедактировать текÑтЭллипÑÐнглийÑкийÐнглийÑкий (ВеликобританиÑ)Введите текÑтСтереть картинку цветом фонаЛаÑтикОтчет об ошибкахОшибка ÑÐ¾Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… в файлЭкÑпортировать &вÑе лиÑты...Ошибка ÑкÑпортаЭкÑпортировать данные в...ЭкÑпорт каждого лиÑта в PDF файлЭкÑпортировать каждый лиÑÑ‚ в Ñерию файлов изображенийЭкÑпорт наÑтроек в...ЭкÑпортировать текущий лиÑÑ‚ в файл изображениÑЭкÑпорт вашего файла наÑтроек WhyteboardЭкÑпортировать файлы данных как изображениÑ/PDFПипеткаÐе удалоÑÑŒ конвертировать файл. Проверьте, уÑтановлен ли GhostScript. http://pages.cs.wisc.edu/~ghost/Файл «%s» не найденРаÑположение Ñ„Ð°Ð¹Ð»Ð°Ð˜Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð°:Ðайти раÑположение...Ðайти...Ð¡Ð¿Ð»Ð¾ÑˆÐ½Ð°Ñ Ð·Ð°Ð»Ð¸Ð²ÐºÐ°Ð¡Ð¿Ð»Ð¾ÑˆÐ½Ð°Ñ Ð·Ð°Ð»Ð¸Ð²ÐºÐ° облаÑтиПапка «%s» не Ñодержит convert.exeШрифты и цветаФранцузÑкийГалиÑийÑкийОбщиеÐемецкийПерейти на Ñледующий лиÑтПерейти на предыдующий лиÑтВыÑота:Файлы Ñправки не найдены, хотите Ñкачать их?Ð’Ñ‹ÑокоеСамое выÑокоеВыделить прозрачной ручкойМаркерХиндиПроигрыватель иÑторииИконкиеÑли вы не ÑохранитеÑÑŒ, Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñо времени поÑледнего %s будут навÑегда потерÑны.Игнорировать цвет фонаИзображениеФайлы изображенийРаÑположение ImageMagickСообщение ImageMagickImageMagick не найден. Ð’Ñ‹ не Ñможете загружать PDF и PS-файлы, пока не уÑтановите его.Импортировать наÑтройки из...Импортировать различные типы файловВведите текÑтДобавить запиÑкуВÑтавить мультимедиа и аудиоÐеверный тип файла Ð´Ð»Ñ ÑкÑпорта как:ИтальÑнÑкийКурÑивЯпонÑкийЛиниÑЗагрузить Whyteboard файл, изображение или конвертировать PDF/PS документЗагрузить файл наÑтроек WhyteboardЗагруженный файлЗагрузка...МультимедиаМедиа-файлыСдвинуть фигуру внизСдвинуть фигуру вверхСдвинуть фигуру к нижнему краюСдвинуть фигуру к верхнему краюСдвигает выбранную фигуру внизСдвигает выбранную фигуру к нижнему краюСдвигает выбранную фигуру к верхнему краюСдвигает выбранную фигуру вверх&Ðовое окноÐовый лиÑтСледующий лиÑтФигуры не нариÑованыОбычноеЗапиÑкаЗамечание: более выÑокое качеÑтво приводит к более долгому конвертированиюЗапиÑкиКоличеÑтво недавно закрытых лиÑтовКол-во точек: %s&Ðедавние файлыОткрыть файлОткрыть файл...Открывает еще один Whyteboard&Параметры...PDF преобразованиеPDF/PS/SVGПараметры С&траницыСтраница:Ð’Ñтавить изображение/текÑтВÑтавить изображение из буфера обмена на новый лиÑтВÑтавить текÑÑ‚ или изображение из буфера обмена в WhyteboardÐ’Ñтавить на &новый лиÑтПуть к изображению %s не найден.ПероПолучить цвет из выбранной точкиПожалуйÑта, введите Ð°Ð´Ñ€ÐµÑ Ñвоей Ñлектронной почтыПожалуйÑта, оÑтавлÑйте Ваши комментарииМногоугольникПортугальÑкийПоложение&ПараметрыПараметрыПредыдущий лиÑтПроÑмотр пе&чатиПроÑмотр печатиПечать текущей ÑтраницыОшибка печатиСвойÑтваКачеÑтвоВыйти из WhyteboardРадиуÑУдалить лиÑÑ‚Ðедавние документыПрÑмоугольникВозвратить поÑледнее незаконченное дейÑтвиеВозвратить поÑледнюю незаконченную операциюПерезагрузить файл наÑтроекПереименовать лиÑтПереименовать текущий лиÑтПереименовать Ñтот лиÑÑ‚ в:Отправить ошибки или проблемы по WhyteboardИзменить размер холÑта...Изменение размера холÑтаПовторить попыткуСкругл. пр-кРуÑÑкийСохранить &как...Сохранить риÑунокСохранить файл?Сохранить как...Сохранить Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð² %s перед закрытием?Сохранить данные WhyteboardСохранить данные Whyteboard в новый файлСохранение...ПоиÑк обновлений WhyteboardВыбрать шрифтВыделить прÑмоугольную облаÑть Ð´Ð»Ñ ÐºÐ¾Ð¿Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ ÐºÐ°Ðº картинкуВыберите фигуру Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ¼ÐµÑ‰ÐµÐ½Ð¸Ñ Ð¸ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ ÐµÐµ размеровРазмер курÑора выделениÑВыбор Ñтой формыУÑтановить цвет фонаУÑтановить цвет переднего планаУÑтановка громкоÑтиÐаÑтроить Ñтраницы Ð´Ð»Ñ Ð¿ÐµÑ‡Ð°Ñ‚Ð¸Ð£Ñтанавливает толщину линийФигурыВыбор фигуры ПроÑмотр фигурФигуры вверху ÑпиÑка отриÑовываютÑÑ Ð¿Ð¾Ð²ÐµÑ€Ñ… фигур внизу ÑпиÑкаЛиÑÑ‚"ГорÑчаÑ" клавиша:Показать и Ñкрыть цветовую ÑеткуПоказывает или Ñкрывает Ñтроку ÑоÑтоÑниÑПоказывает или Ñкрывает панель инÑтрументовПоказать заголовок при печатиИÑпанÑкийМенÑет меÑтами цвета фона и переднего планаП%розрачныйТекÑтВозникла проблема во Ð²Ñ€ÐµÐ¼Ñ Ð¿ÐµÑ‡Ð°Ñ‚Ð¸. Возможно, ваш текущий принтер неправильно наÑтроен?ТолщинаТолщина:ЭÑкизы лиÑтовИнÑтрументы:ПеревеÑти Whyteboard на ваш ÑзыкПереводПрозрачныйВыбран прозрачный битмап (может отриÑовыватьÑÑ Ð¼ÐµÐ´Ð»ÐµÐ½Ð½Ð¾)ТипÐе удаетÑÑ Ð·Ð°Ð³Ñ€ÑƒÐ·Ð¸Ñ‚ÑŒ %s: Ðеподдерживаемый формат?Ðе удалоÑÑŒ воÑпроизвеÑти файл %sОтменить поÑледнее дейÑтвиеВоÑÑтановить поÑледний закрытый лиÑтОтменить поÑледнюю операциюБезымÑнныйОбновлениеОбновлениÑОбновление ÑÑкизов лиÑтовВидео-файлыПроÑмотрПеревеÑти Whyteboard в полноÑкранный режимПроÑмотреть Ñтраницу перед печатьюПроÑмотр и изменение порÑдка отриÑовки фигурПроÑмотреть и проиграть иÑторию вашего риÑованиÑПоказать информацию о WhyteboardПоказывать Ñтроку ÑоÑтоÑниÑПоказывать панель инÑтрументовВаллийÑкий (УÑльÑÑкий)Файлы наÑтроек WhyteboardWhyteboard не поддерживает тип файлаWhyteboard файл Файлы WhyteboardWhyteboard иÑпользует ImageMagick Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸ PDF, SVG и PS файлов. ПожалуйÑта, выберите каталог, где он уÑтановлен.Whyteboard будет переведен на %s поÑле перезапуÑкаШирина:Ð’Ñ‹ иÑпользуете поÑледнюю верÑию.МаÑштабироватьУвеличение и уменьшение лиÑтаÑзыкwhyteboard-0.41.1/locale/ar/LC_MESSAGES/whyteboard.mo0000777000175000017500000000414511444706714021161 0ustar stevesteveÞ•!$/,èéð÷ÿ ! '5EK\ bou ˆ ’ Ÿ ª¶ºÃÊ Úäêð 1 F›Tð ø #2 BL#]‘­µ É×÷ ) > _i z †§¶ È'Ôü %;T  !   &About&Apply&Cancel&Clear Sheets' Drawings&Contents&Copy&Delete Shape&Deselect Shape&Edit&Export Sheet...&File&Full Screen&Help&History Viewer...&Image...&Import File&New Sheet&Next Sheet&OK&Open...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Report a ProblemArrowCircleDraw a rectangleDraw a straight lineDraw an arrowProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-09-17 00:53+0000 Last-Translator: Steven Sproat Language-Team: Arabic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) &حول&تطبق&إلغاء&إنظ٠رسومات الورقة&محتويات&إنسخ&إحذ٠شكلإلغاء Ø¥&ختيار الشكلتـ&ـحريرتـ&ـصدير ورقة...&ملÙملء الشاشة&مساعدةمـ&ـشاهد التريخ...الـ&ـصور...استـ&ـيراد الملÙورقة &جديدةالـ&ـورقة التالية&حسناا&ÙØªØªØ§Ø­...لـ&ـصقالورقة الـ&ـسابقة&طباعة...استـ&ـقالا&عادةإعادة تسـ&ـمية الورقةإبلا&غ مشكلةسهمدائرةارسم مستطيلرسم خط مستقيمارسم سهم.whyteboard-0.41.1/locale/gl/LC_MESSAGES/whyteboard.mo0000777000175000017500000006115711444706711021164 0ustar stevesteveÞ•s´ óL2"Lov…Œ”¬³ Å Ñ Ûåë ó   # 1 : G X c i v | ™ ¦ ¯ º Æ Ê Ó á è ø !!! !*!,E,N,V,\,Ba,%¤, Ê, Ö,á, ç, ó,þ,--'-7-M-`-u- ‡-•- ¥-'²-0Ú-- .%9. _. k. u. €.Ž.ž.¥.,ª.×. Ý.þ./ ./ ;/ G/T/t/„/•/ ¤/ ¯/»/Á/Ç/*Û/:0A0 W0x0%|0"¢0Å0â0 ê0õ0 þ0 11&1 51C1Z1 i1t1|1Œ1“1 §1µ1Í1 ã1í1 2(2E2 X2e2~2)”2¾2 Ð2Þ2ñ2 ÷23 3 3 %303$F3k3&„3 «3 µ3 Ö3-â3$454K4^4 m40{4¬4Å4Þ4í4 5%5 -5 ;5AH5Š5 5ž5»5Ø5÷56%6B6X6k6 s6*€6 «6¸6$½6Sâ6P67 ‡7 ‘7 œ7)§7 Ñ7 ß7%í7 8 !8+-8Y8&^8…8œ8±8Ì8ä8í8ô8ü8 99#!9 E9(f99'¯9&×9$þ9!#:E:Y:j:p:'Œ:´:Å:aÖ:48;Mm;»; Â;#Í; ñ;<'!<I<N<l<q<w<€<‡<<–<ž<9<>"v> ™>¤>¸> Á>Ë>å>í>?? ?&?.? 6?D? Y?e?m? }?ˆ?›? ®? »?Å?Ø?ß? ö?@@ @(@8@ A@K@\@c@ s@€@‡@@ ¢@¯@Æ@ Î@Û@ï@÷@AA 3A!=A_A&dA&‹A²A!ÊAìAÿA0BMBTB [BhByB’B ˜B £B±BÀBÚBòBC$&CKC_CuC•CžC ¶C"ÄCçC4þC43DhD~D•D§D½DÁDÛDðD E#E.ûR:S RSsS"yS)œS&ÆS íS ÷S T T T'T6TOTgTT ”T T©T½TÃTáTñTU .U":U%]U.ƒU²UÑUàUøU4VGV^VtVVœV³V¸V ÈVÖVéV,W0W/NW ~W&ŠW±W@ËW1 X>X^XtXˆX>›XÚX"ôXY$+YPYnYvYŒYPœYíYóY! Z#-Z3QZ(…Z®Z"ÆZ)éZ[&[/[0C[ t[‚['ˆ[]°[k\z\\ ‰\,”\ Á\â\ ] "] 0]@=]~],ƒ](°]Ù] ô]^ 3^ ?^J^Z^r^†^+Š^(¶^/ß^)_(9_)b_+Œ_!¸_Ú_ð_ `'`(:`c`x`s`.aS0a„a ‹a$—a#¼aàa+òab#b=bBbHbObVb^bfbÒ$HáXöLèp1Ü’w@‰‡G/d0)ŽÔÝlAÉŒÎûÅ×N[UVDšÏ\O!Ö¯bs·Kø§(ªóà4ïyÈtHF f"=²Ì-zù“Þ…'¥2,Ð(I<5 Xl@à ç;÷>+î LMʃºæ«%Aieq#,ð/€þr”'C^P& ^MEí±v>Sk©F‹ê¹Ù{†NhÚ])."7œ•8$=eìQmÁq¢S „xI½ä]ò\™–V}õWËp Ø#Wôm5giˆ+J`—2joŸ¬À³ñ¡ÛZ<Oc[¾;ý_annPCÍT?‚a43Ó¸~:­´1GoY¿-¦BǨb*U?Bg89JüdßZՑĤ*TRfã  ˜70éÑ%°®kY EhÆ`sžrµâcŠu¼ú|_Q:3£R»&¶6. jK9ëåÿ!6D ›Â"%s" has corrupt data. This file cannot be loaded."%s" is missing the file save.data&About&Add New Point&Apply&Cancel&Clear Sheets' Drawings&Close&Close All Sheets&Color Grid&Color...&Contents&Copy&Delete&Delete Shape&Deselect Shape&Don't Save&Edit&Edit Note...&Edit...&Export File&Export Sheet...&Export...&File&Full Screen&Help&History Viewer...&Image...&Import File&License&New Sheet&Next Sheet&OK&Open...&PDF Cache...&Paste&Previous Sheet&Print...&Quit&Redo&Rename Sheet...&Rename...&Report a Problem&Save&Select&Shape Viewer...&Sheets&Status Bar&Toolbar&Translate Whyteboard&Undo&Undo Last Closed Sheet&ViewA preview of your current toolA simple whiteboard and PDF annotatorAdd a new sheetAdds a new point to the PolygonAll filesAll suppported filesAn error has occured - please report itArabicArrowAs &PDF...Audio FilesBitmap SelectBoldC&reditsCancelling...Canvas BorderChange the canvas' sizeChange your preferencesCheck for &Updates...Chinese (Traditional)Choose Your Custom Colors:Choose Your Language:Choose a directoryChoose a media fileCircleClear &All SheetsClear &SheetClear All Sheets' &DrawingsClear all sheetsClear all sheets' drawings (keep images)Clear drawings on the current sheet (keep images)Clear the current sheetClose All SheetsClose every sheetClose the current sheetColorConnecting to server...Conversion FailedConversion Quality:Converting...Copy a Bitmap SelectionCopy a Bitmap Selection regionCould not connect to server.Could not connect to server. Check your Internet connection and firewall settingsCreditsCzechDate CachedDe&selectDefault Canvas HeightDefault Canvas WidthDefault Font:Delete ShapeDelete the currently selected shapeDeselects the currently selected shapeDeselects this shapeDownloaded %s of %sDraw a circleDraw a polygonDraw a rectangleDraw a rectangle with rounded edgesDraw a straight lineDraw an arrowDraw an oval shapeDraw strokes with a brushDutchE-mail AddressEdit the textEllipseEnglishEnglish (U.K.)Enter textErase a drawing to the backgroundEraserError ReportError saving file dataExport &All Sheets...Export ErrorExport data to...Export every sheet into a PDF fileExport every sheet to a series of image filesExport preferences to...Export the current sheet to an image fileExport your Whyteboard preferences fileExport your data files as images/PDFsEyedropperFailed to convert file. Ensure GhostScript is installed http://pages.cs.wisc.edu/~ghost/Feedback SentFile %s not foundFile %s not found in the saveFile LocationFilename:Find location...Find...Flood FillFlood fill an areaFolder "%s" does not contain convert.exeFonts and ColorFrenchGalicianGeneralGermanGo to the next sheetGo to the previous sheetHeight:Help files not found, do you want to download them?HighHighestHighlight with a transparent penHighlighterHindiHistory PlayerIconsIf you don't save, changes from the last %s will be permanently lost.Ignores the background colorImageImage FilesImageMagick LocationImageMagick NotificationImageMagick was not found. You will be unable to load PDF and PS files until it is installed.Import Preferences From...Import various file typesInput textInsert a noteInsert media and audioInvalid filetype to export as:ItalianItalicJapaneseLicenseLightLineLoad a Whyteboard save file, an image or convert a PDF/PS documentLoad in a Whyteboard preferences fileLoaded fileLoading...MediaMedia FilesMove &DownMove &UpMove Shape &DownMove Shape &UpMove Shape DownMove Shape To &BottomMove Shape To &TopMove Shape To BottomMove Shape To TopMove Shape UpMove To &BottomMove To &TopMoves the currently selected shape downMoves the currently selected shape to the bottomMoves the currently selected shape to the topMoves the currently selected shape upNew &WindowNew SheetNext SheetNo Date SavedNo shapes drawnNormalNoteNote: Higher quality takes longer to convertNotesNumber of Recently Closed SheetsNumber of Toolbox Columns:Number of points: %sOpen &RecentOpen a FileOpen file...Opens a new Whyteboard instanceP&references...PDF Cache ViewerPDF ConversionPDF/PS/SVGPage Set&upPage:PagesPaste an Image/TextPaste from your clipboard into a new sheetPaste text or an image from your clipboard into WhyteboardPaste to a &New SheetPath for the image %s not found.PenPicks a color from the selected pixelPlease fill out your email addressPlease provide some feedbackPolygonPortuguesePositionPrefere&ncesPreferencesPrevious SheetPrint Pre&viewPrint PreviewPrint the current pagePrinting ErrorPropertiesQualityQuit WhyteboardRadiusRe&load PreferencesRe&move SheetRecently &Closed SheetsRecently Opened FilesRectangleRedo the Last Undone ActionRedo the last undone operationReload your preferences fileRemove cached itemRename sheetRename the current sheetRename this sheet to:Report any bugs or issues with WhyteboardResi&ze Canvas...Resize CanvasRestore sheet "%s"RetryRounded RectRussianSave &As...Save DrawingSave File?Save Whyteboard As...Save changes to "%s" before closing?Save the Whyteboard dataSave the Whyteboard data in a new fileSaving...Search for updates to WhyteboardSelect FontSelect a rectangle region to copy as a bitmapSelect a shape to move and resize itSelection Handle SizeSelects this shapeSend &FeedbackSend FeedbackSend feedback directly to Whyteboard's developerSet the background colorSet the foreground colorSet the volumeSet up the page for printingSets the drawing thicknessSha&pesShape Select Shape ViewerShapes at the top of the list are drawn over shapes at the bottomSheetShortcut Key:Show and hide the color gridShow and hide the status barShow and hide the tool previewShow and hide the toolbarShow the color gridShow the title when printingShow the tool previewSkip to a positionSpanishSwap &ColorsSwaps the foreground and background colorsT&ransparentTextThere are no cached items to displayThere is a new version available, %(version)s File: %(filename)s Size: %(filesize)sThere was a problem printing. Perhaps your current printer is not set correctly?ThicknessThickness:ThumbnailsToggles the selected shape's transparencyTool &PreviewToolbox View:Translate Whyteboard to your languageTranslated byTransparentTransparent Bitmap Select (may draw slowly)TypeUnable to load %s: Unsupported format?Unable to play file %sUndo the Last ActionUndo the last closed sheetUndo the last operationUntitledUpdateUpdatesUpdating ThumbnailsVideo FilesViewView Whyteboard in full-screen modeView Whyteboard's help documentsView a preview of the page to be printedView all recently closed sheetsView and edit the shapes' drawing orderView and modify Whyteboard's PDF CacheView and replay your drawing historyView information about WhyteboardView the status barView the toolbarWelshWhyteboard Preference FilesWhyteboard doesn't support the filetypeWhyteboard file Whyteboard filesWhyteboard uses ImageMagick to load PDF, SVG and PS files. Please select its installed location.Whyteboard will be translated into %s when restartedWhyteboard will load these files from its cache instead of re-converting themWidth:Written byYou are running the latest version.You have not set any preferencesYour Feedback:Your feedback has been sent, thank you.ZoomZoom in and out of the canvashourhourslanguageminuteminutessecondsecondsProject-Id-Version: whyteboard Report-Msgid-Bugs-To: FULL NAME POT-Creation-Date: 2010-09-17 15:46+0100 PO-Revision-Date: 2010-09-17 15:20+0000 Last-Translator: Steven Sproat Language-Team: Galician MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Launchpad-Export-Date: 2010-09-17 15:48+0000 X-Generator: Launchpad (build Unknown) «%s» ten datos danados. O ficheiro non vai ser cargado.«% s» falta o ficheiro save.data&Acerca de&Engadir novo punto&Aplicar&Cancelar&Limpar follas de debuxos&Pechar&Pechar todas as follas&Grella de cor&Cor...&Contidos&Copiar&Borrar&Borrar forma&Deseleccionar forma&Non gardar&Editar&Editar nota...&Editar...&Exportar ficheiro&Exportar folla...&Exportar...&Ficheiro&Pantalla completa&Axuda&Visor do historial...&Imaxe...&Importar ficheiro&Licenza&Nova folla&Seguinte folla&Aceptar&Abrir...Cache de &PDF...&Pegar&Folla anterior&Imprimir...&Saír&Refacer&Renomear a folla&Renomear...&Informar dun problema&Gardar&Seleccionar&Visor de formas...&Follas&Barra de estadoBarra de &ferramentas&Traducir Whyteboard&Desfacer&Desfacer a última folla pechada&VerUnha vista previa da ferramenta actualUnha pizarra sinxela e anotador de PDFEngadir unha nova follaEngade un novo punto ao polígonoTodos os ficheirosTodos os ficheiros admitidosAconteceu un erro - faga o favor de informar delÃrabeFrechaComo &PDF...Ficheiros de sonSeleccionar mapa de bitsNegraC&réditosCancelando...Borde do lenzoCambia o tamaño do lenzoCambiar as preferenciasComprobar &actualizacións...Chinés (tradicional)Elixa as súas cores personalizadas:Elixa o seu idioma:Escolla un directorioElixa un ficheiro de multimediaCírculoLimpar &todas as follasLimpar &follaLimpar &debuxos de todas as follasLimpar todas as follasLimpar debuxos de todas as follas (manter as imaxes)Limpar os debujos da folla actual (manter as imaxes)Limpar a folla actualPechar todas as follasPechar cada follaPechar a folla actualCorConectando ao servidor...Fallou a conversiónCalidade da conversión:Convertendo...Copiar unha selección mapa de bitsCopiar unha área seleccionada á mapa de bitsNon se pode conectar co servidor.Non é posíbel conectar co servidor Comprobe a súa conexión a Internet e a configuración da devasaCréditosChecoDatos na cachéDe&seleccionarAltura predeterminada do lenzoLargura predeterminada do lenzoTipo de letra predeterminado:Eliminar formaEliminar a actual forma seleccionadaDeselecciona a forma actualmente seleccionadaAnula a selección desta formaDescargado %s de %sDebuxar un círculoDebuxar un polígonoDebuxar un rectánguloDebuxar un rectángulo con cantos redondeadosDebuxar unha liña rectaDebuzar unha frechaDebuxar unha forma ovaladaDebuxar trazos cun pincelNeerlandésEnderezo de correo-eEditar o textoElipseInglésIngés (U.K.)Introducir textoBorrar un debuxo do fondoGoma de borrarInforme de errosAconteceu un erro ao gardar os ficheiros de datosExportar &odas as follas...Erro de exportaciónExportar datos a...Exportar cada folla nun ficheiro PDFExportar cada folla a unha serie de ficheiros de imaxeExportar preferencias a...Exportar a folla actual a un ficheiro de imaxeExportar o seu ficheiro de preferencias de WhyteboardRxportar os seus ficheiros de datos como imaxes/PDFContagotasProduciuse un fallo convertendo o ficheiro. Asegure de que GhostScript está instalado http://pages.cs.wisc.edu/~ghost/Comentario enviadoNon se atopou o ficheiro %sO ficheiro %s non aparece nos gardadosLocalización do ficheiroNome do ficheiro:Buscar ubicación...Buscar...Encher de corEncher de cor á áreaO cartafol «%s» non conten convert.exeTipos de letra e coresFrancésGalegoXeralAlemánIr á seguinte follaIr á folla anteriorAltura:Non se atoparon os ficheiros de axuda, quere descargalos?AltoA máis altaResaltar cun lápis transparenteResaltadorHindiReproductor de historialÃconasSe non garda os cambios dos últimos %s perderanse permanentemente.Ignora a cor de fondoImaxeFicheiros de imaxesUbicación de ImageMagickNotificación de ImageMagickNon se atopou ImageMagick. Non poderá cargar ficheiros PDF e PS até que o instale.Preferencias de importación desde...Importar varios tipos de ficheiroTexto de entradaInserir unha notaInserir multimedia e sonTipo de ficheiro non válido para exportar como:ItalianoItálicaXaponésLicenzaLuzLiñaCargue un ficheiro gardado de Whyteboard, unha imaxe ou converta un documento PDF/PSCargar no ficheiro de preferencias de WhyteboardFicheiro cargadoCargando...MultimediaFicheiros multimediaBai&xarS&ubirMover forma cara &abaixoMover forma cara en&ribaMover a forma cara abaixoMover forma ao &péMover forma cara á ci&maMover a forma ao finMover a forma ao principioMover a forma cara enribaMover á &cimaMover á a&baixoMove a forma seleccionada actualmente cara abaixoMove a forma seleccionada actualmente cara arribaMove a forma seleccionada actualmente cara encimaMove a forma seleccionada actualmente cara embaixoNova &xanelaNova follaSeguinte follaDato non gardadoNon hai formas debuxadasNormalNotaNota: unha calidade máis alta tardará máis tempo en converterseNotasNúmero de follas pechadas recentementeNúmero de columnas na caixa de ferramentasNúmero de puntos: %sAbrir un &recenteAbrir un ficheiroAbrir un ficheiro...Abrir unha nova instancia de WhyteboardP&referencias...Visor da cahé de PDFConversión a PDFPDF/PS/SVGConf&igurar páxinaPáxina:PáxinasPegar unha imaxe/textoPegar desde o portapapeis nunha nova follaPegar un texto ou unha imaxe desde o portapapeis no WhyteboardPegar nunha &nova follaNon se atopa a ruta á imaxe %s.LapisColle a cor do píxel seleccionadoPor favor, encha o seu endezo de correo-eSírvase fornecer algunha informaciónPolígonoPortuguésPosiciónPrefere&nciasPreferenciasFolla AnteriorImprimir a vista pre&viaImprimir a vista previaImprimir a páxina actualErro de impresiónPropiedadesCalidadeSaír de WhyteboardRadioVo&lver a cargar preferenciasEli&minar follaFollas pe_chadas recentementeFicheiros abertos recentementeRectánguloRefacer a última acción desfeitaRefacer a última operación desfeitaVolver a cargar o seu ficheiro de preferenciasElimina os elementos da cachéRenomear follaRenomear a folla actualRenomear esta folla como:Informar de calquera erro ou problema con WhyteboardRedimensionar o len&zoRedimensionar o lenzoRecuperar a folla «%s»Volvelo tentarRectángulo redondeadoRusoGardar &como...Gardar debuxoGardar o ficheiro?Gardar Whyteboard como...Gardar os cambios de «%s» antes de pechar?Gardar os datos de WhyteboardGardar os datos de Whyteboard nun ficheiro novoGardando...Buscar actualizacións para WhyteboardEscoller un tipo de letraSeleccionar unha área rectangular para copiar como mapa de bitsSeleccione unha forma para mover ou redimensionarSeleccione o tirador de tamañoSelecciona esta formaEnviar &comentariosEnviar comentariosEnviar comentarios directamente ao desenvolvedor de WhyteboardConfigurar a cor de fondoConfigurar a cor de primeiro planoConfigurar o volumeConfigurar a páxina para impresiónEstabelece o grosor do debuxoFor&masSelección de formas Visor de formasAs formas na parte superior da lista debuxanse sobre as formas da parte inferiorFollaTecla de acceso rápidoMostrar e agochar a grella de corMostrar e agochar a barra de estadoMostrar e agochar a ferramenta de previsualizaciónMostrar e agochar a barra de ferramentasMostrar a grella de corMostrar o título cando se imprimeMostrar a ferramenta de previsualizaciónPasar á posiciónEspañolIntercambiar &coresIntercambia as cores de primeiro e segundo planoT&ransparenteTextoNon hai elementos na cache para mostrarEstá dispoñíbel unha nova versión,%(version)s Ficheiro:%(filename)s Tamaño: %(filesize)sDetectouse un problema na impresión. Pode que a súa impresora actual non estea configurada axeitadamente?GrosorGrosor:MiniaturasCambia a transparencia da forma seleccionadaFerramenta de &previsualizaciónVista da caixa de ferramentas:Traduza Whyteboard ao seu idiomaTraducido porTransparenteSeleccionar mapa de bits transparente (pode debuxalo aos poucos)TipoNon se pode cargar %s: formato non admitido?Non é posíbel reproducir o ficheiro %sDesfacer a última acciónDesfacer a última folla pechadaDesfacer a última operaciónSen títuloActualizarActualizaciónsActualizando miniaturasFicheiros de vídeoVerVer Whyteboard en modo de pantalla completaVer os documentos de axuda de WhyteboardVer unha vista preliminar da páxina a imprimirVer todas as follas pechadas recentementeVer e editar a orde de debuxo das formasVer e modificar a cache PDF de WhyteboardVer e reproducir o seu historial de debuxosVer información sobre WhyteboardVer a barra de estadoVer a barra de ferramentasGalésFicheiros de preferencias de WhyteboardWhyteboard non admite o tipo de ficheiroFicheiro Whyteboard Ficheiros WhyteboardWhyteboard usa ImageMagick para cargar ficheiros PDF, SVG e PS. Por favor seleccione a ubicación da instalación.Whyteboard traducirase ao %s cando se reinicieWhyteboard cargará estos ficheiros da súa caché no canto de volver a convertilosLargo:Escrito porEstá executando a última versión.Non se configuraron as preferenciasO seu comentario:Os seus comentarios foron enviados, grazas.ZoomAcercar e alonxar o lenzohorahorasidiomaminutominutossegundosegundoswhyteboard-0.41.1/whyteboard/lib/pubsub/utils/exchandling.py0000777000175000017500000001217011443222121023217 0ustar stevesteveimport sys, traceback class TracebackInfo: ''' Represent the traceback information for when an exception is raised -- but not caught -- in a listener. The complete traceback cannot be stored since this leads to circular references (see docs for sys.exc_info()) which keeps listeners alive even after the application is no longer referring to them. Instances of this object are given to listeners of the 'uncaughtExcInListener' topic as the excTraceback kwarg. The instance calls sys.exc_info() to get the traceback info but keeps only the following info: * self.ExcClass: the class of exception that was raised and not caught * self.excArg: the argument given to exception when raised * self.traceback: list of quadruples as returned by traceback.extract_tb() Normally you just need to call one of the two getFormatted() methods. ''' def __init__(self): tmpInfo = sys.exc_info() self.ExcClass = tmpInfo[0] self.excArg = tmpInfo[1] self.traceback = traceback.extract_tb(tmpInfo[2].tb_next.tb_next) del tmpInfo def getFormattedList(self): '''Get a list of strings as returned by the traceback module's format_list() and format_exception_only() functions.''' tmp = traceback.format_list(self.traceback) tmp.extend( traceback.format_exception_only(self.ExcClass, self.excArg) ) return tmp def getFormattedString(self): '''Get a string similar to the stack trace that gets printed to stdout by Python interpreter when an exception is not caught.''' return ''.join(self.getFormattedList()) class IExcHandler: '''Interface class for the exception handler. Such handler is called whenever a listener raises an exception during a pub.sendMessage(). You may give an instance of a class derived from IExcHandler to pubsubconf.setListenerExcHandler(). ''' def __call__(self, listenerID): raise NotImplementedError('%s must override __call__()' % self.__class__) class ExcPublisher(IExcHandler): topicUncaughtExc = 'uncaughtExcInListener' def __init__(self): self.__handling = None def init(self, topicMgr): '''Your code must call this after the first import of pubsub. It is possible that some modules imported by your application import pubsub. So it is recommended that you import pubsub in your main script, and right after, call this method.''' self.__topicObj = topicMgr.newTopic( _name = self.topicUncaughtExc, _desc = 'generated when a listener raises an exception', listenerStr = 'string representation of listener', excTraceback = 'instance of TracebackInfo containing exception info') def __call__(self, listenerID): '''An exception has been raised. Now we send the excInfo to all subscribers of topic self.topicUncaughtExc. Note that if one of those listeners raises an exception, this __call__ will be called again by pubsub. So we guard against infinite recursion. In such case, we raise ExcHandlerError, which will interrupt the original sendMessage. ''' tbInfo = TracebackInfo() if self.__handling: raise ExcHandlerError(listenerID, tbInfo, *self.__handling) try: self.__handling = (listenerID, tbInfo) self.__topicObj.publish( dict(listenerStr=listenerID, excTraceback=tbInfo) ) finally: self.__handling = None class ExcHandlerError(RuntimeError): ''' Whenever an exception gets raised within some listener during a sendMessage(), a message of topic ExcPublisher.topicUncaughtExc is sent, and then sending to remaining listeners resumes. However, if a listener of topic ExcPublisher.topicUncaughtExc *also* raises an exception, the original sendMessage() operation must be aborted: an ExcHandlerError exception gets raised. Information about the exception raised in the "uncaught exception" listener is stored as members of class: - self.badExcListenerID = which "uncaught exception" *listener* raised an exception - self.tbInfo = instance of TracebackInfo for that exception - self.origListenerID = which "regular" listener raised the original "uncaught exception" - self.origListenerTbInfo = instance of TracebackInfo, for original "uncaught exception" ''' def __init__(self, badExcListenerID, tbInfo, origListenerID, origListenerTbInfo): self.badExcListenerID = badExcListenerID self.tbInfo = tbInfo self.origListenerID = origListenerID self.origListenerTbInfo = origListenerTbInfo RuntimeError.__init__(self, str(self)) def __str__(self): fmtStr = self.tbInfo.getFormattedString() return 'Exception listener %s raised exception:\n%s' \ % (self.badExcListenerID, fmtStr) whyteboard-0.41.1/whyteboard/lib/pubsub/utils/notification.py0000777000175000017500000003271611443222121023431 0ustar stevesteve''' Provide an interface class for handling pubsub notification messages, and an example class (though very useful in practice) showing how to use it. Notification messages are generated by pubsub - if a handler has been configured via pubsubconf.setNotificationHandler() - when pubsub does certain tasks, such as when a listener subscribes to or unsubscribes from a topic Derive from this class to handle notification events from various parts of pubsub. E.g. when a listener subscribes, unsubscribes, or dies, a notification handler, if you specified one via pubsubconf.setNotificationHandler(), is given the relevant information. ''' from .. import pubsubconf class INotificationHandler: ''' Defines the interface expected by pubsub for notification messages. Any instance that supports the same methods, or derives from this class, will work as a notification handler for pubsub events. ''' def notifySubscribe(self, subdLisnr, topicObj, didit): pass def notifyUnsubscribe(self, subdLisnr, topicObj): pass def notifySend(self, stage, topicObj): pass def notifyNewTopic(self, topicObj, description, required, argsDocs): pass def notifyDelTopic(self, topicName): pass def notifyDeadListener(self, topicObj, listener): pass class NotificationToStdout: ''' Print a message to stdout when a notification is received. ''' def notifySubscribe(self, listener, topicObj, didit): if listener is None: msg = '%sSubscription of "%s" to topic "%s" redundant' else: msg = '%sSubscribed listener "%s" to topic "%s"' print msg % (self.prefix, listener, topicObj.getName()) def notifyUnsubscribe(self, subdLisnr, topicObj): if didit: msg = '%sUnsubscribed listener "%s" from topic "%s"' else: msg = '%sUnsubscription of "%s" from topic "%s" redundant' print msg % (self.prefix, listener, topicObj.getName()) def notifySend(self, stage, topicObj): if stage == 'pre': print '%sSending message of topic "%s"' % (self.prefix, topicObj.getName()) def notifyNewTopic(self, topicObj, description, required, argsDocs): print '%sNew topic "%s" created' % (self.prefix, topicObj.getName()) def notifyDelTopic(self, topicName): print '%sTopic "%s" destroyed' % (self.prefix, topicName) def notifyDeadListener(self, topicObj, listener): print '%sListener "%s" of Topic "%s" has died' % (self.prefix, listener, topic.getName()) class NotifyByPubsubMessage(INotificationHandler): ''' Handle pubsub notification messages by generating messages of a 'pubsub.' subtopic. Also provides an example of how to create a notification handler. Use it by calling:: import pubsub.utils notifHandler = pubsub.utils.useNotifyByPubsubMessage() ... from pubsub import pub notifHandler.createNotificationTopics() ... pub.setNotification(...) E.g. whenever a listener is unsubscribed, a 'pubsub.unsubscribe' message is generated. If you have subscribed a listener of this topic, your listener will be notified of what listener unsubscribed from what topic. ''' topicRoot = 'pubsub' topics = dict( send = '%s.sendMessage' % topicRoot, subscribe = '%s.subscribe' % topicRoot, unsubscribe = '%s.unsubscribe' % topicRoot, newTopic = '%s.newTopic' % topicRoot, delTopic = '%s.delTopic' % topicRoot, deadListener = '%s.deadListener' % topicRoot) def createNotificationTopics(self, topicMgr=None): '''Create the notification topics. The root of the topics created is self.topicRoot. Note that if topicMgr not given, then it will be obtained by importing pubsub. Since it is important that your code control when the first import occurs, this method raises a RuntimeError if pubsub hasn't already been imported. ''' if topicMgr is None: if not pubsubconf.isPackageImported(): raise RuntimeError('your code must import pubsub first') from pubsub import pub topicMgr = pub.getDefaultTopicMgr() # see if the special topics have already been defined try: topicMgr.getTopic(self.topicRoot) except RuntimeError: # no, so create them topicMgr.newTopic( _name = self.topicRoot, _desc = 'root of all pubsub-specific topics') self._pubTopic = topicMgr.getTopic(self.topicRoot) assert hasattr(self, '_pubTopic') _createTopics(self.topics, topicMgr) def notifySubscribe(self, subdLisnr, topicObj, didit): if hasattr(self, '_pubTopic'): pubTopic = self._pubTopic.subscribe if topicObj is not pubTopic: kwargs = dict(listener=subdLisnr, topic=topicObj, didit=didit) pubTopic.publish(kwargs) def notifyUnsubscribe(self, subdLisnr, topicObj): if hasattr(self, '_pubTopic'): pubTopic = self._pubTopic.unsubscribe if topicObj is not pubTopic: kwargs = dict( topic = topicObj, listenerRaw = subdLisnr.getCallable(), listener = subdLisnr) pubTopic.publish(kwargs) def notifySend(self, stage, topicObj): '''Stage must be 'pre' or 'post'.''' if hasattr(self, '_pubTopic'): sendMsgTopic = self._pubTopic.sendMessage if stage == 'pre' and (topicObj is sendMsgTopic): msg = 'Not allowed to send messages of topic %s' % topicObj.getName() raise ValueError(msg) sendMsgTopic.publish( dict(topic=topicObj, stage=stage) ) def notifyNewTopic(self, topicObj, desc, required, argsDocs): if hasattr(self, '_pubTopic'): pubTopic = self._pubTopic.newTopic pubTopic.publish( dict(topic=topicObj, description=desc, required=required, args=argsDocs)) def notifyDelTopic(self, topicName): if hasattr(self, '_pubTopic'): pubTopic = self._pubTopic.delTopic pubTopic.publish( dict(name=topicName) ) def notifyDeadListener(self, topicObj, listener): if hasattr(self, '_pubTopic'): pubTopic = self._pubTopic.deadListener kwargs = dict(topic=topicObj, listener=listener) pubTopic.publish(kwargs) def _createTopics(topicMap, topicMgr): ''' Create notification topics. These are used when some of the notification flags have been set to True (see pub.setNotification(). The topicMap is a dict where key is the notification type, and value is the topic name to create. Notification type is a string in ('send', 'subscribe', 'unsubscribe', 'newTopic', 'delTopic', 'deadListener'. ''' topicMgr.newTopic( _name = topicMap['subscribe'], _desc = 'whenever a listener is subscribed to a topic', topic = 'topic that listener has subscribed to', listener = 'instance of pub.Listener containing listener', didit = 'false if listener already subscribed, true otherwise') topicMgr.newTopic( _name = topicMap['unsubscribe'], _desc = 'whenever a listener is unsubscribed from a topic', topic = 'instance of Topic that listener has been unsubscribed from', listener = 'instance of pub.Listener unsubscribed; None if listener not found', listenerRaw = 'listener unsubscribed') topicMgr.newTopic( _name = topicMap['send'], _desc = 'sent at beginning and end of sendMessage()', topic = 'instance of topic for message being sent', stage = 'stage of send operation: "pre" or "post"') topicMgr.newTopic( _name = topicMap['newTopic'], _desc = 'whenever a new topic is defined', topic = 'instance of Topic created', description = 'description of topic (use)', args = 'the argument names/descriptions for arguments that listeners must accept', required = 'which args are required (all others are optional)') topicMgr.newTopic( _name = topicMap['delTopic'], _desc = 'whenever a topic is deleted', name = 'full name of the Topic instance that was destroyed') topicMgr.newTopic( _name = topicMap['deadListener'], _desc = 'whenever a listener dies without having unsubscribed', topic = 'instance of Topic that listener was subscribed to', listener = 'instance of pub.Listener containing dead listener') class PubsubTopicMsgLogger: ''' Default logger for 'pubsub.*' topics. These topics are used automatically in various pubsub calls; e.g. when a new topic is created, a 'pubsub.newTopic' message is generated. Note that such messages are generated only if pub.setNotification() was called. Each method of PubsubTopicMsgLogger can be given to pub.subscribe() as a listener. A method's name indicates which 'pubsub' subtopic it can be listening for. E.g. you would subscribe the PubsubTopicMsgLogger.subscribe method to listen for 'pubsub.subscribe' messages: from pubsub import pub from pubsubutils import PubsubTopicMsgLogger logger = PubsubTopicMsgLogger(pub) pub.subscribe('pubsub.subscribe', logger.subscribe) Any number of instances can be created. By default, the __init__ will subscribe instance to all 'pubsub' subtopics. Class can also be derived to override default behavior. ''' prefix = 'PUBSUB: ' def __init__(self, publisher=None): '''If publisher is not None, then all self's methods are subscribed to the 'pubsub.*' topics by using publisher.subscribe(). ''' if publisher is not None: pub = publisher pub.subscribe(self.psOnSubscribe, 'pubsub.subscribe') pub.subscribe(self.psOnUnsubscribe, 'pubsub.unsubscribe') pub.subscribe(self.psOnNewTopic, 'pubsub.newTopic') pub.subscribe(self.psOnDelTopic, 'pubsub.delTopic') pub.subscribe(self.psOnSendMessage, 'pubsub.sendMessage') pub.subscribe(self.psOnDeadListener,'pubsub.deadListener') pub.subscribe(self.psOnUncaughtExcInListener, 'uncaughtExcInListener') def psOnSubscribe(self, topic=None, listener=None, didit=None): '''Give this to pub.subscribe() as listener of 'pubsub.subscribe' messages. ''' if listener is None: msg = '%sSubscription of "%s" to topic "%s" redundant' else: msg = '%sSubscribed listener "%s" to topic "%s"' print msg % (self.prefix, listener, topic.getName()) def psOnUnsubscribe(self, topic=None, listener=None, listenerRaw=None): '''Give this to pub.subscribe() as listener of 'pubsub.unsubscribe' messages. ''' if didit: msg = '%sUnsubscribed listener "%s" from topic "%s"' else: msg = '%sUnsubscription of "%s" from topic "%s" redundant' print msg % (self.prefix, listener, topic.getName()) def psOnNewTopic(self, topic=None, description=None, args=None, required=None): '''Give this to pub.subscribe() as listener of 'pubsub.newTopic' messages. ''' print '%sNew topic "%s" created' % (self.prefix, topic.getName()) def psOnDelTopic(self, name=None): '''Give this to pub.subscribe() as listener of 'pubsub.delTopic' messages. ''' print '%sTopic "%s" destroyed' % (self.prefix, name) def psOnSendMessage(self, topic=None, stage=None): '''Give this to pub.subscribe() as listener of 'pubsub.sendMessage' messages. ''' if stage == 'pre': print '%sSending message of topic "%s"' % (self.prefix, topic.getName()) def psOnDeadListener(self, topic=None, listener=None): print '%sListener "%s" of Topic "%s" has died' % (self.prefix, listener, topic.getName()) def psOnUncaughtExcInListener(self, listenerStr=None, excTraceback=None): '''Give this to pub.subscribe() as listener of 'uncaughtExcInListener' messages. ''' print '%sListener "%s" raised an exception. Traceback:' \ % (self.prefix, listenerStr) print excTraceback.getFormattedString() def useNotifyByPubsubMessage(): '''Install an instance of NotifyByPubsubMessage as notification handler. Return the instance. You must call its init() once pubsub imported (since instance defines some pubsub topics): import pubsub.utils notifHandler = pubsub.utils.useNotifyByPubsubMessage() ... from pubsub import pub notifHandler.createNotificationTopics() ''' import pubsub.utils notifHandler = pubsub.utils.NotifyByPubsubMessage() import pubsubconf pubsubconf.setNotificationHandler(notifHandler) return notifHandler def useDefaultLoggingNotification(all=True, **kwargs): '''The kwargs are the same as for PubsubTopicMsgLogger constructor, except that 'all' defaults to True instead of None (ie assumes that if you don't specify what notifications you want, that you want them all).''' import pubsub.utils notifHandler = pubsub.utils.NotifyByPubsubMessage() import pubsubconf pubsubconf.setNotificationHandler(notifHandler) from pubsub import pub notifHandler.createNotificationTopics() pub.setNotification(all=all, **kwargs) return PubsubTopicMsgLogger(pub) whyteboard-0.41.1/whyteboard/lib/pubsub/utils/topicspec.py0000777000175000017500000002320211443222121022722 0ustar stevesteve''' Provides classes useful for specifying topics by lookup. The classes TopicTreeDefnSimple and TopicTreeDefnRobust can be derived into topic trees and given to pubsub's addTopicDefnProvider() function. Doing so will cause pubsub to check the provided topic tree for the topic documentation string and topic message data specification (TMD) whenever a new topic is created, which typically would happen in subscribe() and sendMessage() the first time they are called with a topic unknown to pubsub. A printout of the tree using pubsub.utils.printTreeDocs() will show you which topics your application has created in a particular run, which ones are undocumented (usually an indication of typo in topic name), and what message data is expected/required for each topic. Usage: derive from TopicTreeDefnSimple or TopicTreeDefnRobust and add class data members, nested classes and class documentation to represent the TMD and topic documentation. Also use the '_required' field to specify which TMD are required arguments to any sendMessage() for associated topic. Both TopicTreeDefnSimple and TopicTreeDefnRobust show example of defining the same topic tree, which is a two step process: 1. Define topic hierarchy by deriving from one of the TopicTreeDefn* classes 2. Call pub.addTopicDefnProvider( TopicTreeDefnSimple() ) somewhere in your application, usually in the startup script. The sendMessage() calls below would be valid for the given topic tree definition: sendMessage('topic1', arg1=..., arg2=..., arg3=...) sendMessage('topic1', arg1=..., arg2=...) sendMessage('topic1.subtopic1', arg1=..., arg2=...) sendMessage('topic1.subtopic1', arg1=..., arg2=..., arg3=...) sendMessage('topic1.subtopic1', arg1=..., arg2=..., arg3a=...) sendMessage('topic1.subtopic1', arg1=..., arg2=..., arg3=..., arg3a=...) sendMessage('topic1.subtopic2', arg1=..., arg2=..., arg3b=...) sendMessage('topic1.subtopic2', arg1=..., arg2=..., arg3=..., arg3b=...) would work on it too, it could be called in the sendMessage(arg1=..., arg2=..., arg3=...) The following would be invalid: sendMessage('topic1', arg1=...): required arg2 missing sendMessage('topic1.subtopic1', arg2=..., arg3=..., arg3a=...): required arg1 missing sendMessage('topic1.subtopic2', arg1=..., arg2=...): required arg3b missing Note that you can easily create your own defn provider by deriving from ITopicTreeDefnProvider or TopicTreeDefnBase. ''' from inspect import isclass __all__ = ('ITopicTreeDefnProvider', 'TopicTreeDefnSimple', 'TopicTreeDefnRobust', 'TopicArg', 'TopicDefn') class ITopicTreeDefnProvider: ''' Any topic tree definition provider must provide the following interface ''' def getSubSpec(self, topicNameTuple): '''Get the given topic's message data specification. This returns a pair (argsDocs, requiredArgs) where first item is a map of argument names and documentation for each arg, and second item is a list of the keys in argsDocs that represent topic arguments that are required when pubsub.sendMessage() is called. Returns (None, None) if no specification is available. Note that only the topic's sub-arguments are returned, i.e. if topic is 'a.b' and 'a' has arguments 'arg1', 'arg2' while 'a.b' adds sub-arguments 'arg3', then this method returns an argsDocs having only 'arg3' as key. ''' raise NotImplementedError def getDescription(self, topicNameTuple): '''Return the description string for given topic, or None if none available.''' raise NotImplementedError class TopicTreeDefnBase (ITopicTreeDefnProvider): REQUIRED_ATTR = '_required' HAS_ARGSSPEC_ATTR = '_hasSpecification' argsSpecified = [] def getSubSpec(self, topicNameTuple): obj = self.__findTopicDefn(topicNameTuple) if not self.__hasSpecification(topicNameTuple, obj): return None, None # set description and args argsDocs = dict( [(arg, str(desc)) for (arg, desc) in obj.__dict__.iteritems() if (not arg.startswith('_')) and self._isTopicArg(desc)] ) # set required required = getattr(obj, self.REQUIRED_ATTR, ()) if isinstance(required, str): required = (required,) else: # make sure we have a tuple not a list required = tuple(required) # ok: return argsDocs, required def getDescription(self, topicNameTuple): obj = self.__findTopicDefn(topicNameTuple) if obj is None: return None return obj.__doc__ def _isTopicDefn(self, attr): '''This can be overridden to change how the tree should determine if an attribute is a topic definition. Should return True if attr qualifies as a topic definition (ie it has a documentation string, class attributes that define its message arguments, and nested classes that define subtopics), False otherwise. Called by getDescription() and getSubSpec().''' raise NotImplementedError def _isTopicArg(self, attr): '''This can be overridden to change how the tree should determine if an attribute is a topic argument. Should return True if attr qualifies as a topic argument (ie it represents an argument name and it documentation), False otherwise. Called by getSubSpec().''' raise NotImplementedError def __hasSpecification(self, topicName, topicObj): '''Returns true only if topicObj is not None and has an argument specification.''' if topicObj is None: return False if topicObj.__doc__ is None: return False if hasattr(topicObj, self.HAS_ARGSSPEC_ATTR): hasSpec = getattr(topicObj, self.HAS_ARGSSPEC_ATTR) print 'topic def for %s has %s=%s' % (topicName, self.HAS_ARGSSPEC_ATTR, hasSpec) return hasSpec if topicName in self.argsSpecified: print 'topic in argsSpecified list', topicName return True #print 'all ok, so defaults to has spec', topicName return True def __findTopicDefn(self, topicNameTuple): '''Find the topic definition object for given topic name. Returns the object, or None if not found.''' if not topicNameTuple: return None obj = self exists = True for name in topicNameTuple: obj = getattr(obj, name, None) exists = (obj is not None) and self._isTopicDefn(obj) if not exists: return None return obj class TopicTreeDefnSimple(TopicTreeDefnBase): ''' Subtopics are nested classes, TMD are class data members. For instance: class MyTopicTree(TopicTreeDefnSimple): class topic1: """Docs for topic1, several lines of it, many many lines of it""" arg1 = "explain what arg1 is for" arg2 = "explain what arg2 is for" arg3 = "explain what arg3 is for" _required = ('arg1', 'arg2') class subtopic1: """Docs for subtopic1, several lines of it, many many lines of it""" arg3a = "explain what arg3a is for" class subtopic2: """Docs for subtopic2, several lines of it, many many lines of it""" arg3b = "explain what arg3b is for" _required = 'arg3b' The above example defines topics 'topic1', 'topic1.subtopic1' and 'topic1.subtopic2'. The module documentation describes how the above tree affects the validity of calls to sendMessage(). ''' def _isTopicDefn(self, attr): return isclass(attr) def _isTopicArg(self, attr): return isinstance(attr, str) class TopicTreeDefnRobust(TopicTreeDefnBase): ''' Subtopics are nested classes, TMD are class data members. For instance: class MyTopicTree(TopicTreeDefnRobust): class topic1(TopicDefn): """Docs for topic1, several lines of it, many many lines of it""" arg1 = TopicArg("explain what arg1 is for") arg2 = TopicArg("explain what arg2 is for") arg3 = TopicArg("explain what arg3 is for") _required = ('arg1', 'arg2') class subtopic1(TopicDefn): """Docs for subtopic1, several lines of it, many many lines of it""" arg3a = TopicArg("explain what arg3a is for") class subtopic2(TopicDefn): """Docs for subtopic2, several lines of it, many many lines of it""" arg3b = TopicArg("explain what arg3b is for") _required = 'arg3b' ''' def _isTopicDefn(self, attr): return isinstance(attr, TopicDefn) def _isTopicArg(self, attr): return isinstance(attr, TopicArg) class TopicArg: '''Used to 'tag' a class attribute as a TMD, when using TopicTreeDefnRobust as the base for topic tree definition. ''' def __init__(self, desc): self.value = desc def __str__(self): return self.value class TopicDefn: '''Used to 'tag' a nested class a subtopic, when using TopicTreeDefnRobust as the base for topic tree definition. ''' pass whyteboard-0.41.1/whyteboard/lib/pubsub/utils/utils.py0000777000175000017500000004624211443222121022102 0ustar stevesteve""" Provides useful functions and classes. Most useful are probably printTreeDocs and printTreeSpec. """ __all__ = ('StructMsg', 'Callback', 'TopicTreeTraverser', 'TopicTreePrinter', 'TopicTreeAsSpec', 'printTreeDocs', 'printTreeSpec') class StructMsg: ''' This *can* be used to package message data. Each of the keyword args given at construction will be stored as a member of the 'data' member of instance. E.g. "m=Message2(a=1, b='b')" would succeed "assert m.data.a==1" and "assert m.data.b=='b'". However, use of Message2 makes your messaging code less documented and harder to debug. ''' def __init__(self, **kwargs): class Data: pass self.data = Data() self.data.__dict__.update(kwargs) class Callback: '''This can be used to wrap functions that are referenced by class data if the data should be called as a function. E.g. given >>> def func(): pass >>> class A: ....def __init__(self): self.a = func then doing >>> boo=A(); boo.a() will fail since Python will try to call a() as a method of boo, whereas a() is a free function. But if you have instead "self.a = Callback(func)", then "boo.a()" works as expected. ''' def __init__(self, callable_): self.__callable = callable_ def __call__(self, *args, **kwargs): return self.__callable(*args, **kwargs) from textwrap import TextWrapper, dedent class TopicTreeTraverser: ''' Topic tree traverser. Provides the traverse() method which traverses a topic tree and calls self._onTopic() for each topic in the tree that satisfies self._accept(). Additionally it calls self._startChildren() whenever it starts traversing the subtopics of a topic, and self._endChildren() when it is done with the subtopics. Finally, it calls self._doneTraversal() when traversal has been completed. Derive from TopicTreeTraverser and override one or more of the four self._*() methods described above. Call traverse() on instances to "execute" the traversal. ''' DEPTH = 'Depth first through topic tree' BREADTH = 'Breadth first through topic tree' MAP = 'Sequential through topic manager\'s topics map' def _accept(self, topicObj): '''Override this to filter nodes of topic tree. Must return True (accept node) of False (reject node). Note that rejected nodes cause traversal to move to next branch (no children traversed).''' return True def _startTraversal(self): '''Override this to define what to do when traversal() starts.''' pass def _onTopic(self, topicObj): '''Override this to define what to do for each node.''' pass def _startChildren(self): '''Override this to take special action whenever a new level of the topic hierarchy is started (e.g., indent some output). ''' pass def _endChildren(self): '''Override this to take special action whenever a level of the topic hierarchy is completed (e.g., dedent some output). ''' pass def _doneTraversal(self): '''Override this to take special action when traversal done.''' pass def traverse(self, topicObj, how=DEPTH, onlyFiltered=True): '''Start traversing tree at topicObj. Note that topicObj is a Topic object, not a topic name. The how defines if tree should be traversed breadth or depth first. If onlyFiltered is False, then all nodes are accepted (_accept(node) not called). ''' if how == self.MAP: raise NotImplementedError('not yet available') self._startTraversal() if how == self.BREADTH: self.__traverseBreadth(topicObj, onlyFiltered) else: #if how == self.DEPTH: self.__traverseDepth(topicObj, onlyFiltered) self._doneTraversal() def __traverseBreadth(self, topicObj, onlyFiltered): def extendQueue(subtopics): topics.append(self._startChildren) topics.extend(subtopics) topics.append(self._endChildren) topics = [topicObj] while topics: topicObj = topics.pop(0) if topicObj in (self._startChildren, self._endChildren): topicObj() continue if onlyFiltered: if self._accept(topicObj): extendQueue( topicObj.getSubtopics() ) self._onTopic(topicObj) else: extendQueue( topicObj.getSubtopics() ) if self._accept(topicObj): self._onTopic(topicObj) def __traverseDepth(self, topicObj, onlyFiltered): def extendStack(topicTreeStack, subtopics): topicTreeStack.insert(0, self._endChildren) # marker functor # put subtopics in list in alphabetical order subtopicsTmp = subtopics subtopicsTmp.sort(reverse=True, key=topicObj.__class__.getName) for sub in subtopicsTmp: topicTreeStack.insert(0, sub) # this puts them in reverse order topicTreeStack.insert(0, self._startChildren) # marker functor topics = [topicObj] while topics: topicObj = topics.pop(0) if topicObj in (self._startChildren, self._endChildren): topicObj() continue if onlyFiltered: if self._accept(topicObj): extendStack( topics, topicObj.getSubtopics() ) self._onTopic(topicObj) else: extendStack( topics, topicObj.getSubtopics() ) if self._accept(topicObj): self._onTopic(topicObj) class TopicTreePrinter(TopicTreeTraverser): ''' Example topic tree TopicTreeTraverser that prints a prettified representation of topic tree by doing a depth-first traversal of topic tree and print information at each (topic) node of tree. Extra info to be printed is specified via the 'extra' kwarg. Its value must be a list of characters, the order determines output order: - D: print description of topic - A: print topic kwargs and their description - a: print kwarg names only - L: print listeners currently subscribed to topic E.g. TopicTreePrinter(extra='LaDA') would print, for each topic, the list of subscribed listeners, the topic's list of kwargs, the topic description, and the description for each kwarg, >>> Topic "delTopic" >> Listeners: > listener1_2880 (from yourModule) > listener2_3450 (from yourModule) >> Names of Message arguments: > arg1 > arg2 >> Description: whenever a topic is deleted >> Descriptions of Message arguments: > arg1: (required) its description > arg2: some other description ''' allowedExtras = frozenset('DAaL') # must NOT change def __init__(self, extra=None, width=70, indentStep=4, bulletTopic='\\--', bulletTopicItem='|==', bulletTopicArg='-', fileObj=None): '''Topic tree printer will print listeners for each topic only if printListeners is True. The width will be used to limit the width of text output, while indentStep is the number of spaces added each time the text is indented further. The three bullet parameters define the strings used for each item (topic, topic items, and kwargs). ''' self.__contentMeth = dict( D = self.__printTopicDescription, A = self.__printTopicArgsAll, a = self.__printTopicArgNames, L = self.__printTopicListeners) assert self.allowedExtras == set(self.__contentMeth.keys()) import sys self.__destination = fileObj or sys.stdout self.__output = [] self.__content = extra or '' unknownSel = set(self.__content) - self.allowedExtras if unknownSel: msg = 'These extra chars not known: %s' % ','.join(unknownSel) raise ValueError(msg) self.__width = width self.__wrapper = TextWrapper(width) self.__indent = 0 self.__indentStep = indentStep self.__topicsBullet = bulletTopic self.__topicItemsBullet = bulletTopicItem self.__topicArgsBullet = bulletTopicArg def getOutput(self): return '\n'.join( self.__output ) def _doneTraversal(self): if self.__destination is not None: self.__destination.write(self.getOutput()) def _onTopic(self, topicObj): '''This gets called for each topic. Print as per specified content.''' # topic name self.__wrapper.width = self.__width indent = self.__indent head = '%s Topic "%s"' % (self.__topicsBullet, topicObj.getTailName()) self.__output.append( self.__formatDefn(indent, head) ) indent += self.__indentStep # each extra content (assume constructor verified that chars are valid) for item in self.__content: function = self.__contentMeth[item] function(indent, topicObj) def _startChildren(self): '''Increase the indent''' self.__indent += self.__indentStep def _endChildren(self): '''Decrease the indent''' self.__indent -= self.__indentStep def __formatDefn(self, indent, item, defn='', sep=': '): '''Print a definition: a block of text at a certain indent, has item name, and an optional definition separated from item by sep. ''' if defn: prefix = '%s%s%s' % (' '*indent, item, sep) self.__wrapper.initial_indent = prefix self.__wrapper.subsequent_indent = ' '*(indent+self.__indentStep) return self.__wrapper.fill(defn) else: return '%s%s' % (' '*indent, item) def __printTopicDescription(self, indent, topicObj): # topic description defn = '%s Description' % self.__topicItemsBullet self.__output.append( self.__formatDefn(indent, defn, topicObj.getDescription()) ) def __printTopicArgsAll(self, indent, topicObj, desc=True): # topic kwargs args = topicObj.getArgDescriptions() if args: #required, optional, complete = topicObj.getArgs() headName = 'Names of Message arguments:' if desc: headName = 'Descriptions of message arguments:' head = '%s %s' % (self.__topicItemsBullet, headName) self.__output.append( self.__formatDefn(indent, head) ) tmpIndent = indent + self.__indentStep required = topicObj.getArgs()[0] for key, arg in args.iteritems(): if not desc: arg = '' elif key in required: arg = '(required) %s' % arg msg = '%s %s' % (self.__topicArgsBullet,key) self.__output.append( self.__formatDefn(tmpIndent, msg, arg) ) def __printTopicArgNames(self, indent, topicObj): self.__printTopicArgsAll(indent, topicObj, False) def __printTopicListeners(self, indent, topicObj): if topicObj.hasListeners(): listeners = topicObj.getListeners() item = '%s Listeners:' % self.__topicItemsBullet self.__output.append( self.__formatDefn(indent, item) ) tmpIndent = indent + self.__indentStep for listener in listeners: item = '%s %s (from %s)' % (self.__topicArgsBullet, listener.name(), listener.module()) self.__output.append( self.__formatDefn(tmpIndent, item) ) class TopicTreeAsSpec(TopicTreeTraverser): ''' Prints the class representation of topic tree, as Python code that can be imported. The printout goes to stdout, unless an open file object is given to constructor via the fileObj parameter. ''' def __init__(self, width=70, indentStep=4, header=None, footer=None, fileObj=None): '''Can specify the width of output, the indent step, the header and footer to print, and the destination fileObj. If no destination file, then stdout is assumed. Typically, the only argument is the destination file in which to put the output.''' import sys self.__destination = fileObj or sys.stdout self.__output = [] self.__header = header self.__footer = footer self.__width = width self.__wrapper = TextWrapper(width) self.__indent = 0 self.__indentStep = indentStep from pubsub import pub self.__ROOT_TOPIC = pub.ALL_TOPICS self.__comment = '''\ automatically generated by pubsub.utils.printTreeSpec(**kwargs) with kwargs = %(printKwargs)s ''' % dict(printKwargs = dict(width=width, indentStep=indentStep, header=header, footer=footer, fileObj=fileObj) ) def getOutput(self): return '\n'.join( self.__output ) def _startTraversal(self): # output comment self.__wrapper.initial_indent = '# ' self.__wrapper.subsequent_indent = self.__wrapper.initial_indent self.__output.append( self.__wrapper.fill(self.__comment) ) self.__output.append('') self.__output.append('') # output header: if self.__header: self.__output.append(self.__header) def _doneTraversal(self): if self.__footer: self.__output.append('') self.__output.append('') self.__output.append(self.__footer) if self.__destination is not None: self.__destination.write(self.getOutput()) def _onTopic(self, topicObj): '''This gets called for each topic. Print as per specified content.''' if topicObj.getName() == self.__ROOT_TOPIC: return self.__output.append( '' ) # topic name self.__wrapper.width = self.__width head = 'class %s:' % topicObj.getTailName() self.__formatItem(head) # each extra content (assume constructor verified that chars are valid) self.__printTopicDescription(topicObj) self.__printTopicArgsAll(topicObj) def _startChildren(self): '''Increase the indent''' self.__indent += self.__indentStep def _endChildren(self): '''Decrease the indent''' self.__indent -= self.__indentStep def __printTopicDescription(self, topicObj): indent = self.__indentStep self.__formatItem("'''", indent) self.__formatBlock( topicObj.getDescription(), indent ) self.__formatItem("'''", indent) def __printTopicArgsAll(self, topicObj): indent = self.__indentStep argsDocs = topicObj.getArgDescriptions() for key, argDesc in argsDocs.iteritems(): msg = "%s = '%s'" % (key, argDesc) self.__formatItem(msg, indent) required = topicObj.getArgs()[0] if required: self.__formatItem('_required = %s' % `required`, indent) def __formatItem(self, item, extraIndent=0): indent = extraIndent + self.__indent self.__output.append( '%s%s' % (' '*indent, item) ) def __formatBlock(self, text, extraIndent=0): self.__wrapper.initial_indent = ' '*(self.__indent + extraIndent) self.__wrapper.subsequent_indent = self.__wrapper.initial_indent self.__output.append( self.__wrapper.fill(text) ) def printTreeDocs(rootTopic=None, **kwargs): '''Uses the TopicTreePrinter to print out the topic tree to stdout, starting at rootTopic The kwargs are the same as for TopicTreePrinter constructor. ''' printer = TopicTreePrinter(**kwargs) if rootTopic is None: from pubsub import pub rootTopic = pub.getDefaultRootTopic() assert rootTopic is not None printer.traverse(rootTopic) defaultTopicTreeSpecHeader = \ """\ from pubsub.utils import TopicTreeDefnSimple class MyTopicTree(TopicDefnProvider): ''' Topic tree for application. Note that hierarchy can be extended at run-time (e.g. by modules or plugins), and that more than one hierarchy can be used as specification. In this case, the first "provider" to provide a topic specification will be used. '''\ """ defaultTopicTreeSpecFooter = \ """\ # Following lines will cause the above topic tree # specification to be registered with pubsub as soon as # you import this file. from pubsub import pub pub.addTopicDefnProvider( MyTopicTree() ) """ def printTreeSpec(rootTopic=None, **kwargs): '''Prints the topic tree specification starting from rootTopic. If not specified, the whole topic tree is printed. The kwargs are the same as TopicTreeAsSpec's constructor. If no header or footer are given, the defaults are used (see defaultTopicTreeSpecHeader and defaultTopicTreeSpecFooter), such that the resulting output can be imported in your application. E.g.:: pyFile = file('appTopicTree.py','w') printTreeSpec( pyFile ) pyFile.close() import appTopicTree ''' kwargs.setdefault('header', defaultTopicTreeSpecHeader) kwargs.setdefault('footer', defaultTopicTreeSpecFooter) printer = TopicTreeAsSpec(**kwargs) if rootTopic is None: from pubsub import pub rootTopic = pub.getDefaultRootTopic() assert rootTopic is not None printer.traverse(rootTopic) class Enum: '''Used only internally. Represent one value out of an enumeration set. It is meant to be used as:: class YourAllowedValues: enum1 = Enum() # or: enum2 = Enum(value) # or: enum3 = Enum(value, 'descriptionLine1') # or: enum3 = Enum(None, 'descriptionLine1', 'descriptionLine2', ...) val = YourAllowedValues.enum1 ... if val is YourAllowedValues.enum1: ... ''' nextValue = 0 values = set() def __init__(self, value=None, *desc): '''Use value if given, otherwise use next integer.''' self.desc = '\n'.join(desc) if value is None: assert Enum.nextValue not in Enum.values self.value = Enum.nextValue Enum.values.add(self.value) Enum.nextValue += 1 # check that we haven't run out of integers! if Enum.nextValue == 0: raise RuntimeError('Ran out of enumeration values?') else: try: value + Enum.nextValue raise ValueError('Not allowed to assign integer to enumerations') except TypeError: pass self.value = value if self.value not in Enum.values: Enum.values.add(self.value) whyteboard-0.41.1/whyteboard/lib/pubsub/utils/__init__.py0000777000175000017500000000062411443222121022473 0ustar stevesteve''' Various utility functions/classes that make use of pubsub.core. ''' from utils import * from topicspec import * from exchandling import \ ExcHandlerError, \ TracebackInfo, \ IExcHandler, \ ExcPublisher from notification import \ INotificationHandler, \ NotifyByPubsubMessage, \ PubsubTopicMsgLogger, \ useNotifyByPubsubMessage, \ useDefaultLoggingNotificationwhyteboard-0.41.1/whyteboard/lib/pubsub/core/callables.py0000777000175000017500000001334511443222121022452 0ustar stevesteve''' Low level functions and classes related to callables. ''' from inspect import getargspec, ismethod, isfunction, getmro KWARG_TOPIC = 'msgTopic' # must NOT be changed AUTO_ARG = 'your listener wants topic name' def getModule(obj): '''Get the module in which an object was defined. Returns '__main__' if no module defined (which usually indicates either a builtin, or a definition within main script). ''' if hasattr(obj, '__module__'): module = obj.__module__ else: module = '__main__' return module def getID(callable_): '''Get name and module name for a callable, ie function, bound method or callable instance, by inspecting the callable. E.g. getID(Foo.bar) returns ('Foo.bar', 'a.b') if Foo.bar was defined in module a.b. ''' sc = callable_ if ismethod(sc): module = getModule(sc.im_self) id = '%s.%s' % (sc.im_self.__class__.__name__, sc.im_func.func_name) elif isfunction(sc): module = getModule(sc) id = sc.__name__ else: # must be a functor (instance of a class that has __call__ method) module = getModule(sc) id = sc.__class__.__name__ return id, module def getRawFunction(callable_): '''Given a callable, return (offset, func) where func is the function corresponding to callable, and offset is 0 or 1 to indicate whether the function's first argument is 'self' (1) or not (0).''' firstArg = 0 if isfunction(callable_): #print 'Function', getID(callable_) func = callable_ elif ismethod(callable_): func = callable_ #print 'Method', getID(callable_) firstArg = 1 # don't care about the self arg elif hasattr(callable_, '__call__'): #print 'Functor', getID(callable_) func = callable_.__call__ firstArg = 1 # don't care about the self arg else: msg = 'type %s not recognized' % type(callable_) raise ValueError(msg) return firstArg, func class ListenerInadequate(TypeError): ''' Raised when an attempt is made to subscribe a listener to a topic without satisfying the topic requirements. ''' def __init__(self, msg, listener, *args): idStr, module = getID(listener) msg = 'Listener %s (module %s) inadequate: %s' % (idStr, module, msg) TypeError.__init__(self, msg) self.msg = msg self.args = args self.module = module self.idStr = idStr def __str__(self): return self.msg class ArgsInfo: ''' Represent the "signature" or protocol of a listener in the context of topics. ''' def __init__(self, args, firstArgIdx, defaultVals, acceptsAllKwargs=False): '''Args is the complete set of arguments as obtained form inspect.getargspec(). The firstArgIdx points to the first item in args that is of use, so it is typically 0 if listener is a function, and 1 if listener is a method. After initialization, the self.args will contain subset of args without first firstArgIdx items, the self.numRequired will indicate number of required arguments, and self.wantsTopic will be True only if listener indicated it wanted the topic object to be auto-passed to it in a pubsub.sendMessage(). Note that args may be different upon return.''' self.allArgs = args self.numRequired = None self.wantsTopic = None self.acceptsAllKwargs = acceptsAllKwargs defaultVals = list(defaultVals or ()) self.__cleanup(firstArgIdx, defaultVals) def getRequiredArgs(self): return tuple( self.allArgs[:self.numRequired] ) def __cleanup(self, firstArgIdx, defaultVals): '''Removes unnecessary items from args and defaultVals. Returns a pair (num, wantTopic) where num is how many items in args represent the required arguments, and wantTopic is True if args/defaultVals satisfied the "wantTopic" condition. ''' args = self.allArgs del args[0:firstArgIdx] # does nothing if firstArgIdx == 0 self.numRequired = len(args) - len(defaultVals) assert self.numRequired >= 0 # if listener wants topic, remove that arg from args/defaultVals self.wantsTopic = False if defaultVals is not None: wantTopicIdx = self.__isTopicWanted(defaultVals) if wantTopicIdx >= self.numRequired: del args[wantTopicIdx] del defaultVals[wantTopicIdx - self.numRequired] self.wantsTopic = True def __isTopicWanted(self, defaults): '''Does the listener want topic of message? Returns < 0 if not, otherwise return index of topic kwarg within args.''' args = self.allArgs firstKwargIdx = len(args) - len(defaults) try: findTopicArg = args.index(KWARG_TOPIC, firstKwargIdx) except ValueError: return -1 topicKwargIdx = findTopicArg - firstKwargIdx if defaults[topicKwargIdx] != AUTO_ARG: return -1 return findTopicArg def getArgs(listener): '''Returns an instance of ArgsInfo for the given listener. ''' # figure out what is the actual function object to inspect: try: firstArgIdx, func = getRawFunction(listener) except ValueError, exc: raise ListenerInadequate(str(exc), listener) (args, va, vkwa, defaultVals) = getargspec(func) return ArgsInfo(args, firstArgIdx, defaultVals, vkwa) whyteboard-0.41.1/whyteboard/lib/pubsub/core/datamsg.py0000777000175000017500000000074211443222121022145 0ustar stevesteveclass Message: """ A simple container object for the two components of a message: the topic and the user data. An instance of Message is given to your listener when called by sendMessage(topic). The data is accessed via the 'data' attribute, and can be type of object. """ def __init__(self, topic, data): self.topic = topic self.data = data def __str__(self): return '[Topic: '+`self.topic`+', Data: '+`self.data`+']' whyteboard-0.41.1/whyteboard/lib/pubsub/core/listener.py0000777000175000017500000003726411443222121022363 0ustar stevesteve''' Topic listeners are callables that satisfy the minimum requirements for the topic of interest. The Listener class aggregates the callable with other useful info, such as whether the listener accepts **kwargs, a more 'human friendly' name for the listener. Notes: - A Listener instance holds its callable only by weak reference so it doesn't prevent the callable from being garbage collected when no longer in use by the application. - Listeners subscribing to a Topic are validated for compliance with the topic's TMAS (topic message argument specification). Compliance can be configured via pubsubconf.setListenerValidator(). ''' from weakref import ref as weakref from types import InstanceType from inspect import getargspec, ismethod, isfunction, getmro from pubsubconf import Policies import weakmethod from callables import \ getID, getRawFunction, getArgs,\ ListenerInadequate, \ ArgsInfo, \ KWARG_TOPIC as _KWARG_TOPIC, \ AUTO_ARG as _AUTO_ARG class Message: """ A simple container object for the two components of a message: the topic and the user data. An instance of Message is given to your listener when called by sendMessage(topic). The data is accessed via the 'data' attribute, and can be type of object. """ def __init__(self, topicNameTuple, data): self.topic = topicNameTuple self.data = data def __str__(self): return '[Topic: '+`self.topic`+', Data: '+`self.data`+']' class Listener: ''' Represent a listener of messages of a given Topic. Each Listener has a name and module, determined via introspection. Note that listeners that have 'msgTopic=AUTO_ARG' as a kwarg will be given the topic object for the message when called by a sendMessage(). ''' KWARG_TOPIC = _KWARG_TOPIC AUTO_ARG = _AUTO_ARG Validator = None def __init__(self, callable_, argsInfo, onDead=None): '''Use callable_ as a listener of topicName. The argsInfo is the return value from a Validator, ie an instance of callables.ArgsInfo. If given, the onDead will be called if/when callable_ gets garbage collected (callable_ is held only by weak reference). ''' # set call policies self.acceptsAllKwargs = argsInfo.acceptsAllKwargs self.__wantsTopic = argsInfo.wantsTopic if onDead is None: self._callable = weakmethod.getWeakRef(callable_) else: self._callable = weakmethod.getWeakRef(callable_, self.__notifyOnDead) self.__onDead = onDead # save identity now in case callable dies: name, mod = getID(callable_) # self.__nameID = name self.__module = mod self.__id = str(id(callable_))[-4:] # only last four digits of id def __call__(self, *args, **kwargs): raise NotImplementedError def name(self, instance=True): '''Return a human readable name for listener. If instance is True, then append part of the id(callable_) given at construction (for uniqueness). Note that the id() was saved at construction time so return value is not necessarily unique if the callable has died (because id's can be re-used after garbage collection).''' if instance: return '%s_%s' % (self.__nameID, self.__id) else: return self.__nameID def module(self): '''Get the module in which callable type/class was defined.''' return self.__module def getCallable(self): '''Get the listener that was given at construction. Note that this could be None if it has been garbage collected (e.g. if it was created as a wrapper of some other callable, and not stored locally).''' if self._callable is None: return None else: return self._callable() def isDead(self): '''Return True if this listener died (has been garbage collected)''' return self._callable is None def _unlinkFromTopic_(self): '''Tell self that it is no longer used by a Topic. This allows to break some cyclical references.''' self.__onDead = None def __callWhenDead(self, actualTopic, *args, **kwargs): raise RuntimeError('BUG: Dead Listener called, still subscribed!') def __notifyOnDead(self, ref): '''This gets called when listener weak ref has died. Propagate info to Topic).''' notifyDeath = self.__onDead self._unlinkFromTopic_() self._callable = None self.__call__ = self.__callWhenDead if notifyDeath is not None: notifyDeath(self) def __eq__(self, rhs): '''Compare for equality to rhs. This returns true if id(rhs) is same as id(self) or id(callable in self). ''' if hasattr(rhs,'_Listener__nameID'): return self is rhs else: if self._callable is None: raise RuntimeError('BUG: Comparing a dead Listener!') return self._callable() == rhs def __str__(self): '''String rep is the callable''' return self.__nameID class ListenerKwargs(Listener): def __call__(self, actualTopic, kwargs): '''Call the listener with **kwargs. Note that it raises RuntimeError if listener is dead. Should always return True (False would require the callable_ be dead but self hasn't yet been notified of it...).''' cb = self._callable() if cb: if self._Listener__wantsTopic: cb(msgTopic=actualTopic, **kwargs) else: cb(**kwargs) return True else: return False class ListenerDataMsg(Listener): def __call__(self, actualTopic, data): '''Call the listener with data. Note that it raises RuntimeError if listener is dead. Should always return True (False would require the callable_ be dead but self hasn't yet been notified of it...).''' cb = self._callable() if cb: msg = Message(actualTopic.getNameTuple(), data) if self._Listener__wantsTopic: cb(msg, msgTopic=actualTopic) else: cb(msg) return True else: return False def isValid(listener, topicReqdArgs, topicOptArgs): '''Return true only if listener can subscribe to messages where topic has kwargs keys topicKwargKeys and required args names topicArgs. Just calls validate() in a try-except clause.''' validator = listener.Validator(topicReqdArgs, topicOptArgs) try: validator.validate(listener) return True except ListenerInadequate: return False class Validator: ''' Validates listeners. It checks whether the listener given to validate() method complies with required and optional arguments specified for topic. ''' def __init__(self, topicArgs, topicKwargs): '''topicArgs is a list of argument names that will be required when sending a message to listener. Hence order of items in topicArgs matters. The topicKwargs is a list of argument names that will be optional, ie given as keyword arguments when sending a message to listener. The list is unordered. ''' self.topicArgs = topicArgs self.topicKwargs = topicKwargs def validate(self, listener): '''Validate that listener satisfies the requirements of being a topic listener, if topic's kwargs keys are topicKwargKeys (so only the list of keyword arg names for topic are necessary). Raises ListenerInadequate if listener not usable for topic. Otherwise, returns whether listener wants topic name (signified by a kwarg key,value = KWARG_TOPIC,AUTO_ARG in listener protocol) when sent messages. E.g. def fn1(msgTopic=Listener.AUTO_ARG) would cause validate(fn1) to return True, whereas any other kwarg name or value would cause a False to be returned. ''' # figure out what is the actual function object to inspect: try: firstArg, func = getRawFunction(listener) except ValueError, exc: raise ListenerInadequate(str(exc), listener) (args, va, vkwa, defaultVals) = getargspec(func) if defaultVals is None: defaultVals = [] else: defaultVals = list(defaultVals) return self.__validateArgs(listener, firstArg, args, va, vkwa, defaultVals) def isValid(self, listener): '''Return true only if listener can subscribe to messages where topic has kwargs keys topicKwargKeys. Just calls validate() in a try-except clause.''' try: self.validate(listener) return True except ListenerInadequate: return False def __validateArgs(self, listener, firstArg, args, va, vkwa, defaultVals): # get the listener's signature (protocol), remove 'self' and # auto-pass topic kwarg and etc acceptsAllKwargs = (vkwa is not None) argsInfo = ArgsInfo(args, firstArg, defaultVals, acceptsAllKwargs) # now validate: self._validateVarArg_(listener, va) self._validateVarKwarg_(listener, vkwa) self._validateArgs_(listener, argsInfo.allArgs, argsInfo.numRequired, va) self._validateKwargs_(listener, argsInfo.allArgs, argsInfo.numRequired, defaultVals, vkwa) return argsInfo def _validateVarArg_(self, listener, va): raise NotImplementedError def _validateVarKwarg_(self, listener, vkwa): raise NotImplementedError def _validateArgs_(self, listener, args, numReqdArgs, va): raise NotImplementedError def _validateKwargs_(self, listener, args, firstKwargIdx, defaultVals, vkwa): raise NotImplementedError def acceptVarArg(self, listener, vaName): '''Accept listener even if a vararg is used.''' pass def rejectVarArg(self, listener, vaName): '''Reject if listener uses a vararg (*arg).''' if vaName is not None: msg = 'can\'t have a *arg' raise ListenerInadequate(msg, listener, (vaName,)) def rejectArgsReqdAny(self, listener, args, numReqdArgs, vaName): '''Reject if ANY required arguments are present (ie numReqdArgs>0), regardless of whether the vararg name vaName is None. ''' if numReqdArgs > 0: # some args are required: only kwargs allowed msg = 'can\'t have required args (has %s too many)' % numReqdArgs raise ListenerInadequate(msg, listener, args[:numReqdArgs]) def rejectArgsReqdNotSame(self, listener, args, numReqdArgs, vaName): listenerArgs = args[:numReqdArgs] self.__rejectArgsNotSame(listener, listenerArgs, self.topicArgs, vaName, ordered=True) def rejectKwargsNotSame (self, listener, args, firstKwargIdx, vkwa): listenerKwargs = args[firstKwargIdx:] self.__rejectArgsNotSame(listener, listenerKwargs, self.topicKwargs, vkwa) def rejectKwargsMissing (self, listener, args, firstKwargIdx, vkwa): if vkwa is None: listenerKwargs = args[firstKwargIdx:] self.__rejectArgsMissing(listener, listenerKwargs, self.topicKwargs) def _rejectArgsExtra( self, listener, listenerArgs, topicArgs): '''Verify that listener doesn't have more kwargs than Topic''' extraArgs = set(listenerArgs) - set(topicArgs) if extraArgs: if topicArgs: msg = 'args (%s) not allowed, should be (%s)' \ % (','.join(extraArgs), ','.join(topicArgs)) else: msg = 'no args allowed, has (%s)' % ','.join(extraArgs) raise ListenerInadequate(msg, listener, extraArgs) def __rejectArgsNotSame(self, listener, listenerArgs, topicArgs, vaName, ordered=False): '''If ordered=True, the listenerArgs will be compared to topicArgs taking order into consideration, otherwise just the sets of values are compared.''' self._rejectArgsExtra(listener, listenerArgs, topicArgs) if vaName is None: self.__rejectArgsMissing(listener, listenerArgs, topicArgs) if ordered: wrong = [a for a,b in zip(listenerArgs, topicArgs) if a!=b] if wrong: msg = 'has some args %s in wrong order' % wrong raise ListenerInadequate(msg, listener, wrong) def __rejectArgsMissing(self, listener, listenerArgs, topicArgs): '''Verify that listener has at least all the kwargs defined for topic''' missingArgs = set(topicArgs) - set(listenerArgs) if missingArgs: msg = 'needs to accept %s more args (%s)' \ % (len(missingArgs), ''.join(missingArgs)) raise ListenerInadequate(msg, listener, missingArgs) class ValidatorSameKwargsOnly(Validator): ''' Do not accept any required args or *args; accept any **kwarg, and require that the Listener have at least all the kwargs (can have extra) of Topic. ''' def _validateVarArg_(self, listener, va): pass def _validateVarKwarg_(self, listener, vkwa): pass def _validateArgs_(self, listener, args, numReqdArgs, va): self.rejectArgsReqdNotSame(listener, args, numReqdArgs, va) def _validateKwargs_(self, listener, args, firstKwargIdx, defaultVals, vkwa): self.rejectKwargsNotSame(listener, args, firstKwargIdx, vkwa) class ValidatorOneArgAnyKwargs(Validator): ''' Accept one arg or *args; accept any **kwarg, and require that the Listener have at least all the kwargs (can have extra) of Topic. ''' def _validateVarArg_(self, listener, va): '''accept *arg''' pass def _validateVarKwarg_(self, listener, vkwa): '''accept **kwarg''' pass def _validateArgs_(self, listener, args, numReqdArgs, va): '''accept if exactly one arg, regardless of name''' if numReqdArgs > 1: msg = 'cannot require more than one arg' effTopicArgs = ['msg'] raise ListenerInadequate(msg, listener, effTopicArgs) if numReqdArgs == 1: # if no policy set, any name ok; otherwise validate name: needArgName = Policies._msgDataArgName if needArgName is None or args[0] == needArgName: return msg = 'listener arg name must be %s (is %s)' % (needArgName, args[0]) effTopicArgs = [needArgName] raise ListenerInadequate(msg, listener, effTopicArgs) # numReqdArgs < 1: assert numReqdArgs == 0 if va is not None: # then user specified *args, so ok: return if args: # then there are no required arg, but the first # kwarg will be able to take the arg, so ok: return # nothing goes, so raise: msg = 'Must take one arg (any name) or *arg' effTopicArgs = ['msg'] raise ListenerInadequate(msg, listener, effTopicArgs) def _validateKwargs_(self, listener, args, firstKwargIdx, defaultVals, vkwa): '''accept any keyword args''' pass _ListenerClasses = dict( kwargs = ListenerKwargs, dataArg = ListenerDataMsg) _ListenerValidatorClasses = dict( kwargs = ValidatorSameKwargsOnly, dataArg = ValidatorOneArgAnyKwargs) ListenerValidator = _ListenerValidatorClasses[Policies._msgDataProtocol] Listener = _ListenerClasses[Policies._msgDataProtocol] whyteboard-0.41.1/whyteboard/lib/pubsub/core/publisher.py0000777000175000017500000002153611443222121022526 0ustar stevesteve''' Provides the Publisher class, which manages subscribing callables to topics and sending messages. ''' from pubsubconf import Policies class PublisherBase: ''' Represent the class that send messages to listeners of given topics and that knows how to subscribe/unsubscribe listeners from topics. ''' def __init__(self, topicMgr): self.__notifyOnSend = False self.__notifyOnSubscribe = False self.__notifyOnUnsubscribe = False self.__topicMgr = topicMgr assert self.__topicMgr is not None def __call__(self): '''For backwards compatilibity with pubsub v1 (wxPython).''' return self def sendMessage(self, topicName, *args, **kwargs): raise NotImplementedError def setNotification(self, sendMessage=None, subscribe=None, unsubscribe=None): '''Note that notifications that are None are left at their current value.''' # create special topics if not already done if (sendMessage or subscribe or unsubscribe): # otherwise topics not needed if Policies._notificationHandler is None: raise RuntimeError('Must call pubsubconf.setNotificationHandler() first' ) if sendMessage is not None: self.__notifyOnSend = sendMessage if subscribe is not None: self.__notifyOnSubscribe = subscribe if unsubscribe is not None: self.__notifyOnUnsubscribe = unsubscribe def subscribe(self, listener, topicName): '''Subscribe listener to named topic. Raises ListenerInadequate if listener isn't compatible with the topic's args. Returns (Listener, didit), where didit is False if listener was already subscribed, and Listener is instance of pub.Listener wrapping listener. Note that, if 'subscribe' notification was turned on via setNotification(), the handler's notifySubscribe is called.''' topicObj = self.__topicMgr.getTopicOrNone(topicName) if topicObj is None: noDesc = 'TBD (defined from listener)' topicObj = self.__topicMgr._newTopicFromTemplate_( topicName, desc=noDesc, usingCallable=listener) elif not topicObj.isSendable(): topicObj._updateArgsSpec_(listener, self.__topicMgr) assert topicObj is not None assert topicObj.isSendable() # subscribe listener subdLisnr, didit = topicObj._subscribe_(listener) # notify of subscription if self.__notifyOnSubscribe: Policies._notificationHandler.notifySubscribe(subdLisnr, topicObj, didit) return subdLisnr, didit def unsubscribe(self, listener, topicName): '''Unsubscribe from given topic. If 'unsubscribe' notification is on, notification handler will be called. Returns the pub.Listener instance that has unsubscribed listener.''' topicObj = self.__topicMgr.getTopic(topicName) unsubdLisnr = topicObj._unsubscribe_(listener) if self.__notifyOnUnsubscribe: assert listener == unsubdLisnr.getCallable() Policies._notificationHandler.notifyUnsubscribe(unsubdLisnr, topicObj) return unsubdLisnr def unsubAll(self, topicName = None, listenerFilter = None, topicFilter = None): '''Unsubscribe all listeners from specified topicName. If no topic name given, will unsubscribe all listeners that satisfy listenerFilter(listener) == True, from all topics that satisfy topicFilter(topicName) == True. If no listener or topic filter is given, 'accept all' is assumed. Note: call will generate one notification (see pubsub.setNotification()) message for each unsubscription.''' unsubdListeners = [] if topicName is None: # unsubscribe all listeners from all non-pubsub topics topicsMap = self.__topicMgr._topicsMap for topicName, topicObj in topicsMap.iteritems(): if topicFilter is None or topicFilter(topicName): tmp = topicObj._unsubscribeAllListeners_(listenerFilter) unsubdListeners.extend(tmp) else: topicObj = self.__topicMgr.getTopic(topicName) unsubdListeners = topicObj._unsubscribeAllListeners_(listenerFilter) # send notification regarding all listeners actually unsubscribed if self.__notifyOnUnsubscribe: for unsubdLisnr in unsubdListeners: Policies._notificationHandler.notifyUnsubscribe(unsubdLisnr, topicObj) return unsubdListeners class PublisherKwargs(PublisherBase): ''' Publisher used for kwargs protocol, ie when sending message data via kwargs. ''' def sendMessage(self, _topicName, **kwargs): '''Send message of type _topicName to all subscribed listeners, with message data in kwargs. If topicName is a subtopic, listeners of topics more general will also get the message. Note also that kwargs must be compatible with topic. Note that any listener that lets a raised exception escape will interrupt the send operation, unless an exception handler was specified via pubsubconf.setListenerExcHandler(). ''' topicMgr = self._PublisherBase__topicMgr topicObj = topicMgr.getTopicOrNone(_topicName) if topicObj is None: args = ','.join( kwargs.keys() ) desc = 'Topic created from sendMessage(%s)' % args topicObj = topicMgr._newTopicNoSpec_(_topicName, desc) # don't care if topic not final: topicObj.getListeners() # will return nothing if not final but notification will still work # check that _topic isn't 'pubsub.sendMessage' if self._PublisherBase__notifyOnSend: Policies._notificationHandler.notifySend('pre', topicObj) topicObj.publish(kwargs) Policies._notificationHandler.notifySend('post', topicObj) else: topicObj.publish(kwargs) class PublisherKwargsAsDataMsg(PublisherKwargs): ''' This is used when transitioning from DataMsg to Kwargs messaging protocol. ''' def __init__(self, topicMgr): PublisherKwargs.__init__(self, topicMgr) from datamsg import Message self.Msg = Message #from topicutils import tupleize def tupleize(name): return name self.tupleize = tupleize def sendMessage(self, _topicName, **kwargs): commonArgName = Policies._msgDataArgName data = kwargs.get(commonArgName, None) kwargs[commonArgName] = self.Msg( self.tupleize(_topicName), data) PublisherKwargs.sendMessage( self, _topicName, **kwargs ) class PublisherDataMsg(PublisherBase): ''' Publisher that allows old-style Message.data messages to be sent to listeners. Listeners take one arg (required, unless there is an *arg), but can have kwargs (since they have default values). ''' def __getTopicObj(self, topicName, data): topicMgr = self._PublisherBase__topicMgr topicObj = topicMgr.getTopicOrNone(topicName) if topicObj is None: argVal = '' if data is not None: argVal = 'data=%s' % (data,) desc = 'Topic created from sendMessage(%s)' % argVal topicObj = topicMgr._newTopicNoSpec_(topicName, desc) return topicObj def sendMessage(self, topicName, data=None): '''Send message of type topicName to all subscribed listeners, with message data. If topicName is a subtopic, listeners of topics more general will also get the message. Note that any listener that lets a raised exception escape will interrupt the send operation, unless an exception handler was specified via pubsubconf.setListenerExcHandler(). ''' topicObj = self.__getTopicObj(topicName, data) # don't care if topic not final: topicObj.getListeners() # will return nothing if not final but notification will still work if self._PublisherBase__notifyOnSend: Policies._notificationHandler.notifySend('pre', topicObj) topicObj.publish(data) Policies._notificationHandler.notifySend('post', topicObj) else: topicObj.publish(data) # select which publisher to use at first load: _PublisherClasses = dict( kwargs = (PublisherKwargsAsDataMsg, PublisherKwargs), dataArg = (PublisherDataMsg, PublisherDataMsg) ) Publisher = _PublisherClasses[Policies._msgDataProtocol][Policies._msgDataArgName is None] whyteboard-0.41.1/whyteboard/lib/pubsub/core/pubsub1.py0000777000175000017500000007711511443222121022116 0ustar stevesteve #--------------------------------------------------------------------------- """ This module provides a publish-subscribe component that allows listeners to subcribe to messages of a given topic. Contrary to the original wxPython.lib.pubsub module (which it is based on), it uses weak referencing to the subscribers so the lifetime of subscribers is not affected by Publisher. Also, callable objects can be used in addition to functions and bound methods. See Publisher class docs for more details. The publisher is a singleton instance of the PublisherClass class. You access the instance via the Publisher object available from the module:: from wx.lib.pubsub import Publisher Publisher().subscribe(...) Publisher().sendMessage(...) ... Thanks to Robb Shecter and Robin Dunn for having provided the basis for this module (which was originally in the wxPython project as wx.lib.pubsub but has no dependencies on wxPython). :Author: Oliver Schoenborn :Since: Apr 2004 :Version: $Id: pubsub.py,v 1.8 2006/06/11 00:12:59 RD Exp $ :Copyright: \(c) 2004 Oliver Schoenborn :License: Python Software Foundation """ _implNotes = """ Implementation notes -------------------- In class PublisherClass, I represent the topics-listener set as a tree where each node is a topic, and contains a list of listeners of that topic, and a dictionary of subtopics of that topic. When the publisher is told to send a message for a given topic, it traverses the tree down to the topic for which a message is being generated, all listeners on the way get sent the message. PublisherClass currently uses a weak listener topic tree to store the topics for each listener, and if a listener dies before being unsubscribed, the tree is notified, and the tree eliminates the listener from itself. Ideally, _TopicTreeNode would be a generic _TreeNode with named subnodes, and _TopicTreeRoot would be a generic _Tree with named nodes, and PublisherClass would store listeners in each node and a topic tuple would be converted to a path in the tree. This would lead to a much cleaner separation of concerns. But time is over, time to move on. """ import pubsubconf pubsubconf.packageImported = True PUBSUB_VERSION = 1 # must be copied into Publisher singleton #--------------------------------------------------------------------------- # for function and method parameter counting: from types import InstanceType from inspect import getargspec, ismethod, isfunction # for weakly bound methods: from weakref import ref as WeakRef from weakmethod import getWeakRef as _getWeakRef # ----------------------------------------------------------------------------- def _isbound(method): """Return true if method is a bound method, false otherwise""" assert ismethod(method) return method.im_self is not None def _paramMinCountFunc(function): """Given a function, return pair (min,d) where min is minimum # of args required, and d is number of default arguments.""" assert isfunction(function) (args, va, kwa, dflt) = getargspec(function) lenDef = len(dflt or ()) lenArgs = len(args or ()) lenVA = int(va is not None) return (lenArgs - lenDef + lenVA, lenDef) def _paramMinCount(callableObject): """ Given a callable object (function, method or callable instance), return pair (min,d) where min is minimum # of args required, and d is number of default arguments. The 'self' parameter, in the case of methods, is not counted. """ try: func = callableObject.__call__.im_func except AttributeError: try: func = callableObject.im_func except AttributeError: try: return _paramMinCountFunc(callableObject) except exc: raise 'Cannot determine type of callable: %s' % repr(callableObject) min, d = _paramMinCountFunc(func) return min-1, d def _tupleize(items): """Convert items to tuple if not already one, so items must be a list, tuple or non-sequence""" if isinstance(items, list): raise TypeError, 'Not allowed to tuple-ize a list' elif isinstance(items, (str, unicode)) and items.find('.') != -1: items = tuple(items.split('.')) elif not isinstance(items, tuple): items = (items,) return items def _getCallableName(callable): """Get name for a callable, ie function, bound method or callable instance""" if ismethod(callable): return '%s.%s ' % (callable.im_self, callable.im_func.func_name) elif isfunction(callable): return '%s ' % callable.__name__ else: return '%s ' % callable def _removeItem(item, fromList): """Attempt to remove item from fromList, return true if successful, false otherwise.""" try: fromList.remove(item) return True except ValueError: return False # ----------------------------------------------------------------------------- def getStrAllTopics(): """Function to call if, for whatever reason, you need to know explicitely what is the string to use to indicate 'all topics'.""" return '' # alias, easier to see where used ALL_TOPICS = getStrAllTopics() # ----------------------------------------------------------------------------- class _NodeCallback: """Encapsulate a weak reference to a method of a _TopicTreeNode in such a way that the method can be called, if the node is still alive, but the callback does not *keep* the node alive. Also, define two methods, preNotify() and noNotify(), which can be redefined to something else, very useful for testing. """ def __init__(self, obj): self.objRef = _getWeakRef(obj) def __call__(self, weakCB): notify = self.objRef() if notify is not None: self.preNotify(weakCB) notify(weakCB) else: self.noNotify() def preNotify(self, dead): """'Gets called just before our callback (self.objRef) is called""" pass def noNotify(self): """Gets called if the _TopicTreeNode for this callback is dead""" pass def _setDeadCallback(newCallback): """When a message is sent via sendMessage(), the listener is tested for "livelyhood" (ie there must be at least one place in your code that is still referring to it). If it is dead, newCallback will be called as newCallback(weakref), where weakref is the weak reference object created for the listener when the listener subscribed. This is useful primarily for testing.""" _NodeCallback.preNotify = newCallback class _TopicTreeNode: """A node in the topic tree. This contains a list of callables that are interested in the topic that this node is associated with, and contains a dictionary of subtopics, whose associated values are other _TopicTreeNodes. The topic of a node is not stored in the node, so that the tree can be implemented as a dictionary rather than a list, for ease of use (and, likely, performance). Note that it uses _NodeCallback to encapsulate a callback for when a registered listener dies, possible thanks to WeakRef. Whenever this callback is called, the onDeadListener() function, passed in at construction time, is called (unless it is None). """ def __init__(self, topicPath, onDeadListenerWeakCB): self.__subtopics = {} self.__callables = [] self.__topicPath = topicPath self.__onDeadListenerWeakCB = onDeadListenerWeakCB def getPathname(self): """The complete node path to us, ie., the topic tuple that would lead to us""" return self.__topicPath def createSubtopic(self, subtopic, topicPath): """Create a child node for subtopic""" return self.__subtopics.setdefault(subtopic, _TopicTreeNode(topicPath, self.__onDeadListenerWeakCB)) def hasSubtopic(self, subtopic): """Return true only if topic string is one of subtopics of this node""" return self.__subtopics.has_key(subtopic) def getNode(self, subtopic): """Return ref to node associated with subtopic""" return self.__subtopics[subtopic] def addCallable(self, callable): """Add a callable to list of callables for this topic node""" try: id = self.__callables.index(_getWeakRef(callable)) return self.__callables[id] except ValueError: wrCall = _getWeakRef(callable, _NodeCallback(self.__notifyDead)) self.__callables.append(wrCall) return wrCall def getCallables(self): """Get callables associated with this topic node""" return [cb() for cb in self.__callables if cb() is not None] def hasCallable(self, callable): """Return true if callable in this node""" try: self.__callables.index(_getWeakRef(callable)) return True except ValueError: return False def sendMessage(self, message): """Send a message to our callables""" deliveryCount = 0 for cb in self.__callables[:]: listener = cb() if listener is not None: listener(message) deliveryCount += 1 return deliveryCount def removeCallable(self, callable): """Remove weak callable from our node (and return True). Does nothing if not here (and returns False).""" try: self.__callables.remove(_getWeakRef(callable)) return True except ValueError: return False def clearCallables(self): """Abandon list of callables to caller. We no longer have any callables after this method is called.""" tmpList = [cb for cb in self.__callables if cb() is not None] self.__callables = [] return tmpList def __notifyDead(self, dead): """Gets called when a listener dies, thanks to WeakRef""" #print 'TreeNODE', `self`, 'received death certificate for ', dead self.__cleanupDead() if self.__onDeadListenerWeakCB is not None: cb = self.__onDeadListenerWeakCB() if cb is not None: cb(dead) def __cleanupDead(self): """Remove all dead objects from list of callables""" self.__callables = [cb for cb in self.__callables if cb() is not None] def __str__(self): """Print us in a not-so-friendly, but readable way, good for debugging.""" strVal = [] for callable in self.getCallables(): strVal.append(_getCallableName(callable)) for topic, node in self.__subtopics.iteritems(): strVal.append(' (%s: %s)' %(topic, node)) return ''.join(strVal) class _TopicTreeRoot(_TopicTreeNode): """ The root of the tree knows how to access other node of the tree and is the gateway of the tree user to the tree nodes. It can create topics, and and remove callbacks, etc. For efficiency, it stores a dictionary of listener-topics, so that unsubscribing a listener just requires finding the topics associated to a listener, and finding the corresponding nodes of the tree. Without it, unsubscribing would require that we search the whole tree for all nodes that contain given listener. Since Publisher is a singleton, it will contain all topics in the system so it is likely to be a large tree. However, it is possible that in some runs, unsubscribe() is called very little by the user, in which case most unsubscriptions are automatic, ie caused by the listeners dying. In this case, a flag is set to indicate that the dictionary should be cleaned up at the next opportunity. This is not necessary, it is just an optimization. """ def __init__(self): self.__callbackDict = {} self.__callbackDictCleanup = 0 # all child nodes will call our __rootNotifyDead method # when one of their registered listeners dies _TopicTreeNode.__init__(self, (ALL_TOPICS,), _getWeakRef(self.__rootNotifyDead)) def addTopic(self, topic, listener): """Add topic to tree if doesnt exist, and add listener to topic node""" assert isinstance(topic, tuple) topicNode = self.__getTreeNode(topic, make=True) weakCB = topicNode.addCallable(listener) assert topicNode.hasCallable(listener) theList = self.__callbackDict.setdefault(weakCB, []) assert self.__callbackDict.has_key(weakCB) # add it only if we don't already have it try: weakTopicNode = WeakRef(topicNode) theList.index(weakTopicNode) except ValueError: theList.append(weakTopicNode) assert self.__callbackDict[weakCB].index(weakTopicNode) >= 0 def getTopics(self, listener): """Return the list of topics for given listener""" weakNodes = self.__callbackDict.get(_getWeakRef(listener), []) return [weakNode().getPathname() for weakNode in weakNodes if weakNode() is not None] def isSubscribed(self, listener, topic=None): """Return true if listener is registered for topic specified. If no topic specified, return true if subscribed to something. Use topic=getStrAllTopics() to determine if a listener will receive messages for all topics.""" weakCB = _getWeakRef(listener) if topic is None: return self.__callbackDict.has_key(weakCB) else: topicPath = _tupleize(topic) for weakNode in self.__callbackDict[weakCB]: if topicPath == weakNode().getPathname(): return True return False def unsubscribe(self, listener, topicList): """Remove listener from given list of topics. If topicList doesn't have any topics for which listener has subscribed, nothing happens.""" weakCB = _getWeakRef(listener) if not self.__callbackDict.has_key(weakCB): return cbNodes = self.__callbackDict[weakCB] if topicList is None: for weakNode in cbNodes: weakNode().removeCallable(listener) del self.__callbackDict[weakCB] return for weakNode in cbNodes: node = weakNode() if node is not None and node.getPathname() in topicList: success = node.removeCallable(listener) assert success == True cbNodes.remove(weakNode) assert not self.isSubscribed(listener, node.getPathname()) def unsubAll(self, topicList, onNoSuchTopic): """Unsubscribe all listeners registered for any topic in topicList. If a topic in the list does not exist, and onNoSuchTopic is not None, a call to onNoSuchTopic(topic) is done for that topic.""" for topic in topicList: node = self.__getTreeNode(topic) if node is not None: weakCallables = node.clearCallables() for callable in weakCallables: weakNodes = self.__callbackDict[callable] success = _removeItem(WeakRef(node), weakNodes) assert success == True if weakNodes == []: del self.__callbackDict[callable] elif onNoSuchTopic is not None: onNoSuchTopic(topic) def sendMessage(self, topic, message, onTopicNeverCreated): """Send a message for given topic to all registered listeners. If topic doesn't exist, call onTopicNeverCreated(topic).""" # send to the all-toipcs listeners deliveryCount = _TopicTreeNode.sendMessage(self, message) # send to those who listen to given topic or any of its supertopics node = self for topicItem in topic: assert topicItem != '' if node.hasSubtopic(topicItem): node = node.getNode(topicItem) deliveryCount += node.sendMessage(message) else: # topic never created, don't bother continuing if onTopicNeverCreated is not None: onTopicNeverCreated(topic) break return deliveryCount def numListeners(self): """Return a pair (live, dead) with count of live and dead listeners in tree""" dead, live = 0, 0 for cb in self.__callbackDict: if cb() is None: dead += 1 else: live += 1 return live, dead # clean up the callback dictionary after how many dead listeners callbackDeadLimit = 10 def __rootNotifyDead(self, dead): #print 'TreeROOT received death certificate for ', dead self.__callbackDictCleanup += 1 if self.__callbackDictCleanup > _TopicTreeRoot.callbackDeadLimit: self.__callbackDictCleanup = 0 oldDict = self.__callbackDict self.__callbackDict = {} for weakCB, weakNodes in oldDict.iteritems(): if weakCB() is not None: self.__callbackDict[weakCB] = weakNodes def __getTreeNode(self, topic, make=False): """Return the tree node for 'topic' from the topic tree. If it doesnt exist and make=True, create it first.""" # if the all-topics, give root; if topic == (ALL_TOPICS,): return self # not root, so traverse tree node = self path = () for topicItem in topic: path += (topicItem,) if topicItem == ALL_TOPICS: raise ValueError, 'Topic tuple must not contain ""' if make: node = node.createSubtopic(topicItem, path) elif node.hasSubtopic(topicItem): node = node.getNode(topicItem) else: return None # done return node def printCallbacks(self): strVal = ['Callbacks:\n'] for listener, weakTopicNodes in self.__callbackDict.iteritems(): topics = [topic() for topic in weakTopicNodes if topic() is not None] strVal.append(' %s: %s\n' % (_getCallableName(listener()), topics)) return ''.join(strVal) def __str__(self): return 'all: %s' % _TopicTreeNode.__str__(self) # ----------------------------------------------------------------------------- class _SingletonKey: """Used to "prevent" instantiating a _PublisherClass from outside the module""" pass class PublisherClass: """ The publish/subscribe manager. It keeps track of which listeners are interested in which topics (see subscribe()), and sends a Message for a given topic to listeners that have subscribed to that topic, with optional user data (see sendMessage()). The three important concepts for pubsub are: - listener: a function, bound method or callable object that can be called with one parameter (not counting 'self' in the case of methods). The parameter will be a reference to a Message object. E.g., these listeners are ok:: class Foo: def __call__(self, a, b=1): pass # can be called with only one arg def meth(self, a): pass # takes only one arg def meth2(self, a=2, b=''): pass # can be called with one arg def func(a, b=''): pass Foo foo import pubsub as Publisher Publisher.subscribe(foo) # functor Publisher.subscribe(foo.meth) # bound method Publisher.subscribe(foo.meth2) # bound method Publisher.subscribe(func) # function The three types of callables all have arguments that allow a call with only one argument. In every case, the parameter 'a' will contain the message. - topic: a single word, a tuple of words, or a string containing a set of words separated by dots, for example: 'sports.baseball'. A tuple or a dotted notation string denotes a hierarchy of topics from most general to least. For example, a listener of this topic:: ('sports','baseball') would receive messages for these topics:: ('sports', 'baseball') # because same ('sports', 'baseball', 'highscores') # because more specific but not these:: 'sports' # because more general ('sports',) # because more general () or ('') # because only for those listening to 'all' topics ('news') # because different topic - message: this is an instance of Message, containing the topic for which the message was sent, and any data the sender specified. :note: This class is not directly visible to importers of pubsub. A singleton instance of it, named Publisher, is created by the module at load time, allowing to write 'Publisher.method()'. All the singleton's methods are made accessible at module level so that knowing about the singleton is not necessary. This does imply that help docs generated from this module via help(pubsub) will show several module-level functions, whose first parameter is `self`. You should ignore that parameter and consider it to be implicitely refering to the singleton. E.g. if help() lists `getDeliveryCount(self)`, you call it as `pubsub.getDeliveryCount()`. """ __ALL_TOPICS_TPL = (ALL_TOPICS, ) PUBSUB_VERSION = PUBSUB_VERSION def __init__(self, singletonKey): """Construct a Publisher. This can only be done by the pubsub module. You just use pubsub.Publisher().""" if not isinstance(singletonKey, _SingletonKey): raise invalid_argument("Use Publisher() to get access to singleton") self.__messageCount = 0 self.__deliveryCount = 0 self.__topicTree = _TopicTreeRoot() # # Public API # def getDeliveryCount(self): """How many listeners have received a message since beginning of run""" return self.__deliveryCount def getMessageCount(self): """How many times sendMessage() was called since beginning of run""" return self.__messageCount def subscribe(self, listener, topic = ALL_TOPICS): """ Subscribe listener for given topic. If topic is not specified, listener will be subscribed for all topics (that listener will receive a Message for any topic for which a message is generated). This method may be called multiple times for one listener, registering it with many topics. It can also be invoked many times for a particular topic, each time with a different listener. See the class doc for requirements on listener and topic. :note: The listener is held only by *weak* reference. This means you must ensure you have at least one strong reference to listener, otherwise it will be DOA ("dead on arrival"). This is particularly easy to forget when wrapping a listener method in a proxy object (e.g. to bind some of its parameters), e.g.:: class Foo: def listener(self, event): pass class Wrapper: def __init__(self, fun): self.fun = fun def __call__(self, *args): self.fun(*args) foo = Foo() Publisher().subscribe( Wrapper(foo.listener) ) # whoops: DOA! wrapper = Wrapper(foo.listener) Publisher().subscribe(wrapper) # good! :note: Calling this method for the same listener, with two topics in the same branch of the topic hierarchy, will cause the listener to be notified twice when a message for the deepest topic is sent. E.g. subscribe(listener, 't1') and then subscribe(listener, ('t1','t2')) means that when calling sendMessage('t1'), listener gets one message, but when calling sendMessage(('t1','t2')), listener gets message twice. """ self.validate(listener) if topic is None: raise TypeError, 'Topic must be either a word, tuple of '\ 'words, or getStrAllTopics()' self.__topicTree.addTopic(_tupleize(topic), listener) def isSubscribed(self, listener, topic=None): """Return true if listener has subscribed to topic specified. If no topic specified, return true if subscribed to something. Use topic=getStrAllTopics() to determine if a listener will receive messages for all topics.""" return self.__topicTree.isSubscribed(listener, topic) def validate(self, listener): """Similar to isValid(), but raises a TypeError exception if not valid""" # check callable if not callable(listener): raise TypeError, 'Listener '+`listener`+' must be a '\ 'function, bound method or instance.' # ok, callable, but if method, is it bound: elif ismethod(listener) and not _isbound(listener): raise TypeError, 'Listener '+`listener`+\ ' is a method but it is unbound!' # check that it takes the right number of parameters min, d = _paramMinCount(listener) if min > 1: raise TypeError, 'Listener '+`listener`+" can't"\ ' require more than one parameter!' if min <= 0 and d == 0: raise TypeError, 'Listener '+`listener`+' lacking arguments!' assert (min == 0 and d>0) or (min == 1) def isValid(self, listener): """Return true only if listener will be able to subscribe to Publisher.""" try: self.validate(listener) return True except TypeError: return False def unsubAll(self, topics=None, onNoSuchTopic=None): """Unsubscribe all listeners subscribed for topics. Topics can be a single topic (string or tuple) or a list of topics (ie list containing strings and/or tuples). If topics is not specified, all listeners for all topics will be unsubscribed, ie. there will be no topics and no listeners left. If onNoSuchTopic is given, it will be called as onNoSuchTopic(topic) for each topic that is unknown. """ if topics is None: del self.__topicTree self.__topicTree = _TopicTreeRoot() return # make sure every topics are in tuple form if isinstance(topics, list): topicList = [_tupleize(x) for x in topics] else: topicList = [_tupleize(topics)] # unsub every listener of topics self.__topicTree.unsubAll(topicList, onNoSuchTopic) def unsubscribe(self, listener, topics=None): """Unsubscribe listener. If topics not specified, listener is completely unsubscribed. Otherwise, it is unsubscribed only for the topic (the usual tuple) or list of topics (ie a list of tuples) specified. Nothing happens if listener is not actually subscribed to any of the topics. Note that if listener subscribed for two topics (a,b) and (a,c), then unsubscribing for topic (a) will do nothing. You must use getAssociatedTopics(listener) and give unsubscribe() the returned list (or a subset thereof). """ self.validate(listener) topicList = None if topics is not None: if isinstance(topics, list): topicList = [_tupleize(x) for x in topics] else: topicList = [_tupleize(topics)] self.__topicTree.unsubscribe(listener, topicList) def getAssociatedTopics(self, listener): """Return a list of topics the given listener is registered with. Returns [] if listener never subscribed. :attention: when using the return of this method to compare to expected list of topics, remember that topics that are not in the form of a tuple appear as a one-tuple in the return. E.g. if you have subscribed a listener to 'topic1' and ('topic2','subtopic2'), this method returns:: associatedTopics = [('topic1',), ('topic2','subtopic2')] """ return self.__topicTree.getTopics(listener) def sendMessage(self, topic=ALL_TOPICS, data=None, onTopicNeverCreated=None): """Send a message for given topic, with optional data, to subscribed listeners. If topic is not specified, only the listeners that are interested in all topics will receive message. The onTopicNeverCreated is an optional callback of your choice that will be called if the topic given was never created (i.e. it, or one of its subtopics, was never subscribed to by any listener). It will be called as onTopicNeverCreated(topic).""" aTopic = _tupleize(topic) message = Message(aTopic, data) self.__messageCount += 1 # send to those who listen to all topics self.__deliveryCount += \ self.__topicTree.sendMessage(aTopic, message, onTopicNeverCreated) # # Private methods # def __call__(self): """Allows for singleton""" return self def __str__(self): return str(self.__topicTree) #--------------------------------------------------------------------------- from datamsg import Message #--------------------------------------------------------------------------- # Create the Publisher singleton. We prevent users from (inadvertently) # instantiating more than one object, by requiring a key that is # accessible only to module. From # this point forward any calls to Publisher() will invoke the __call__ # of this instance which just returns itself. # # The only flaw with this approach is that you can't derive a new # class from Publisher without jumping through hoops. If this ever # becomes an issue then a new Singleton implementaion will need to be # employed. _key = _SingletonKey() Publisher = PublisherClass(_key) #print 'dirs', dir(Publisher), dir(PublisherClass) # Other than Message, the module's public API consists of the # bound methods taken from the _PublisherClass singleton: #for _methName in [_meth for _meth in dir(Publisher) if not _meth.startswith('_')]: # locals()[_methName] = getattr(Publisher, _methName) def importForTesting(): """This is used only by testpubsub.py. Use at your own risk ;)""" global TopicTreeRoot, TopicTreeNode, paramMinCount, setDeadCallback TopicTreeRoot = _TopicTreeRoot TopicTreeNode = _TopicTreeNode paramMinCount = _paramMinCount setDeadCallback = _setDeadCallback ## modObj.TopicTreeRoot = _TopicTreeRoot ## modObj.TopicTreeNode = _TopicTreeNode ## modObj.paramMinCount = _paramMinCount ## modObj.setDeadCallback = _setDeadCallback ## TopicTreeRoot = _TopicTreeRoot ## TopicTreeNode = _TopicTreeNode ## paramMinCount = _paramMinCount ## globals().update(locals()) whyteboard-0.41.1/whyteboard/lib/pubsub/core/pubsub2.py0000777000175000017500000005565311443222121022122 0ustar stevesteve''' This module provides publish-subscribe functions that allow your methods, functions, and any other callable object to subscribe to messages of a given topic, sent from anywhere in your application. It therefore provides a powerful decoupling mechanism, e.g. between GUI and application logic: senders and listeners don't need to know about each other. E.g. the following sends a message of type 'MsgType' to a listener, carrying data 'some data' (in this case, a string, but could be anything):: import pubsub2 as ps class MsgType(ps.Message): pass def listener(msg, data): print 'got msg', data ps.subscribe(listener, MsgType) ps.sendMessage(MsgType('some data')) The only requirement on your listener is that it be a callable that takes the message instance as the first argument, and any args/kwargs come after. Contrary to pubsub, with pubsub2 the data sent with your message is specified in the message instance constructor, and those parameters are passed on directly to your listener via its parameter list. The important concepts of pubsub2 are: - topic: the message type. This is a 'dotted' sequence of class names, defined in your messaging module e.g. yourmsgs.py. The sequence denotes a hierarchy of topics from most general to least. For example, a listener of this topic:: Sports.Baseball would receive messages for these topics:: Sports.Baseball # because same Sports.Baseball.Highscores # because more specific but not these:: Sports # because more general News # because different topic Defining a topic hierarchy is trivial: in yourmsgs.py you would do e.g.:: import pubsub2 as ps class Sports(ps.Message): class Baseball(ps.Message): class Highscores(ps.Message): pass class Lowscores(ps.Message): pass class Hockey(ps.Message): class Highscores(ps.Message): pass ps.setupMsgTree(Sports) # don't forget this! Note that the above allows you to document your message topic tree using standard Python techniques, and to define specific __init__() for your data. - listener: a function, bound method or callable object. The first argument will be a reference to a Message object. The order of call of the listeners is not specified. Here are examples of valid listeners (see the Sports.subscribe() calls):: class Foo: def __call__(self, m): pass def meth(self, m): pass def meth2(self, m, arg1=''): pass # arg1 is optional so valid foo = Foo() def func(m, arg1=None, arg2=''): pass # both arg args are optional from yourmsgs import Sports Sports.Hockey.subscribe(func) # function Sports.Baseball.subscribe(foo.meth) # bound method Sports.Hockey.subscribe(foo.meth2) # bound method Sports.Hockey.subscribe(foo) # functor (Foo.__call__) In every case, the parameter `m` will contain the message instance, and the remaining arguments are those given to the message constructor. - message: an instance of a message of a certain type. You create the instance, giving it data via keyword arguments, which become instance attributes. E.g. :: from yourmsgs import sendMessage, Sports sendMessage( Sports.Hockey(a=1, b='c') ) will cause the previous example's `func` listener to get an instance m of Sports.Hockey, with m.a==1 and m.b=='c'. Note that every message instance has a subTopic attribute. If this attribute is not None, it means that the message instance is not for the topic given to the sendMessage(), but for a more generic topic (closer to the root of the message type tree):: def handleSports(msg): assert msg.subTopic == Sports.Hockey def handleHockey(msg): assert msg.subTopic == None Sports.Hockey.subscribe(handleHockey) Sports.subscribe(handleSports) sendMessage(Sports.Hockey()) - sender: the part of your code that calls send():: # Sports.Hockey is defined in yourmsgs.py, so: from yourmsgs import sendMessage, Sports # now send something: msg = Sports.Hockey(arg1) sendMessage( msg ) Note that the above will cause your listeners to be called as f(msg, arg1). - log output: using a messaging system has the disadvantage that "tracking" data/events can be more difficult. As an aid, information is sent to a log function, which by default just discards the information. You can set your own logger via setLog() or logToStdOut(). An extra string can be given in the send() or subscribe() calls. For send(), this string allows you to identify the "send point": if you don't see it on your log output, then you know that your code doesn't reach the call to send(). For subscribe(), it identifies the listener with a string of your choice, otherwise it would be the (rather cryptic) Python name for the listener callable. - exceptions while sending: what should happen if a listener (or something it calls) raises an exception? The listeners must be independent of each other because the order of calls is not specified. Certain types of exceptions might be handlable by the sender, so simply stopping the send loop is rather extreme. Instead, the send() aggregates the exception objects and when it has sent to all listeners, raises a ListenerError exception. This has an attribute `exceptions` that is a list of ExcInfo instances, one for each exception raised during the send(). - infinite recursion: it is possible, though not likely, that one of your messages causes another message to get sent, which in turn causes the first type of message to get sent again, thereby leading to an infinite loop. There is currently no guard against this, though adding one would not be difficult. To summarize: - First, create a file e.g. yourmsgs.py in which you define and document your message topics tree and in which you call setupMsgTree(); - Subscribe your listeners to some of those topics by importing yourmsgs.py, and calling subscribe() on the message topic to listen for; - Anywhere in your code, you can send a message by importing yourmsgs.py, and calling `sendMessage( MsgTopicSeq(data) )` or MsgTopic(data).send() - Debugging your messaging: - If you are not seeing all the messages that you expect, add some identifiers to the send/subscribe calls. - Turn logging on with logToStdOut() (or use setLog(yourLogFunction) - The class mechanism will lead to runtime exception if msg topic doesn't exist. Note: Listeners (callbacks) are held only by weak reference, which in general is adequate (this prevents the messaging system from keeping alive callables that are no longer used by anyone). However, if you want the callback to be a wrapper around one of your functions, that wrapper must be stored somewhere so that the weak reference isn't the only reference to it (which will cause it to die). :Author: Oliver Schoenborn :Since: Apr 2004 :Version: 2.01 :Copyright: \(c) 2007 Oliver Schoenborn :License: Python Software Foundation ''' PUBSUB_VERSION = 2 import weakmethod, sys, traceback __all__ = [ # listener stuff: 'Listener', 'ListenerError', 'ExcInfo', # topic stuff: 'Message', # publisher stuff: 'subscribe', 'unsubscribe', 'sendMessage', # misc: 'PUBSUB_VERSION', 'logToStdOut', 'setLog', 'setupMsgTree', ] def subscribe(listener, MsgClass, id=None): '''DEPRECATED (use MsgClass.subscribe() instead). Subscribe listener to messages of type MsgClass. If id is given, it is used to identify the listener in a more human-readable fashion in log messages. Note that log messages are only produced if setLog() was given a non-null writer. ''' MsgClass.subscribe(listener, id) def unsubscribe(listener, MsgClass, id=None): '''DEPRECATED (use MsgClass.subscribe() instead). Unsubscribe listener to messages of type MsgClass. If id is given, it is used to identify the listener in a more human-readable fashion in log messages. Note that log messages are only produced if setLog() was given a non-null writer. ''' MsgClass.unsubscribe(listener, id) def sendMessage(msg, id=None): '''Send a message to its registered listeners. The msg is an instance of class derived from Message. If id is given, it is used to identify the sender in a more human-readable fashion in log messages. Note that log messages are only produced if setLog() was given a non-null writer. Note also that all listener exceptions are caught, so that all listeners get a chance at receiving the message. Once all listeners have been sent the message, a ListenerException will be raised containing a list of all exceptions raised during the send.''' msg.send(id) class ExcInfo: '''Represent an exception raised by a listener. It contains the info returned by sys.exc_info() (self.type, self.arg, self.traceback), as well as the sender ID (self.senderID), and ID of listener that raised the exception (self.listenerID).''' def __init__(self, senderID, listenerID, excInfo): self.type = excInfo[0] # class of exception self.arg = excInfo[1] # value given to constructor self.traceback = excInfo[2] # traceback self.senderID = senderID or 'anonymous' # id of sender for which raised self.listenerID = listenerID # id of listener in which raised def __str__(self): '''Regular stack-trace message''' return ''.join(traceback.format_exception( self.type, self.arg, self.traceback)) class ListenerError(RuntimeError): '''Gets raised when one or more listeners raise an exception while they receive a message. An attribute `exceptions` is a list of ExcInfo objects, one for each exception raised.''' def __init__(self, exceps): self.exceptions = exceps RuntimeError.__init__(self, '%s exceptions raised' % len(exceps)) def getTracebacks(self): '''Get a list of strings, one for each exception's traceback''' return [str(ei) for ei in self.exceptions] def __str__(self): '''Create one long string, where tracebacks are separated by ---''' sep = '\n%s\n\n' % ('-'*15) return sep.join( self.getTracebacks() ) # the logger used by all text output; defaults to null logger _log = None def setLog(writer): '''Set the logger used by this module. The 'writer' must be a callable taking one argument (a text string to be logged), or an object that has a write() method, or None to turn off logging. If this function is not called, no logging occurs. Setting a logger may be useful to help discover when certain messages are sent but not received, etc. ''' global _log if callable(writer): _log = writer elif writer is not None: _log = writer.write else: _log = None def logToStdOut(): '''Shortcut for import sys; setLog(sys.stdout). ''' import sys setLog(sys.stdout) def setupMsgTree(RootClass, yourModuleLocals=None): '''Call this function to setup your message module for use by pubsub2. The RootClass is your class (derived from Message) that is at the root of your message tree. The yourModuleLocals, if given, should be locals(). E.g. #yourMsgs.py: import pubsub2 as ps class A(ps.Message): class B(ps.Message): pass ps.setupMsgTree(A, locals()) The above does two things: 1. when a message of type B eventually gets sent, listeners for messages of type A will also receive it since A is more generic than B; 2. when a module does "import yourMsgs", that module sees pubsub2's functions and classes as though they were in yourMsgs.py, so you can write e.g. "yourMsgs.sendMessage()" rather than "yourMsgs.pubsub2.sendMessage()" or "import pubsub2; pubsub2.sendMessage()". ''' RootClass._setupChaining() if yourModuleLocals is not None: gg = [(key, val) for key, val in globals().iteritems() if not key.startswith('_') and key not in ('setupMsgTree','weakmethod')] yourModuleLocals.update(dict(gg)) class Listener: ''' Represent a listener of messages of a given class. An identifier string can accompany the callback, it will be used in text messages. Note that callback must give callable(callback) == True. Note also that two Listener object compare as equal if they are for the same callback, regardless of id: >>> Listener(cb, 'id1') == Listener(cb, 'id2') True ''' def __init__(self, callback, id=None): assert callable(callback), '%s is not callable' % callback self.__callable = weakmethod.getWeakRef(callback) self.id = id self.weakID = str(self) # save this now in case callable weak ref dies def getCallable(self): '''Get the callback that was given at construction. Note that this could be None if it no longer exists in system (if it was created as a wrapper of some other callable, and not stored locally).''' return self.__callable() def __call__(self, *args, **kwargs): cb = self.__callable() if cb: cb(*args, **kwargs) else: msg = 'Callback %s no longer exists (maybe it was wrapped?)' % self.weakID raise RuntimeError(msg) def __eq__(self, rhs): return self.__callable() == rhs.__callable() def __str__(self): '''String rep is the id, if given, or if not, the str(callback)''' return self.id or str(self.__callable()) class Message: ''' Represent a message to be sent from a sender to a listener. This class should be derived, and the derived class should be documented, to help explain the message and its data. E.g. provide a documented __init__() to help explain the data carried by the message, the purpose of this type of message, etc. ''' _listeners = None # class-wide registry of listeners _parentClass = None # class-wide parent of messages of our type _type = 'Message' # a string for type _childrenClasses = None # keep track of children def __init__(self, subTopic=None, **kwargs): '''The kwargs will be given to listener callback when message delivered. Subclasses of Message can define an __init__ that has specific attributes to better document the message data.''' self.__kwargs = kwargs self.subTopic = subTopic def __getattr__(self, name): if name not in self.__kwargs: raise AttributeError("%s instance has no attribute '%s'" \ % (self.__class__.__name__, name)) return self.__kwargs[name] def send(self, senderID=None): '''Send this instance to registered listeners, including listeners of more general versions of this message topic. If any listener raises an exception, a ListenerError is raised after all listeners have been sent the message. The senderID is used in logged output (if setLog() was called) and in ListenerError. ''' exceps = self.__deliver(senderID) # make parents up chain send with same data ParentCls = self._parentClass while ParentCls is not None: subTopic = self.subTopic or self.__class__ msg = ParentCls(subTopic=subTopic, **self.__kwargs) ParentCls, exceptInfo = msg.sendSpecific(senderID) exceps.extend(exceptInfo) if exceps: raise ListenerError(exceps) def sendSpecific(self, senderID=None): '''Send self to registered listeners, but don't "continue up the message tree", ie listeners of more general versions of this topic will not receive the message. See send() for description of senderID. Returns self's parent message class and a list of exceptions raised by listeners.''' exceptInfo = self.__deliver(senderID) return self._parentClass, exceptInfo def __deliver(self, senderID): '''Do the actual message delivery. Logs output if setLog() was called, and accumulates exception information.''' if not self._listeners: if _log and senderID: _log( 'No listeners of %s for sender "%s"\n' % (self.getType(), senderID) ) return [] if _log and senderID: _log( 'Message of type %s from sender "%s" should reach %s listeners\n' % (self.getType(), senderID, len(self._listeners)) ) received = 0 exceptInfo = [] for listener in self._listeners: if _log and (senderID or listener.id): _log( 'Sending message from sender "%s" to listener "%s"\n' % (senderID or 'anonymous', str(listener))) try: listener(self) received += 1 except Exception: excInfo = ExcInfo(senderID, str(listener), sys.exc_info()) exceptInfo.append( excInfo ) if _log and senderID: _log( 'Delivered message from sender "%s" to %s listeners\n' % (senderID, received)) return exceptInfo @classmethod def getType(cls): '''Return a string representing the type of this message, e.g. A.B.C.''' return cls._type @classmethod def hasListeners(cls): '''Return True only if at least one listener is registered for this class of messages.''' return cls._listeners is not None @classmethod def hasListenersAny(cls): '''Return True only if at least one listener is registered for this class of messages OR any of the more general topics.''' hasListeners = cls.hasListeners() parent = cls._parentClass while parent and not hasListeners: hasListeners = parent.hasListeners() parent = parent._parentClass return hasListeners @classmethod def countListeners(cls): '''Count how many listeners this class has registered''' if cls._listeners: return len(cls._listeners) return 0 @classmethod def countAllListeners(cls): '''Count how many listeners will get this type of message''' count = cls.countListeners() parent = cls._parentClass while parent: count += parent.countListeners() parent = parent._parentClass return count @classmethod def subscribe(cls, who, id=None): '''Subscribe `who` to messages of our class.''' if _log and id: _log( 'Subscribing %s to messages of type %s\n' % (id or who, cls.getType()) ) listener = Listener(who, id) if cls._listeners is None: cls._listeners = [listener] else: if listener in cls._listeners: idx = cls._listeners.index(listener) origListener = cls._listeners[idx] if listener.id != origListener.id: if _log: _log('Changing id of Listener "%s" to "%s"\n' % (origListener.id or who, listener.id or 'anonymous')) origListener.id = listener.id elif _log and listener.id: _log( 'Listener %s already subscribed (as "%s")\n' % (who, id) ) else: cls._listeners.append( listener ) @classmethod def unsubscribe(cls, listener): '''Unsubscribe the given listener (given as `who` in subscribe()). Does nothing if listener not registered. Unsubscribes all direct listeners if listener is the string 'all'. ''' if listener == 'all': cls._listeners = None if _log: _log('Unsubscribed all listeners') return ll = Listener(listener) try: idx = cls._listeners.index(ll) llID = cls._listeners[idx].id del cls._listeners[idx] except ValueError: if _log: _log('Could not unsubscribe listener "%s" from %s' \ % (llID or listener, cls._type)) else: if _log: _log('Unsubscribed listener "%s"' % llID or listener) @classmethod def clearSubscriptions(cls): '''Unsubscribe all listeners of this message type. Same as unsubscribe('all').''' cls.unsubscribe('all') '''Remove all registered listeners from this type of message''' cls._listeners = None @classmethod def getListeners(cls): '''Get a list of listeners for this message class. Each item is an instance of Listener.''' #_log( 'Listeners of %s: %s' % (cls, cls._listeners) ) if not cls._listeners: return [] return cls._listeners[:] # return a copy! @classmethod def getAllListeners(cls): '''This returns all listeners that will be notified when a send() is done on this message type. The return is a dictionary where key is message type, and value is the list of listeners registered for that message type. E.g. A.B.getAllListeners() returns `{['A':[lis1,lis2],'A.B':[lis3]}`.''' ll = {} ll[cls._type] = cls.getListeners() parent = cls._parentClass while parent: parentLL = parent.getListeners() if parentLL: ll[parent._type] = parentLL parent = parent._parentClass return ll @classmethod def _setupChaining(cls, parents=None): '''Chain all the message classes children of cls so that, when a message of type 'cls.childA.subChildB' is sent, listeners of type cls.childA and of type cls get it too. ''' # parent: if parents: cls._parentClass = parents[-1] lineage = parents[:] + [cls] cls._type = '.'.join(item.__name__ for item in lineage) if _log: _log( '%s will chain up to %s\n' % (cls._type, cls._parentClass.getType()) ) else: cls._parentClass = None lineage = [cls] cls._type = cls.__name__ if _log: _log( '%s is at root (top) of messaging tree\n' % cls._type ) # go down into children: cls._childrenClasses = [] for childName, child in vars(cls).iteritems(): if (not childName.startswith('_')) and issubclass(child, Message): cls._childrenClasses.append(child) child._setupChaining(lineage) whyteboard-0.41.1/whyteboard/lib/pubsub/core/pubsub3.py0000777000175000017500000001207411443222121022111 0ustar stevesteve''' This is the top-level API to pubsub version 3. This package can be configured via the pubsubconf module (available outside of the pubsub package, but installed alongside it). :Author: Oliver Schoenborn :Since: Oct 2007 :Version: 1.0 :Copyright: \(c) 2007 Oliver Schoenborn :License: Python Software Foundation ''' PUBSUB_VERSION = 3 # DO NOT CHANGE import pubsubconf pubsubconf.packageImported = True Policies = pubsubconf.Policies from listener import \ Listener, \ getID as getListenerID, \ ListenerInadequate, \ isValid as _isValid from topics import \ ALL_TOPICS, \ TopicUnspecifiedError, \ MissingReqdArgs, \ UnknownOptArgs, \ Topic, \ TopicManager as _TopicManager from publisher import Publisher __all__ = [ # listener stuff: 'Listener', 'ListenerInadequate', 'isValid', 'validate', # topic stuff: 'ALL_TOPICS', 'Topic', 'topics', 'topicsMap', 'AUTO_ARG', 'newTopic', 'delTopic', 'getTopic', 'getAssociatedTopics', 'getDefaultTopicMgr', 'getDefaultRootTopic', 'TopicUnspecifiedError', 'addTopicDefnProvider', # publisher stuff: 'Publisher', 'subscribe', 'unsubscribe', 'isSubscribed', 'unsubAll', 'sendMessage', 'setNotification', 'MissingReqdArgs', 'UnknownOptArgs', # misc: 'PUBSUB_VERSION', ] # --------------------------------------------- _topicMgr = _TopicManager() topics = _topicMgr._rootTopic topicsMap = _topicMgr._topicsMap AUTO_ARG = Listener.AUTO_ARG def isValid(listener, topicName): '''Return true only if listener can subscribe to messages of type topicName.''' return _topicMgr.getTopic(topicName).isValid(listener) def validate(listener, topicName): '''Checks if listener can subscribe to topicName. Raises ListenerInadequate if not. Otherwise, returns whether listener accepts topicName as one of its arguments. ''' return _topicMgr.getTopic(topicName).validate(listener) def isSubscribed(listener, topicName): '''Returns true if listener has subscribed to topicName, false otherwise. Note that a false return is not a guarantee that listener won't get messages of topicName: it could get messages of a subtopic of topic if some are sent. ''' return _topicMgr.getTopic(topicName).hasListener(listener) def getDefaultTopicMgr(): '''Get the topic manager that is created by default when you import package.''' return _topicMgr def getDefaultRootTopic(): '''Get the root topic that is created by default when you import package. All top-level topics are children of that topic. ''' return _topicMgr._rootTopic newTopic = _topicMgr.newTopic delTopic = _topicMgr.delTopic getTopic = _topicMgr.getTopic getAssociatedTopics = _topicMgr.getTopics addTopicDefnProvider = _topicMgr.addDefnProvider # --------------------------------------------- from pubsubconf import Policies _publisher = Publisher( _topicMgr ) subscribe = _publisher.subscribe unsubscribe = _publisher.unsubscribe unsubAll = _publisher.unsubAll sendMessage = _publisher.sendMessage Publisher = _publisher # for backward compat with pubsub1 def setNotification(subscribe=None, unsubscribe=None, deadListener=None, sendMessage=None, newTopic=None, delTopic=None, all=None): '''Set the notification on/off for various aspects of pubsub: - subscribe: send a 'pubsub.subscribe' message whenever a listener subscribes to a topic; - unsubscribe: send a 'pubsub.unsubscribe' message whenever a listener unsubscribes from a topic; - deadListener: send a 'pubsub.deadListener' message whenever pubsub finds out that a listener has died; - send: send a 'pubsub.sendMessage' message whenever the user calls sendMessage(); - newTopic: send a 'pubsub.newTopic' message whenever the user defines a new topic; - delTopic: send a 'pubsub.delTopic' message whenever the user undefines a topic. - all: set all of the above to the given value. The kwargs that are None are left at their current value. The 'all' is set first, then the others. E.g. pubsub.setNotification(all=True, delTopic=False) will toggle all notifications on, but will turn off the 'delTopic' notification. Note that setNotification() merely sets what notifications are given, not how they take place. The how is defined by setting the notifier class to use, via a call to pubsubconf.setNotificationHandler() once when the application starts. ''' if all is not None: _publisher.setNotification(all, all, all) _topicMgr.setNotification(all, all, all) _publisher.setNotification( subscribe=subscribe, unsubscribe=unsubscribe, sendMessage=sendMessage) _topicMgr.setNotification( newTopic=newTopic, delTopic=delTopic, deadListener=deadListener) whyteboard-0.41.1/whyteboard/lib/pubsub/core/topicargspec.py0000777000175000017500000002715111443222121023213 0ustar stevesteve''' Definitions related to topic message argument specification. ''' from pubsubconf import Policies from topicutils import stringize from listener import getArgs as getListenerArgs def topicArgsFromCallable(_callable): '''Get the topic arguments and list of those that are required, by introspecting given listener. Returns a pair, (args, required) where args is a dictionary of allowed arguments, and required states which args are required rather than optional.''' argsInfo = getListenerArgs(_callable) required = argsInfo.getRequiredArgs() defaultDoc = 'NEEDS TO BE DOCUMENTED!!!' args = dict.fromkeys(argsInfo.allArgs, defaultDoc) return args, required class InvalidArgsSpec(RuntimeError): def __init__(self, msg, args): argsMsg = msg % ','.join(args) RuntimeError.__init__(self, 'Invalid arguments: ' + argsMsg) class MissingReqdArgs(RuntimeError): def __init__(self, argNames, missing): argsStr = ','.join(argNames) missStr = ','.join(missing) msg = "Some required args missing in call to sendMessage(%s): %s" % (argsStr, missStr) RuntimeError.__init__(self, msg) class UnknownOptArgs(RuntimeError): def __init__(self, argNames, extra): argsStr = ','.join(argNames) extraStr = ','.join(extra) msg = "Some optional args unknown in call to sendMessage(%s): %s" % (argsStr, extraStr) RuntimeError.__init__(self, msg) def verifyArgsDifferent(allArgs, allParentArgs, topicName): extra = set(allArgs).intersection(allParentArgs) if extra: msg = 'Args %%s already used in parent of "%s"' % topicName raise InvalidArgsSpec( msg, tuple(extra) ) def verifySubset(all, sub, topicName, extraMsg=''): '''Verify that sub is a subset of all for topicName''' notInAll = set(sub).difference(all) if notInAll: msg = 'Args (%%s) missing from %s"%s"' % (extraMsg, topicName) raise InvalidArgsSpec(msg, tuple(notInAll) ) class ArgsInfoBase: SPEC_NONE = 0 # specification not given SPEC_SUBONLY = 1 # only subtopic args specified SPEC_ALL = 2 # all args specified SPEC_MISSING = 3 # no specification SPEC_USERDEFD = 4 # only user-specified sub args SPEC_COMPLETE = 5 # all args, but not confirmed via user spec SPEC_COMPLETE_FINAL = 6 # all args, confirmed by user class ArgsInfoKwargs( ArgsInfoBase ): def __init__(self, getArgsSpec, topicNameTuple, parent, argsDocs, reqdArgs, argsSpec): argsDocs = argsDocs or {} reqdArgs = tuple(reqdArgs or ()) # check that all args marked as required are in argsDocs missingArgs = set(reqdArgs).difference(argsDocs.keys()) if missingArgs: msg = 'The argsDocs dict doesn\'t contain keys (%s) given in reqdArgs' raise InvalidArgsSpec(msg, missingArgs) self.allOptional = None # list of topic message optional argument names self.allRequired = None # list of topic message required argument names self.subArgsDocs = None # documentation for each subtopic arg (dict) self.subArgsReqd = None # which keys in subArgsDocs repr. required args (tuple) self.argsSpec = self.SPEC_NONE topicName = stringize(topicNameTuple) if argsSpec == self.SPEC_NONE: # other self.all* members will be updated when our sub args get set assert not argsDocs assert not reqdArgs assert self.argsSpec == self.SPEC_NONE subArgsDocs, subArgsReqd = getArgsSpec(topicNameTuple) if subArgsDocs is not None: self.__setSubArgs(subArgsDocs, subArgsReqd, parent, topicName) elif argsSpec == self.SPEC_SUBONLY: self.__setSubArgs(argsDocs.copy(), reqdArgs, parent, topicName) else: assert argsSpec == self.SPEC_ALL self.__setAllArgs(getArgsSpec, topicNameTuple, parent, argsDocs.copy(), reqdArgs) def isComplete(self): return self.allOptional is not None def getArgs(self): return set(self.allOptional + self.allRequired) def numArgs(self): return len(self.allOptional or ()) + len(self.allRequired or ()) def subKnown(self): return self.subArgsDocs is not None def check(self, msgKwargs): '''Check that the message arguments given satisfy the topic arg specification. Raises MissingReqdArgs if some required args are missing or not known, and raises UnknownOptArgs if some optional args are unknown. ''' all = set(msgKwargs) # check that it has all required args needReqd = set(self.allRequired) hasReqd = (needReqd <= all) if not hasReqd: raise MissingReqdArgs(msgKwargs.keys(), needReqd - all) # check that all other args are among the optional spec optional = all - needReqd ok = (optional <= set(self.allOptional)) if not ok: raise UnknownOptArgs( msgKwargs.keys(), optional - set(self.allOptional) ) def filterArgs(self, msgKwargs): '''Returns a dict which contains only those items of msgKwargs which are defined for topic. E.g. if msgKwargs is {a:1, b:'b'} and topic arg spec is ('a',) then return {a:1}. The returned dict is valid only if checkArgs(msgKwargs) was called, though that call can be on self OR child topic (assuming that child topics have superset of self arg spec).''' assert self.isComplete() if len(msgKwargs) == self.numArgs(): return msgKwargs # only keep the keys from msgKwargs that are also in topic's kwargs # method 1: SLOWEST #newKwargs = dict( (k,msgKwargs[k]) for k in self.__msgArgs.allOptional if k in msgKwargs ) #newKwargs.update( (k,msgKwargs[k]) for k in self.__msgArgs.allRequired ) # method 2: FAST: #argNames = self.__msgArgs.getArgs() #newKwargs = dict( (key, val) for (key, val) in msgKwargs.iteritems() if key in argNames ) # method 3: FASTEST: argNames = self.getArgs().intersection(msgKwargs) newKwargs = dict( (k,msgKwargs[k]) for k in argNames ) return newKwargs def __setAllArgs(self, getArgsSpec, topicNameTuple, parent, argsDocs, reqdArgs): self.argsSpec = self.SPEC_NONE subArgsDocs, subArgsReqd = getArgsSpec(topicNameTuple) topicName = stringize(topicNameTuple) if subArgsDocs is None: # no user spec available, create spec from args given: allOptional = set( argsDocs.keys() ).difference( reqdArgs ) self.allOptional = tuple(allOptional) self.allRequired = reqdArgs self.argsSpec = self.SPEC_COMPLETE if parent is None: self.subArgsDocs = argsDocs.copy() self.subArgsReqd = reqdArgs self.argsSpec = self.SPEC_COMPLETE_FINAL elif parent.argsSpecComplete(): # verify that parent args is a subset of spec given: parentReqd, parentOpt, dummySpec = parent.getArgs() verifySubset(argsDocs.keys(), parentReqd+parentOpt, topicName) verifySubset(reqdArgs, parentReqd, topicName, 'list of required args for ') # ok, good to go: subArgsOpt = allOptional.difference(parentOpt) subArgsReqd = set(reqdArgs).difference(parentReqd) self.subArgsReqd = tuple(subArgsReqd) subArgs = tuple(subArgsOpt)+self.subArgsReqd self.subArgsDocs = dict( (k,argsDocs[k]) for k in subArgs ) else: # user spec available, takes precedence if parent is None: if set(argsDocs) != set(subArgsDocs): raise ValueError("bad listener due to args") if set(reqdArgs) != set(subArgsReqd): raise ValueError("bad listener due to reqd args") elif parent.argsSpecComplete(): # then arg spec given must be equal to parent spec + user def parentReqd, parentOpt, dummySpec = parent.getArgs() if set(argsDocs) != set(subArgsDocs).union(parentReqd+parentOpt): raise ValueError("bad listener due to args") allReqd = set(subArgsReqd).union(parentReqd) if set(reqdArgs) != allReqd: print 'all, sub', allReqd, reqdArgs raise ValueError("bad listener due to reqd args") self.__setSubArgs(subArgsDocs, subArgsReqd, parent, topicName) if self.argsSpec == self.SPEC_SUBONLY: # then parent spec incomplete assert (parent is not None) and not parent.argsSpecComplete() allOptional = set( argsDocs.keys() ).difference( reqdArgs ) verifySubset(allOptional, subArgsDocs.keys(), topicName) verifySubset(reqdArgs, subArgsReqd, topicName, 'list of required args for ') self.allOptional = tuple(allOptional) self.allRequired = reqdArgs self.argsSpec = self.SPEC_COMPLETE def __setSubArgs(self, subDocs, subReqd, parent, topicName): '''Set the topic sub args, i.e. the args that topic adds to the args specified by parent. ''' self.subArgsDocs, self.subArgsReqd = subDocs, subReqd self.argsSpec = self.SPEC_SUBONLY # see if all args can be infered: if parent is None: subOptional = set( subDocs.keys() ).difference( subReqd ) self.allOptional = tuple(subOptional) self.allRequired = subReqd self.argsSpec = self.SPEC_COMPLETE_FINAL elif parent.argsSpecComplete(): # check that none of the subArgs are already used by parent: parentReqd, parentOpt, dummySpec = parent.getArgs() subOptional = set( subDocs.keys() ).difference( subReqd ) verifyArgsDifferent(subOptional, parentOpt, topicName) assert not set(subReqd).intersection(parentReqd) # ok: self.allOptional = parentOpt + tuple(subOptional) self.allRequired = parentReqd + subReqd self.argsSpec = self.SPEC_COMPLETE_FINAL class ArgsInfoDataMsg( ArgsInfoBase ): def __init__(self, topicDefnProvider, topicNameTuple, parent, argsDocs, reqdArgs, argsSpec): if not argsDocs: self.argsSpec = self.SPEC_COMPLETE argsDocs = {'data':'message data'} else: self.argsSpec = self.SPEC_COMPLETE_FINAL self.allOptional = () # list of topic message optional argument names self.allRequired = ('data',) # list of topic message required argument names self.subArgsDocs = argsDocs # documentation for each subtopic arg (dict) self.subArgsReqd = ('data',) # which keys in subArgsDocs repr. required args (tuple) def isComplete(self): return True def getArgs(self): return set(self.allOptional + self.allRequired) def numArgs(self): return len(self.allOptional or ()) + len(self.allRequired or ()) def subKnown(self): return self.subArgsDocs is not None _argsInfoClasses = dict( kwargs = ArgsInfoKwargs, dataArg = ArgsInfoDataMsg) ArgsInfo = _argsInfoClasses[ Policies._msgDataProtocol ] whyteboard-0.41.1/whyteboard/lib/pubsub/core/topics.py0000777000175000017500000010057111443222121022027 0ustar stevesteve''' Everything regarding the concept of topic. Note that name can be in the 'dotted' format 'topic.sub[.subsub[.subsubsub[...]]]' or in tuple format ('topic','sub','subsub','subsubsub',...). E.g. 'nasa.rocket.apollo13' or ('nasa', 'rocket', 'apollo13'). Copyright Oliver Schoenborn, 2008- ''' from weakref import ref as weakref from pubsubconf import \ Policies from listener import \ Listener, \ ListenerValidator from topicutils import \ smartDedent, \ stringize, \ tupleize, \ TopicNameInvalid from topicargspec import \ ArgsInfo, \ verifySubset, \ topicArgsFromCallable, \ InvalidArgsSpec, \ MissingReqdArgs, \ UnknownOptArgs # just want something unlikely to clash with user's topic names ALL_TOPICS = '!__ALL_TOPICS__!' class _TopicDefnProvider: ''' Stores a list of topic definition providers. Gets the argument specification and description for given topics, as returned by one of providers added. ''' def __init__(self): self.__providers = [] def addProvider(self, provider): if provider not in self.__providers: self.__providers.append(provider) def clear(self): self.__providers = [] def getSubSpec(self, topicNameTuple): for provider in self.__providers: argsDocs, required = provider.getSubSpec(topicNameTuple) if argsDocs is not None: verifySubset(argsDocs.keys(), required, topicNameTuple, "arg list, or _required too large") return argsDocs, required if Policies._raiseOnTopicUnspecified: raise TopicUnspecifiedError(topicNameTuple, self.__providers) return None, None def getDescription(self, topicNameTuple): for provider in self.__providers: desc = provider.getDescription(topicNameTuple) if desc is not None: return desc if Policies._raiseOnTopicUnspecified: raise TopicUnspecifiedError(topicNameTuple, self.__providers) return None # --------------------------------------------------------- class ListenerNotValidatable(RuntimeError): def __init__(self): RuntimeError.__init__('Topics args not set yet, cannot validate listener') class TopicAlreadyDefined(RuntimeError): def __init__(self, msg): RuntimeError.__init__(self, msg) class UndefinedTopic(RuntimeError): def __init__(self, topicName): RuntimeError.__init__(self, 'Topic "%s" doesn\'t exist' % topicName) class UndefinedSubtopic(RuntimeError): def __init__(self, parentName, subName): msg = 'Topic "%s" doesn\'t have "%s" as subtopic' RuntimeError.__init__(self, msg % (parentName, subName)) class TopicUnspecifiedError(RuntimeError): def __init__(self, topicNameTuple, providers): if providers: msg = ("No topic specification for topic '%s' " % stringize(topicNameTuple) + "found from registered providers (%s)." % providers) else: msg = "No topic specification for topic '%s'." % stringize(topicNameTuple) RuntimeError.__init__(self, msg + " See pub.newTopic(), pub.addTopicDefnProvider(), and/or pubsubconf.setTopicUnspecifiedFatal()") # --------------------------------------------------------- ARGS_SPEC_NONE = ArgsInfo.SPEC_NONE # specification not given ARGS_SPEC_SUBONLY = ArgsInfo.SPEC_SUBONLY # only subtopic args specified ARGS_SPEC_ALL = ArgsInfo.SPEC_ALL # all args specified # the root topic of all topics is different based on messaging protocol def _getRootTopicSpecProtoKwargs(): '''If using kwargs protocol, then root topic takes no args.''' argsDocs = None reqdArgs = () return argsDocs, reqdArgs def _getRootTopicSpecProtoDataMsg(): '''If using dataArg protocol, then root topic has one arg; if Policies._msgDataArgName is something, then use it as arg name.''' argName = Policies._msgDataArgName or 'data' argsDocs = {argName : 'data for message sent'} reqdArgs = (argName,) return argsDocs, reqdArgs _rootTopicSpecs = dict( kwargs = _getRootTopicSpecProtoKwargs, dataArg = _getRootTopicSpecProtoDataMsg) _getRootTopicSpec = _rootTopicSpecs[ Policies._msgDataProtocol ] class TopicManager: '''Manages the registry of all topics and creation/deletion of topics. All methods that start with an underscore are part of the private API. Some argument names start with an underscore to decrease the likelyhood that some names of the **kwargs will clash with the library's argument names. ''' def __init__(self): self._rootTopic = None # root of topic tree self._topicsMap = {} # registry of all topics self.__defnProvider = _TopicDefnProvider() self.__notifyOnNewTopic = False self.__notifyOnDelTopic = False self.__notifyOnDeadListener = False if self._rootTopic is None: argsDocs, reqdArgs = _getRootTopicSpec() self._rootTopic = \ self.__createTopic((ALL_TOPICS,), desc='root of all topics', argsDocs=argsDocs, reqdArgs=reqdArgs, argsSpec=ARGS_SPEC_ALL) def addDefnProvider(self, provider): '''Register provider as topic specification provider. Whenever a topic must be created, the first provider that has a specification for the created topic is used to initialize the topic. The given provider must be an object that has a getDescription(topicNameTuple) and getArgs(topicNameTuple) that return a description string and a pair (argsDocs, requiredArgs), respectively.''' self.__defnProvider.addProvider(provider) def clearDefnProviders(self): '''Remove all registered topic specification providers''' self.__defnProvider.clear() def _getDefnProvider_(self): return self.__defnProvider def newTopic(self, _name, _desc, _required=(), _argsSpec=ARGS_SPEC_SUBONLY, **args): '''Create a new topic of given _name, with description desc explaining the topic (for documentation purposes). The **args defines the data that can be given as part of messages of this topic: the keys define what arguments names must be present for listeners of this topic, whereas the values describe each argument (for documentation purposes). Returns True only if a new topic was created, False if it already existed identically (same description, same args -- in which case the operation is a no-op). Otherwise raises ValueError. ''' # check _name topicTuple = tupleize(_name) # create only if doesn't exist: nameDotted = stringize(_name) #print 'Checking for "%s"' % nameDotted if self._topicsMap.has_key(nameDotted): msg = 'Topic "%s" already exists' % nameDotted raise TopicAlreadyDefined(msg) # get parent in which to create topic path = topicTuple[:-1] if path: pathDotted = stringize(path) parent = self._topicsMap.get(pathDotted, None) if parent is None: msg = 'Parent topic "%s" does not exist, cannot create' raise UndefinedTopic(pathDotted) else: parent = self._rootTopic # ok to create! newTopicObj = self.__createTopic( topicTuple, desc=_desc, parent=parent, argsSpec=_argsSpec, argsDocs=args, reqdArgs=_required) return newTopicObj def _newTopicFromTemplate_(self, topicName, desc, usingCallable=None): '''Return a new topic object created from protocol of callable referenced by usingCallable. Creates missing parents.''' assert not self._topicsMap.has_key( stringize(topicName) ) topicNameTuple = tupleize(topicName) parentObj = self.__createParentTopics(topicName) # now the final topic object, args from listener if provided allArgsDocs, required, argsSpec = None, None, ARGS_SPEC_NONE if usingCallable is not None: allArgsDocs, required = topicArgsFromCallable(usingCallable) argsSpec=ARGS_SPEC_ALL # if user description exists, use it rather than desc: desc = self.__defnProvider.getDescription(topicNameTuple) or desc return self.__createTopic( topicNameTuple, desc, parent=parentObj, argsSpec=argsSpec, argsDocs=allArgsDocs, reqdArgs=required) def _newTopicNoSpec_(self, topicName, desc): '''Create an unspecified topic''' return self._newTopicFromTemplate_(topicName, desc) def delTopic(self, name): '''Undefines the named topic. Returns True if the subtopic was removed, false otherwise (ie the topic doesn't exist). Also unsubscribes any listeners of topic. Note that it must undefine all subtopics to all depths, and unsubscribe their listeners. ''' # find from which parent the topic object should be removed dottedName = stringize(name) try: obj = weakref( self._topicsMap[dottedName] ) except KeyError: return False assert obj().getName() == dottedName # notification must be before deletion in case if self.__notifyOnDelTopic: Policies._notificationHandler.notifyDelTopic(dottedName) obj()._undefineSelf_(self._topicsMap) assert obj() is None return True def getTopic(self, name): '''Get the Topic instance that corresponds to the given topic name path. Raises an UndefinedTopic or UndefinedSubtopic error if the path cannot be resolved. ''' if not name: raise TopicNameInvalid(name, 'Empty topic name not allowed') topicNameDotted = stringize(name) obj = self._topicsMap.get(topicNameDotted, None) if obj is not None: return obj # NOT FOUND! Determine what problem is and raise accordingly: # find the closest parent up chain that does exists: parentObj, subtopicNames = self.__getClosestParent(topicNameDotted) assert subtopicNames subtopicName = subtopicNames[0] if parentObj is self._rootTopic: raise UndefinedTopic(subtopicName) raise UndefinedSubtopic(parentObj.getName(), subtopicName) def getTopicOrNone(self, name): '''Get the named topic, or None if doesn't exist''' name = stringize(name) obj = self._topicsMap.get(name, None) return obj def getTopics(self, listener): '''Get the list of Topic objects that given listener has subscribed to. Keep in mind that the listener can get messages from sub-topics of those Topics.''' assocTopics = [] for topicObj in self._topicsMap.values(): if topicObj.hasListener(listener): assocTopics.append(topicObj) return assocTopics def setNotification(self, newTopic=None, delTopic=None, deadListener=None): '''See pub.setNotification() for docs. ''' # create special topics if not already done if newTopic or delTopic or deadListener: # otherwise topics not needed if Policies._notificationHandler is None: raise RuntimeError('Must call pubsubconf.setNotificationHandler() first' ) if newTopic is not None: self.__notifyOnNewTopic = newTopic if delTopic is not None: self.__notifyOnDelTopic = delTopic if deadListener is not None: self.__notifyOnDeadListener = deadListener def __getClosestParent(self, topicNameDotted): subtopicNames = [] headTail = topicNameDotted.rsplit('.', 1) while len(headTail) > 1: parentName = headTail[0] subtopicNames.insert( 0, headTail[1] ) obj = self._topicsMap.get( parentName, None ) if obj is not None: return obj, subtopicNames headTail = parentName.rsplit('.', 1) subtopicNames.insert( 0, headTail[0] ) return self._rootTopic, subtopicNames def __onDeadListener(self, topicObj, listener): '''This has to get called by topicObj when a listener subscribed to topicObj has died in case there are other listeners who want to know when listeners die. Will send a message of topic topics.pubsub.deadListener if that notification is on. ''' if self.__notifyOnDeadListener: Policies._notificationHandler.notifyDeadListener(topicObj, listener) def __createParentTopics(self, topicName): assert self.getTopicOrNone(topicName) is None parentObj, subtopicNames = self.__getClosestParent(stringize(topicName)) # will create subtopics of parentObj one by one from subtopicNames if parentObj is self._rootTopic: nextTopicNameList = [] else: nextTopicNameList = list(parentObj.getNameTuple()) desc = 'Defined from listener of subtopic "%s"' % stringize(topicName) for name in subtopicNames[:-1]: nextTopicNameList.append(name) parentObj = self.__createTopic( tuple(nextTopicNameList), desc = desc, parent = parentObj, argsSpec = ARGS_SPEC_NONE) return parentObj def __createTopic(self, topicTuple, desc, parent=None, argsSpec=None, argsDocs=None, reqdArgs=()): '''Actual topic creation step. Adds new Topic instance to topic map, and sends notification message (of topic 'pubsub.newTopic') about new topic having been created.''' #print '__createTopic:', topicTuple, parent and parent.getNameTuple(), args, reqdArgs, argsSpec argsDocs = argsDocs or {} attrName = topicTuple[-1] if parent is not None and hasattr(parent, attrName): reason = '"%s" is an attribute of Topic class' % attrName raise TopicNameInvalid(topicTuple, reason) newTopicObj = Topic(self, topicTuple, desc, parent=parent, argsSpec=argsSpec, reqdArgs=reqdArgs, msgArgs=argsDocs, deadListenerCB=self.__onDeadListener) # sanity checks: assert not self._topicsMap.has_key(newTopicObj.getName()) if parent is self._rootTopic: assert len( newTopicObj.getNameTuple() ) == 1 else: assert parent.getNameTuple() == newTopicObj.getNameTuple()[:-1] self._topicsMap[ newTopicObj.getName() ] = newTopicObj assert topicTuple == newTopicObj.getNameTuple() if self.__notifyOnNewTopic: Policies._notificationHandler.notifyNewTopic( newTopicObj, desc, reqdArgs, argsDocs) return newTopicObj def __validateHierarchy(self, topicTuple): '''Check that names in topicTuple are valid: no spaces, not empty. Raise ValueError if fails check. E.g. ('',) and ('a',' ') would both fail, but ('a','b') would be ok. ''' for indx, topic in enumerate(topicTuple): errMsg = None if topic is None: topicName = list(topicTuple) topicName[indx] = 'None' errMsg = 'None at level #%s' elif not topic: topicName = stringize(topicTuple) errMsg = 'empty element at level #%s' elif topic.isspace(): topicName = stringize(topicTuple) errMsg = 'blank element at level #%s' if errMsg: raise TopicNameInvalid(topicName, errMsg % indx) class PublisherMixinKwargs: def publish(self, msgKwargs): '''Send the message for given topic with data in msgKwargs. This sends message to listeners of of parent topics as well. Note that at each level, msgKwargs is filtered so only those args that are defined for the topic are sent to listeners. ''' # check valid args; only possible if topic spec complete, otherwise # will check first complete parent (assumes we are # traversing topic tree up from children to parents): argsChecked = False if self.argsSpecComplete(): self.argsSpec.check(msgKwargs) argsChecked = True fullTopic = self filteredArgs = msgKwargs topicObj = self while topicObj is not None: if topicObj.hasListeners(): # need to filter the args since not all args accepted filteredArgs = topicObj.argsSpec.filterArgs(filteredArgs) # if no check of args yet, do it now: if not argsChecked: topicObj.argsSpec.check(filteredArgs) argsChecked = True # now send message data to each listener for current topic: for listener in topicObj.getListeners(): try: if listener.acceptsAllKwargs: listener(fullTopic, msgKwargs) else: listener(fullTopic, filteredArgs) except Exception, exc: # if exception handling is on, handle, otherwise re-raise handler = Policies._listenerExcHandler if handler: handler( listener.name(instance=True) ) else: raise # done for this topic, continue up branch to parent towards root topicObj = topicObj.getParent() class PublisherMixinDataMsg: def publish(self, data): '''Send the message for given topic with data. This sends message to listeners of parent topics as well. If an exception is raised in a listener, the publish is aborted, except if there is a handler (see pubsubconf.setListenerExcHandler).''' fullTopic = self topicObj = self while topicObj is not None: if topicObj.hasListeners(): # now send message data to each listener for current topic: for listener in topicObj.getListeners(): try: listener(fullTopic, data) except Exception, exc: # if exception handling is on, handle, otherwise re-raise handler = Policies._listenerExcHandler if handler: handler( listener.name(instance=True) ) else: raise # done for this topic, continue up branch to parent towards root topicObj = topicObj.getParent() _publisherMixins = dict( kwargs = PublisherMixinKwargs, dataArg = PublisherMixinDataMsg) PublisherMixin = _publisherMixins[ Policies._msgDataProtocol ] class Topic(PublisherMixin): ''' Represent a message topic. This keeps track of which call arguments (msgArgs) can be given as message data to subscribed listeners, it supports documentation of msgArgs and topic itself, and allows Python-like access to subtopics (e.g. A.B is subtopic B of topic A) and keeps track of listeners of topic. ''' UNDERSCORE = '_' # topic name can't start with this class InvalidName(ValueError): ''' Raised when attempt to create a topic with name that is not allowed (contains reserved characters etc). ''' def __init__(self, name, reason): msg = 'Invalid topic name "%s": %s' % (name or '', reason) ValueError.__init__(self, ) def __init__(self, topicMgr, nameTuple, description, parent=None, argsSpec=None, reqdArgs=(), msgArgs=None, deadListenerCB=None): '''Specify the name, description, and parent of this Topic. Any remaining keyword arguments (which will be put in msgArgs) describe the arguments that a listener of this topic must support (i.e., the key is the argument name and the value is a documentation string explaining what the argument is for). The reqdArgs is an optional list of names identifying which variables in msgArgs keys are required arguments. E.g. Topic(('a','b'), 'what is topic for', parentTopic, _reqdArgs=('c','d'), c='what is c for', d='what is d for', e='what is e for') would create a Topic whose listeners would have to be of the form callable(c, d, e=...) ie callable(c, d, e=...) callable(self, c, d, e=..., **kwargs) (method) would all be valid listeners but callable(c, e=...) # error: required d is missing callable(c, d, e) # error: e is optional would not be valid listeners of this topic. The _useKwa is only used by the package to indicate whether the arguments are specified as part of __init__ (there is no other way since msgArgs cannot be None). ''' self.__validateName(nameTuple, parent is None) self.__tupleName = nameTuple self.__validator = None self.__listeners = [] self.__deadListenerCB = deadListenerCB # specification: self.__description = None self.setDescription(description) getArgsSpec = topicMgr._getDefnProvider_().getSubSpec self.__msgArgs = ArgsInfo(getArgsSpec, nameTuple, parent, msgArgs, reqdArgs, argsSpec) if self.__msgArgs.isComplete(): self.__finalize() self.argsSpec = self.__msgArgs # now that we know the args are fine, we can link to parent self.__parentTopic = None if parent is None: assert self.isSendable() else: self.__parentTopic = weakref(parent) parent.__setSubtopic( self.getTailName(), self ) def setDescription(self, desc): '''Set the 'docstring' of topic''' self.__description = desc or 'UNDOCUMENTED' def getDescription(self): '''Return the 'docstring' of topic''' return smartDedent(self.__description) def argsSpecComplete(self): '''Return true only if topic's spec is complete''' return self.__msgArgs.isComplete() def getArgs(self): '''Returns a triplet (reqdArgs, optArgs, isComplete) where reqdArgs is the names of required message arguments, optArgs same for optional arguments, and isComplete is same as would be returned from self.argsSpecComplete().''' return (self.__msgArgs.allRequired, self.__msgArgs.allOptional, self.__msgArgs.isComplete()) def getArgDescriptions(self): '''Get a **copy** of the topic's kwargs given at construction time. Returns None if args not described yet. ''' if self.__parentTopic is None: return self.__msgArgs.subArgsDocs.copy() parentDescs = self.__parentTopic().getArgDescriptions() parentDescs.update( self.__msgArgs.subArgsDocs or {}) return parentDescs def isSendable(self): '''Return true if messages can be sent for this topic''' return self.__validator is not None def getName(self): '''Return dotted form of full topic name''' return stringize(self.__tupleName) def getNameTuple(self): '''Return tuple form of full topic name''' return self.__tupleName def getTailName(self): '''Return the last part of the topic name (has no dots)''' name = self.__tupleName[-1] if name is ALL_TOPICS: return 'ALL_TOPICS' assert name.find('.') < 0 return name def getParent(self): '''Get Topic object that is parent of self (i.e. self is a subtopic of parent).''' if self.__parentTopic is None: return None return self.__parentTopic() def hasSubtopic(self, name=None): '''Return true only if name is a subtopic of self. If name not specified, return true only if self has at least one subtopic.''' if name is None: for attr in self.__dict__.values(): if isinstance(attr, Topic): return True return False elif hasattr(self, name): return isinstance(getattr(self, name), Topic) return False def getSubtopics(self): '''Get a list of Topic instances that are subtopics of self.''' st = [] for attr in self.__dict__.values(): if isinstance(attr, Topic): st.append(attr) return st def getNumListeners(self): '''Return number of listeners currently subscribed to topic. This is different from number of listeners that will get notified since more general topics up the topic tree may have listeners.''' return len(self.__listeners) def hasListener(self, listener): '''Return true if listener is subscribed to this topic.''' return listener in self.__listeners def hasListeners(self): '''Return true if there are any listeners subscribed to this topic, false otherwise.''' return self.__listeners != [] def getListeners(self): '''Get a **copy** of Listener objects for listeners subscribed to this topic.''' return self.__listeners[:] def validate(self, listener): '''Same as self.isValid(listener) but raises ListenerInadequate instead of returning False. Returns nothing. ''' if not self.isSendable(): raise ListenerNotValidatable() return self.__validator.validate(listener) def isValid(self, listener): '''Return True only if listener can subscribe to messages of this topic, otherwise returns False. Raises ListenerNotValidatable if not self.isSendable().''' if not self.isSendable(): raise ListenerNotValidatable() return self.__validator.isValid(listener) def __call__(self, subtopicName): '''Return the Topic object that represents the subtopic of given name''' return getattr(self, subtopicName) # Impementation API: def _updateArgsSpec_(self, usingCallable, topicMgr): '''Update the argument spec of topic using given callable. ''' assert self.__parentTopic is not None assert not self.argsSpecComplete() argsDocs, required = topicArgsFromCallable(usingCallable) getArgsSpec = topicMgr._getDefnProvider_().getSubSpec self.__msgArgs = ArgsInfo(getArgsSpec, self.getName(), self.__parentTopic(), argsDocs, required, ARGS_SPEC_ALL) self.argsSpec = self.__msgArgs # validate that our new spec agrees with complete children for child in self.getSubtopics(): # get difference between child and our parent # this must contain our difference from our parent pass if self.__msgArgs.isComplete(): self.__finalize() def _subscribe_(self, listener): '''This method must only be called from within pubsub, as indicated by the surrounding underscores.''' # add to list if not already there: if listener in self.__listeners: assert self.isSendable() idx = self.__listeners.index(listener) return self.__listeners[idx], False else: if not self.isSendable(): raise RuntimeError('Incomplete topic, can\'t register listeners') else: argsInfo = self.__validator.validate(listener) weakListener = Listener( listener, argsInfo, onDead=self.__onDeadListener) self.__listeners.append(weakListener) return weakListener, True def _unsubscribe_(self, listener): try: idx = self.__listeners.index(listener) except ValueError: return None tmp = self.__listeners.pop(idx) tmp._unlinkFromTopic_() return tmp def _unsubscribeAllListeners_(self, filter=None): '''Clears list of subscribed listeners. If filter is given, it must be a function that takes a listener and returns true if the listener should be unsubscribed. Returns the list of listeners that were unsubscribed.''' index = 0 unsubd = [] for listener in self.__listeners[:] : if filter is None or filter(listener): listener._unlinkFromTopic_() assert listener is self.__listeners[index] del self.__listeners[index] unsubd.append(listener) else: index += 1 return unsubd def _undefineSelf_(self, topicsMap): if self.__parentTopic is not None: delattr(self.__parentTopic(), self.__tupleName[-1]) self.__undefineSelf(topicsMap) def __finalize(self): '''Change the arguments of topic. They can be different from those set (if any) at construction time, however any subscribed listeners must remain valid with new args/required otherwise a ValueError exception is raised. ''' assert not self.isSendable() #assert self.__msgArgs.isFinal() # must make sure can adopt a validator required = self.__msgArgs.allRequired optional = self.__msgArgs.allOptional self.__validator = ListenerValidator(required, list(optional) ) assert not self.__listeners def __undefineSelf(self, topicsMap): '''Unsubscribe all our listeners, remove all subtopics from self, then detach from parent. ''' #print 'Remove %s listeners (%s)' % (self.getName(), self.getNumListeners()) self._unsubscribeAllListeners_() self.__parentTopic = None for subName, subObj in self.__dict__.items(): # COPY since modify!! if isinstance(subObj, Topic) and not subName.startswith('_'): #print 'Unlinking %s from parent' % subObj.getName() delattr(self, subName) subObj.__undefineSelf(topicsMap) del topicsMap[self.getName()] def __validateName(self, nameTuple, isRootTopic): '''Raise TopicNameInvalid if nameTuple not valid as topic name.''' if not nameTuple: reason = 'name tuple must have at least one item!' raise TopicNameInvalid(None, reason) tailName = nameTuple[-1] if not tailName: reason = 'can\'t contain empty string or None' raise TopicNameInvalid(None, reason) if tailName.startswith(self.UNDERSCORE): reason = 'must not start with "%s"' % self.UNDERSCORE raise TopicNameInvalid(tailName, reason) if tailName == ALL_TOPICS and not isRootTopic: reason = 'only root topic can contain "%s"' % ALL_TOPICS raise TopicNameInvalid(tailName, reason) assert tailName != ALL_TOPICS or isRootTopic def __setSubtopic(self, attrName, topicObj): '''Link self to a Topic instance via self.attrName. Always succeeds.''' assert topicObj.__parentTopic() is self setattr(self, attrName, topicObj) def __onDeadListener(self, weakListener): '''One of our subscribed listeners has died, so remove it and notify others''' ll = self.__listeners.index(weakListener) listener = self.__listeners[ll] llID = str(listener) del self.__listeners[ll] self.__deadListenerCB(self, listener) def __str__(self): return "%s, %s" % (self.getName(), self.getNumListeners()) whyteboard-0.41.1/whyteboard/lib/pubsub/core/topicutils.py0000777000175000017500000000440711443222121022726 0ustar stevesteve''' Various little utilities used by topic-related modules. ''' from textwrap import TextWrapper, dedent def smartDedent(paragraph): ''' Dedents a paragraph that is a triple-quoted string. If the first line of the paragraph does not contain blanks, the dedent is applied to the remainder of the paragraph. This handles the case where a user types a documentation string as """A long string spanning several lines.""" Regular textwrap.dedent() will do nothing to this text because of the first line. Requiring that the user type the docs as """\ with line continuation is not acceptable. ''' if paragraph.startswith(' '): para = dedent(paragraph) else: lines = paragraph.split('\n') exceptFirst = dedent('\n'.join(lines[1:])) para = lines[0]+exceptFirst return para class TopicNameInvalid(RuntimeError): def __init__(self, name, reason): msg = 'Topic name "%s" invalid: %s' % (name, reason) RuntimeError.__init__(self, msg) def stringize(topicNameTuple): '''If topicName is a string, do nothing and return it as is. Otherwise, convert it to one, using dotted notation, i.e. ('a','b','c') => 'a.b.c'. Empty name is not allowed (ValueError). The reverse operation is tupleize(topicName).''' if isinstance(topicNameTuple, str): return topicNameTuple try: name = '.'.join(topicNameTuple) except Exception, exc: raise TopicNameInvalid(topicNameTuple, str(exc)) return name def tupleize(topicName): '''If topicName is a tuple of strings, do nothing and return it as is. Otherwise, convert it to one, assuming dotted notation used for topicName. I.e. 'a.b.c' => ('a','b','c'). Empty topicName is not allowed (ValueError). The reverse operation is stringize(topicNameTuple).''' # assume name is most often str; if more often tuple, # then better use isinstance(name, tuple) if isinstance(topicName, str): topicTuple = tuple(topicName.split('.')) else: topicTuple = tuple(topicName) # assume already tuple of strings if not topicTuple: raise TopicNameInvalid(topicTuple, "Topic name can't be empty!") return topicTuple whyteboard-0.41.1/whyteboard/lib/pubsub/core/weakmethod.py0000777000175000017500000000762311443222121022662 0ustar stevesteve''' This module provides a basic "weak method" implementation. It is necessary because the weakref module does not support weak methods (in the sense that, counter-intuitively, a user who creates a weakref.ref(obj.method), a reasonable action, get a weak ref that is None. ''' # for function and method parameter counting: from inspect import ismethod # for weakly bound methods: from new import instancemethod as InstanceMethod from weakref import ref as WeakRef class WeakMethod: """Represent a weak bound method, i.e. a method which doesn't keep alive the object that it is bound to. It uses WeakRef which, used on its own, produces weak methods that are dead on creation, not very useful. Typically, you will use the getWeakRef() module function instead of using this class directly. """ def __init__(self, method, notifyDead = None): """The method must be bound. notifyDead will be called when object that method is bound to dies. """ assert ismethod(method) if method.im_self is None: raise ValueError('Unbound methods cannot be weak-referenced.') self.notifyDead = None if notifyDead is None: self.objRef = WeakRef(method.im_self) else: self.notifyDead = notifyDead self.objRef = WeakRef(method.im_self, self.__onNotifyDeadObj) self.fun = method.im_func self.cls = method.im_class def __onNotifyDeadObj(self, ref): if self.notifyDead: try: self.notifyDead(self) except Exception, exc: import traceback traceback.print_exc() def __call__(self): """Returns a new.instancemethod if object for method still alive. Otherwise return None. Note that instancemethod causes a strong reference to object to be created, so shouldn't save the return value of this call. Note also that this __call__ is required only for compatibility with WeakRef.ref(), otherwise there would be more efficient ways of providing this functionality.""" if self.objRef() is None: return None else: return InstanceMethod(self.fun, self.objRef(), self.cls) def __eq__(self, method2): """Two WeakMethod objects compare equal if they refer to the same method of the same instance. Thanks to Josiah Carlson for patch and clarifications on how dict uses eq/cmp and hashing. """ if not isinstance(method2, WeakMethod): return False return ( self.fun is method2.fun and self.objRef() is method2.objRef() and self.objRef() is not None ) def __hash__(self): """Hash is an optimization for dict searches, it need not return different numbers for every different object. Some objects are not hashable (eg objects of classes derived from dict) so no hash(objRef()) in there, and hash(self.cls) would only be useful in the rare case where instance method was rebound. """ return hash(self.fun) def __repr__(self): dead = '' if self.objRef() is None: dead = '; DEAD' obj = '<%s at %s%s>' % (self.__class__, id(self), dead) return obj def refs(self, weakRef): """Return true if we are storing same object referred to by weakRef.""" return self.objRef == weakRef def getWeakRef(obj, notifyDead=None): """Get a weak reference to obj. If obj is a bound method, a WeakMethod object, that behaves like a WeakRef, is returned; if it is anything else a WeakRef is returned. If obj is an unbound method, a ValueError will be raised.""" if ismethod(obj): createRef = WeakMethod else: createRef = WeakRef return createRef(obj, notifyDead) whyteboard-0.41.1/whyteboard/lib/pubsub/core/__init__.py0000777000175000017500000000000011443222121022247 0ustar stevestevewhyteboard-0.41.1/whyteboard/lib/pubsub/core/pubsubconf.py0000777000175000017500000001156711443222121022702 0ustar stevesteve""" Allows user to configure pubsub. Most important: - setVersion(N): State which version of pubsub should be used (N=1, 2 or 3; defaults to latest). E.g. to use version 1 of pubsub: # in your main script only: import pubsubconf pubsubconf.setVersion(1) # in main script and all other modules imported: from pubsub import pub # usual line - Several functions specific to version 3: - setListenerExcHandler(handler): set handling of exceptions raised in listeners (default: None). - setTopicUnspecifiedFatal(val=True): state whether unspecified topics should be creatable (default: False). - setNotificationhandler(notificationhandler): what class to instantiate for processing notification events (default: None). - transitionV1ToV3(commonName, stage=1): set policies that support migrating an application from pubsub version 1 to version 3. """ packageImported = False class Version: DEFAULT = 3 value = None output = None def setVersion(val, output=None): '''Set the version of package to be used when imported. If output is set to a file object (has write() method), a message will be written to that file indicating which version of pubsub has been imported. E.g. setVersion(2, sys.stdout).''' if val < 1 or val > 3: raise ValueError('val = %s invalid, need 1 <= val <= 3' % val) Version.value = val Version.output = output def getVersion(): '''Get version number selected for import (via setVersion, or default version if setVersion not called).''' return Version.value or Version.DEFAULT def isVersionChosen(): '''Return True if setVersion() was called at least once.''' return Version.value is not None def getDefaultVersion(): '''Get version number imported by default.''' return Version.default def getVersionOutput(): '''Return the file object to be used for messaging about imported version''' return Version.output class Policies: ''' Define the policies used by pubsub, when several alternatives exist. ''' _notificationHandler = None _listenerExcHandler = None _raiseOnTopicUnspecified = False _msgDataProtocol = 'kwargs' _msgDataArgName = None def setTopicUnspecifiedFatal(val=True): '''When called with val=True (default), causes pubsub to raise an UnspecifiedTopicError when attempting to create a topic that has no specification. This happens when pub.addTopicDefnProvider() was never called, or none of the given providers specify the topic (or a super topic of it) that was given to pub.subscribe(). If True, the topic will be created with argument specification inferred from first listener subscribed. ''' Policies._raiseOnTopicUnspecified = val def setNotificationHandler(notificationHandler): '''The notifier should be a class that follows the API of pubsub.utils.INotificationHandler. If no notifier is set, then the default will be used. ''' Policies._notificationHandler = notificationHandler def setListenerExcHandler(handler): '''Set the handler to call when a listener raises an exception during a sendMessage(). Without a handler, the send operation aborts, whereas with one, the exception information is sent to it (where it can be logged, printed, whatever), and sendMessage() continues to send messages to remaining listeners. ''' Policies._listenerExcHandler = handler def isPackageImported(): '''Can be used to determine if pubsub package has been imported by your application (or by any modules imported by it). ''' return packageImported def setMsgProtocol(protocol): '''Messaging protocol defaults to 'kwargs'. It can be set to 'dataArg' to support legacy code or simple pub-sub architectures. ''' if protocol not in ('dataArg', 'kwargs'): raise NotImplementedError('The protocol "%s" is not supported' % protocol) Policies._msgDataProtocol = protocol def transitionV1ToV3(commonName, stage=1): '''Use this to help with migrating code from protocol DATA_ARG to KW_ARGS. This only makes sense in an application that has been using setMsgProtocol('dataArg') and wants to move to the more robust 'kwargs'. This function is designed to support a three-stage process: (stage 1) make all listeners use the same argument name (commonName); (stage 2) make all senders use the kwargs protocol and all listeners use kwargs rather than Message.data. The third stage, for which you don't use this function, consists in splitting up your message data into more kwargs and further refining your topic specification tree. See the docs for more info. ''' Policies._msgDataArgName = commonName if stage <= 1: Policies._msgDataProtocol = 'dataArg'