diff options
author | Julien Valroff <julien@kirya.net> | 2009-07-26 12:15:05 +0000 |
---|---|---|
committer | Julien Valroff <julien@kirya.net> | 2009-07-26 12:15:05 +0000 |
commit | f794c204aa3af1798684522cfcf2024e90752310 (patch) | |
tree | d718a9124857a5cbcd62268d44c092471d2e7288 /rapid |
[svn-inject] Installing original source of rapid-photo-downloader
Diffstat (limited to 'rapid')
-rw-r--r-- | rapid/AUTHORS | 1 | ||||
-rw-r--r-- | rapid/COPYING | 339 | ||||
-rw-r--r-- | rapid/ChangeLog | 393 | ||||
-rw-r--r-- | rapid/INSTALL | 36 | ||||
-rw-r--r-- | rapid/TODO | 26 | ||||
-rw-r--r-- | rapid/ValidatedEntry.py | 383 | ||||
-rw-r--r-- | rapid/__init__.py | 1 | ||||
-rw-r--r-- | rapid/common.py | 181 | ||||
-rw-r--r-- | rapid/config.py | 58 | ||||
-rw-r--r-- | rapid/glade3/image-missing.svg | 94 | ||||
-rw-r--r-- | rapid/glade3/rapid-photo-downloader-about.png | bin | 0 -> 8084 bytes | |||
-rw-r--r-- | rapid/glade3/rapid.glade | 2716 | ||||
-rw-r--r-- | rapid/gnomeglade.py | 166 | ||||
-rw-r--r-- | rapid/higdefaults.py | 8 | ||||
-rw-r--r-- | rapid/idletube.py | 205 | ||||
-rwxr-xr-x | rapid/media.py | 180 | ||||
-rwxr-xr-x | rapid/metadata.py | 387 | ||||
-rw-r--r-- | rapid/misc.py | 45 | ||||
-rw-r--r-- | rapid/paths.py | 40 | ||||
-rw-r--r-- | rapid/prefs.py | 182 | ||||
-rwxr-xr-x | rapid/rapid.py | 3050 | ||||
-rw-r--r-- | rapid/renamesubfolderprefs.py | 1470 | ||||
-rw-r--r-- | rapid/renamesubfolderprefstest.py | 190 | ||||
-rw-r--r-- | rapid/tableplusminus.py | 284 |
24 files changed, 10435 insertions, 0 deletions
diff --git a/rapid/AUTHORS b/rapid/AUTHORS new file mode 100644 index 0000000..c89d793 --- /dev/null +++ b/rapid/AUTHORS @@ -0,0 +1 @@ +Damon Lynch <damonlynch@gmail.com> diff --git a/rapid/COPYING b/rapid/COPYING new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/rapid/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + 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 +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision 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, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This 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. diff --git a/rapid/ChangeLog b/rapid/ChangeLog new file mode 100644 index 0000000..430aab8 --- /dev/null +++ b/rapid/ChangeLog @@ -0,0 +1,393 @@ +Version 0.0.10 +-------------- + +2009-06-05 + +Updated Russian translation by Sergei Sedov. + +Fixed bug #383028: program would crash when using an automatically configured +backup device and gvfs. + + + +Version 0.0.9 +------------- + +2009-06-02 + +Added Italian translation by Marco Solari and Luca Reverberi. + +Added German translation by Martin Egger and Daniel Passler. + +Added Russian translation by Sergei Sedov. + +Added Finnish translation by Mikko Ruohola. + +A Help button has been added to Preferences dialog window. Clicking it takes you +to the documentation found online at the program's website. This documentation +is now complete. + +The Preferences Dialog Window is now navigated using a list control, as it was +in early versions of the program. This change was necesseary because with some +translations, the dialog window was becoming too wide with the normal tab +layout. Usability of the preferences dialog is improved: it will now resize +itself based on its content. + +Better integration with Nautilus is now possible through the setting of +MimeType=x-content/image-dcf in the program's .desktop file. + + + +Version 0.0.9 beta 4 +-------------------- + +2009-05-26 + +Added Spanish translation by Jose Luis Navarro and Abel O'Rian. + +Whenever subfolder preferences are modified in the Preferences Dialog window, +they are now checked to see if they contain any extraneous entries. If +necessary, any entries like this are removed when the dialog window is closed. + +Bug fix: Changes in preferences should be applied to devices that have already +been scanned, but their images not yet downloaded. This bug was introduced in +beta 2 when fixing bug #368098. + +Bug fix: check subfolder preferences for validity before beginning download. +While image rename preferences were checked, this check was neglected. + +Bug fix: do not allow automatic downloading when there is an error in the +preferences. + + + +Version 0.0.9 beta 3 +-------------------- + +2009-05-25 + +Added command line options for controlling verbosity, displaying which image +file types are recognized, and printing the program version. + +Updated man page to reflect recent program changes and new command line options. + +Prepared program for translation into other languages. Thanks go to Mark Mruss +and his blog http://www.learningpython.com for code examples and explanations. + +Polish translation by Michal Predotka. Coming soon: French, German and +Spanish translations. + +To install the program using python setup.py, the program msgfmt must now be +present. On most Linux distributions, this is found in the package gettext. + +Updated INSTALL file to reflect minimum version of pyexiv2 needed, and included +information about handling any error related to msgfmt not being installed. + +Minor fixes to logic that checks whether the Download button should be disabled +or not. This should now be more reliable. + +Bug fix: error log window can now be reopened after being closed with the "x" +button. Thanks go to ESR and his Python FAQ entry for this fix. + +Bug fix: example of subfolder name now has word wrap. Thanks go to Michal +Predotka for reporting this. + +Bug fix: don't crash when a thumbnail image is missing and the 'orientation' +variable has not yet been assigned. + + + +Version 0.0.9 beta 2 +-------------------- + +2009-05-12 + +By popular demand, allow direct downloading from cameras. This support is +experimental and may not work with your camera. This is possible through the use +of the new gvfs service, provided by GIO, that exists in recent versions of +Linux. A recent version of Linux is a must. The camera must also be supported by +libgphoto2 in combination with gvfs. If you cannot browse the camera's contents +in a file manager (e.g. Nautilus), the camera download will not work until the +gvfs support is improved. + +Although this is a popular request, the reality is that downloading images +directly from the camera is often extremely slow in comparison to popping the +memory card into a card reader and downloading from that. + +Fix bug #368098: the program now starts more quickly and does not become +unresponsive when scanning devices with a large number of images. This will +hardly be noticeable by users that download from memory cards, but for those +who download from hard drives with hundreds of GBs of files -- they'll notice +a big difference. + +Fix bug #372284: for image renaming, the "image number" component is more +robust. Now, only the series of digits at the end of a filename are recognized +as the image number (obviously the file's extension is not included as being +part of the filename in this case). This allows takes in account files from +cameras like the Canon 1D series, which can have filenames like VD1D7574.CR2. + +Bug fix: don't download from volumes mounted while the program is already +running unless auto detection is specified. This bug could occur when auto +detection was enabled, then disabled, and then a volume was mounted. + + + +Version 0.0.8 +------------- + +2009-05-01 + +Added stored and downloads today sequence numbers: + +* The stored sequence number is remembered each time the program is run. + +* Downloads today tracks how many downloads are made on a given day. The time a + day "starts" is set via a new preference value, day start. This is useful if + you often photograph something late at night (e.g. concerts) and want a new + day to "start" at 3am, for instance. + +Make estimate of time remaining to download images much more accurate. + +Display download speed in status bar. + +Reorganized sequence number/letter selection in preferences. + +Add feature to detect change in program version, upgrading preferences where +necessary. + +Only allow one instance of the program to be run -- raise existing window if it +is run again. This is very useful when Rapid Photo Downloader is set to run +automatically upon insertion of a memory card. + +Add "exit at end of successful download" automation feature. + +When an image's download is skipped, the thumbnail is now lightened. + +Show a missing image icon if the thumbnail cannot be displayed for some reason. +(See bug #369640 for why thumbnail images from certain RAW files are not +displayed). + +Resize main window when an image device is inserted -- it now expands to show +each device that is inserted. + +Do not proceed with download if there is an error in the image rename or +download subfolder preferences. Instead, indicate a download error. + +Allow version 0.1.1 of pyexiv2 to be used (an older version of the library code +that is used to get information on the images, found in distributions like +Ubuntu 8.04 Hardy Heron). + +In cases where image rename or download subfolder preferences are invalid, +more helpful information is printed to the console output. + +Bug fix: better handle automated shortening Canon names like 'Canon 5D Mark II'. +It is now shortened to '5DMkII' instead of merely '5D'. + +Bug fix: reenable example of image renaming and subfolder name generation by +using first image from the first available download device. This was +inadvertently disabled in an earlier beta. + +Bug fix: make default download subfolder YYYY/YYYYMMDD again. It was +inadvertently set to DDMMYYYY/YYYYMMDD in beta 6. + +Bug fix: don't change download button label to "pause" when "Start downloading +on program startup" is set to true. + +Bug fix: implement code to warn / give error about missing backup devices. + +Bug fix: reset progress bar after completion of successful download. + +Fix bug #317404 when clearing completed downloads. + + + +Version 0.0.8 beta 7 +-------------------- + +2009-04-07 + +Added serial number metadata option for select Nikon, Canon, Olympus, Fuji, +Panasonic, and Kodak cameras. + +Added shutter count metadata option for select Nikon cameras, e.g. Nikon D300, +D3 etc. + +Add owner name metadata option for select Canon cameras, e.g. 5D Mk II etc. + + +Version 0.0.8 beta 6 +-------------------- + +2009-03-31 + +Add YYYY-MM-DD and YY-MM-DD options in date time renaming, suggested by +Andreas F.X. Siegert and Paul Gear. + +Fix bug #352242 where image has no metadata. + +Handle images with corrupt metadata more gracefully. + + +Version 0.0.8 beta 5 +-------------------- + +2009-03-30 + +Reduce console output. + + +Version 0.0.8 beta 4 +-------------------- + +2009-03-25 + +Updated Ubuntu package. + +Version 0.0.8 beta 3 +-------------------- + +2009-03-25 + +Updated Ubuntu package. + + +Version 0.0.8 beta 2 +-------------------- + +2009-03-25 + +First Ububtu package. + +Rename tarball package to suit package name. + +Updated README. + +Version 0.0.8 beta 1 +-------------------- + +2009-03-20 + +Make file renaming thread safe, fixing a long-standing (if difficult to +activate) bug. + +Implement add unique identifier when file name is not unique. + +Added "Report a Problem", "Get Help Online", "Make a Donation" to Help menu. + +Implemented "Clear completed downloads" menu item. + +Download images in order they were taken (checked by time they modified). + +Fixed bug where choosing text as the first item in a download subfolder caused a +crash. + +Fixed bug where date and time choices based on when image is downloaded caused a +crash. + +Initial code to show error message when image renaming preferences have an +error. + +Fixed bug where some invalid preferences were not being caught. + +Run default python, not one specified in env, as per recommendations in Debian +Python Policy. + +Remove initial period from filename extension when generating a subfolder name +(or else the folder will be hidden). + +Check to see if metadata is essential to generate image names is now more +robust. + +Remove list control from preferences, reverting to normal tabbed preferences, +as the window was becoming too wide. + +Show notifcations via libnotify. + +Error and warning icons can now be clicked on to open log window. + +Finally, last but certainly not least--implemented sequence number and sequence +letter generation: + * session sequence number + * sequence letter +Coming soon: + * downloads today sequence number + * subfolder sequence number + * stored sequence number + + +Version 0.0.7 +------------- + +2009-01-13 + +Implemented option for automatic detection of Portal Storage Devices. + +Version 0.0.6 +------------- + +2009-01-11 + +Fixed extremely annoying bug where memory cards could not be unmounted. + +Made sample image selection for preferences more robust. + +Added license details to about dialog. + +Fix bug where image rename preferences entry boxes vertically expanded, looking +very ugly indeed. + +Wrap new filename in image rename preferences when it becomes too long. + +Make default download folder selection more robust. + +Remove sequence number and sequence letter from list of choices for image rename +(not yet implemented). + +Bug #314825: fix by not calling gnomevfs.get_local_path_from_uri() unless +strictly necessary. + +Version 0.0.5 +------------- + +2009-01-09 + +Implement auto download on device insertion, and auto download on program +startup. + +Increase default width of preferences dialog box. + +Add vertical scrollbar to image rename preferences. + +Fixes for bugs #313463 & #313462. + +Version 0.0.4 +------------- + +2009-01-06 + +Bug #314284: Implement backup functionality. + +Bug #314285: Insert debugging code to help determine the cause of this bug. + +Version 0.0.3 +------------- + +2009-01-03 + +Bug #313398: Fix bug where application needed to be restarted for new +preferences to take effect. + +Added setup.py installer. + +Version 0.0.2 +------------- + +Dependencies: +Pyexiv 0.1.2 + +Updated metadata code to reflect changes in pyexiv library. + +Version 0.0.1 +------------- + +Initial release. diff --git a/rapid/INSTALL b/rapid/INSTALL new file mode 100644 index 0000000..27f9b07 --- /dev/null +++ b/rapid/INSTALL @@ -0,0 +1,36 @@ +Rapid Photo Downloader depends on the following software: + +- GNOME 2.18 +- GTK+ 2.10 +- Python 2.5 +- pygtk 2.10 +- python-gconf 2.18 +- python-glade2 2.10 +- gnome-python 2.10 +- libexiv2 0.15 +- pyexiv2 0.1.1 + +To run Rapid Photo Downloader you will need all the software mentioned above. To +start from a fairly basic system, I suggest the following: + +1. Install the required gnome packages from your linux distribution's package + repositories. On a recent linux distribution, expect all to be available for + easy download and installation. If you already use gnome, most of the + packages will already be installed. Do confirm that python-gconf, + python-glade2, and gnome-python are installed, because they + may not be quite so common in a base system. + +2. Install pyexiv2. You can probably do that using your distributions's package + repositories (look for python-pyexiv2). But if not, there are instructions + here: + + http://tilloy.net/dev/pyexiv2/developers.htm + +3. Install this application from the tarball. You probably need to be the + super user (root) to be able to do this: + + sudo python setup.py install + + If you get an error saying 'Error while running msgfmt', you will need to + install the gettext package on your system, which contains the msgfmt + program. diff --git a/rapid/TODO b/rapid/TODO new file mode 100644 index 0000000..5465850 --- /dev/null +++ b/rapid/TODO @@ -0,0 +1,26 @@ +For major release (0.10.0): +=========================== + +* sequencing: + - allow generation of unique sequences based on date time for last 1 - n days, via menu + +* download profiles (combinations of subfolder and image renaming preferences) +* better handle preference changes while downloads are occurring +* handle unexpected media removal, disk full, all kinds of write errors +* indicate different levels of error better in log window +* sum amount of images to be downloaded, and how much free space on the file system +* check download locations and sources still valid when starting the application +* rework checkPrefsForValidity() to account for multiple subfolder problems + +For future releases: +==================== + +* Better application icon -- help needed! +* Get more metadata + - lens +* User defined camera model mapping +* Job codes +* use python's high performance deque to improve idletube + + + diff --git a/rapid/ValidatedEntry.py b/rapid/ValidatedEntry.py new file mode 100644 index 0000000..cb453f4 --- /dev/null +++ b/rapid/ValidatedEntry.py @@ -0,0 +1,383 @@ +# Copyright (c) 2006, Daniel J. Popowich +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Send bug reports and contributions to: +# +# dpopowich AT astro dot umass dot edu +# + +''' +ValidatedEntry.py + +Provides ValidatedEntry, a subclass of gtk.Entry which validates +input. + +Usage: create an instance of ValidatedEntry, specifying the function +to validate input. E.g.: + + : def money(text): + : "validate input to be monetary value" + : ... + : + : money_entry = ValidatedEntry(money) + +Validation functions must accept one argument, the text to be +validated, and must return one of: + + 1: the input is valid. + 0: the input is invalid and should not be displayed. + -1: the input is partially valid and will be displayed (and by + default with a different background color). + +Three module-level variables are defined for the convenience of +validation function writers: VALID (1), INVALID (0), PARTIAL (-1). + +There is one public method, isvalid(), which will return True if the +current text is valid. + +Note: care should be taken when implementing validation functions to +allow empty strings to be VALID or at least PARTIAL. An empty string +should never be INVALID. + +Note: the hooks for calling the validation function are implemented by +connecting the object to handlers for the gtk.Editable "insert-text" +and "delete-text" signals. These handlers are connected to instances +in the constructor, so will, by default, be called before other +handlers connected to the widgets for "*-text" signals. When input is +INVALID, stop_emission() is called, so later handlers for "*-text" +signals will not be called. + +See the doc string for ValidatedEntry.__init__ for more details. + +''' + +import pygtk +pygtk.require('2.0') + +import gtk +import gtk.gdk + +if gtk.gtk_version < (2, 8): + import warnings + + msg ='''This module was developed and tested with version 2.8.9 of gtk. +You are using version %d.%d.%d. Your milage may vary''' % gtk.gtk_version + warnings.warn(msg) + +# major, minor, patch +version = 1, 0, 4 + +PARTIAL, INVALID, VALID = range(-1,2) + +class ValidatedEntry(gtk.Entry): + + white = gtk.gdk.color_parse('white') + yellow = gtk.gdk.color_parse('yellow') + + def __init__(self, valid_func, + max=0, + use_bg=True, valid_bg=white, partial_bg=yellow, + error_func=None): + ''' + Create instance of validating gtk.Entry. + + valid_func: the function to validate input. See module doc + string for details. + + max: passed to gtk.Entry constructor. (default: 0) + + use_bg: if True (the default) set the base color of the + widget to indicate validity; see valid_bg and partial_bg. + + valid_bg: a gtk.gdk.Color; the base color of the widget when + the input is valid. (default: white) + + partial_bg: a gtk.gdk.Color; the base color of the widget when + the input is partially valid. (default: yellow) + + error_func: a function to call (with no arguments) when + valid_func returns INVALID. If None (the default) + the default action will be to emit a short beep. + ''' + + assert valid_func('') != INVALID, 'valid_func cannot return INVALID for an empty string' + + gtk.Entry.__init__(self, max) + + self.__valid_func = valid_func + self.__use_bg = use_bg + self.__valid_bg = valid_bg + self.__partial_bg = partial_bg + self.__error_func = (error_func or + gtk.gdk.display_get_default().beep) + + self.connect('insert-text', self.__insert_text_cb) + self.connect('delete-text', self.__delete_text_cb) + + # bootstrap with an empty string (so the box will appear with + # the partial_bg if an empty string is PARTIAL) + self.insert_text('') + + def isvalid(self): + return self.__isvalid + + def __insert_text_cb(self, entry, text, length, position): + 'callback for "insert-text" signal' + + # generate what the new text will be + text = text[:length] + pos = self.get_position() + old = self.get_text() + new = old[:pos] + text + old[pos:] + + # validate the new text + self.__validate(new, 'insert-text') + + def __delete_text_cb(self, entry, start, end): + 'callback for "delete-text" signal' + + # generate what the new text will be + old = self.get_text() + new = old[:start] + old[end:] + + # validate the new text + self.__validate(new, 'delete-text') + + def __validate(self, text, signal): + 'calls the user-provided validation function' + + # validate + r = self.__valid_func(text) + if r == VALID: + self.__isvalid = True + if self.__use_bg: + self.modify_base(gtk.STATE_NORMAL, self.__valid_bg) + elif r == PARTIAL: + self.__isvalid = False + if self.__use_bg: + self.modify_base(gtk.STATE_NORMAL, self.__partial_bg) + else: + # don't set self.__isvalid: since we're not displaying the + # new value, the validity should be whatever it was before + self.stop_emission(signal) + self.__error_func() + + +###################################################################### +# +# Sample validation functions to use with ValidatedEntry +# +###################################################################### + +import re + + +# STRING (non-empty after stripping) +def v_nonemptystring(value): + ''' + VALID: non-empty string after stripping whitespace + PARTAL: empty or all whitespace + INVALID: N/A + ''' + if value.strip(): + return VALID + return PARTIAL + +# INT +def v_int(value): + ''' + VALID: any postive or negative integer + PARTAL: empty or leading "-" + INVALID: non-numeral + ''' + v = value.strip() + if not v or v == '-': + return PARTIAL + try: + int(value) + return VALID + except: + return INVALID + +# FLOAT +def v_float(value): + ''' + VALID: any postive or negative floating point + PARTAL: empty or leading "-", "." + INVALID: non-numeral + ''' + v = value.strip() + if not v or v in ('-', '.', '-.'): + return PARTIAL + try: + float(value) + return VALID + except: + return INVALID + + +# ISBN +_isbnpartial = re.compile('[0-9]{0,9}[0-9xX]?$') +def v_isbn(v): + + '''Validate ISBN input. + + From the isbn manual, section 4.4: + + The check digit is the last digit of an ISBN. It is calculated on + a modulus 11 with weights 10-2, using X in lieu of 10 where ten + would occur as a check digit. This means that each of the first + nine digits of the ISBN -- excluding the check digit itself -- is + multiplied by a number ranging from 10 to 2 and that the resulting + sum of the products, plus the check digit, must be divisible by 11 + without a remainder.''' + + + if _isbnpartial.match(v): + # isbn is ten characters in length + if len(v) < 10: + return PARTIAL + + s = 0 + + for i, c in enumerate(v): + s += (c in 'xX' and 10 or int(c)) * (10 - i) + + if s % 11 == 0: + return VALID + + return INVALID + +# MONEY +# re for (possibly negative) money +_money_re = re.compile('-?\d*(\.\d{1,2})?$') +# validation function for money +def v_money(value): + ''' + VALID: any postive or negative floating point with at most two + digits after the decimal point. + PARTAL: empty or leading "-", "." + INVALID: non-numeral or more than two digits after the decimal + point. + ''' + if not value or value == '-' or value[-1] == '.': + return PARTIAL + + if _money_re.match(value): + return VALID + + return INVALID + +# PHONE +# the characters in a phone number +_phonechars = re.compile('[- 0-9]*$') +# valid phone number: [AC +]EXT-LINE +_phone = re.compile('([2-9][0-8][0-9]\s+)?[2-9][0-9]{2}-[0-9]{4}$') +def v_phone(value): + ''' + VALID: any phone number of the form: EXT-LINE -or- AC EXT-LINE. + PARTAL: any characters that make up a valid #. + INVALID: characters that are not used in a phone #. + ''' + if _phone.match(value): + return VALID + if _phonechars.match(value): + return PARTIAL + return INVALID + +def empty_valid(vfunc): + + ''' + empty_valid is a factory function returning a validation function. + All of the validation functions in this module return PARTIAL for + empty strings which, in effect, forces non-empty input. There may + be a case where, e.g., you want money input to be optional, but + v_money will not consider empty input VALID. Instead of writing + another validation function you can instead use empty_valid(). By + wrapping a validation function with empty_valid(), input (after + stripping), if empty, will be considered VALID. E.g.: + + ventry = ValidatedEntry(empty_valid(v_money)) + + It is recommended that all your validation functions treat empty + input as PARTIAL, for consistency across all validation functions + and for use with empty_valid(). + ''' + + def validate(value): + if not value.strip(): + return VALID + return vfunc(value) + + return validate + + +def bounded(vfunc, conv, minv=None, maxv=None): + + ''' + bounded is a factory function returning a validation function + providing bounded input. E.g., you may want an entry that accepts + integers, but within a range, say, a score on a test graded in + whole numbers from 0 to 100: + + score_entry = ValidatedEntry(bounded(v_int, int, 0, 100)) + + Arguments: + + vfunc: A validation function. + conv: A callable that accepts a string argument (the text in + the entry) and returns a value to be compared to minv + and maxv. + minv: None or a value of the same type returned by conv. If + None, there is no minimum value enforced. If a value, + it will be the minimum value considered VALID. + maxv: None or a value of the same type returned by conv. If + None, there is no maximum value enforced. If a value, + it will be the maximum value considered VALID. + + One or both of minv/maxv must be specified. + + The function returned will call vfunc on entry input and if vfunc + returns VALID, the input will be converted by conv and compared to + minv/maxv. If the converted value is within the bounds of + minv/maxv then VALID will be returned, else PARTIAL will be + returned. + + ''' + + assert minv is not None or maxv is not None, \ + 'One of minv/maxv must be specified' + + def F(value): + + r = vfunc(value) + if r == VALID: + v = conv(value) + if minv is not None and v < minv: + return PARTIAL + if maxv is not None and v > maxv: + return PARTIAL + return r + + return F + + diff --git a/rapid/__init__.py b/rapid/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/rapid/__init__.py @@ -0,0 +1 @@ +# diff --git a/rapid/common.py b/rapid/common.py new file mode 100644 index 0000000..d687049 --- /dev/null +++ b/rapid/common.py @@ -0,0 +1,181 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007-09 Damon Lynch <damonlynch@gmail.com> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import sys +import gc +import distutils.version +import gtk.gdk as gdk + +import config + +import locale +import gettext + +class Configi18n: + """ Setup translation + + Adapated from code example of Mark Mruss http://www.learningpython.com. + Unlike his example, this code uses a local locale directory only if the environment + variable LOCALEDIR has been set to some or other value. + """ + + # Do not put this code block in __init__, because it needs to be run only once + + # if the evironment value 'LOCAELDIR' is set, then use this as the source of translation data + # otherwise, rely on the system-wide data + locale_path = os.environ.get('LOCALEDIR', None) + + # Init the list of languages to support + langs = [] + #Check the default locale + lc, encoding = locale.getdefaultlocale() + if (lc): + #If we have a default, it's the first in the list + langs = [lc] + # Now let's get all of the supported languages on the system + language = os.environ.get('LANGUAGE', None) + if (language): + # langage comes back something like en_CA:en_US:en_GB:en + langs += language.split(":") + + # add on to the back of the list the translations that we know that we have, our defaults + langs += ["en_US"] + + # Now langs is a list of all of the languages that we are going + # to try to use. First we check the default, then what the system + # told us, and finally the 'known' list + + gettext.bindtextdomain(config.APP_NAME, locale_path) + gettext.textdomain(config.APP_NAME) + # Get the language to use + lang = gettext.translation(config.APP_NAME, locale_path, languages=langs, fallback = True) + # Install the language, map _() (which we marked our + # strings to translate with) to self.lang.gettext() which will + # translate them. + _ = lang.gettext + + +def pythonifyVersion(v): + """ makes version number a version number in distutils sense""" + return distutils.version.StrictVersion(v.replace( '~','')) + +def getFullProgramName(): + """ return the full name of the process running """ + return os.path.basename(sys.argv[0]) + +def getProgramName(): + """ return the name of the process running, removing the .py extension if it exists """ + programName = getFullProgramName() + if programName.find('.py') > 0: + programName = programName[:programName.find('.py')] + return programName + +def splitDirectories(directories): + """ split directories specified in string into a list """ + if directories.find(',') > 0: + d = directories.split(',') + else: + d = directories.split() + directories = [] + for i in d: + directories.append(i.strip()) + return directories + + + +def getFullPath(path): + """ make path relative to home directory if not an absolute path """ + if os.path.isabs(path): + return path + else: + return os.path.join(os.path.expanduser('~'), path) + + +def escape(s): + """ + Replace special characters by SGML entities. + """ + entities = ("&&", "<<", ">>") + for e in entities: + s = s.replace(e[0], e[1:]) + return s + +def formatSizeForUser(bytes, zeroString="", withDecimals=True, kbOnly=False): + """Format an int containing the number of bytes into a string suitable for + printing out to the user. zeroString is the string to use if bytes == 0. + source: https://develop.participatoryculture.org/trac/democracy/browser/trunk/tv/portable/util.py?rev=3993 + + """ + if bytes > (1 << 30) and not kbOnly: + value = (bytes / (1024.0 * 1024.0 * 1024.0)) + if withDecimals: + format = "%1.1fGB" + else: + format = "%dGB" + elif bytes > (1 << 20) and not kbOnly: + value = (bytes / (1024.0 * 1024.0)) + if withDecimals: + format = "%1.1fMB" + else: + format = "%dMB" + elif bytes > (1 << 10): + value = (bytes / 1024.0) + if withDecimals: + format = "%1.1fKB" + else: + format = "%dKB" + elif bytes > 1: + value = bytes + if withDecimals: + format = "%1.1fB" + else: + format = "%dB" + else: + return zeroString + return format % value + +def scale2pixbuf(width_max, height_max, pixbuf, return_size=False): + """ + Scale to width_max and height_max. + Keep aspect ratio. + Code adapted from gthumpy, by guettli + """ + + width_orig = float(pixbuf.get_width()) + height_orig = float(pixbuf.get_height()) + if (width_orig / width_max) > (height_orig / height_max): + height = int((height_orig / width_orig) * width_max) + width = width_max + else: + width = int((width_orig / height_orig) * height_max) + height=height_max + + pixbuf = pixbuf.scale_simple(width, height, gdk.INTERP_BILINEAR) + gc.collect() # Tell Python to clean up the memory + if return_size: + return pixbuf, width_orig, height_orig + return pixbuf + + + +if __name__ == '__main__': + i = Configi18n() + _ = i._ + print _("hello world") diff --git a/rapid/config.py b/rapid/config.py new file mode 100644 index 0000000..2e59841 --- /dev/null +++ b/rapid/config.py @@ -0,0 +1,58 @@ +# -*- coding: latin1 -*- +### Copyright (C) 2007, 2008, 2009 Damon Lynch <damonlynch@gmail.com> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +version = '0.0.10' + +GCONF_KEY="/apps/rapid-photo-downloader" +GLADE_FILE = "glade3/rapid.glade" + +DBUS_NAME = "net.damonlynch.RapidPhotoDownloader" + +#i18n +APP_NAME = "rapid-photo-downloader" + +MEDIA_LOCATION = "/media" + +SKIP_DOWNLOAD = "skip download" +ADD_UNIQUE_IDENTIFIER = "add unique identifier" + +REPORT_WARNING = "warning" +REPORT_ERROR = "error" +IGNORE = "ignore" + +DEFAULT_PHOTO_LOCATIONS = ['Pictures', 'Photos'] +DEFAULT_BACKUP_LOCATION = 'Pictures' + +MAX_NO_READERS = 20 + +RAW_FILE_EXTENSIONS = ['arw', 'dcr', 'cr2', 'crw', 'dng', 'mef', 'mos', 'mrw', + 'nef', 'orf', 'pef', 'raf', 'raw', 'sr2'] + +#exiv2 0.18.1 introduces support for Panasonic .RW2 files + +NON_RAW_IMAGE_FILE_EXTENSIONS = ['jpg', 'jpe', 'jpeg', 'tif', 'tiff'] + +CRITICAL_ERROR = 1 +SERIOUS_ERROR = 2 +WARNING = 3 + +MAX_LENGTH_DEVICE_NAME = 15 + +#logging - to be implemented +#LOGFILE_DIRECTORY = '.rapidPhotoDownloader' # relative to home directory +#MAX_LOGFILE_SIZE = 100 * 1024 # bytes +#MAX_LOGFILES = 5 diff --git a/rapid/glade3/image-missing.svg b/rapid/glade3/image-missing.svg new file mode 100644 index 0000000..4351feb --- /dev/null +++ b/rapid/glade3/image-missing.svg @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="48px" height="48px" id="svg1306" sodipodi:version="0.32" inkscape:version="0.45" sodipodi:docbase="/home/dobey/Projects/gnome-icon-theme/scalable/status" sodipodi:docname="image-missing.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape"> + <defs id="defs1308"> + <linearGradient inkscape:collect="always" id="linearGradient6346"> + <stop style="stop-color: rgb(85, 87, 83); stop-opacity: 1;" offset="0" id="stop6348"/> + <stop style="stop-color: rgb(46, 52, 54); stop-opacity: 1;" offset="1" id="stop6350"/> + </linearGradient> + <linearGradient inkscape:collect="always" id="linearGradient5060"> + <stop style="stop-color: black; stop-opacity: 1;" offset="0" id="stop5062"/> + <stop style="stop-color: black; stop-opacity: 0;" offset="1" id="stop5064"/> + </linearGradient> + <linearGradient id="linearGradient5048"> + <stop style="stop-color: black; stop-opacity: 0;" offset="0" id="stop5050"/> + <stop id="stop5056" offset="0.5" style="stop-color: black; stop-opacity: 1;"/> + <stop style="stop-color: black; stop-opacity: 0;" offset="1" id="stop5052"/> + </linearGradient> + <linearGradient inkscape:collect="always" id="linearGradient6431"> + <stop style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="0" id="stop6433"/> + <stop style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1" id="stop6435"/> + </linearGradient> + <linearGradient inkscape:collect="always" id="linearGradient6390"> + <stop style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="0" id="stop6392"/> + <stop style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1" id="stop6394"/> + </linearGradient> + <linearGradient id="linearGradient6374"> + <stop style="stop-color: rgb(85, 87, 83); stop-opacity: 1;" offset="0" id="stop6376"/> + <stop style="stop-color: rgb(136, 138, 133); stop-opacity: 1;" offset="1" id="stop6378"/> + </linearGradient> + <linearGradient id="linearGradient6366"> + <stop style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="0" id="stop6368"/> + <stop style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1" id="stop6370"/> + </linearGradient> + <linearGradient id="linearGradient6327"> + <stop style="stop-color: rgb(136, 138, 133); stop-opacity: 1;" offset="0" id="stop6329"/> + <stop style="stop-color: rgb(238, 238, 236); stop-opacity: 1;" offset="1" id="stop6331"/> + </linearGradient> + <linearGradient inkscape:collect="always" xlink:href="#linearGradient6327" id="linearGradient6333" x1="42.999424" y1="36.811924" x2="40.621296" y2="34.433796" gradientUnits="userSpaceOnUse"/> + <linearGradient inkscape:collect="always" xlink:href="#linearGradient6327" id="linearGradient6360" x1="61.18124" y1="137.97644" x2="20.420683" y2="2.6749926" gradientUnits="userSpaceOnUse"/> + <linearGradient inkscape:collect="always" xlink:href="#linearGradient6366" id="linearGradient6372" x1="38.908649" y1="35.960426" x2="35.032925" y2="30.679369" gradientUnits="userSpaceOnUse"/> + <radialGradient inkscape:collect="always" xlink:href="#linearGradient6374" id="radialGradient6382" cx="39.437065" cy="34.33852" fx="39.437065" fy="34.33852" r="6" gradientTransform="matrix(0.1875, -1.05343, 0.718081, 0.127811, 7.82271, 71.0304)" gradientUnits="userSpaceOnUse"/> + <radialGradient inkscape:collect="always" xlink:href="#linearGradient6390" id="radialGradient6396" cx="20.236877" cy="25.043303" fx="20.236877" fy="25.043303" r="22" gradientTransform="matrix(0.940906, -0.20665, 0.109821, 0.50003, -1.55441, 13.2196)" gradientUnits="userSpaceOnUse"/> + <radialGradient inkscape:collect="always" xlink:href="#linearGradient6431" id="radialGradient6437" cx="40.179535" cy="34.080399" fx="40.179535" fy="34.080399" r="4.125" gradientTransform="matrix(1, 0, 0, 0.935606, 0, 2.30711)" gradientUnits="userSpaceOnUse"/> + <radialGradient inkscape:collect="always" xlink:href="#linearGradient5060" id="radialGradient6909" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.0160648, 0, 0, 0.0741176, 11.6816, -1.17508)" cx="605.71429" cy="486.64789" fx="605.71429" fy="486.64789" r="117.14286"/> + <radialGradient inkscape:collect="always" xlink:href="#linearGradient5060" id="radialGradient6912" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.104421, 0, 0, 0.0741176, -27.9305, -1.17508)" cx="605.71429" cy="486.64789" fx="605.71429" fy="486.64789" r="117.14286"/> + <linearGradient inkscape:collect="always" xlink:href="#linearGradient5048" id="linearGradient6915" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.0683432, 0, 0, 0.0741176, -6.20118, -1.17508)" x1="302.85715" y1="366.64789" x2="302.85715" y2="609.50507"/> + <radialGradient inkscape:collect="always" xlink:href="#linearGradient6346" id="radialGradient6352" cx="24.013514" cy="39.717846" fx="24.013514" fy="39.717846" r="20.013513" gradientTransform="matrix(2.00322, 0, 0, 1.45136, -24.0909, -22.3947)" gradientUnits="userSpaceOnUse"/> + </defs> + <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1" inkscape:cx="50.321592" inkscape:cy="19.160509" inkscape:current-layer="layer1" showgrid="false" inkscape:grid-bbox="true" inkscape:document-units="px" fill="#fcaf3e" stroke="#555753" showguides="true" inkscape:guide-bbox="false" inkscape:window-width="895" inkscape:window-height="760" inkscape:window-x="208" inkscape:window-y="229" inkscape:showpageshadow="false" showborder="false" gridspacingx="0.5px" gridspacingy="0.5px" gridempspacing="2" inkscape:grid-points="false" gridtolerance="50" inkscape:object-paths="false"/> + <metadata id="metadata1311"> + <rdf:RDF> + <cc:Work rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + <dc:title>Generic Image</dc:title> + <dc:creator> + <cc:Agent> + <dc:title>Lapo Calamandrei</dc:title> + </cc:Agent> + </dc:creator> + <dc:source>http://www.gnome.org</dc:source> + <dc:contributor> + <cc:Agent> + <dc:title>Jakub Steiner, Andreas Nilsson</dc:title> + </cc:Agent> + </dc:contributor> + <cc:license rdf:resource="http://creativecommons.org/licenses/GPL/2.0/"/> + </cc:Work> + <cc:License rdf:about="http://creativecommons.org/licenses/GPL/2.0/"> + <cc:permits rdf:resource="http://web.resource.org/cc/Reproduction"/> + <cc:permits rdf:resource="http://web.resource.org/cc/Distribution"/> + <cc:requires rdf:resource="http://web.resource.org/cc/Notice"/> + <cc:permits rdf:resource="http://web.resource.org/cc/DerivativeWorks"/> + <cc:requires rdf:resource="http://web.resource.org/cc/ShareAlike"/> + <cc:requires rdf:resource="http://web.resource.org/cc/SourceCode"/> + </cc:License> + </rdf:RDF> + </metadata> + <g id="layer1" inkscape:label="Layer 1" inkscape:groupmode="layer"> + <g id="g6917"> + <rect y="26" x="2" height="18" width="33" id="rect6057" style="overflow: visible; marker: none; opacity: 0.39196; color: rgb(0, 0, 0); fill: url(#linearGradient6915) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0pt; stroke-opacity: 1; visibility: visible; display: inline;"/> + <path sodipodi:nodetypes="ccscc" id="path6059" d="M 35,26.00062 C 35,26.00062 35,43.999627 35,43.999627 C 37.30962,44.01418 40.033409,43.272315 42.389531,42.027493 C 45.519106,40.37403 48.000002,37.833194 48,34.998965 C 48,30.030967 41.999197,26.000621 35,26.00062 z " style="overflow: visible; marker: none; opacity: 0.402062; color: rgb(0, 0, 0); fill: url(#radialGradient6912) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0pt; stroke-opacity: 1; visibility: visible; display: inline;"/> + <path style="overflow: visible; marker: none; opacity: 0.402062; color: rgb(0, 0, 0); fill: url(#radialGradient6909) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0pt; stroke-opacity: 1; visibility: visible; display: inline;" d="M 2,26.00062 C 2,26.00062 2,43.999627 2,43.999627 C 1.172704,44.03351 1.3577825e-06,39.966963 1.3577825e-06,34.998965 C 1.3577825e-06,30.030967 0.92320113,26.000621 2,26.00062 z " id="path6061" sodipodi:nodetypes="cccc"/> + </g> + <path style="fill: url(#linearGradient6360) rgb(0, 0, 0); fill-opacity: 1; stroke: rgb(136, 138, 133); stroke-width: 1; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dashoffset: 0pt; stroke-opacity: 1;" d="M 2.767767,6.5 L 45.232237,6.5 C 45.93458,6.5 46.500004,7.0654241 46.500004,7.767767 L 46.5,30.5 C 46.5,31.202343 36.202343,40.5 35.5,40.5 L 2.767767,40.5 C 2.0654241,40.5 1.5,39.934576 1.5,39.232233 L 1.5,7.767767 C 1.5,7.0654241 2.0654241,6.5 2.767767,6.5 z " id="rect5350" sodipodi:nodetypes="ccccccccc"/> + <path style="fill: url(#radialGradient6352) rgb(0, 0, 0); fill-opacity: 1; stroke: rgb(136, 138, 133); stroke-width: 1; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dashoffset: 0pt; stroke-opacity: 1;" d="M 4.9874617,9.5 L 43.039565,9.5 C 43.309619,9.5 43.527027,9.7276458 43.527027,10.010417 L 43.527027,33.989583 L 40.039565,37.5 L 4.9874617,37.5 C 4.7174079,37.5 4.5,37.272354 4.5,36.989583 L 4.5,10.010417 C 4.5,9.7276458 4.7174079,9.5 4.9874617,9.5 z " id="rect2063" sodipodi:nodetypes="ccccccccc"/> + <path style="opacity: 0.172414; fill: url(#linearGradient6372) rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dashoffset: 0pt; stroke-opacity: 1;" d="M 14.78125,40 L 35.40625,40 C 35.41645,39.995518 35.423208,39.996022 35.46875,39.96875 C 35.559833,39.914206 35.707858,39.809986 35.875,39.6875 C 36.209283,39.442528 36.659837,39.093896 37.1875,38.65625 C 38.242827,37.780957 39.61066,36.608491 40.9375,35.40625 C 42.26434,34.204009 43.563777,32.96157 44.53125,32 C 45.014987,31.519215 45.417906,31.118778 45.6875,30.8125 C 45.822297,30.659361 45.910802,30.519277 45.96875,30.4375 C 45.983237,30.417056 45.991685,30.419536 46,30.40625 L 46,13.78125 L 14.78125,40 z " id="path6364" sodipodi:nodetypes="ccsssssssccc"/> + <path style="fill: none; fill-opacity: 1; stroke: rgb(255, 255, 255); stroke-width: 1; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dashoffset: 0pt; stroke-opacity: 1;" d="M 2.78125,7.5 C 2.6253137,7.5 2.5,7.6253137 2.5,7.78125 L 2.5,39.21875 C 2.5,39.374686 2.6253136,39.5 2.78125,39.5 L 37.28125,39.5 C 40.921301,36.704635 42.365769,35.606734 45.5,32.25 L 45.5,7.78125 C 45.5,7.6253155 45.374683,7.5 45.21875,7.5 L 2.78125,7.5 z " id="path6351" sodipodi:nodetypes="ccccccccc"/> + <path style="fill: url(#linearGradient6333) rgb(0, 0, 0); fill-rule: evenodd; stroke: url(#radialGradient6382) rgb(0, 0, 0); stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: round; stroke-opacity: 1; fill-opacity: 1;" d="M 46.5,30.5 C 46.5,35.5 40.5,40.5 35.5,40.5 C 35.5,40.5 39.932134,38.33738 39.5,33.5 C 43.879686,33.916135 46.5,30.5 46.5,30.5 z " id="path6322" sodipodi:nodetypes="cccc"/> + <path style="fill: url(#radialGradient6437) rgb(0, 0, 0); fill-opacity: 1; fill-rule: evenodd; stroke: none; stroke-width: 1px; stroke-linecap: butt; stroke-linejoin: round; stroke-opacity: 1; opacity: 0.732759;" d="M 45.71875 31.96875 C 44.577589 32.932531 42.679856 34.08934 40.03125 34.03125 C 40.059733 36.774444 38.699098 38.58751 37.46875 39.6875 C 38.953141 38.706251 40.825166 37.371805 41.03125 35.03125 C 43.192381 35.027997 44.619214 33.310655 45.6875 32.03125 C 45.693038 32.009581 45.713422 31.990426 45.71875 31.96875 z " id="path6339"/> + <path style="fill: url(#radialGradient6396) rgb(0, 0, 0); fill-opacity: 1; stroke: none; stroke-width: 1; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dashoffset: 0pt; stroke-opacity: 1; opacity: 0.344828;" d="M 2.78125,7 C 2.3536576,7 2,7.3536576 2,7.78125 L 2,31.09375 C 14.714701,22.184679 34.055102,15.625228 46,19.3125 L 46,7.78125 C 46,7.3536585 45.64634,7 45.21875,7 L 2.78125,7 z " id="path6384" sodipodi:nodetypes="ccccccc"/> + <path sodipodi:type="inkscape:offset" inkscape:radius="-1" inkscape:original="M 2.78125 6.5 C 2.0789071 6.5 1.5 7.0789071 1.5 7.78125 L 1.5 39.21875 C 1.5 39.921093 2.0789071 40.5 2.78125 40.5 L 35.5 40.5 C 40.5 40.5 46.5 35.5 46.5 30.5 L 46.5 7.78125 C 46.5 7.0789071 45.921091 6.5 45.21875 6.5 L 2.78125 6.5 z " style="fill: none; fill-opacity: 1; stroke: rgb(255, 255, 255); stroke-width: 1; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dashoffset: 0pt; stroke-opacity: 1; opacity: 0.418103;" id="path6427" d="M 2.78125,7.5 C 2.628408,7.5 2.5,7.628408 2.5,7.78125 L 2.5,39.21875 C 2.5,39.371592 2.6284079,39.5 2.78125,39.5 L 35.5,39.5 C 37.666667,39.5 40.249358,38.362698 42.21875,36.625 C 44.188142,34.887302 45.5,32.6 45.5,30.5 L 45.5,7.78125 C 45.5,7.6284098 45.371589,7.5 45.21875,7.5 L 2.78125,7.5 z "/> + </g> +</svg>
\ No newline at end of file diff --git a/rapid/glade3/rapid-photo-downloader-about.png b/rapid/glade3/rapid-photo-downloader-about.png Binary files differnew file mode 100644 index 0000000..0aefb1d --- /dev/null +++ b/rapid/glade3/rapid-photo-downloader-about.png diff --git a/rapid/glade3/rapid.glade b/rapid/glade3/rapid.glade new file mode 100644 index 0000000..be01fa4 --- /dev/null +++ b/rapid/glade3/rapid.glade @@ -0,0 +1,2716 @@ +<?xml version="1.0"?> +<glade-interface> + <!-- interface-requires gtk+ 2.16 --> + <!-- interface-naming-policy toplevel-contextual --> + <widget class="GtkDialog" id="preferencesdialog"> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Preferences: Rapid Photo Downloader</property> + <property name="modal">True</property> + <property name="window_position">center-on-parent</property> + <property name="default_height">500</property> + <property name="icon">rapid-photo-downloader-about.png</property> + <property name="type_hint">dialog</property> + <property name="has_separator">False</property> + <signal name="destroy" handler="on_preferencesdialog_destroy"/> + <signal name="response" handler="on_response"/> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox2"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkHBox" id="hbox3"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkScrolledWindow" id="scrolled_window"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="resize_mode">queue</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> + <child> + <widget class="GtkTreeView" id="treeview"> + <property name="width_request">100</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="headers_visible">False</property> + <signal name="cursor_changed" handler="on_treeview_cursor_changed"/> + </widget> + </child> + </widget> + <packing> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkNotebook" id="notebook"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="show_border">False</property> + <child> + <widget class="GtkVBox" id="folder_tab"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkVBox" id="vbox6"> + <property name="visible">True</property> + <child> + <widget class="GtkHBox" id="hbox4"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="stock">gtk-open</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="label" translatable="yes"><span weight="bold" size="x-large">Download Folder</span></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHSeparator" id="hseparator1"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox9"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkLabel" id="label16"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkTable" id="download_folder_table"> + <property name="visible">True</property> + <property name="n_rows">7</property> + <property name="n_columns">3</property> + <child> + <widget class="GtkVBox" id="subfolder_vbox"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="y_padding">12</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="example_download_path_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><i>Example: /home/user/photos</i></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><b>Download Subfolders</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="right_attach">3</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="lblPhotos1"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Download folder:</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options"></property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="ypad">12</property> + <property name="label" translatable="yes">Choose the download folder. Subfolders for the downloaded photos will be automatically created in this folder using the structure specified below.</property> + <property name="use_markup">True</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><b>Download Folder</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="right_attach">3</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="subfolder_warning_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="wrap">True</property> + <property name="wrap_mode">word-char</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="top_attach">6</property> + <property name="bottom_attach">7</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label23"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="menu_label">Download Folders</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="download_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Download Folder</property> + </widget> + <packing> + <property name="tab_fill">False</property> + <property name="type">tab</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="rename_tab"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkVBox" id="vbox7"> + <property name="visible">True</property> + <child> + <widget class="GtkHBox" id="hbox5"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkImage" id="image3"> + <property name="visible">True</property> + <property name="stock">gtk-convert</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="label" translatable="yes"><span weight="bold" size="x-large">Image Rename</span> </property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHSeparator" id="hseparator2"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox12"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkLabel" id="label24"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="rename_vbox"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkLabel" id="label42"> + <property name="xalign">0</property> + <property name="label" translatable="yes"><b>Image Rename</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkScrolledWindow" id="rename_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="window_placement_set">True</property> + <child> + <widget class="GtkViewport" id="viewport2"> + <property name="visible">True</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <widget class="GtkVBox" id="rename_table_vbox"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </widget> + </child> + </widget> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkTable" id="rename_example_table"> + <property name="visible">True</property> + <property name="n_rows">3</property> + <property name="n_columns">3</property> + <child> + <widget class="GtkLabel" id="label17"> + <property name="visible">True</property> + <property name="label" translatable="yes"> </property> + </widget> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label15"> + <property name="visible">True</property> + <property name="label" translatable="yes"> </property> + </widget> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options"></property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="new_name_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="label" translatable="yes">translators please ignore this</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="original_name_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">translators please ignore this</property> + </widget> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label21"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="label" translatable="yes"><i>New:</i></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label20"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><i>Original:</i></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><b>Example</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="right_attach">3</property> + <property name="y_padding">12</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label25"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="menu_label">Rename</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="rename_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Image Rename</property> + </widget> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + <property name="type">tab</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="rename_options_tab"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkVBox" id="vbox14"> + <property name="visible">True</property> + <child> + <widget class="GtkHBox" id="hbox18"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkImage" id="image7"> + <property name="visible">True</property> + <property name="icon_name">input-keyboard</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="label" translatable="yes"><span weight="bold" size="x-large">Rename Options</span></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHSeparator" id="hseparator4"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="vbox12"> + <property name="visible">True</property> + <child> + <widget class="GtkLabel" id="label29"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes"><b>Sequence numbers</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox21"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkLabel" id="label46"> + <property name="visible">True</property> + <property name="xpad">12</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="vbox13"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkLabel" id="label47"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Specify the time in 24 hour format at which the <i>Downloads today</i> sequence number should be reset.</property> + <property name="use_markup">True</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox23"> + <property name="visible">True</property> + <child> + <widget class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkLabel" id="label49"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Day start:</property> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label51"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Downloads today:</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label52"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Stored number:</property> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label54"> + <property name="visible">True</property> + <property name="xpad">6</property> + <property name="label" translatable="yes"> </property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="sequence_vbox"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkHBox" id="hbox22"> + <property name="visible">True</property> + <child> + <widget class="GtkSpinButton" id="hour_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="width_chars">2</property> + <property name="xalign">1</property> + <property name="truncate_multiline">True</property> + <property name="adjustment">0 0 23 1 10 0</property> + <property name="numeric">True</property> + <signal name="value_changed" handler="on_hour_spinbutton_value_changed"/> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label50"> + <property name="visible">True</property> + <property name="label" translatable="yes">:</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkSpinButton" id="minute_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="width_chars">2</property> + <property name="xalign">1</property> + <property name="truncate_multiline">True</property> + <property name="adjustment">0 0 59 1 10 0</property> + <property name="numeric">True</property> + <signal name="value_changed" handler="on_minute_spinbutton_value_changed"/> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label53"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"> hh:mm</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">3</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label48"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label44"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes"><b>Compatibility with Other Operating Systems</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox16"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkLabel" id="label32"> + <property name="visible">True</property> + <property name="xpad">12</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkTable" id="compatibility_table"> + <property name="visible">True</property> + <property name="n_rows">9</property> + <property name="n_columns">2</property> + <child> + <widget class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Specify whether image and folder names should have any characters removed that are not allowed by other operating systems.</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="right_attach">2</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="strip_characters_checkbutton"> + <property name="label" translatable="yes">Strip incompatible characters</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_strip_characters_checkbutton_toggled"/> + </widget> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label33"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="padding">12</property> + <property name="position">3</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="rename_options_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Rename Options</property> + </widget> + <packing> + <property name="position">2</property> + <property name="tab_fill">False</property> + <property name="type">tab</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="device_tab"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkVBox" id="vbox3"> + <property name="visible">True</property> + <child> + <widget class="GtkHBox" id="hbox2"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkImage" id="image6"> + <property name="visible">True</property> + <property name="icon_name">media-flash</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label22"> + <property name="visible">True</property> + <property name="label" translatable="yes"><span weight="bold" size="x-large">Image Devices</span></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHSeparator" id="hseparator5"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="vbox5"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkLabel" id="label41"> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes"><b>Image Devices</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label18"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes">Image devices are devices from which to download photos, such as cameras, memory cards or Portable Storage Devices. + +You can download photos from multiple image devices simultaneously. + +<i>If downloading directly from your camera works poorly or not at all, try setting it to PTP mode. If that is not possible, consider using a card reader.</i></property> + <property name="use_markup">True</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox14"> + <property name="visible">True</property> + <child> + <widget class="GtkLabel" id="label26"> + <property name="visible">True</property> + <property name="xpad">3</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkTable" id="devices_table"> + <property name="visible">True</property> + <property name="n_rows">3</property> + <property name="n_columns">2</property> + <property name="row_spacing">3</property> + <child> + <widget class="GtkCheckButton" id="autodetect_psd_checkbutton"> + <property name="label" translatable="yes">Automatically detect Portable Storage Devices</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_autodetect_psd_checkbutton_toggled"/> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="autodetect_device_checkbutton"> + <property name="label" translatable="yes">Automatically detect image devices</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_autodetect_device_checkbutton_toggled"/> + </widget> + <packing> + <property name="right_attach">2</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="autodetect_image_devices_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="ypad">6</property> + <property name="label" translatable="yes">If you enable automatic detection of Portable Storage Devices, the entire device will be scanned for images. On large devices, this could take some time.</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label28"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox15"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkTable" id="devices2_table"> + <property name="visible">True</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <child> + <widget class="GtkLabel" id="device_location_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes">Image location:</property> + </widget> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_SHRINK</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="device_location_explanation_label"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="ypad">12</property> + <property name="label" translatable="yes">If you disable automatic detection, choose the exact location of the images.</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="right_attach">2</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <widget class="GtkLabel" id="label30"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">3</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">3</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="device_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Image Devices</property> + </widget> + <packing> + <property name="position">3</property> + <property name="tab_fill">False</property> + <property name="type">tab</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="backup_tab"> + <property name="visible">True</property> + <child> + <widget class="GtkVBox" id="vbox2"> + <property name="visible">True</property> + <child> + <widget class="GtkHBox" id="hbox7"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkImage" id="image8"> + <property name="visible">True</property> + <property name="icon_name">drive-removable-media</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label27"> + <property name="visible">True</property> + <property name="label" translatable="yes"><span weight="bold" size="x-large">Backup</span> </property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHSeparator" id="hseparator6"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="vbox9"> + <property name="visible">True</property> + <child> + <widget class="GtkLabel" id="label43"> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes"><b>Backup</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox20"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkLabel" id="label38"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkTable" id="backup_table"> + <property name="visible">True</property> + <property name="n_rows">8</property> + <property name="n_columns">4</property> + <child> + <widget class="GtkLabel" id="backup_location_explanation_label"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="xalign">0</property> + <property name="ypad">12</property> + <property name="label" translatable="yes">If you disable automatic detection, choose the exact backup location.</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">4</property> + <property name="top_attach">6</property> + <property name="bottom_attach">7</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="auto_detect_backup_checkbutton"> + <property name="label" translatable="yes">Automatically detect backup devices</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_auto_detect_backup_checkbutton_toggled"/> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">4</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label11"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="ypad">12</property> + <property name="label" translatable="yes">You can have your photos backed up to multiple locations as they are downloaded, e.g. external hard drives.</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="right_attach">4</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="backup_checkbutton"> + <property name="label" translatable="yes">Backup photos when downloading</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_backup_checkbutton_toggled"/> + </widget> + <packing> + <property name="right_attach">4</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="backup_identifier_explanation_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="ypad">6</property> + <property name="label" translatable="yes">Specify the folder in which backups are stored on the device. + +<i>Note: this will also be used to determine whether or not the device is used for backups. For each device you wish to use for backing up to, create a folder in it with this name.</i></property> + <property name="use_markup">True</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">4</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="backup_location_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Backup location:</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="top_attach">7</property> + <property name="bottom_attach">8</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="backup_identifier_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Backup folder name:</property> + </widget> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">6</property> + <property name="bottom_attach">5</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="backup_example_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="ypad">6</property> + <property name="label" translatable="yes"><i>Example:</i></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="example_backup_path_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="ypad">6</property> + <property name="label" translatable="yes"><i>/media/externaldrive/Photos</i></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="left_attach">3</property> + <property name="right_attach">4</property> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + </packing> + </child> + <child> + <widget class="GtkEntry" id="backup_identifier_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <signal name="changed" handler="on_backup_identifier_entry_changed"/> + </widget> + <packing> + <property name="left_attach">3</property> + <property name="right_attach">4</property> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="y_options"></property> + <property name="y_padding">12</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label39"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">4</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="backup_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Backup</property> + </widget> + <packing> + <property name="position">4</property> + <property name="tab_fill">False</property> + <property name="type">tab</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="automation_tab"> + <property name="visible">True</property> + <child> + <widget class="GtkVBox" id="vbox4"> + <property name="visible">True</property> + <child> + <widget class="GtkHBox" id="hbox10"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkImage" id="image4"> + <property name="visible">True</property> + <property name="stock">gtk-execute</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label31"> + <property name="visible">True</property> + <property name="label" translatable="yes"><span weight="bold" size="x-large">Automation</span></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHSeparator" id="hseparator7"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="vbox11"> + <property name="visible">True</property> + <child> + <widget class="GtkLabel" id="label45"> + <property name="xalign">0</property> + <property name="xpad">12</property> + <property name="label" translatable="yes"><b>Program Automation</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox17"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkLabel" id="label34"/> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkTable" id="automation_table"> + <property name="visible">True</property> + <property name="n_rows">7</property> + <property name="n_columns">3</property> + <child> + <widget class="GtkCheckButton" id="auto_unmount_checkbutton"> + <property name="label" translatable="yes">Unmount ("eject") image device upon download completion</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_auto_unmount_checkbutton_toggled"/> + </widget> + <packing> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="auto_startup_checkbutton"> + <property name="label" translatable="yes">Start downloading at program startup</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_auto_startup_checkbutton_toggled"/> + </widget> + <packing> + <property name="right_attach">3</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="auto_insertion_checkbutton"> + <property name="label" translatable="yes">Start downloading upon image device insertion</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_auto_insertion_checkbutton_toggled"/> + </widget> + <packing> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="auto_exit_checkbutton"> + <property name="label" translatable="yes">Exit program after completion of successful download</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_auto_exit_checkbutton_toggled"/> + </widget> + <packing> + <property name="right_attach">3</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="padding">24</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label35"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">5</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="automation_label"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Automation</property> + </widget> + <packing> + <property name="position">5</property> + <property name="tab_fill">False</property> + <property name="type">tab</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="error_tab"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkVBox" id="vbox8"> + <property name="visible">True</property> + <child> + <widget class="GtkHBox" id="hbox13"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkImage" id="image9"> + <property name="visible">True</property> + <property name="stock">gtk-dialog-error</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label40"> + <property name="visible">True</property> + <property name="label" translatable="yes"><span weight="bold" size="x-large">Error Handling</span></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHSeparator" id="hseparator9"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox19"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkLabel" id="label36"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkTable" id="error_table"> + <property name="visible">True</property> + <property name="n_rows">14</property> + <property name="n_columns">2</property> + <child> + <widget class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes"> </property> + </widget> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label12"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><b>Image Name Conflicts</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="right_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkRadioButton" id="backup_ignore_radiobutton"> + <property name="label" translatable="yes">Ignore</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + <property name="group">backup_error_radiobutton</property> + <signal name="toggled" handler="on_backup_ignore_radiobutton_toggled"/> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">13</property> + <property name="bottom_attach">14</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkRadioButton" id="backup_warning_radiobutton"> + <property name="label" translatable="yes">Report a warning</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + <property name="group">backup_error_radiobutton</property> + <signal name="toggled" handler="on_backup_warning_radiobutton_toggled"/> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">12</property> + <property name="bottom_attach">13</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkRadioButton" id="backup_error_radiobutton"> + <property name="label" translatable="yes">Report an error</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_backup_error_radiobutton_toggled"/> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">11</property> + <property name="bottom_attach">12</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="missing_backup_label"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="xalign">0</property> + <property name="ypad">12</property> + <property name="label" translatable="yes"><b>Missing Backup Devices</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">9</property> + <property name="bottom_attach">10</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkRadioButton" id="add_identifier_radiobutton"> + <property name="label" translatable="yes">Add unique identifier</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + <property name="group">skip_download_radiobutton</property> + <signal name="toggled" handler="on_add_identifier_radiobutton_toggled"/> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkRadioButton" id="skip_download_radiobutton"> + <property name="label" translatable="yes">Skip download</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="ypad">12</property> + <property name="label" translatable="yes">Choose whether to skip downloading the image, or to add a unique indentifier.</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="indicate_download_error_checkbutton"> + <property name="label" translatable="yes">Report an error</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_indicate_download_error_checkbutton_toggled"/> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label19"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="ypad">12</property> + <property name="label" translatable="yes">Specify what to do when an image of the same name has already been downloaded or backed up.</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label3"> + <property name="xalign">0</property> + <property name="ypad">12</property> + <property name="label" translatable="yes">Specify what to do when there are no backup devices.</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">10</property> + <property name="bottom_attach">11</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="ypad">12</property> + <property name="label" translatable="yes">When backing up, choose whether to overwrite an image on the backup device that has the same name, or skip backing it up.</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">6</property> + <property name="bottom_attach">7</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <widget class="GtkRadioButton" id="backup_duplicate_overwrite_radiobutton"> + <property name="label" translatable="yes">Overwrite</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + <property name="group">backup_duplicate_skip_radiobutton</property> + <signal name="toggled" handler="on_backup_duplicate_overwrite_radiobutton_toggled"/> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">7</property> + <property name="bottom_attach">8</property> + </packing> + </child> + <child> + <widget class="GtkRadioButton" id="backup_duplicate_skip_radiobutton"> + <property name="label" translatable="yes">Skip</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_backup_duplicate_skip_radiobutton_toggled"/> + </widget> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">8</property> + <property name="bottom_attach">9</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label37"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">6</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="error_label"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Error Handling</property> + </widget> + <packing> + <property name="position">6</property> + <property name="tab_fill">False</property> + <property name="type">tab</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">6</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area2"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <widget class="GtkButton" id="help_button"> + <property name="label">gtk-help</property> + <property name="response_id">-11</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + <property name="secondary">True</property> + </packing> + </child> + <child> + <widget class="GtkButton" id="close_button"> + <property name="label">gtk-close</property> + <property name="response_id">-7</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_close_button_clicked"/> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </widget> + </child> + </widget> + <widget class="GtkAboutDialog" id="about"> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="border_width">5</property> + <property name="destroy_with_parent">True</property> + <property name="icon">rapid-photo-downloader-about.png</property> + <property name="type_hint">normal</property> + <property name="program_name">Rapid Photo Downloader</property> + <property name="copyright" translatable="yes">Copyright Damon Lynch 2007-09</property> + <property name="comments" translatable="yes">Import your images efficiently and reliably</property> + <property name="website">http://www.damonlynch.net/rapid</property> + <property name="license" translatable="yes">Rapid Photo Downloader is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + +Rapid Photo Downloader 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 Rapid Photo Downloader; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</property> + <property name="authors">Damon Lynch <damonlynch@gmail.com></property> + <property name="translator_credits" comments="TRANSLATORS: Replace this string with your names, one name per line.">Martin Egger <martin.egger@gmx.net> +Jose Luis Navarro <jlnavarro111@gmail.com> +Abel O'Rian <abel.orian@gmail.com> +Daniel Paessler <daniel@paessler.org> +Michal Predotka <mpredotka@googlemail.com> +Luca Reverberi <thereve@gmail.com> +Mikko Ruohola <polarfox@polarfox.net> +Sergei Sedov <sedov@webmail.perm.ru> +Marco Solari <marcosolari@gmail.com> +Julien Valroff <julien@kirya.net></property> + <property name="logo">rapid-photo-downloader-about.png</property> + <property name="wrap_license">True</property> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <placeholder/> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="layout_style">end</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </widget> + </child> + </widget> + <widget class="GtkWindow" id="rapidapp"> + <property name="width_request">600</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="title" translatable="yes">Rapid Photo Downloader</property> + <property name="icon">rapid-photo-downloader-about.png</property> + <signal name="destroy" handler="on_rapidapp_destroy"/> + <child> + <widget class="GtkVBox" id="vbox10"> + <property name="visible">True</property> + <child> + <widget class="GtkMenuBar" id="menubar3"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <widget class="GtkMenuItem" id="menuitem7"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Photos</property> + <property name="use_underline">True</property> + <child> + <widget class="GtkMenu" id="menuitem7_menu"> + <child> + <widget class="GtkImageMenuItem" id="menu_download_pause"> + <property name="label" translatable="yes">Download / Pause</property> + <property name="visible">True</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_download_button_clicked"/> + <accelerator key="Return" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <child internal-child="image"> + <widget class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="stock">gtk-convert</property> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkImageMenuItem" id="menu_resequence"> + <property name="label">Resequence</property> + <property name="use_stock">False</property> + <accelerator key="E" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <child internal-child="image"> + <widget class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="stock">gtk-sort-descending</property> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkSeparatorMenuItem" id="separator2"/> + </child> + <child> + <widget class="GtkImageMenuItem" id="menu_preferences"> + <property name="label">gtk-preferences</property> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_menu_preferences_activate"/> + </widget> + </child> + <child> + <widget class="GtkImageMenuItem" id="menu_quit"> + <property name="label">gtk-quit</property> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_rapidapp_destroy"/> + </widget> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkMenuItem" id="menuitem10"> + <property name="visible">True</property> + <property name="label" translatable="yes">_View</property> + <property name="use_underline">True</property> + <child> + <widget class="GtkMenu" id="menuitem10_menu"> + <child> + <widget class="GtkCheckMenuItem" id="menu_display_thumbnails"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Thumbnails</property> + <property name="use_underline">True</property> + <signal name="toggled" handler="on_menu_display_thumbnails_toggled"/> + </widget> + </child> + <child> + <widget class="GtkCheckMenuItem" id="menu_log_window"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Error Log</property> + <property name="use_underline">True</property> + <signal name="toggled" handler="on_menu_log_window_toggled"/> + </widget> + </child> + <child> + <widget class="GtkSeparatorMenuItem" id="separator5"> + <property name="visible">True</property> + </widget> + </child> + <child> + <widget class="GtkMenuItem" id="menu_clear"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Clear Completed Downloads</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_menu_clear_activate"/> + </widget> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkMenuItem" id="help_menuitem"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Help</property> + <property name="use_underline">True</property> + <child> + <widget class="GtkMenu" id="help_menu"> + <child> + <widget class="GtkMenuItem" id="menu_get_help_online"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Get Help Online...</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_menu_get_help_online_activate"/> + </widget> + </child> + <child> + <widget class="GtkMenuItem" id="menu_report_problem"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Report a Problem...</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_menu_report_problem_activate"/> + </widget> + </child> + <child> + <widget class="GtkMenuItem" id="menu_donate"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Make a Donation...</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_menu_donate_activate"/> + </widget> + </child> + <child> + <widget class="GtkMenuItem" id="menu_translate"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Translate this Application...</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_menu_translate_activate"/> + </widget> + </child> + <child> + <widget class="GtkSeparatorMenuItem" id="separator1"> + <property name="visible">True</property> + </widget> + </child> + <child> + <widget class="GtkImageMenuItem" id="menu_about"> + <property name="label">gtk-about</property> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_menu_about_activate"/> + </widget> + </child> + </widget> + </child> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="main_vbox"> + <property name="visible">True</property> + <property name="spacing">12</property> + <child> + <widget class="GtkHBox" id="hbox6"> + <property name="visible">True</property> + <child> + <widget class="GtkScrolledWindow" id="media_collection_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <widget class="GtkViewport" id="media_collection_viewport"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <widget class="GtkVBox" id="media_collection_vbox"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </widget> + </child> + </widget> + </child> + </widget> + <packing> + <property name="padding">12</property> + <property name="position">0</property> + </packing> + </child> + </widget> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="hbox8"> + <property name="visible">True</property> + <child> + <widget class="GtkScrolledWindow" id="image_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="vscrollbar_policy">never</property> + <child> + <widget class="GtkViewport" id="image_viewport"> + <property name="height_request">112</property> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <placeholder/> + </child> + </widget> + </child> + </widget> + <packing> + <property name="padding">12</property> + <property name="position">0</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="buttons_hbox"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="statusbar_hbox"> + <property name="visible">True</property> + <child> + <widget class="GtkHBox" id="download_progressbar_hbox"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkStatusbar" id="rapid_statusbar"> + <property name="visible">True</property> + <property name="has_resize_grip">False</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkVSeparator" id="vseparator1"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <widget class="GtkVBox" id="vbox15"> + <property name="visible">True</property> + <child> + <widget class="GtkHSeparator" id="hseparator3"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="speed_label"> + <property name="visible">True</property> + <property name="label" translatable="yes"> </property> + <property name="width_chars">9</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <widget class="GtkVSeparator" id="vseparator2"> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">4</property> + </packing> + </child> + <child> + <widget class="GtkEventBox" id="error_eventbox"> + <property name="visible">True</property> + <signal name="button_press_event" handler="on_error_eventbox_button_press_event"/> + <child> + <widget class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <child> + <widget class="GtkImage" id="warning_image"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="xpad">3</property> + <property name="stock">gtk-dialog-warning</property> + <property name="icon-size">1</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <widget class="GtkImage" id="error_image"> + <property name="visible">True</property> + <property name="xpad">3</property> + <property name="stock">gtk-dialog-error</property> + <property name="icon-size">1</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">5</property> + </packing> + </child> + <child> + <widget class="GtkStatusbar" id="statusbar1"> + <property name="width_request">15</property> + <property name="visible">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">6</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">2</property> + </packing> + </child> + </widget> + </child> + </widget> + <widget class="GtkDialog" id="logdialog"> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Error Log</property> + <property name="default_width">600</property> + <property name="default_height">400</property> + <property name="destroy_with_parent">True</property> + <property name="icon">rapid-photo-downloader-about.png</property> + <property name="type_hint">dialog</property> + <property name="has_separator">False</property> + <signal name="close" handler="on_logdialog_close"/> + <signal name="response" handler="on_logdialog_response"/> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox4"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkScrolledWindow" id="log_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <widget class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <widget class="GtkTextView" id="log_textview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="editable">False</property> + <property name="cursor_visible">False</property> + </widget> + </child> + </widget> + </child> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area4"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <placeholder/> + </child> + <child> + <widget class="GtkButton" id="button3"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </widget> + </child> + </widget> +</glade-interface> diff --git a/rapid/gnomeglade.py b/rapid/gnomeglade.py new file mode 100644 index 0000000..c0b0860 --- /dev/null +++ b/rapid/gnomeglade.py @@ -0,0 +1,166 @@ +### Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +"""Utility classes for working with glade files. + +""" +# modified by Damon Lynch May 2009 to update i18n + +import gtk +import gtk.glade +import gnome +import gnome.ui +#import gettext +import config +from common import Configi18n + +class Base(object): + """Base class for all glade objects. + + This class handles loading the xml glade file and connects + all methods name 'on_*' to the signals in the glade file. + + The handle to the xml file is stored in 'self.xml'. The + toplevel widget is stored in 'self.widget'. + + In addition it calls widget.set_data("pyobject", self) - this + allows us to get the python object given only the 'raw' gtk+ + object, which is sadly sometimes necessary. + """ + + def __init__(self, file, root, override={}): + """Load the widgets from the node 'root' in file 'file'. + + Automatically connects signal handlers named 'on_*'. + """ + global _ + _ = Configi18n._ + if Configi18n.locale_path: + gtk.glade.bindtextdomain(config.APP_NAME, Configi18n.locale_path) + gtk.glade.textdomain(config.APP_NAME) + self.xml = gtk.glade.XML(file, root, typedict=override ) + handlers = {} + for h in filter(lambda x:x.startswith("on_"), dir(self.__class__)): + handlers[h] = getattr(self, h) + self.xml.signal_autoconnect( handlers ) + self.widget = getattr(self, root) + self.widget.set_data("pyobject", self) + + def __getattr__(self, key): + """Allow glade widgets to be accessed as self.widgetname. + """ + widget = self.xml.get_widget(key) + if widget: # cache lookups + setattr(self, key, widget) + return widget + raise AttributeError(key) + + def flushevents(self): + """Handle all the events currently in the main queue and return. + """ + while gtk.events_pending(): + gtk.main_iteration(); + + def _map_widgets_into_lists(self, widgetnames): + """Put sequentially numbered widgets into lists. + + e.g. If an object had widgets self.button0, self.button1, ..., + then after a call to object._map_widgets_into_lists(["button"]) + object has an attribute self.button == [self.button0, self.button1, ...]." + """ + for item in widgetnames: + setattr(self,item, []) + lst = getattr(self,item) + i = 0 + while 1: + key = "%s%i"%(item,i) + try: + val = getattr(self, key) + except AttributeError: + break + lst.append(val) + i += 1 + + +class Component(Base): + """A convenience base class for widgets which use glade. + """ + + def __init__(self, file, root, override={}): + Base.__init__(self, file, root, override) + + +class GtkApp(Base): + """A convenience base class for gtk+ apps created in glade. + """ + + def __init__(self, file, root=None): + Base.__init__(self, file, root) + + def main(self): + """Enter the gtk main loop. + """ + gtk.main() + + def quit(self, *args): + """Signal the gtk main loop to quit. + """ + gtk.main_quit() + + +class GnomeApp(GtkApp): + """A convenience base class for apps created in glade. + """ + + def __init__(self, name, version, file, root): + """Initialise program 'name' and version from 'file' containing root node 'root'. + """ + self.program = gnome.program_init(name, version) + GtkApp.__init__(self,file,root) + if 0: + self.client = gnome.ui.Client() + self.client.disconnect() + def connected(*args): + print "CONNECTED", args + def cb(name): + def cb2(*args): + print name, args, "\n" + return cb2 + self.client.connect("connect", cb("CON")) + self.client.connect("die", cb("DIE")) + self.client.connect("disconnect", cb("DIS")) + self.client.connect("save-yourself", cb("SAVE")) + self.client.connect("shutdown-cancelled", cb("CAN")) + self.client.connect_to_session_manager() + + +def load_pixbuf(fname, size=0): + """Load an image from a file as a pixbuf, with optional resizing. + """ + image = gtk.Image() + image.set_from_file(fname) + image = image.get_pixbuf() + if size: + aspect = float(image.get_height()) / image.get_width() + image = image.scale_simple(size, int(aspect*size), 2) + return image + +def url_show(url): + return gnome.url_show(url) + +def FileEntry(*args): + return gnome.ui.FileEntry(*args) + diff --git a/rapid/higdefaults.py b/rapid/higdefaults.py new file mode 100644 index 0000000..9dfd7e5 --- /dev/null +++ b/rapid/higdefaults.py @@ -0,0 +1,8 @@ +# -*- coding: latin1 -*- +VERTICAL_CONTROL_LABEL_SPACE = CONTROL_LABEL_SPACE = 12 +CONTROL_IN_TABLE_SPACE = 6 +WINDOW_BORDER_SPACE = 12 +NESTED_CONTROLS_SPACE = 24 +VERTICAL_CONTROL_SPACE = 18 +VERTICAL_GROUP_SPACE = 24 +HORIZONTAL_GROUP_SPACE = 24 diff --git a/rapid/idletube.py b/rapid/idletube.py new file mode 100644 index 0000000..86ff1a4 --- /dev/null +++ b/rapid/idletube.py @@ -0,0 +1,205 @@ + +# Copyright (c) 2005 Antoon Pardon +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from threading import Lock +from thread import get_ident + +from types import BooleanType as UnConnected + +UnRegistered, Registered = False, True + +class EOInformation(Exception): + pass + +class TubeAccess(Exception): + pass + +class Fifo: + + def __init__(self): + self.fifo = [] + + def put(self, item): + self.fifo.append(item) + + def get(self): + return self.fifo.pop(0) + + def size(self): + return len(self.fifo) + +class Tube: + + def __init__(self, maxsize, lck = Lock, container = None): + if container is None: + container = Fifo() + self.readers = set() + self.writers = set() + self.container = container + self.maxsize = maxsize + self.cb_arglst = [] + self.cb_src = UnRegistered + self.in_use = Lock() + self.nowriter = lck() + self.full = lck() + self.empty = lck() + self.empty.acquire() + self.nowriter.acquire() + + def open(self, access = 'r', *to): + thrd = get_ident() + access = access.lower() + self.in_use.acquire() + if 'w' in access: + if len(self.writers) == 0: + for _ in self.readers: + self.nowriter.release() + self.writers.add(thrd) + if 'r' in access: + self.readers.add(thrd) + if len(self.writers) == 0: + self.in_use.release() + self.nowriter.acquire(*to) + else: + self.in_use.release() + else: + self.in_use.release() + + def close(self, access = 'rw'): + thrd = get_ident() + access = access.lower() + self.in_use.acquire() + if 'r' in access: + self.readers.discard(thrd) + if 'w' in access: + self.writers.discard(thrd) +## print "have", self.writers, "writers" + if len(self.writers) == 0: + if self.container.size() == 0: +## print "emptying container, as size is", self.container.size() + self.empty.release() + if self.cb_src is Registered and len(self.readers) > 0: +## print "adding callback" + self.cb_src = gob.idle_add(self._idle_callback) +## else: +## print "container size not empty, is", self.container.size() + for _ in self.readers: +## print "putting EOInformation" + self.container.put(EOInformation) + self.in_use.release() + + def size(self): + self.in_use.acquire() + size = self.container.size() + self.in_use.release() + return size + + def get(self, *to): + thrd = get_ident() + if thrd not in self.readers: + raise TubeAccess, "Thread has no read access for tube" + self.empty.acquire(*to) + self.in_use.acquire() + size = self.container.size() + if size == self.maxsize: + self.full.release() + item = self.container.get() + if size != 1: + self.empty.release() + elif type(self.cb_src) is not UnConnected: + gob.source_remove(self.cb_src) + self.cb_src = Registered + self.in_use.release() + if item is EOInformation: + raise EOInformation + else: + return item + + def put(self, item, *to): + thrd = get_ident() + if thrd not in self.writers: + raise TubeAccess, "Thread has no write access for tube" + if thrd in self.readers: + self._put_rw(item) + else: + self._put_wo(item, *to) + + def _put_wo(self, item, *to): + self.full.acquire(*to) + self.in_use.acquire() + size = self.container.size() + if size == 0: + self.empty.release() + if self.cb_src is Registered: + #gdk.threads_enter() + self.cb_src = gob.idle_add(self._idle_callback) + #gdk.threads_leave() + self.container.put(item) + if size + 1 < self.maxsize: + self.full.release() + self.in_use.release() + + def _put_rw(self, item): + self.in_use.acquire() + size = self.container.size() + if size == 0: + self.empty.release() + if self.cb_src is Registered: + self.cb_src = gob.idle_add(self._idle_callback) + self.container.put(item) + self.in_use.release() + + def _idle_callback(self): + self.in_use.acquire() + lst = self.cb_arglst.pop(0) + self.in_use.release() + func = lst[0] + lst[0] = self + ret_val = func(*lst) + self.in_use.acquire() + if ret_val: + lst[0] = func + self.cb_arglst.append(lst) + elif self.cb_arglst == []: + self.cb_src = UnRegistered + self.in_use.release() + return self.cb_src is not UnRegistered + + +def tube_add_watch(tube, callback, *args): + + global gob #, gdk + import gobject as gob + #import gtk.gdk as gdk + + tube.in_use.acquire() + tube.cb_arglst.append([callback] + list(args)) + if tube.cb_src is UnRegistered: + if tube.container.size() == 0: + tube.cb_src = Registered + else: + tube.cb_src = gob.idle_add(tube._idle_callback) + tube.in_use.release() + +def tube_remove_watch(tube): +## tube.in_use.acquire() +## gob.source_remove(tube.cb_src) +## tube._idle_callback.handler_block(tube.cb_src) + pass diff --git a/rapid/media.py b/rapid/media.py new file mode 100755 index 0000000..f4342c6 --- /dev/null +++ b/rapid/media.py @@ -0,0 +1,180 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007 Damon Lynch <damonlynch@gmail.com> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os + +import config +import common + +import operator + +def getDefaultPhotoLocation(): + for default in config.DEFAULT_PHOTO_LOCATIONS: + path = common.getFullPath(default) + if os.path.isdir(path): + return path + return common.getFullPath('') + +def isImageMedia(path): + """ Returns true if directory specifies some media with photos on it """ + + if os.path.isdir(os.path.join(path, "DCIM")): + # is very likely a memory card, or something like that! + return True + else: + return False + + +def isBackupMedia(path, identifier, writeable=True): + """ Test to see if path is used as a backup medium for storing images + + If writeable is True, the directory must be writeable by the user """ + suitable = False + if os.path.isdir(os.path.join(path, identifier)): + if writeable: + suitable = os.access(os.path.join(path, identifier), os.W_OK) + else: + suitable = True + return suitable + +def isImage(fileName): + ext = os.path.splitext(fileName)[1].lower()[1:] + return (ext in config.RAW_FILE_EXTENSIONS) or (ext in config.NON_RAW_IMAGE_FILE_EXTENSIONS) + +class Media: + """ Generic class for media holding images """ + def __init__(self, path, volume = None): + """ + volume is a gnomevfs volume + """ + + self.path = path + self.volume = volume + + + def prettyName(self, limit=config.MAX_LENGTH_DEVICE_NAME): + """ + Returns a name for the media, useful for display. + + If the media is from a gnomevfs volume, returns the gnome name. + + Else. returns the last part of the mount point after stripping out + underscores. + """ + + if self.volume: + return self.volume.get_name(limit) + else: + name = os.path.split(self.path)[1] + name = name.replace('_', ' ') + v = name + if limit: + if len(v) > limit: + v = v[:limit] + '...' + return v + + def getPath(self): + return self.path + + +class CardMedia(Media): + """Compact Flash cards, etc.""" + def __init__(self, path, volume = None, doNotScan=True): + """ + volume is a gnomevfs volume + """ + Media.__init__(self, path, volume) + if not doNotScan: + self.scanMedia() + + def scanMedia(self): + """ creates a list of images on a path, recursively scanning + + images are sorted by modification time""" + + self.images = [] + self.imageSizeSum = 0 + for root, dirs, files in os.walk(self.path): + for name in files: + if isImage(name): + image = os.path.join(root, name) + size = os.path.getsize(image) + modificationTime = os.path.getmtime(image) + self.images.append((name, root, size, modificationTime),) + self.imageSizeSum += size + self.images.sort(key=operator.itemgetter(3)) + self.noImages = len(self.images) + + def setMedia(self, images, imageSizeSum, noImages): + self.images = images + self.imageSizeSum = imageSizeSum + self.noImages = noImages + + def numberOfImages(self): + return self.noImages + + def sizeOfImages(self, humanReadable = True): + if humanReadable: + return common.formatSizeForUser(self.imageSizeSum) + else: + return self.imageSizeSum + + def firstImage(self): + if self.images: + return self.images[0] + else: + return None + + +def scanForImageMedia(path): + """ returns a list of paths that contain images on media produced by a digital camera """ + + media = [] + for i in os.listdir(path): + p = os.path.join(path, i) + if os.path.isdir(p): + if isImageMedia(p): + media.append(p) + return media + +def scanForBackupMedia(path, identifier): + """ returns a list of paths that contains backed up images """ + + media = [] + for i in os.listdir(path): + p = os.path.join(path, i) + if os.path.isdir(p): + if isBackupMedia(p, identifier): + media.append(os.path.join(p, identifier)) + return media + + +if __name__ == '__main__': + print "Card media:" + for m in scanForImageMedia('/media'): + media = CardMedia(m) + print media.prettyName() + print media.numberOfImages() + print media.sizeOfImages() + + print "\nBackup media:" + for m in scanForBackupMedia('/media', 'photos'): + print m + + print "\nDefault download folder: ", getDefaultPhotoLocation() diff --git a/rapid/metadata.py b/rapid/metadata.py new file mode 100755 index 0000000..8fc6820 --- /dev/null +++ b/rapid/metadata.py @@ -0,0 +1,387 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007 Damon Lynch <damonlynch@gmail.com> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import re +import datetime +import sys +import config + +try: + import pyexiv2 +except ImportError: + sys.stderr.write("You need to install pyexiv2, the python binding for exiv2, to run this program.\n" ) + sys.exit(1) + +#only pyexiv2 0.1.2 and 0.1.3 use the "Rational" class +#is there a superior way to find which version of pyexiv2 is being used? +if 'Rational' in dir(pyexiv2): + usesRational = True +else: + usesRational = False + + +class MetaData(pyexiv2.Image): + """ + Class providing human readable access to image metadata + """ + + def aperture(self, missing=''): + """ + Returns in string format the floating point value of the image's aperture. + + Returns missing if the metadata value is not present. + """ + + try: + if usesRational: + a = self["Exif.Photo.FNumber"] + a0, a1 = str(a).split('/') + else: + a0, a1 = self["Exif.Photo.FNumber"] + a = float(a0) / float(a1) + return "%.1f" % a + except: + return missing + + def iso(self, missing=''): + """ + Returns in string format the integer value of the image's ISO. + + Returns missing if the metadata value is not present. + """ + try: + return "%s" % (self["Exif.Photo.ISOSpeedRatings"]) + except: + return missing + + def exposureTime(self, alternativeFormat=False, missing=''): + """ + Returns in string format the exposure time of the image. + + Returns missing if the metadata value is not present. + + alternativeFormat is useful if the value is going to be used in a + purpose where / is an invalid character, e.g. file system names. + + alternativeFormat is False: + For exposures less than one second, the result is formatted as a + fraction e.g. 1/125 + For exposures greater than or equal to one second, the value is + formatted as an integer e.g. 30 + + alternativeFormat is True: + For exposures less than one second, the result is formatted as an + integer e.g. 125 + For exposures less than one second but more than or equal to + one tenth of a second, the result is formatted as an integer + e.g. 3 representing 3/10 of a second + For exposures greater than or equal to one second, the value is + formatted as an integer with a trailing s e.g. 30s + """ + + try: + if usesRational: + e = str(self["Exif.Photo.ExposureTime"]) + e0, e1 = e.split('/') + e0 = int(e0) + e1 = int(e1) + else: + e0, e1 = self["Exif.Photo.ExposureTime"] + + if e1 > e0: + if alternativeFormat: + if e0 == 1: + return str(e1) + else: + return str(e0) + else: + return e + elif e0 > e1: + e = float(e0) / e1 + if alternativeFormat: + return "%.0fs" % e + else: + return "%.0f" % e + else: + return "1s" + except: + return missing + + def focalLength(self, missing=''): + """ + Returns in string format the focal length of the lens used to record the image. + + Returns missing if the metadata value is not present. + """ + try: + if usesRational: + f = str(self["Exif.Photo.FocalLength"]) + f0, f1 = f.split('/') + else: + f0, f1 = self["Exif.Photo.FocalLength"] + + f0 = float(f0) + if not f1: + f1 = 1.0 + else: + f1 = float(f1) + + return "%.0f" % (f0 / f1) + except: + return missing + + + def cameraMake(self, missing=''): + """ + Returns in string format the camera make (manufacturer) used to record the image. + + Returns missing if the metadata value is not present. + """ + try: + return self["Exif.Image.Make"].strip() + except: + return missing + + def cameraModel(self, missing=''): + """ + Returns in string format the camera model used to record the image. + + Returns missing if the metadata value is not present. + """ + try: + return self["Exif.Image.Model"].strip() + except: + return missing + + def cameraSerial(self, missing=''): + try: + keys = self.exifKeys() + if 'Exif.Canon.SerialNumber' in keys: + v = self['Exif.Canon.SerialNumber'] + elif 'Exif.Nikon3.SerialNumber' in keys: + v = self['Exif.Nikon3.SerialNumber'] + elif 'Exif.OlympusEq.SerialNumber' in keys: + v = self['Exif.OlympusEq.SerialNumber'] + elif 'Exif.Olympus.SerialNumber' in keys: + v = self['Exif.Olympus.SerialNumber'] + elif 'Exif.Olympus.SerialNumber2' in keys: + v = self['Exif.Olympus.SerialNumber2'] + elif 'Exif.Panasonic.SerialNumber' in keys: + v = self['Exif.Panasonic.SerialNumber'] + elif 'Exif.Fujifilm.SerialNumber' in keys: + v = self['Exif.Fujifilm.SerialNumber'] + elif 'Exif.Image.CameraSerialNumber' in keys: + v = self['Exif.Image.CameraSerialNumber'] + else: + return missing + return str(v) + except: + return missing + + def shutterCount(self, missing=''): + try: + keys = self.exifKeys() + if 'Exif.Nikon3.ShutterCount' in keys: + v = self['Exif.Nikon3.ShutterCount'] + elif 'Exif.Canon.ImageNumber' in keys: + v = self['Exif.Canon.ImageNumber'] + else: + return missing + return str(v) + except: + return missing + + def ownerName(self, missing=''): + """ returns camera name recorded by select Canon cameras""" + try: + return self['Exif.Canon.OwnerName'].strip() + except: + return missing + + def shortCameraModel(self, includeCharacters = '', missing=''): + """ + Returns in shorterned string format the camera model used to record the image. + + Returns missing if the metadata value is not present. + + The short format is determined by the first occurence of a digit in the + camera model, including all alphaNumeric characters before and after + that digit up till a non-alphanumeric character. + + Canon "Mark" designations are shortened prior to conversion. + + Examples: + Canon EOS 300D DIGITAL -> 300D + Canon EOS 5D -> 5D + Canon EOS 5D Mark II -> 5DMkII + NIKON D2X -> D2X + NIKON D70 -> D70 + X100,D540Z,C310Z -> X100 + + The optional includeCharacters allows additional characters to appear + before and after the digits. + Note: special includeCharacters MUST be escaped as per syntax of a + regular expressions (see documentation for module re) + + Examples: + + includeCharacters = '': + DSC-P92 -> P92 + includeCharacters = '\-': + DSC-P92 -> DSC-P92 + + If a digit is not found in the camera model, the full length camera + model is returned. + + Note: assume exif values are in ENGLISH, regardless of current platform + """ + m = self.cameraModel() + m = m.replace(' Mark ', 'Mk') + if m: + s = r"(?:[^a-zA-Z0-9%s]?)(?P<model>[a-zA-Z0-9%s]*\d+[a-zA-Z0-9%s]*)"\ + % (includeCharacters, includeCharacters, includeCharacters) + r = re.search(s, m) + if r: + return r.group("model") + else: + return m.strip() + else: + return missing + + def dateTime(self, missing=''): + """ + Returns in python datetime format the date and time the image was + recorded. + + Trys to get value from exif key "Exif.Photo.DateTimeOriginal". + If that does not exist, trys key "Exif.Image.DateTime" + + Returns missing either metadata value is not present. + """ + keys = self.exifKeys() + try: + if "Exif.Photo.DateTimeOriginal" in keys: + return self["Exif.Photo.DateTimeOriginal"] + else: + return self["Exif.Image.DateTime"] + except: + return missing + + def subSeconds(self, missing='00'): + """ returns the subsecond the image was taken, as recorded by the camera""" + try: + return str(self["Exif.Photo.SubSecTimeOriginal"]) + except: + return missing + + def orientation(self, missing=''): + """ + Returns the orientation of the image, as recorded by the camera + """ + try: + return self['Exif.Image.Orientation'] + except: + return missing + +class DummyMetaData(MetaData): + """ + Class which gives metadata values for an imaginary image. + + Useful for displaying in preference examples etc. when no image is ready to + be downloaded. + + See MetaData class for documentation of class methods. + """ + + def __init__(self): + pass + + def readMetadata(self): + pass + + def aperture(self, missing=''): + return "2.0" + + def iso(self, missing=''): + return "100" + + def exposureTime(self, alternativeFormat=False, missing=''): + if alternativeFormat: + return "4000" + else: + return "1/4000" + + def focalLength(self, missing=''): + return "135" + + def cameraMake(self, missing=''): + return "Canon" + + def cameraModel(self, missing=''): + return "Canon EOS 5D" + + def shortCameraModel(self, includeCharacters = '', missing=''): + return "5D" + + def cameraSerial(self, missing=''): + return '730402168' + + def shutterCount(self, missing=''): + return '387' + + def ownerName(self, missing=''): + return 'Photographer Name' + + def dateTime(self, missing=''): + return datetime.datetime.now() + + def subSeconds(self, missing='00'): + return '57' + + def orientation(self, missing=''): + return 1 + +if __name__ == '__main__': + import sys + + if (len(sys.argv) != 2): + print 'Usage: ' + sys.argv[0] + ' path/to/photo/containing/metadata' + m = DummyMetaData() + + else: + m = MetaData(sys.argv[1]) + m.readMetadata() + +# for i in m.exifKeys(): +# print i + print "f"+ m.aperture('missing ') + print "ISO " + m.iso('missing ') + print m.exposureTime(missing='missing ') + " sec" + print m.exposureTime(alternativeFormat=True, missing='missing ') + print m.focalLength('missing ') + "mm" + print m.cameraMake() + print m.cameraModel() + print m.shortCameraModel() + print m.shortCameraModel(includeCharacters = "\-") + print m.dateTime() + print m.orientation() + print 'Serial number:', m.cameraSerial() + print 'Shutter count:', m.shutterCount() + print 'Subseconds:', m.subSeconds() + diff --git a/rapid/misc.py b/rapid/misc.py new file mode 100644 index 0000000..1e023ad --- /dev/null +++ b/rapid/misc.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# modified by Damon Lynch 2009 to remove default bold formatting and alignment +"""Module of commonly used helper classes and functions + +""" + +import gtk + +def run_dialog( text, secondarytext=None, parent=None, messagetype=gtk.MESSAGE_WARNING, buttonstype=gtk.BUTTONS_OK, extrabuttons=[]): + """Run a dialog with text 'text'. + Extra buttons are passed as tuples of (button label, response id). + """ + d = gtk.MessageDialog(None, + gtk.DIALOG_DESTROY_WITH_PARENT, + messagetype, + buttonstype, + text + ) + if parent: + d.set_transient_for(parent.widget.get_toplevel()) + for b,rid in extrabuttons: + d.add_button(b,rid) + d.vbox.set_spacing(12) + d.format_secondary_text(secondarytext) + ret = d.run() + d.destroy() + return ret diff --git a/rapid/paths.py b/rapid/paths.py new file mode 100644 index 0000000..0653324 --- /dev/null +++ b/rapid/paths.py @@ -0,0 +1,40 @@ +### Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os + +_locale_dir = ( #LOCALEDIR# +) +_help_dir = ( #HELPDIR# +) +_share_dir = ( #SHAREDIR# +) + +appdir = os.path.dirname(__file__) + +if not _locale_dir: _locale_dir = os.path.join(appdir,"po") +if not _help_dir: _help_dir = os.path.join(appdir,"help") +if not _share_dir: _share_dir = appdir + +def locale_dir(*args): # i18n files + return os.path.join(_locale_dir, *args) + +def help_dir(*args): # help + return os.path.join(_help_dir, *args) + +def share_dir(*args): # glade + pixmaps + return os.path.join(_share_dir, *args) + diff --git a/rapid/prefs.py b/rapid/prefs.py new file mode 100644 index 0000000..6912138 --- /dev/null +++ b/rapid/prefs.py @@ -0,0 +1,182 @@ +### Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +### Modified August 2007 by Damon Lynch to allow use of list value preferences + +"""Module to help implement 'instant-apply' preferences. + +Usage: + +import prefs +defaults = { + "colour" : prefs.Value(prefs.STRING, "red") + "size" : prefs.Value(prefs.INT, 10) +} + +p = prefs.Preferences("/apps/myapp", defaults) +# use variables as if they were normal attributes. +draw(p.colour, p.size) +# settings are persistent. (saved in gconf) +p.color = "blue" + +""" + +class Value(object): + """Represents a settable preference. + """ + + __slots__ = ["type", "default", "current"] + + def __init__(self, t, d): + """Create a value. + + t : a string : one of ("bool", "int", "string", "list") + d : the default value, also the initial value + """ + self.type = t + self.default = d + self.current = d + + def setfunc(self, gconf, rootkey, attr): + setfunc = getattr(gconf, "set_%s" % self.type) + setfunc("%s/%s" % (rootkey, attr), self.current) + + def getfunc(self, gconf, rootkey, attr): + getfunc = getattr(gconf, "get_%s" % self.type) + return getfunc("%s/%s" % (rootkey, attr)) + + +class ListValue(Value): + """ + Represents a list type settable preference. + """ + + __slots__ = Value.__slots__ + ["list_type"] + def __init__(self, list_type, d): + """ + Create a list value. + + d : the default value, also the initial value + list_type: the type of elements the list contains + """ + Value.__init__(self, LIST, d) + self.list_type = list_type + + def setfunc(self, gconf, rootkey, attr): + setfunc = getattr(gconf, "set_list") + setfunc("%s/%s" % (rootkey, attr), self.list_type, self.current) + + def getfunc(self, gconf, rootkey, attr): + getfunc = getattr(gconf, "get_list") + return getfunc("%s/%s" % (rootkey, attr), self.list_type) + + +# maybe fall back to ConfigParser if gconf is unavailable. +import gconf + +# types of values allowed +BOOL = "bool" +INT = "int" +STRING = "string" +FLOAT = "float" +LIST = "list" +# PAIR = "pair" +STRING_LIST = gconf.VALUE_STRING +INT_LIST = gconf.VALUE_INT +BOOL_LIST = gconf.VALUE_BOOL +FLOAT_LIST = gconf.VALUE_FLOAT +## + +class Preferences(object): + """Persistent preferences object. + + Example: + import prefs + defaults = {"spacing": prefs.Value(prefs.INT, 4), + "font": prefs.Value(prefs.STRING, "monospace") } + p = prefs.Prefs("myapp", defaults) + print p.font + p.font = "sans" # written to gconf too + p2 = prefs.Prefs("myapp", defaults) + print p.font # prints "sans" + """ + + def __init__(self, rootkey, initial): + """Create a preferences object. + + Settings are initialised with 'initial' and then overriden + from values in the gconf database if available. + + rootkey : the root gconf key where the values will be stored + initial : a dictionary of string to Value objects. + """ + self.__dict__["_gconf"] = gconf.client_get_default() + self.__dict__["_listeners"] = [] + self.__dict__["_rootkey"] = rootkey + self.__dict__["_prefs"] = initial + self._gconf.add_dir(rootkey, gconf.CLIENT_PRELOAD_NONE) + self._gconf.notify_add(rootkey, self._on_preference_changed) + for key, value in self._prefs.items(): + gval = self._gconf.get_without_default("%s/%s" % (rootkey, key) ) + if gval != None: + value.current = value.getfunc(self._gconf, rootkey, key) + + def __getattr__(self, attr): + return self._prefs[attr].current + + def get_default(self, attr): + return self._prefs[attr].default + + def __setattr__(self, attr, val): + value = self._prefs[attr] + + if value.current != val: + value.current = val + value.setfunc(self._gconf, self._rootkey, attr) + + try: + for l in self._listeners: + l(attr,val) + except StopIteration: + pass + + def _on_preference_changed(self, client, timestamp, entry, extra): + attr = entry.key[ entry.key.rindex("/")+1 : ] + try: + valuestruct = self._prefs[attr] + except KeyError: # unknown key, we don't care about it + pass + else: + if entry.value != None: # value has changed + newval = valuestruct.getfunc(self._gconf, self._rootkey, attr) + setattr( self, attr, newval) + else: # value has been deleted + setattr( self, attr, valuestruct.default ) + + def notify_add(self, callback): + """Register a callback to be called when a preference changes. + + callback : a callable object which take two parameters, 'attr' the + name of the attribute changed and 'val' the new value. + """ + self._listeners.append(callback) + + def dump(self): + """Print all preferences. + """ + for k,v in self._prefs.items(): + print k, v.type, v.current + diff --git a/rapid/rapid.py b/rapid/rapid.py new file mode 100755 index 0000000..0dcbb21 --- /dev/null +++ b/rapid/rapid.py @@ -0,0 +1,3050 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007, 2008, 2009 Damon Lynch <damonlynch@gmail.com> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +#needed for python 2.5, unneeded for python 2.6 +from __future__ import with_statement + +import sys +import os +import shutil +import time +import datetime +import atexit +import tempfile +import webbrowser +import operator + +import dbus +import dbus.bus +import dbus.service +from dbus.mainloop.glib import DBusGMainLoop +DBusGMainLoop(set_as_default=True) + +from threading import Thread, Lock +from thread import error as thread_error +from thread import get_ident + +import gtk.gdk as gdk +import pango + +try: + import gio + using_gio = True +except ImportError: + import gnomevfs + using_gio = False + + +import prefs +import paths +import gnomeglade + +from optparse import OptionParser + +import pynotify + +import ValidatedEntry + +import idletube as tube + +import config +import common +import misc +import higdefaults as hd + +from media import getDefaultPhotoLocation +from media import CardMedia + +import media + +import metadata + +import renamesubfolderprefs as rn + +import tableplusminus as tpm + +__version__ = config.version + +try: + import pygtk + pygtk.require("2.0") +except: + pass +try: + import gtk + import gtk.glade +except: + sys.exit(1) + +from common import Configi18n +global _ +_ = Configi18n._ + +#Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html +PROGRAM_NAME = _('Rapid Photo Downloader') + +def today(): + return datetime.date.today().strftime('%Y-%m-%d') + + + +def cmd_line(msg): + if verbose: + print msg + +exiting = False + +def updateDisplay(display_queue): + + try: + if display_queue.size() != 0: + call, args = display_queue.get() + if not exiting: + call(*args) +# else do not update display + else: + sys.stderr.write("Empty display queue!\n") + return True + + except tube.EOInformation: + for w in workers.getStartedWorkers(): + w.join() + gtk.main_quit() + + return False + + +class Queue(tube.Tube): + def __init__(self, maxSize = config.MAX_NO_READERS): + tube.Tube.__init__(self, maxSize) + + def setMaxSize(self, maxSize): + self.maxsize = maxSize + + +# Module wide values - +# set up thesse variable in global name space, and initialize with proper +# values later +# this is ugly but I don't know a better way :( + +display_queue = Queue() +media_collection_treeview = image_hbox = log_dialog = None + + +class ThreadManager: + _workers = [] + + + def append(self, w): + self._workers.append(w) + + def __getitem__(self, i): + return self._workers[i] + + def __len__(self): + return len(self._workers) + + def disableWorker(self, thread_id): + """ + set so a worker will not run, or if it is running, make it quit and therefore complete + """ + + self._workers[thread_id].manuallyDisabled = True + if self._workers[thread_id].hasStarted: + self._workers[thread_id].quit() + + else: + self._workers[thread_id].doNotStart = True + + def _isReadyToStart(self, w): + """ + Returns True if the worker is ready to start + and has not been disabled + """ + return not w.hasStarted and not w.doNotStart and not w.manuallyDisabled + + def _isReadyToDownload(self, w): + return w.scanComplete and not w.downloadStarted and not w.doNotStart and w.isAlive() and not w.manuallyDisabled + + def _isDownloading(self, w): + return w.downloadStarted and w.isAlive() and not w.downloadComplete + + def _isFinished(self, w): + """ + Returns True if the worker has finished running + + It does not signify it finished a download + """ + + return (w.hasStarted and not w.isAlive()) or w.manuallyDisabled + + def completedDownload(self, w): + return w.completedDownload + + def firstWorkerReadyToStart(self): + for w in self._workers: + if self._isReadyToStart(w): + return w + return None + + def firstWorkerReadyToDownload(self): + for w in self._workers: + if self._isReadyToDownload(w): + return w + return None + + def startWorkers(self): + for w in self.getReadyToStartWorkers(): + w.start() + + def startDownloadingWorkers(self): + for w in self.getReadyToDownloadWorkers(): + w.startStop() + + def quitAllWorkers(self): + global exiting + exiting = True + for w in self._workers: + w.quit() + + def getWorkers(self): + for w in self._workers: + yield w + + def getNonFinishedWorkers(self): + for w in self._workers: + if not self._isFinished(w): + yield w + + def getStartedWorkers(self): + for w in self._workers: + if w.hasStarted: + yield w + + def getReadyToStartWorkers(self): + for w in self._workers: + if self._isReadyToStart(w): + yield w + + def getReadyToDownloadWorkers(self): + for w in self._workers: + if self._isReadyToDownload(w): + yield w + + def getNotDownloadingWorkers(self): + for w in self._workers: + if w.hasStarted and not w.downloadStarted: + yield w + + def noReadyToStartWorkers(self): + n = 0 + for w in self._workers: + if self._isReadyToStart(w): + n += 1 + return n + + def noReadyToDownloadWorkers(self): + n = 0 + for w in self._workers: + if self._isReadyToDownload(w): + n += 1 + return n + + def getRunningWorkers(self): + for w in self._workers: + if w.hasStarted and w.isAlive(): + yield w + + def getDownloadingWorkers(self): + for w in self._workers: + if self._isDownloading(w): + yield w + + + def getPausedWorkers(self): + for w in self._workers: + if w.hasStarted and not w.running: + yield w + + def getPausedDownloadingWorkers(self): + for w in self._workers: + if w.downloadStarted and not w.running: + yield w + + def getFinishedWorkers(self): + for w in self._workers: + if self._isFinished(w): + yield w + + def noDownloadingWorkers(self): + i = 0 + for w in self._workers: + if self._isDownloading(w): + i += 1 + return i + + def noRunningWorkers(self): + i = 0 + for w in self._workers: + if w.hasStarted and w.isAlive(): + i += 1 + return i + + def getNextThread_id(self): + return len(self._workers) + + def printWorkerStatus(self, worker=None): + if worker: + l = [worker] + else: + l = range(len(self._workers)) + for i in l: + print "\nThread %i\n=======\n" % i + w = self._workers[i] + print "Volume / source:", w.cardMedia.prettyName(limit=0) + print "Do not start:", w.doNotStart + print "Started:", w.hasStarted + print "Running:", w.running + print "Scan completed:", w.scanComplete + print "Download started:", w.downloadStarted + print "Download completed:", w.downloadComplete + print "Finished:", self._isFinished(w) + print "Alive:", w.isAlive() + print "Manually disabled:", w.manuallyDisabled, "\n" + + + +workers = ThreadManager() + +class RapidPreferences(prefs.Preferences): + defaults = { + "program_version": prefs.Value(prefs.STRING, ""), + "download_folder": prefs.Value(prefs.STRING, + getDefaultPhotoLocation()), + "subfolder": prefs.ListValue(prefs.STRING_LIST, rn.DEFAULT_SUBFOLDER_PREFS), + "image_rename": prefs.ListValue(prefs.STRING_LIST, [rn.FILENAME, + rn.NAME_EXTENSION, + rn.ORIGINAL_CASE]), + "device_autodetection": prefs.Value(prefs.BOOL, True), + "device_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), + "device_autodetection_psd": prefs.Value(prefs.BOOL, False), + "backup_images": prefs.Value(prefs.BOOL, False), + "backup_device_autodetection": prefs.Value(prefs.BOOL, True), + "backup_identifier": prefs.Value(prefs.STRING, + config.DEFAULT_BACKUP_LOCATION), + "backup_location": prefs.Value(prefs.STRING, os.path.expanduser('~')), + "strip_characters": prefs.Value(prefs.BOOL, True), + "auto_download_at_startup": prefs.Value(prefs.BOOL, False), + "auto_download_upon_device_insertion": prefs.Value(prefs.BOOL, False), + "auto_unmount": prefs.Value(prefs.BOOL, False), + "auto_exit": prefs.Value(prefs.BOOL, False), + "indicate_download_error": prefs.Value(prefs.BOOL, True), + "download_conflict_resolution": prefs.Value(prefs.STRING, + config.SKIP_DOWNLOAD), + "backup_duplicate_overwrite": prefs.Value(prefs.BOOL, False), + "backup_missing": prefs.Value(prefs.STRING, config.IGNORE), + "display_thumbnails": prefs.Value(prefs.BOOL, True), + "show_log_dialog": prefs.Value(prefs.BOOL, False), + "day_start": prefs.Value(prefs.STRING, "03:00"), + "downloads_today": prefs.ListValue(prefs.STRING_LIST, [today(), '0']), + "stored_sequence_no": prefs.Value(prefs.INT, 0), + } + + def __init__(self): + prefs.Preferences.__init__(self, config.GCONF_KEY, self.defaults) + + def getAndMaybeResetDownloadsToday(self): + v = self.getDownloadsToday() + if v <= 0: + self.resetDownloadsToday() + return v + + def getDownloadsToday(self): + """Returns the preference value for the number of downloads performed today + + If value is less than zero, that means the date has changed""" + + hour, minute = self.getDayStart() + adjustedToday = datetime.datetime.strptime("%s %s:%s" % (self.downloads_today[0], hour, minute), "%Y-%m-%d %H:%M") + + now = datetime.datetime.today() + + if now < adjustedToday : + try: + return int(self.downloads_today[1]) + except ValueError: + sys.stderr.write(_("Invalid Downloads Today value.\n")) + sys.stderr.write(_("Resetting value to zero.\n")) + self.setDownloadsToday(self.downloads_today[0] , 0) + return 0 + else: + return -1 + + def setDownloadsToday(self, date, value=0): + self.downloads_today = [date, str(value)] + + def incrementDownloadsToday(self): + """ returns true if day changed """ + v = self.getDownloadsToday() + if v >= 0: + self.setDownloadsToday(self.downloads_today[0] , v + 1) + return False + else: + self.resetDownloadsToday(1) + return True + + def resetDownloadsToday(self, value=0): + now = datetime.datetime.today() + hour, minute = self.getDayStart() + t = datetime.time(hour, minute) + if now.time() < t: + date = today() + else: + d = datetime.datetime.today() + datetime.timedelta(days=1) + date = d.strftime(('%Y-%m-%d')) + + self.setDownloadsToday(date, value) + + def setDayStart(self, hour, minute): + self.day_start = "%s:%s" % (hour, minute) + + def getDayStart(self): + try: + t1, t2 = self.day_start.split(":") + return (int(t1), int(t2)) + except ValueError: + sys.stderr.write(_("'Start of day' preference value is corrupted.\n")) + sys.stderr.write(_("Resetting to midnight.\n")) + self.day_start = "0:0" + return 0, 0 + + +class ImageRenameTable(tpm.TablePlusMinus): + + def __init__(self, parentApp, adjustScrollWindow): + + tpm.TablePlusMinus.__init__(self, 1, 3) + self.parentApp = parentApp + self.adjustScrollWindow = adjustScrollWindow + if adjustScrollWindow: + self.connect("size-request", self.size_adjustment) + self.connect("add", self.size_adjustment) + self.tableWidth = self.allocation.width + # vbar is '1' if there is not vertical scroll bar + # if there is a vertical scroll bar, then it will have a the width of the bar + self.vbar = self.adjustScrollWindow.get_vscrollbar().allocation.width + + self.getParentAppPrefs() + self.getPrefsFactory() + + if not hasattr(self, "errorTitle"): + self.errorTitle = _("Error in Image Rename preferences") + + try: + self.prefsFactory.checkPrefsForValidity() + + except (rn.PrefValueInvalidError, rn.PrefLengthError, + rn.PrefValueKeyComboError, rn.PrefKeyError), e: + + sys.stderr.write(self.errorTitle + "\n") + sys.stderr.write(_("Sorry,these preferences contain an error:\n")) + sys.stderr.write(self.prefsFactory.formatPreferencesForPrettyPrint() + "\n") + + # the preferences were invalid + # reset them to their default + + self.prefList = self.prefsFactory.defaultPrefs + self.getPrefsFactory() + self.updateParentAppPrefs() + + msg = "%s.\n" % e + msg += _("Resetting to default values.") + sys.stderr.write(msg) + + + misc.run_dialog(self.errorTitle, msg, + parentApp, + gtk.MESSAGE_ERROR) + + for row in self.prefsFactory.getWidgetsBasedOnPreferences(): + self.append(row) + + + + + def updatePreferences(self): + prefList = [] + for row in self.pm_rows: + for col in range(self.pm_noColumns): + widget = row[col] + if widget: + name = widget.get_name() + if name == 'GtkComboBox': + value = widget.get_active_text() + elif name == 'GtkEntry': + value = widget.get_text() + else: + sys.stderr.write("Program error: Unknown preference widget!") + value = '' + else: + value = '' + prefList.append(value) + + self.prefList = prefList + self.updateParentAppPrefs() + self.prefsFactory.prefList = prefList + self.updateExample() + + + def size_adjustment(self, arg1, arg2): + """ Adjust scrolledwindow width in preferences dialog to reflect width of image rename table + + The algorithm is complicated by the need to tak into account the presence of a vertical scrollbar""" + + if self.adjustScrollWindow: + if self.adjustScrollWindow.get_vscrollbar().allocation.width > 1: + extra = self.adjustScrollWindow.get_vscrollbar().allocation.width + 10 + else: + extra = 0 + if self.vbar <= 1: + if self.allocation.width > self.tableWidth: + self.adjustScrollWindow.set_size_request(self.allocation.width + extra, -1) + self.tableWidth = self.allocation.width + extra + elif self.allocation.width - extra > self.tableWidth: + self.adjustScrollWindow.set_size_request(self.allocation.width + extra, -1) + self.tableWidth = self.allocation.width + extra + self.vbar = self.adjustScrollWindow.get_vscrollbar().allocation.width + + def getParentAppPrefs(self): + self.prefList = self.parentApp.prefs.image_rename + + + + + def getPrefsFactory(self): + self.prefsFactory = rn.ImageRenamePreferences(self.prefList, self, + sequences = sequences) + + def updateParentAppPrefs(self): + self.parentApp.prefs.image_rename = self.prefList + + def updateExample(self): + self.parentApp.updateImageRenameExample() + + def getDefaultRow(self): + return self.prefsFactory.getDefaultRow() + + def on_combobox_changed(self, widget, rowPosition): + + for col in range(self.pm_noColumns): + if self.pm_rows[rowPosition][col] == widget: + break + selection = [] + for i in range(col + 1): + # ensure it is a combo box we are getting the value from + w = self.pm_rows[rowPosition][i] + name = w.get_name() + if name == 'GtkComboBox': + selection.append(w.get_active_text()) + else: + selection.append(w.get_text()) + + for i in range(col + 1, self.pm_noColumns): + selection.append('') + + if col <> (self.pm_noColumns - 1): + widgets = self.prefsFactory.getWidgetsBasedOnUserSelection(selection) + + for i in range(col + 1, self.pm_noColumns): + oldWidget = self.pm_rows[rowPosition][i] + if oldWidget: + self.remove(oldWidget) + if oldWidget in self.pm_callbacks: + del self.pm_callbacks[oldWidget] + newWidget = widgets[i] + self.pm_rows[rowPosition][i] = newWidget + if newWidget: + self._createCallback(newWidget, rowPosition) + self.attach(newWidget, i, i+1, rowPosition, rowPosition + 1) + newWidget.show() + self.updatePreferences() + + + def on_entry_changed(self, widget, rowPosition): + self.updatePreferences() + + def on_rowAdded(self, rowPosition): + """ + Update preferences, as a row has been added + """ + self.updatePreferences() + + # if this was the last row or 2nd to last row, and another has just been added, move vertical scrollbar down + if rowPosition in range(self.pm_noRows - 3, self.pm_noRows - 2): + adjustment = self.parentApp.rename_scrolledwindow.get_vadjustment() + adjustment.set_value(adjustment.upper) + + + def on_rowDeleted(self, rowPosition): + """ + Update preferences, as a row has been deleted + """ + self.updatePreferences() + +class SubfolderTable(ImageRenameTable): + def __init__(self, parentApp, adjustScollWindow): + self.errorTitle = _("Error in Download Subfolder preferences") + ImageRenameTable.__init__(self, parentApp, adjustScollWindow) + + def getParentAppPrefs(self): + self.prefList = self.parentApp.prefs.subfolder + + def getPrefsFactory(self): + self.prefsFactory = rn.SubfolderPreferences(self.prefList, self) + + def updateParentAppPrefs(self): + self.parentApp.prefs.subfolder = self.prefList + + def updateExample(self): + self.parentApp.updateDownloadFolderExample() + + +class PreferencesDialog(gnomeglade.Component): + def __init__(self, parentApp): + gnomeglade.Component.__init__(self, + paths.share_dir(config.GLADE_FILE), + "preferencesdialog") + + self.widget.set_transient_for(parentApp.widget) + self.prefs = parentApp.prefs + + self._setupTabSelector() + + self._setupControlSpacing() + + # get example image data + + try: + w = workers.firstWorkerReadyToDownload() + root, self.sampleImageName = w.firstImage() + image = os.path.join(root, self.sampleImageName) + self.sampleImage = metadata.MetaData(image) + self.sampleImage.readMetadata() + except: + self.sampleImage = metadata.DummyMetaData() + self.sampleImageName = 'IMG_0524.CR2' + + # setup tabs + self._setupDownloadFolderTab() + self._setupImageRenameTab() + self._setupRenameOptionsTab() + self._setupDeviceTab() + self._setupBackupTab() + self._setupAutomationTab() + self._setupErrorTab() + + self.widget.realize() + + #set the width of the left column for selecting values + #note: this must be called after self.widget.realize(), or else the width calculation will fail + width_of_widest_sel_row = self.treeview.get_background_area(1, self.treeview_column)[2] + self.scrolled_window.set_size_request(width_of_widest_sel_row + 2, -1) + + #set the minimum width of the scolled window holding the image rename table + if self.rename_scrolledwindow.get_vscrollbar(): + extra = self.rename_scrolledwindow.get_vscrollbar().allocation.width + 10 + else: + extra = 10 + self.rename_scrolledwindow.set_size_request(self.rename_table.allocation.width + extra, -1) + + self.widget.show() + + def on_preferencesdialog_destroy(self, widget): + """ Delete variables from memory that cause a file descriptor to be created on a mounted media""" + del self.sampleImage, self.rename_table.prefsFactory, self.subfolder_table.prefsFactory + + def _setupTabSelector(self): + self.notebook.set_show_tabs(0) + self.model = gtk.ListStore(type("")) + column = gtk.TreeViewColumn() + rentext = gtk.CellRendererText() + column.pack_start(rentext, expand=0) + column.set_attributes(rentext, text=0) + self.treeview_column = column + self.treeview.append_column(column) + self.treeview.props.model = self.model + for c in self.notebook.get_children(): + label = self.notebook.get_tab_label(c).get_text() + if not label.startswith("_"): + self.model.append( (label,) ) + + + # select the first value in the list store + self.treeview.set_cursor(0,column) + + def on_download_folder_filechooser_button_selection_changed(self, widget): + self.prefs.download_folder = widget.get_current_folder() + self.updateDownloadFolderExample() + + def on_backup_folder_filechooser_button_selection_changed(self, widget): + self.prefs.backup_location = widget.get_current_folder() + self.updateBackupExample() + + def on_device_location_filechooser_button_selection_changed(self, widget): + self.prefs.device_location = widget.get_current_folder() + + def _setupControlSpacing(self): + """ + set spacing of some but not all controls + + not currently used + """ + + self._setupTableSpacing(self.download_folder_table) + self.download_folder_table.set_row_spacing(2, + hd.VERTICAL_CONTROL_SPACE) + self._setupTableSpacing(self.rename_example_table) + self.devices_table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) + + self._setupTableSpacing(self.backup_table) + self.backup_table.set_col_spacing(1, hd.NESTED_CONTROLS_SPACE) + self.backup_table.set_col_spacing(2, hd.CONTROL_LABEL_SPACE) + self._setupTableSpacing(self.compatibility_table) + self.compatibility_table.set_row_spacing(0, + hd.VERTICAL_CONTROL_LABEL_SPACE) + self._setupTableSpacing(self.error_table) + self.error_table.set_row_spacing(5, hd.VERTICAL_CONTROL_SPACE / 2) + + + def _setupTableSpacing(self, table): + table.set_col_spacing(0, hd.NESTED_CONTROLS_SPACE) + table.set_col_spacing(1, hd.CONTROL_LABEL_SPACE) + + def _setupSubfolderTable(self): + self.subfolder_table = SubfolderTable(self, None) + self.subfolder_vbox.pack_start(self.subfolder_table) + self.subfolder_table.show_all() + + def _setupDownloadFolderTab(self): + self.download_folder_filechooser_button = gtk.FileChooserButton( + _("Select a folder to download photos to")) + self.download_folder_filechooser_button.set_current_folder( + self.prefs.download_folder) + self.download_folder_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + self.download_folder_filechooser_button.connect("selection-changed", + self.on_download_folder_filechooser_button_selection_changed) + + self.download_folder_table.attach( + self.download_folder_filechooser_button, + 2, 3, 2, 3, yoptions = gtk.SHRINK) + self.download_folder_filechooser_button.show() + + self._setupSubfolderTable() + self.updateDownloadFolderExample() + + def _setupImageRenameTab(self): + + self.rename_table = ImageRenameTable(self, self.rename_scrolledwindow) + self.rename_table_vbox.pack_start(self.rename_table) + self.rename_table.show_all() + self.original_name_label.set_markup("<i>%s</i>" % self.sampleImageName) + self.updateImageRenameExample() + + def _setupRenameOptionsTab(self): + self.downloads_today_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 0)) + self.stored_number_entry = ValidatedEntry.ValidatedEntry(ValidatedEntry.bounded(ValidatedEntry.v_int, int, 1)) + self.downloads_today_entry.connect('changed', self.on_downloads_today_entry_changed) + self.stored_number_entry.connect('changed', self.on_stored_number_entry_changed) + v = self.prefs.getAndMaybeResetDownloadsToday() + self.downloads_today_entry.set_text(str(v)) + # make the displayed value of stored sequence no 1 more than actual value + # so as not to confuse the user + self.stored_number_entry.set_text(str(self.prefs.stored_sequence_no+1)) + self.sequence_vbox.pack_start(self.downloads_today_entry, expand=True, fill=True) + self.sequence_vbox.pack_start(self.stored_number_entry, expand=False) + self.downloads_today_entry.show() + self.stored_number_entry.show() + hour, minute = self.prefs.getDayStart() + self.hour_spinbutton.set_value(float(hour)) + self.minute_spinbutton.set_value(float(minute)) + + self.strip_characters_checkbutton.set_active( + self.prefs.strip_characters) + + def _setupDeviceTab(self): + self.device_location_filechooser_button = gtk.FileChooserButton( + _("Select an image folder")) + self.device_location_filechooser_button.set_current_folder( + self.prefs.device_location) + self.device_location_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + + self.device_location_filechooser_button.connect("selection-changed", + self.on_device_location_filechooser_button_selection_changed) + + self.devices2_table.attach(self.device_location_filechooser_button, + 1, 2, 1, 2, xoptions = gtk.EXPAND|gtk.FILL, yoptions = gtk.SHRINK) + self.device_location_filechooser_button.show() + self.autodetect_device_checkbutton.set_active( + self.prefs.device_autodetection) + self.autodetect_psd_checkbutton.set_active( + self.prefs.device_autodetection_psd) + + self.updateDeviceControls() + + + def _setupBackupTab(self): + self.backup_folder_filechooser_button = gtk.FileChooserButton( + _("Select a folder in which to backup images")) + self.backup_folder_filechooser_button.set_current_folder( + self.prefs.backup_location) + self.backup_folder_filechooser_button.set_action( + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + self.backup_folder_filechooser_button.connect("selection-changed", + self.on_backup_folder_filechooser_button_selection_changed) + self.backup_table.attach(self.backup_folder_filechooser_button, + 3, 4, 7, 8, yoptions = gtk.SHRINK) + self.backup_folder_filechooser_button.show() + self.backup_identifier_entry.set_text(self.prefs.backup_identifier) + + #setup controls for manipulating sensitivity + self._backupControls0 = [self.auto_detect_backup_checkbutton, + self.missing_backup_label, + self.backup_error_radiobutton, + self.backup_warning_radiobutton, + self.backup_ignore_radiobutton] + self._backupControls1 = [self.backup_identifier_explanation_label, + self.backup_identifier_label, + self.backup_identifier_entry, + self.example_backup_path_label, + self.backup_example_label,] + self._backupControls2 = [self.backup_location_label, + self.backup_folder_filechooser_button, + self.backup_location_explanation_label] + self._backupControls = self._backupControls0 + self._backupControls1 + \ + self._backupControls2 + + #assign values to checkbuttons only when other controls + #have been setup, because their toggle signal is activated + #when a value is assigned + + self.backup_checkbutton.set_active(self.prefs.backup_images) + self.auto_detect_backup_checkbutton.set_active( + self.prefs.backup_device_autodetection) + self.updateBackupControls() + self.updateBackupExample() + + def _setupAutomationTab(self): + self.auto_startup_checkbutton.set_active( + self.prefs.auto_download_at_startup) + self.auto_insertion_checkbutton.set_active( + self.prefs.auto_download_upon_device_insertion) + self.auto_unmount_checkbutton.set_active( + self.prefs.auto_unmount) + self.auto_exit_checkbutton.set_active( + self.prefs.auto_exit) + + + def _setupErrorTab(self): + self.indicate_download_error_checkbutton.set_active( + self.prefs.indicate_download_error) + + if self.prefs.download_conflict_resolution == config.SKIP_DOWNLOAD: + self.skip_download_radiobutton.set_active(True) + else: + self.add_identifier_radiobutton.set_active(True) + + if self.prefs.backup_missing == config.REPORT_ERROR: + self.backup_error_radiobutton.set_active(True) + elif self.prefs.backup_missing == config.REPORT_WARNING: + self.backup_warning_radiobutton.set_active(True) + else: + self.backup_ignore_radiobutton.set_active(True) + + if self.prefs.backup_duplicate_overwrite: + self.backup_duplicate_overwrite_radiobutton.set_active(True) + else: + self.backup_duplicate_skip_radiobutton.set_active(True) + + def updateImageRenameExample(self): + """ + Displays example image name to the user + """ + + if hasattr(self, 'rename_table'): + name, problem = self.rename_table.prefsFactory.generateNameUsingPreferences( + self.sampleImage, self.sampleImageName, + self.prefs.strip_characters, sequencesPreliminary=False) + else: + name = problem = '' + + # since this is markup, escape it + text = "<i>%s</i>" % common.escape(name) + + if problem: + text += "\n" + # Translators: please do not modify or leave out html formatting tags like <i> and <b>. These are used to format the text the users sees + text += _("<i><b>Warning:</b> There is insufficient image metatdata to fully generate the name. Please use other renaming options.</i>") + + self.new_name_label.set_markup(text) + + def updateDownloadFolderExample(self): + """ + Displays example subfolder name(s) to the user + """ + + if hasattr(self, 'subfolder_table'): + path, problem = self.subfolder_table.prefsFactory.generateNameUsingPreferences( + self.sampleImage, self.sampleImageName, + self.prefs.strip_characters) + else: + path = problem = '' + + text = os.path.join(self.prefs.download_folder, path) + # since this is markup, escape it + path = common.escape(text) + if problem: + warning = _("<i><b>Warning:</b> There is insufficient image metatdata to fully generate subfolders. Please use other subfolder naming options.</i>" ) + else: + warning = "" + # Translators: you should not modify or leave out the %s. This is a code used by the programming language python to insert a value that thes user will see + self.example_download_path_label.set_markup(_("<i>Example: %s</i>") % text) + self.subfolder_warning_label.set_markup(warning) + + def on_hour_spinbutton_value_changed(self, spinbutton): + hour = spinbutton.get_value_as_int() + minute = self.minute_spinbutton.get_value_as_int() + self.prefs.setDayStart(hour, minute) + self.on_downloads_today_entry_changed(self.downloads_today_entry) + + def on_minute_spinbutton_value_changed(self, spinbutton): + hour = self.hour_spinbutton.get_value_as_int() + minute = spinbutton.get_value_as_int() + self.prefs.setDayStart(hour, minute) + self.on_downloads_today_entry_changed(self.downloads_today_entry) + + def on_downloads_today_entry_changed(self, entry): + if workers.noRunningWorkers() == 0: + # do not update value if a download is occurring - it will mess it up! + v = entry.get_text() + try: + v = int(v) + except: + v = 0 + if v < 0: + v = 0 + self.prefs.resetDownloadsToday(v) + sequences.setDownloadsToday(v) + self.updateImageRenameExample() + + def on_stored_number_entry_changed(self, entry): + if workers.noRunningWorkers() == 0: + # do not update value if a download is occurring - it will mess it up! + v = entry.get_text() + try: + # the displayed value of stored sequence no 1 more than actual value + # so as not to confuse the user + v = int(v) - 1 + except: + v = 0 + if v < 0: + v = 0 + self.prefs.stored_sequence_no = v + sequences.setStoredSequenceNo(v) + self.updateImageRenameExample() + + def on_response(self, dialog, arg): + if arg == gtk.RESPONSE_HELP: + webbrowser.open("http://www.damonlynch.net/rapid/documentation") + else: + # arg==gtk.RESPONSE_CLOSE, or the user hit the 'x' to close the window + self.prefs.backup_identifier = self.backup_identifier_entry.get_property("text") + + #check subfolder preferences for bad values + filtered, prefList = rn.filterSubfolderPreferences(self.prefs.subfolder) + if filtered: + cmd_line(_("The subfolder preferences had some unnecessary values removed.")) + if prefList: + self.prefs.subfolder = prefList + else: + #Preferences list is now empty + msg = _("The subfolder preferences entered are invalid and cannot be used.\nThey will be reset to their default values.") + sys.stderr.write(msg + "\n") + misc.run_dialog(PROGRAM_NAME, msg) + self.prefs.subfolder = self.prefs.get_default("subfolder") + + self.widget.destroy() + + + def on_auto_startup_checkbutton_toggled(self, checkbutton): + self.prefs.auto_download_at_startup = checkbutton.get_active() + + def on_auto_insertion_checkbutton_toggled(self, checkbutton): + self.prefs.auto_download_upon_device_insertion = checkbutton.get_active() + + def on_auto_unmount_checkbutton_toggled(self, checkbutton): + self.prefs.auto_unmount = checkbutton.get_active() + + def on_auto_exit_checkbutton_toggled(self, checkbutton): + self.prefs.auto_exit = checkbutton.get_active() + + def on_autodetect_device_checkbutton_toggled(self, checkbutton): + self.prefs.device_autodetection = checkbutton.get_active() + self.updateDeviceControls() + + def on_autodetect_psd_checkbutton_toggled(self, checkbutton): + self.prefs.device_autodetection_psd = checkbutton.get_active() + + def on_backup_duplicate_overwrite_radiobutton_toggled(self, widget): + self.prefs.backup_duplicate_overwrite = widget.get_active() + + def on_backup_duplicate_skip_radiobutton_toggled(self, widget): + self.prefs.backup_duplicate_overwrite = not widget.get_active() + + def on_backup_error_radiobutton_toggled(self, widget): + self.prefs.backup_missing = config.REPORT_ERROR + + def on_backup_warning_radiobutton_toggled(self, widget): + self.prefs.backup_missing = config.REPORT_WARNING + + def on_backup_ignore_radiobutton_toggled(self, widget): + self.prefs.backup_missing = config.IGNORE + + def on_treeview_cursor_changed(self, tree): + path, column = tree.get_cursor() + self.notebook.set_current_page(path[0]) + + + def on_strip_characters_checkbutton_toggled(self, check_button): + self.prefs.strip_characters = check_button.get_active() + self.updateImageRenameExample() + self.updateDownloadFolderExample() + + def on_indicate_download_error_checkbutton_toggled(self, check_button): + self.prefs.indicate_download_error = check_button.get_active() + + def on_add_identifier_radiobutton_toggled(self, widget): + if widget.get_active(): + self.prefs.download_conflict_resolution = config.ADD_UNIQUE_IDENTIFIER + else: + self.prefs.download_conflict_resolution = config.SKIP_DOWNLOAD + + + def updateDeviceControls(self): + """ + Sets sensitivity of image device controls + """ + controls = [self.device_location_explanation_label, + self.device_location_label, + self.device_location_filechooser_button] + + if self.prefs.device_autodetection: + for c in controls: + c.set_sensitive(False) + self.autodetect_psd_checkbutton.set_sensitive(True) + self.autodetect_image_devices_label.set_sensitive(True) + else: + for c in controls: + c.set_sensitive(True) + self.autodetect_psd_checkbutton.set_sensitive(False) + self.autodetect_image_devices_label.set_sensitive(False) + + def updateBackupControls(self): + """ + Sets sensitivity of backup related widgets + """ + + if not self.backup_checkbutton.get_active(): + for c in self._backupControls: + c.set_sensitive(False) + + else: + for c in self._backupControls0: + c.set_sensitive(True) + + self.updateBackupControlsAuto() + + def updateBackupControlsAuto(self): + """ + Sets sensitivity of subset of backup related widgets + """ + + if self.auto_detect_backup_checkbutton.get_active(): + for c in self._backupControls1: + c.set_sensitive(True) + for c in self._backupControls2: + c.set_sensitive(False) + else: + for c in self._backupControls1: + c.set_sensitive(False) + for c in self._backupControls2: + c.set_sensitive(True) + + def on_auto_detect_backup_checkbutton_toggled(self, widget): + self.prefs.backup_device_autodetection = widget.get_active() + self.updateBackupControlsAuto() + + def on_backup_checkbutton_toggled(self, widget): + self.prefs.backup_images = self.backup_checkbutton.get_active() + self.updateBackupControls() + + def on_backup_identifier_entry_changed(self, widget): + self.updateBackupExample() + + def on_backup_scan_folder_on_entry_changed(self, widget): + self.updateBackupExample() + + def updateBackupExample(self): + # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. + path = os.path.join(config.MEDIA_LOCATION, _("externaldrive1")) + # Translators: this value is used as an example device when automatic backup device detection is enabled. You should translate this. + path2 = os.path.join(config.MEDIA_LOCATION, _("externaldrive2")) + + path = os.path.join(path, self.backup_identifier_entry.get_text()) + path2 = os.path.join(path2, self.backup_identifier_entry.get_text()) + path = common.escape(path) + path2 = common.escape(path2) + self.example_backup_path_label.set_markup("<i>%s</i>\n<i>%s</i>" % (path, + path2)) + + + + +class CopyPhotos(Thread): + """Copies photos from source to destination, backing up if needed""" + def __init__(self, thread_id, parentApp, fileRenameLock, fileSequenceLock, statsLock, downloadStats, autoStart = False, cardMedia = None): + self.parentApp = parentApp + self.thread_id = thread_id + self.ctrl = True + self.running = False + self.manuallyDisabled = False + # enable the capacity to block oneself with a lock + # the lock will be first set when the thread begins + # it will then be locked when the thread needs to be paused + # releasing it will cause the code to restart from where it + # left off + self.lock = Lock() + + self.fileRenameLock = fileRenameLock + self.fileSequenceLock = fileSequenceLock + self.statsLock = statsLock + + self.downloadStats = downloadStats + + self.hasStarted = False + self.doNotStart = False + + self.autoStart = autoStart + self.cardMedia = cardMedia + + self.initializeDisplay(thread_id, self.cardMedia) + + self.noErrors = self.noWarnings = 0 + + self.scanComplete = self.downloadStarted = self.downloadComplete = False + + Thread.__init__(self) + + + def initializeDisplay(self, thread_id, cardMedia = None): + + if self.cardMedia: + media_collection_treeview.addCard(thread_id, self.cardMedia.prettyName(), + '', 0, progress=0.0, + # This refers to when a device like a hard drive is having its contents scanned, + # looking for images. It is visible initially in the progress bar for each device + # (which normally holds "x of y images copied"). + # It maybe displayed only briefly if the contents of the device being scanned is small. + progressBarText=_('scanning...')) + + + def firstImage(self): + """ + returns name, path and size of the first image + """ + name, root, size, modificationTime = self.cardMedia.firstImage() + return root, name + + def handlePreferencesError(self, e, prefsFactory): + sys.stderr.write(_("Sorry,these preferences contain an error:\n")) + sys.stderr.write(prefsFactory.formatPreferencesForPrettyPrint() + "\n") + msg = str(e) + sys.stderr.write(msg + "\n") + + def initializeFromPrefs(self, notifyOnError): + """ + Setup thread so that user preferences are handled + """ + self.prefs = self.parentApp.prefs + + self.imageRenamePrefsFactory = rn.ImageRenamePreferences(self.prefs.image_rename, self, + self.fileSequenceLock, sequences) + try: + self.imageRenamePrefsFactory.checkPrefsForValidity() + except (rn.PrefValueInvalidError, rn.PrefLengthError, + rn.PrefValueKeyComboError, rn.PrefKeyError), e: + if notifyOnError: + self.handlePreferencesError(e, self.imageRenamePrefsFactory) + raise rn.PrefError + + + self.subfolderPrefsFactory = rn.SubfolderPreferences( + self.prefs.subfolder, self) + try: + self.subfolderPrefsFactory.checkPrefsForValidity() + except (rn.PrefValueInvalidError, rn.PrefLengthError, + rn.PrefValueKeyComboError, rn.PrefKeyError), e: + if notifyOnError: + self.handlePreferencesError(e, self.subfolderPrefsFactory) + raise rn.PrefError + + # copy this variable, as it is used heavily in the loop + # and it is perhaps relatively expensive to read + self.stripCharacters = self.prefs.strip_characters + + + def run(self): + """ + Copy photos from device to local drive, and if requested, backup + + 1. Should the image be downloaded? + 1.a generate file name + 1.a.1 generate sequence numbers if needed + 1.a.2 FIFO queue sequence numbers to indicate that they could + potentially be used in a filename + 1.b check to see if a file exists with the same name in the place it will + be downloaded to + 1.c if it exisits, and unique identifiers are not being used: + 1.b.1 if using sequence numbers or letters, then potentially any of the + sequence numbers in the queue could be used to make the filename + 1.b.1.a generate and check each filename using sequence numbers in the queue + 1.b.1.b if one of these filenames is unique, then image needs to be downloaded + 1.b.2 do not do not download + + + 2. Download the image + 2.a copy it to temporary folder (this takes time) + 2.b is the file name still unique? Perhaps a new file was created with this name in the meantime + (either by another thread or another program) + 2.b.1 don't allow any other thread to rename a file + 2.b.2 check file name + 2.b.3 adding suffix if it is not unique, being careful not to overwrite any existing file with a suffix + 2.b.4 rename it to the "real"" name, effectively performing a mv + 2.b.5 allow other threads to rename files + + 3. Backup the image, using the same filename as was used when it was downloaded + 3.a does a file with the same name already exist on the backup medium? + 3.b if so, user preferences determine whether it should be overwritten or not + """ + + def getPrefs(notifyOnError): + try: + self.initializeFromPrefs(notifyOnError) + return True + except rn.PrefError: + if notifyOnError: + display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) + msg = _("There is an error in the program preferences.") + msg += _("\nPlease check preferences, restart the program, and try again.") + logError(config.CRITICAL_ERROR, _("Download cannot proceed"), msg) + cmd_line(_("Download cannot proceed")) + cmd_line(msg) + return False + def scanMedia(): + + images = [] + imageSizeSum = 0 + for root, dirs, files in os.walk(self.cardMedia.getPath()): + for name in files: + if not self.running: + self.lock.acquire() + self.running = True + + if not self.ctrl: + self.running = False + display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) + display_queue.close("rw") + return + + if media.isImage(name): + image = os.path.join(root, name) + size = os.path.getsize(image) + modificationTime = os.path.getmtime(image) + images.append((name, root, size, modificationTime),) + imageSizeSum += size + images.sort(key=operator.itemgetter(3)) + noImages = len(images) + + self.scanComplete = True + + if noImages: + self.cardMedia.setMedia(images, imageSizeSum, noImages) + # Translators: as already, mentioned the %s value should not be modified or left out. It may be moved if necessary. + # It refers to the actual number of images that can be copied. For example, the user might see the following: + # '0 of 512 images copied'. + # This particular text is displayed to the user before the download has started. + display = _("0 of %s images copied") % noImages + display_queue.put((media_collection_treeview.updateCard, (self.thread_id, self.cardMedia.sizeOfImages(), noImages))) + display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, 0.0, display, 0))) + display_queue.put((self.parentApp.timeRemaining.add, (self.thread_id, imageSizeSum))) + display_queue.put((self.parentApp.setDownloadButtonSensitivity, ())) + + # Translators: as you have already seen, the text can contain values that should not be modified or left out by you, for example %s. + # This text is another example of that, but it is is a little more complex. Here there are two values which will be displayed + # to the user when they run the program, signifying the number of images found, and the device they were found on. + # %(number)s should be left exactly as is: 'number' should not be translated. The same applies to %(device)s: 'device' should + # not be translated. Generally speaking, if translating the sentence requires it, you can move items like '%(xyz)s' around + # in a sentence, but you should never modify them or leave them out. + cmd_line(_("Device scan complete: found %(number)s images on %(device)s") % + {'number': noImages, 'device': self.cardMedia.prettyName(limit=0)}) + return True + else: + # it might be better to display "0 of 0" here + display_queue.put((media_collection_treeview.removeCard, (self.thread_id, ))) + cmd_line(_("Device scan complete: no images found on %s") % self.cardMedia.prettyName(limit=0)) + return False + + def cleanUp(): + """ + Cleanup functions that must be performed whether the thread exits + early or when it has completed its run. + """ + + # possibly delete any lingering files + tf = os.listdir(tempWorkingDir) + if tf: + for f in tf: + os.remove(os.path.join(tempWorkingDir, f)) + + os.rmdir(tempWorkingDir) + + + def logError(severity, problem, details, resolution=None): + display_queue.put((log_dialog.addMessage, (self.thread_id, severity, problem, details, + resolution))) + if severity == config.WARNING: + self.noWarnings += 1 + else: + self.noErrors += 1 + + + def checkProblemWithImageNameGeneration(newName, image, problem): + if not newName: + # a serious problem - a filename should never be blank! + logError(config.SERIOUS_ERROR, + _("Image filename could not be generated"), + # '%(source)s' and '%(problem)s' are two more examples of text that should not be modified or left out + _("Source: %(source)s\nProblem: %(problem)s") % {'source': image, 'problem': problem}, + IMAGE_SKIPPED) + elif problem: + logError(config.WARNING, + _("Image filename could not be properly generated. Check to ensure there is sufficient image metadata."), + _("Source: %(source)s\nDestination: %(destination)s\nProblem: %(problem)s") % + {'source': image, 'destination': newName, 'problem': problem}) + + def generateSubfolderAndFileName(image, name, needMetaDataToCreateUniqueImageName, + needMetaDataToCreateUniqueSubfolderName): + skipImage = False + try: + imageMetadata = metadata.MetaData(image) + except IOError: + logError(config.CRITICAL_ERROR, _("Could not open image"), + _("Source: %s") % image, + IMAGE_SKIPPED) + skipImage = True + imageMetadata = newName = newFile = path = subfolder = None + else: + imageMetadata.readMetadata() + if not imageMetadata.exifKeys() and (needMetaDataToCreateUniqueSubfolderName or + (needMetaDataToCreateUniqueImageName and + not addUniqueIdentifier)): + logError(config.SERIOUS_ERROR, _("Image has no metadata"), + _("Metadata is essential for generating subfolders / image names.\nSource: %s") % image, + IMAGE_SKIPPED) + skipImage = True + newName = newFile = path = subfolder = None + + else: + subfolder, problem = self.subfolderPrefsFactory.generateNameUsingPreferences( + imageMetadata, name, + self.stripCharacters) + + if problem: + logError(config.WARNING, + _("Subfolder name could not be properly generated. Check to ensure there is sufficient image metadata."), + _("Subfolder: %(subfolder)s\nImage: %(image)s\nProblem: %(problem)s") % + {'subfolder': subfolder, 'image': image, 'problem': problem}) + + # pass the subfolder the image will go into, as this is needed to determine subfolder sequence numbers + # indicate that sequences chosen should be queued + + newName, problem = self.imageRenamePrefsFactory.generateNameUsingPreferences( + imageMetadata, name, self.stripCharacters, subfolder, + sequencesPreliminary = True) + + path = os.path.join(baseDownloadDir, subfolder) + newFile = os.path.join(path, newName) + + if not newName: + skipImage = True + checkProblemWithImageNameGeneration(newName, image, problem) + + return (skipImage, imageMetadata, newName, newFile, path, subfolder) + + def downloadImage(path, newFile, newName, originalName, image, imageMetadata, subfolder): + try: + imageDownloaded = False + if not os.path.isdir(path): + os.makedirs(path) + + nameUniqueBeforeCopy = True + downloadNonUniqueFile = True + + + # do a preliminary check to see if a file with the same name already exists + if os.path.exists(newFile): + nameUniqueBeforeCopy = False + if not addUniqueIdentifier: + downloadNonUniqueFile = False + if usesSequenceElements: + # potentially, a unique image name could still be generated + # investigate this possibility + with self.fileSequenceLock: + for possibleName, problem in self.imageRenamePrefsFactory.generateNameSequencePossibilities(imageMetadata, + originalName, self.stripCharacters, subfolder): +# print "checking", possibleName, "using", originalName + if possibleName: + # no need to check for any problems here, it's just a temporary name + possibleFile = os.path.join(path, possibleName) + possibleTempFile = os.path.join(tempWorkingDir, possibleName) + if not os.path.exists(possibleFile) and not os.path.exists(possibleTempFile): + downloadNonUniqueFile = True + break + + + if self.prefs.indicate_download_error and not downloadNonUniqueFile: + logError(config.SERIOUS_ERROR, IMAGE_ALREADY_EXISTS, + _("Source: %(source)s\nDestination: %(destination)s") % + {'source': image, 'destination': newFile}, + IMAGE_SKIPPED) + + if nameUniqueBeforeCopy or downloadNonUniqueFile: + tempWorkingfile = os.path.join(tempWorkingDir, newName) + shutil.copy2(image, tempWorkingfile) + + with self.fileRenameLock: + doRename = True + if usesSequenceElements: + with self.fileSequenceLock: + # get a filename and use this as the "real" filename + newName, problem = self.imageRenamePrefsFactory.generateNameUsingPreferences( + imageMetadata, originalName, self.stripCharacters, subfolder, + sequencesPreliminary = False) + checkProblemWithImageNameGeneration(newName, image, problem) + if not newName: + # there was a serious error generating the filename + doRename = False + else: + newFile = os.path.join(path, newName) + # check if the file exists again + if os.path.exists(newFile): + if not addUniqueIdentifier: + doRename = False + if self.prefs.indicate_download_error: + logError(config.SERIOUS_ERROR, IMAGE_ALREADY_EXISTS, + _("Source: %(source)s\nDestination: %(destination)s") + % {'source': image, 'destination': newFile}, + IMAGE_SKIPPED) + else: + # add basic suffix to make the filename unique + name = os.path.splitext(newName) + suffixAlreadyUsed = True + while suffixAlreadyUsed: + if newFile in duplicate_files: + duplicate_files[newFile] += 1 + else: + duplicate_files[newFile] = 1 + identifier = '_%s' % duplicate_files[newFile] + newName = name[0] + identifier + name[1] + possibleNewFile = os.path.join(path, newName) + suffixAlreadyUsed = os.path.exists(possibleNewFile) + + if self.prefs.indicate_download_error: + logError(config.SERIOUS_ERROR, IMAGE_ALREADY_EXISTS, + _("Source: %(source)s\nDestination: %(destination)s") + % {'source': image, 'destination': newFile}, + _("Unique identifier '%s' added") % identifier) + + newFile = possibleNewFile + + + if doRename: + os.rename(tempWorkingfile, newFile) + imageDownloaded = True + if usesSequenceElements: + with self.fileSequenceLock: + self.imageRenamePrefsFactory.sequences.imageCopySucceeded() + if usesStoredSequenceNo: + self.prefs.stored_sequence_no += 1 + + with self.fileSequenceLock: + if self.prefs.incrementDownloadsToday(): + # A new day, according the user's preferences of what time a day begins, has started + cmd_line(_("New day has started - resetting 'Downloads Today' sequence number")) + + sequences.setDownloadsToday(0) + + except IOError, (errno, strerror): + logError(config.SERIOUS_ERROR, _('Download copying error'), + _("Source: %(source)s\nDestination: %(destination)s\nError: %(errorno)s %(strerror)s") + % {'source': image, 'destination': newFile, 'errorno': errno, 'strerror': strerror}, + _('The image was not copied.')) + + except OSError, (errno, strerror): + logError(config.CRITICAL_ERROR, _('Download copying error'), + "Source: %(source)s\nDestination: %(destination)s\nError: %(errorno)s %(strerror)s" + % {'source': image, 'destination': newFile, 'errorno': errno, 'strerror': strerror}, + ) + + if usesSequenceElements: + if not imageDownloaded: + self.imageRenamePrefsFactory.sequences.imageCopyFailed() + + return (imageDownloaded, newName, newFile) + + + def backupImage(subfolder, newName, imageDownloaded, newFile, image): + """ backup image to path(s) chosen by the user + + there are two scenarios: + (1) image has just been downloaded and should now be backed up + (2) image was already downloaded on some previous occassion and should still be backed up, because it hasn't been yet + (3) image has been backed up already (or at least, a file with the same name already exists) + """ + + try: + for backupDir in self.parentApp.backupVolumes: + backupPath = os.path.join(backupDir, subfolder) + newBackupFile = os.path.join(backupPath, newName) + copyBackup = True + if os.path.exists(newBackupFile): + # again, not thread safe + copyBackup = self.prefs.backup_duplicate_overwrite + if self.prefs.indicate_download_error: + severity = config.SERIOUS_ERROR + problem = _("Backup image already exists") + details = _("Source: %(source)s\nDestination: %(destination)s") \ + % {'source': image, 'destination': newBackupFile} + if copyBackup : + resolution = IMAGE_OVERWRITTEN + else: + resolution = IMAGE_SKIPPED + logError(severity, problem, details, resolution) + + if copyBackup: + if imageDownloaded: + fileToCopy = newFile + else: + fileToCopy = image + if not os.path.isdir(backupPath): + # recreate folder structure in backup location + # cannot do os.makedirs(backupPath) - it gives bad results when using external drives + # we know backupDir exists + # all the components of subfolder may not + folders = subfolder.split(os.path.sep) + folderToMake = backupDir + for f in folders: + if f: + folderToMake = os.path.join(folderToMake, f) + if not os.path.isdir(folderToMake): + try: + os.mkdir(folderToMake) + except (errno, strerror): + logError(config.SERIOUS_ERROR, _('Backing up error'), + _("Destination directory could not be created\n%(directory)s\nError: %(errno)s %(strerror)s") + % {'directory': folderToMake, 'errno': errno, 'strerror': strerror}, + ) + + shutil.copy2(fileToCopy, newBackupFile) + + except IOError, (errno, strerror): + logError(config.SERIOUS_ERROR, _('Backing up error'), + _("Source: %(source)s\nDestination: %(destination)s\nError: %(errno)s %(strerror)s") + % {'source': image, 'destination': newBackupFile, 'errno': errno, 'strerror': strerror}, + _('The image was not copied.')) + + except OSError, (errno, strerror): + logError(config.CRITICAL_ERROR, _('Backing up error'), + _("Source: %(source)s\nDestination: %(destination)s\nError: %(errno)s %(strerror)s") + % {'source': image, 'destination': newBackupFile, 'errno': errno, 'strerror': strerror} + ) + + def notifyAndUnmount(): + if not self.cardMedia.volume: + unmountMessage = "" + notificationName = PROGRAM_NAME + else: + notificationName = self.cardMedia.volume.get_name() + if self.prefs.auto_unmount: + self.cardMedia.volume.unmount(self.on_volume_unmount) + # This message informs the user that the device (e.g. camera, hard drive or memory card) was automatically unmounted and they can now remove it + unmountMessage = _("The device can now be safely removed") + else: + unmountMessage = "" + + message = _("%s images downloaded") % noImagesDownloaded + if noImagesSkipped: + message += "\n" + _("%s images skipped") % noImagesSkipped + + if unmountMessage: + message = "%s\n%s" % (message, unmountMessage) + + if self.noWarnings: + message = "%s\n%s " % (message, self.noWarnings) + _("warnings") + if self.noErrors: + message = "%s\n%s " % (message, self.noErrors) + _("errors") + + n = pynotify.Notification(notificationName, message) + n.show() + + + + + self.hasStarted = True + display_queue.open('w') + + #Do not try to handle any preference errors here + getPrefs(False) + + #check for presence of backup meditum + if self.prefs.backup_images: + if self.prefs.backup_missing <> config.IGNORE: + if not len(self.parentApp.backupVolumes): + if self.prefs.backup_missing == config.REPORT_ERROR: + e = config.SERIOUS_ERROR + else: + e = config.WARNING + logError(e, _("Backup device missing"), _("No backup device was detected.")) + + + if not scanMedia(): + cmd_line(_("This device has no images to download from.")) + display_queue.close("rw") + self.running = False + self.lock.release() + return + elif not self.autoStart: + # halt thread, waiting to be restarted so download proceeds + self.running = False + self.lock.acquire() + + if not self.ctrl: + # thread will restart at this point, when the program is exiting + # so must exit if self.ctrl indicates this + + self.running = False + display_queue.close("rw") + return + + self.running = True + + if not getPrefs(True): + self.running = False + display_queue.close("rw") + return + + self.downloadStarted = True + cmd_line(_("Download has started from %s") % self.cardMedia.prettyName(limit=0)) + + # Some images may not have metadata (this + # is unlikely for images straight out of a + # camera, but it is possible for images that have been edited). If + # only non-dynamic components make up the rest of an image name + # (e.g. text specified by the user), then relying on metadata will + # likely produce duplicate names. + + needMetaDataToCreateUniqueImageName = self.imageRenamePrefsFactory.needImageMetaDataToCreateUniqueName() + + # subfolder generation also need to be examined, but here the need is + # not so exacting, since subfolders contain images, and naturally the + # requirement to be unique is far more relaxed. However if subfolder + # generation relies entirely on metadata, that is a problem worth + # looking for + needMetaDataToCreateUniqueSubfolderName = self.subfolderPrefsFactory.needMetaDataToCreateUniqueName() + + i = 0 + sizeDownloaded = noImagesDownloaded = noImagesSkipped = 0 + + sizeImages = self.cardMedia.sizeOfImages(humanReadable = False) + display_queue.put((self.parentApp.addToTotalDownloadSize, (sizeImages, ))) + display_queue.put((self.parentApp.setOverallDownloadMark, ())) + display_queue.put((self.parentApp.postStartDownloadTasks, ())) + + sizeImages = float(sizeImages) + noImages = self.cardMedia.numberOfImages() + + baseDownloadDir = self.prefs.download_folder + #create a temporary directory in which to download the photos to + #don't want to put it in system temp folder, as that is likely + #to be on another partition and hence copying files from it + #to the download folder will be slow! + tempWorkingDir = tempfile.mkdtemp(prefix='rapid-tmp-', + dir=baseDownloadDir) + + IMAGE_SKIPPED = _("Image skipped") + IMAGE_OVERWRITTEN = _("Image overwritten") + IMAGE_ALREADY_EXISTS = _("Image already exists") + + addUniqueIdentifier = self.prefs.download_conflict_resolution == config.ADD_UNIQUE_IDENTIFIER + usesSequenceElements = self.imageRenamePrefsFactory.usesSequenceElements() + usesStoredSequenceNo = self.imageRenamePrefsFactory.usesTheSequenceElement(rn.STORED_SEQ_NUMBER) + sequences. setUseOfSequenceElements( + self.imageRenamePrefsFactory.usesTheSequenceElement(rn.SESSION_SEQ_NUMBER), + self.imageRenamePrefsFactory.usesTheSequenceElement(rn.SEQUENCE_LETTER)) + + + while i < noImages: + if not self.running: + self.lock.acquire() + self.running = True + + if not self.ctrl: + self.running = False + cleanUp() + display_queue.close("rw") + return + + # get information about the image to deduce image name and path + name, root, size, modificationTime = self.cardMedia.images[i] + image = os.path.join(root, name) + + skipImage, imageMetadata, newName, newFile, path, subfolder = generateSubfolderAndFileName( + image, name, needMetaDataToCreateUniqueImageName, + needMetaDataToCreateUniqueSubfolderName) + + if skipImage: + noImagesSkipped += 1 + else: + imageDownloaded, newName, newFile = downloadImage(path, newFile, newName, name, image, + imageMetadata, subfolder) + + if self.prefs.backup_images: + backupImage(subfolder, newName, imageDownloaded, newFile, image) + + if imageDownloaded: + noImagesDownloaded += 1 + else: + noImagesSkipped += 1 + try: + thumbnailType, thumbnail = imageMetadata.getThumbnailData() + except: + logError(config.WARNING, _("Image has no thumbnail"), image) + thumbnail = orientation = None + else: + orientation = imageMetadata.orientation(missing=None) + display_queue.put((image_hbox.addImage, (self.thread_id, thumbnail, orientation, image, imageDownloaded))) + + sizeDownloaded += size + percentComplete = (sizeDownloaded / sizeImages) * 100 + if sizeDownloaded == sizeImages: + self.downloadComplete = True + progressBarText = _("%(number)s of %(total)s images copied") % {'number': i + 1, 'total': noImages} + display_queue.put((media_collection_treeview.updateProgress, (self.thread_id, percentComplete, progressBarText, size))) + + i += 1 + + with self.statsLock: + self.downloadStats.adjust(sizeDownloaded, noImagesDownloaded, noImagesSkipped, self.noWarnings, self.noErrors) + + # must manually delete these variables, or else the media cannot be unmounted (bug in pyexiv or exiv2) + del self.subfolderPrefsFactory, self.imageRenamePrefsFactory + try: + del imageMetadata + except: + pass + + notifyAndUnmount() + cmd_line(_("Download complete from %s") % self.cardMedia.prettyName(limit=0)) + display_queue.put((self.parentApp.notifyUserAllDownloadsComplete,())) + display_queue.put((self.parentApp.resetSequences,())) + + cleanUp() + display_queue.put((self.parentApp.exitOnDownloadComplete, ())) + display_queue.close("rw") + + self.running = False + if noImages: + self.lock.release() + + def startStop(self): + if self.isAlive(): + if self.running: + self.running = False + else: + try: + self.lock.release() + + except thread_error: + sys.stderr.write(self.thread_id + " thread error\n") + + def quit(self): + """ + Quits the thread + + A thread can be in one of four states: + + Not started (not alive, nothing to do) + Started and actively running (alive) + Started and paused (alive) + Completed (not alive, nothing to do) + """ + + if self.hasStarted: + if self.isAlive(): + self.ctrl = False + + if not self.running: + released = False + while not released: + try: + self.lock.release() + released = True + except thread_error: + sys.stderr.write("Could not release lock for thread %s\n" % self.thread_id) + + + + def on_volume_unmount(self, data1, data2): + """ needed for call to unmount volume""" + pass + + +class MediaTreeView(gtk.TreeView): + """ + TreeView display of memory cards and associated copying progress. + + Assumes a threaded environment. + """ + def __init__(self, parentApp): + + self.parentApp = parentApp + # card name, size of images, number of images, copy progress, copy text + self.liststore = gtk.ListStore(str, str, int, float, str) + self.mapThreadToRow = {} + + gtk.TreeView.__init__(self, self.liststore) + + self.props.enable_search = False + # make it impossible to select a row + selection = self.get_selection() + selection.set_mode(gtk.SELECTION_NONE) + + # Device refers to a thing like a camera, memory card in its reader, external hard drive, Portable Storage Device, etc. + column0 = gtk.TreeViewColumn(_("Device"), gtk.CellRendererText(), + text=0) + self.append_column(column0) + + # Size refers to the total size of images on the device, typically in MB or GB + column1 = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text=1) + self.append_column(column1) + + column2 = gtk.TreeViewColumn(_("Download Progress"), + gtk.CellRendererProgress(), value=3, text=4) + self.append_column(column2) + self.show_all() + + def addCard(self, thread_id, cardName, sizeImages, noImages, progress = 0.0, + progressBarText = ''): + if not progressBarText: + progressBarText = _("0 of %s images copied") % (noImages) + + # add the row, and get a temporary pointer to the row + iter = self.liststore.append((cardName, sizeImages, noImages, + progress, progressBarText)) + + self._setThreadMap(thread_id, iter) + + # adjust scrolled window height, based on row height and number of ready to start downloads + if workers.noReadyToStartWorkers() >= 1 or workers.noRunningWorkers() > 0: + # please note, at program startup, self.rowHeight() will be less than it will be when already running + # e.g. when starting with 3 cards, it could be 18, but when adding 2 cards to the already running program + # (with one card at startup), it could be 21 + height = (workers.noReadyToStartWorkers() + workers.noRunningWorkers() + 2) * (self.rowHeight()) + self.parentApp.media_collection_scrolledwindow.set_size_request(-1, height) + + + def updateCard(self, thread_id, sizeImages, noImages): + if thread_id in self.mapThreadToRow: + iter = self._getThreadMap(thread_id) + self.liststore.set_value(iter, 1, sizeImages) + self.liststore.set_value(iter, 2, noImages) + else: + sys.stderr.write("FIXME: this card is unknown") + + def removeCard(self, thread_id): + if thread_id in self.mapThreadToRow: + iter = self._getThreadMap(thread_id) + self.liststore.remove(iter) + del self.mapThreadToRow[thread_id] + + + def _setThreadMap(self, thread_id, iter): + """ + convert the temporary iter into a tree reference, which is + permanent + """ + + path = self.liststore.get_path(iter) + treerowRef = gtk.TreeRowReference(self.liststore, path) + self.mapThreadToRow[thread_id] = treerowRef + + def _getThreadMap(self, thread_id): + """ + return the tree iter for this thread + """ + + treerowRef = self.mapThreadToRow[thread_id] + path = treerowRef.get_path() + iter = self.liststore.get_iter(path) + return iter + + def updateProgress(self, thread_id, percentComplete, progressBarText, imageSize): + + iter = self._getThreadMap(thread_id) + + self.liststore.set_value(iter, 3, percentComplete) + self.liststore.set_value(iter, 4, progressBarText) + if percentComplete or imageSize: + self.parentApp.updateOverallProgress(thread_id, imageSize, percentComplete) + + + def rowHeight(self): + if not self.mapThreadToRow: + return 0 + else: + index = self.mapThreadToRow.keys()[0] + path = self.mapThreadToRow[index].get_path() + col = self.get_column(0) + return self.get_background_area(path, col)[3] + +class ImageHBox(gtk.HBox): + """ + Displays thumbnails of the images being downloaded + """ + + def __init__(self, parentApp): + gtk.HBox.__init__(self) + self.parentApp = parentApp + self.padding = hd.CONTROL_IN_TABLE_SPACE / 2 + + #create image used to lighten thumbnails + self.white = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, width=100, height=100) + #fill with white + self.white.fill(0xffffffff) + + #load missing image + self.missingThumbnail = gtk.gdk.pixbuf_new_from_file_at_size(paths.share_dir('glade3/image-missing.svg'), 100, 100) + + def addImage(self, thread_id, thumbnail, orientation, filename, imageDownloaded): + """ + Add thumbnail + + Orientation indicates if the thumbnail needs to be rotated or not. + """ + + if not thumbnail: + pixbuf = self.missingThumbnail + else: + try: + pbloader = gdk.PixbufLoader() + pbloader.write(thumbnail) + # Get the resulting pixbuf and build an image to be displayed + pixbuf = pbloader.get_pixbuf() + pbloader.close() + + except: + log_dialog.addMessage(thread_id, config.WARNING, + _('Thumbnail cannot be displayed'), filename, + _('It may be corrupted')) + pixbuf = self.missingThumbnail + + if not pixbuf: + log_dialog.addMessage(thread_id, config.WARNING, + _('Thumbnail cannot be displayed'), filename, + _('It may be corrupted')) + pixbuf = self.missingThumbnail + else: + # rotate if necessary + if orientation == 8: + pixbuf = pixbuf.rotate_simple(gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE) + + # scale to size + pixbuf = common.scale2pixbuf(100, 100, pixbuf) + if not imageDownloaded: + # lighten it + self.white.composite(pixbuf, 0, 0, pixbuf.props.width, pixbuf.props.height, 0, 0, 1.0, 1.0, gtk.gdk.INTERP_HYPER, 180) + + image = gtk.Image() + image.set_from_pixbuf(pixbuf) + + self.pack_start(image, expand=False, padding=self.padding) + image.show() + + # move viewport to display the latest image + adjustment = self.parentApp.image_scrolledwindow.get_hadjustment() + adjustment.set_value(adjustment.upper) + + +class LogDialog(gnomeglade.Component): + """ + Displays a log of errors, warnings or other information to the user + """ + + def __init__(self, parentApp): + """ + Initialize values for log dialog, but do not display. + """ + + gnomeglade.Component.__init__(self, + paths.share_dir(config.GLADE_FILE), + "logdialog") + + + self.widget.connect("delete-event", self.hide_window) + + self.parentApp = parentApp + self.log_textview.set_cursor_visible(False) + self.textbuffer = self.log_textview.get_buffer() + + self.problemTag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD) + self.resolutionTag = self.textbuffer.create_tag(style=pango.STYLE_ITALIC) + + def addMessage(self, thread_id, severity, problem, details, resolution): + if severity in [config.CRITICAL_ERROR, config.SERIOUS_ERROR]: + self.parentApp.error_image.show() + elif severity == config.WARNING: + self.parentApp.warning_image.show() + + iter = self.textbuffer.get_end_iter() + self.textbuffer.insert_with_tags(iter, problem +"\n", self.problemTag) + iter = self.textbuffer.get_end_iter() + self.textbuffer.insert(iter, details + "\n") + if resolution: + iter = self.textbuffer.get_end_iter() + self.textbuffer.insert_with_tags(iter, resolution +"\n", self.resolutionTag) + + iter = self.textbuffer.get_end_iter() + self.textbuffer.insert(iter, "\n") + + # move viewport to display the latest message + adjustment = self.log_scrolledwindow.get_vadjustment() + adjustment.set_value(adjustment.upper) + + + def on_logdialog_response(self, dialog, arg): + if arg == gtk.RESPONSE_CLOSE: + pass + self.parentApp.error_image.hide() + self.parentApp.warning_image.hide() + self.parentApp.prefs.show_log_dialog = False + self.widget.hide() + return True + + def hide_window(self, window, event): + window.hide() + return True + + + +class RapidApp(gnomeglade.GnomeApp, dbus.service.Object): + def __init__(self, bus, path, name): + + dbus.service.Object.__init__ (self, bus, path, name) + self.running = False + + gladefile = paths.share_dir(config.GLADE_FILE) + + gnomeglade.GnomeApp.__init__(self, "rapid", __version__, gladefile, "rapidapp") + + + # notifications + self.displayDownloadSummaryNotification = False + self.initPyNotify() + + self.prefs = RapidPreferences() + self.prefs.notify_add(self.on_preference_changed) + + self.testing = False + if self.testing: + self.setTestingEnv() + +# sys.exit(0) + + self.widget.show() + + displayPreferences = self.checkForUpgrade(__version__) + self.prefs.program_version = __version__ + + self.timeRemaining = TimeRemaining() + self._resetDownloadInfo() + self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress") + + self.error_image.hide() + self.warning_image.hide() + + if not displayPreferences: + displayPreferences = not self.checkPreferencesOnStartup() + + # display download information using threads + global media_collection_treeview, image_hbox, log_dialog + global download_queue, image_queue, log_queue + global workers + + #track files that should have a suffix added to them + global duplicate_files + + # control sequence numbers and letters + global sequences + + duplicate_files = {} + + downloadsToday = self.prefs.getAndMaybeResetDownloadsToday() + sequences = rn.Sequences(downloadsToday, self.prefs.stored_sequence_no) + + self.downloadStats = DownloadStats() + + # set the number of seconds gap with which to measure download time remaing + self.downloadTimeGap = 3 + + #locks for threadsafe file downloading and stats gathering + self.fileRenameLock = Lock() + self.fileSequenceLock = Lock() + self.statsLock = Lock() + + # log window, in dialog format + # used for displaying download information to the user + + log_dialog = LogDialog(self) + + + self.volumeMonitor = None + if self.usingVolumeMonitor(): + self.startVolumeMonitor() + + + # set up tree view display + media_collection_treeview = MediaTreeView(self) + + self.media_collection_vbox.pack_start(media_collection_treeview) + + #thumbnail display + image_hbox = ImageHBox(self) + self.image_viewport.add(image_hbox) + self.image_viewport.modify_bg(gtk.STATE_NORMAL, gdk.color_parse("white")) + self.set_display_thumbnails(self.prefs.display_thumbnails) + + self.backupVolumes = {} + + self._setupDownloadbutton() + + #status bar progress bar + self.download_progressbar = gtk.ProgressBar() + self.download_progressbar.set_size_request(150, -1) + self.download_progressbar.show() + self.download_progressbar_hbox.pack_start(self.download_progressbar, expand=False, + fill=0) + + + # menus +# self.menu_resequence.set_sensitive(False) + + self.menu_display_thumbnails.set_active(self.prefs.display_thumbnails) + self.menu_clear.set_sensitive(False) + + self.setupAvailableImageAndBackupMedia(onStartup=True, onPreferenceChange=False, doNotAllowAutoStart = displayPreferences) + + #adjust viewport size for displaying media + #this is important because the code in MediaTreeView.addCard() is inaccurate at program startup + + height = self.media_collection_viewport.size_request()[1] + self.media_collection_scrolledwindow.set_size_request(-1, height) + + self.download_button.grab_focus() + + if displayPreferences: + PreferencesDialog(self) + + + + @dbus.service.method (config.DBUS_NAME, + in_signature='', out_signature='b') + def is_running (self): + return self.running + + @dbus.service.method (config.DBUS_NAME, + in_signature='', out_signature='') + def start (self): + if self.is_running(): + self.rapidapp.present() + else: + self.running = True + self.main() + self.running = False + + def setTestingEnv(self): + self.prefs.program_version = '0.0.8~b7' + r = ['Date time', 'Image date', 'YYYYMMDD', 'Text', '-', '', 'Date time', 'Image date', 'HHMM', 'Text', '-', '', 'Session number', '1', 'Three digits', 'Text', '-iso', '', 'Metadata', 'ISO', '', 'Text', '-f', '', 'Metadata', 'Aperture', '', 'Text', '-', '', 'Metadata', 'Focal length', '', 'Text', 'mm-', '', 'Metadata', 'Exposure time', '', 'Filename', 'Extension', 'lowercase'] + self.prefs.image_rename = r + + + + def checkPreferencesOnStartup(self): + prefsOk = rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder) + if not prefsOk: + title = PROGRAM_NAME + msg = _("There is an error in the program preferences.") + msg += " " + _("Some preferences will be reset.") + sys.stderr.write(msg +'\n') +# misc.run_dialog(title, msg) + return prefsOk + + def checkForUpgrade(self, runningVersion): + """ Checks if the running version of the program is different from the version recorded in the preferences. + + If the version is different, then the preferences are checked to see whether they should be upgraded or not. + + returns True if program preferences window should be opened """ + + displayPrefs = upgraded = False + + previousVersion = self.prefs.program_version + if previousVersion: + # the program has been run previously for this user + + pv = common.pythonifyVersion(previousVersion) + rv = common.pythonifyVersion(runningVersion) + + title = PROGRAM_NAME + imageRename = subfolder = None + + if pv != rv: + if pv > rv: + prefsOk = rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder) + + msg = _("A newer version of this program was previously run on this computer.\n\n") + if prefsOk: + msg += _("Program preferences appear to be valid, but please check them to ensure correct operation.") + else: + msg += _("Sorry, some preferences are invalid and will be reset.") + sys.stderr.write(_("Warning:") + " %s\n" % msg) + misc.run_dialog(title, msg) + displayPrefs = True + + else: + cmd_line(_("This version of the program is newer than the previously run version. Checking preferences.")) +# if True: + if rn.checkPreferencesForValidity(self.prefs.image_rename, self.prefs.subfolder, previousVersion): + upgraded, imageRename, subfolder = rn.upgradePreferencesToCurrent(self.prefs.image_rename, self.prefs.subfolder, previousVersion) + if upgraded: + self.prefs.image_rename = imageRename + self.prefs.subfolder = subfolder + cmd_line(_("Preferences were modified.")) + msg = _('This version of the program uses different preferences than the old version. Your preferences have been updated.\n\nPlease check them to ensure correct operation.') + misc.run_dialog(title, msg) + displayPrefs = True + else: + cmd_line(_("No preferences needed to be changed.")) + else: + msg = _('This version of the program uses different preferences than the old version. Some of your previous preferences were invalid, and could not be updated. They will be reset.') + sys.stderr.write(msg + "\n") + misc.run_dialog(title, msg) + displayPrefs = True + + return displayPrefs + + def initPyNotify(self): + if not pynotify.init("TestCaps"): + sys.stderr.write(_("Problem using pynotify.") + "\n") + sys.exit(1) + + capabilities = {'actions': False, + 'body': False, + 'body-hyperlinks': False, + 'body-images': False, + 'body-markup': False, + 'icon-multi': False, + 'icon-static': False, + 'sound': False, + 'image/svg+xml': False, + 'append': False} + + caps = pynotify.get_server_caps () + if caps is None: + sys.stderr.write(_("Failed to receive pynotify server capabilities.") + "\n") + sys.exit (1) + + for cap in caps: + capabilities[cap] = True + + info = pynotify.get_server_info() + + def usingVolumeMonitor(self): + """ + Returns True if programs needs to use gnomevfs volume monitor + """ + + return (self.prefs.device_autodetection or + (self.prefs.backup_images and + self.prefs.backup_device_autodetection + )) + + + def startVolumeMonitor(self): + if not self.volumeMonitor: + self.volumeMonitor = VMonitor(self) + + def displayBackupVolumes(self): + """ + Create a message to be displayed to the user showing which backup volumes will be used + """ + message = '' + + paths = self.backupVolumes.keys() + i = 0 + v = len(paths) + prefix = '' + for b in paths: + if v > 1: + if i < (v -1) and i > 0: + prefix = ', ' + elif i == (v - 1) : + prefix = " " + _("and") + " " + i += 1 + message = "%s%s'%s'" % (message, prefix, self.backupVolumes[b].get_name()) + + if v > 1: + message = _("Using backup devices") + " %s" % message + elif v == 1: + message = _("Using backup device") + " %s" % message + else: + message = _("No backup devices detected") + + return message + + def searchForPsd(self): + """ + Check to see if user preferences are to automatically search for Portable Storage Devices or not + """ + return self.prefs.device_autodetection_psd and self.prefs.device_autodetection + + + def isGProxyShadowMount(self, gvfsVolume): + + """ gvfs GProxyShadowMount are used for camera specific things, not the data in the memory card """ + if using_gio: + #FIXME: this is a hack, but what is the correct function? + return str(type(gvfsVolume)).find('GProxyShadowMount') >= 0 + else: + return False + + def workerHasThisPath(self, path): + havePath= False + for w in workers.getNonFinishedWorkers(): + if w.cardMedia.path == path: + havePath = True + break + return havePath + + def on_volume_mounted(self, monitor, mount): + """ + callback run when gnomevfs indicates a new volume + has been mounted + """ + + if self.usingVolumeMonitor(): + volume = Volume(mount) + path = volume.get_path() + if not self.isGProxyShadowMount(mount): + self._printDetectedDevice(volume.get_name(limit=0), path) + + isBackupVolume = self.checkIfBackupVolume(path) + + if isBackupVolume: + backupPath = os.path.join(path, self.prefs.backup_identifier) + if path not in self.backupVolumes: + self.backupVolumes[backupPath] = volume + self.rapid_statusbar.push(self.statusbar_context_id, self.displayBackupVolumes()) + + elif media.isImageMedia(path) or self.searchForPsd(): + cardMedia = CardMedia(path, volume, True) + i = workers.getNextThread_id() + + self._printAutoStart(self.prefs.auto_download_upon_device_insertion) + + workers.append(CopyPhotos(i, self, self.fileRenameLock, self.fileSequenceLock, self.statsLock, + self.downloadStats, self.prefs.auto_download_upon_device_insertion, + cardMedia)) + + + self.setDownloadButtonSensitivity() + self.startScan() + + + def on_volume_unmounted(self, monitor, volume): + """ + callback run when gnomevfs indicates a volume + has been unmounted + """ + volume = Volume(volume) + path = volume.get_path() + + # four scenarios - + # volume is waiting to be scanned + # the volume has been scanned but downloading has not yet started + # images are being downloaded from volume (it must be a messy unmount) + # images finished downloading from volume + + if path: + # first scenario + for w in workers.getReadyToStartWorkers(): + if w.cardMedia.volume == volume: + media_collection_treeview.removeCard(w.thread_id) + workers.disableWorker(w.thread_id) + # second scenario + for w in workers.getReadyToDownloadWorkers(): + if w.cardMedia.volume == volume: + media_collection_treeview.removeCard(w.thread_id) + workers.disableWorker(w.thread_id) + + # fourth scenario - nothing to do + + # remove backup volumes + backupPath = os.path.join(path, self.prefs.backup_identifier) + if backupPath in self.backupVolumes: + del self.backupVolumes[backupPath] + self.rapid_statusbar.push(self.statusbar_context_id, self.displayBackupVolumes()) + + + + def clearCompletedDownloads(self): + """ + clears the display of completed downloads + """ + + for w in workers.getFinishedWorkers(): + media_collection_treeview.removeCard(w.thread_id) + + + + + def clearNotStartedDownloads(self): + """ + Clears the display of the download and instructs the thread not to run + """ + + for w in workers.getNotDownloadingWorkers(): + media_collection_treeview.removeCard(w.thread_id) + workers.disableWorker(w.thread_id) + + def checkIfBackupVolume(self, path): + """ + Checks to see if backups are enabled and path represents a valid backup location + + Checks against user preferences. + """ + if self.prefs.backup_images: + if self.prefs.backup_device_autodetection: + if media.isBackupMedia(path, self.prefs.backup_identifier): + return True + elif path == self.prefs.backup_location: + # user manually specified the path + return True + return False + + def _printDetectedDevice(self, volume_name, path): + cmd_line (_("Detected %(device)s with path %(path)s") % {'device': volume_name, 'path': path}) + + def _printAutoStart(self, autoStart): + if autoStart: + cmd_line(_("Automatically start download is true") ) + else: + cmd_line(_("Automatically start download is false") ) + + def setupAvailableImageAndBackupMedia(self, onStartup, onPreferenceChange, doNotAllowAutoStart): + """ + Creates a list of CardMedia + + onStartup should be True if the program is still starting, i.e. this is being called from the + program's initialization. + + onPreferenceChange should be True if this is being called as the result of a preference + bring changed + + Removes any image media that are currently not downloaded, + or finished downloading + """ + + self.clearNotStartedDownloads() + + cardMediaList = [] + self.backupVolumes = {} + + if not workers.noDownloadingWorkers(): + self.downloadStats.clear() + self._resetDownloadInfo() + + if self.usingVolumeMonitor(): + # either using automatically detected backup devices + # or image devices + + for v in self.volumeMonitor.get_mounts(): + volume = Volume(v) + path = volume.get_path(avoid_gnomeVFS_bug = True) + + if path: + if not self.isGProxyShadowMount(v): + self._printDetectedDevice(volume.get_name(limit=0), path) + isBackupVolume = self.checkIfBackupVolume(path) + if isBackupVolume: + backupPath = os.path.join(path, self.prefs.backup_identifier) + self.backupVolumes[backupPath] = volume + elif self.prefs.device_autodetection and (media.isImageMedia(path) or self.searchForPsd()): + cardMediaList.append(CardMedia(path, volume, True)) + + + if not self.prefs.device_autodetection: + # user manually specified the path from which to download images + path = self.prefs.device_location + if path: + cmd_line(_("Using manually specified path") + " %s" % path) + cardMedia = CardMedia(path, None, True) + cardMediaList.append(cardMedia) + + if self.prefs.backup_images: + if not self.prefs.backup_device_autodetection: + # user manually specified backup location + self.backupVolumes[self.prefs.backup_location] = None + self.rapid_statusbar.push(self.statusbar_context_id, '') + else: + self.rapid_statusbar.push(self.statusbar_context_id, self.displayBackupVolumes()) + + else: + self.rapid_statusbar.push(self.statusbar_context_id, '') + + # add each memory card / other device to the list of threads + j = workers.getNextThread_id() + + if doNotAllowAutoStart: + autoStart = False + else: + autoStart = (not onPreferenceChange) and ((self.prefs.auto_download_at_startup and onStartup) or (self.prefs.auto_download_upon_device_insertion and not onStartup)) + + self._printAutoStart(autoStart) + + for i in range(j, j + len(cardMediaList)): + cardMedia = cardMediaList[i - j] + workers.append(CopyPhotos(i, self, self.fileRenameLock, self.fileSequenceLock, self.statsLock, + self.downloadStats, autoStart, cardMedia)) + + + self.setDownloadButtonSensitivity() + self.startScan() + + def _setupDownloadbutton(self): + + self.download_hbutton_box = gtk.HButtonBox() + self.download_button_is_download = True + self.download_button = gtk.Button() + self.download_button.set_use_underline(True) + self._set_download_button() + self.download_button.connect('clicked', self.on_download_button_clicked) + self.download_hbutton_box.set_layout(gtk.BUTTONBOX_START) + self.download_hbutton_box.pack_start(self.download_button) + self.download_hbutton_box.show_all() + self.buttons_hbox.pack_start(self.download_hbutton_box, + padding=hd.WINDOW_BORDER_SPACE) + + self.setDownloadButtonSensitivity() + + + def set_display_thumbnails(self, value): + if value: + self.image_scrolledwindow.show_all() + else: + self.image_scrolledwindow.hide() + + + def _resetDownloadInfo(self): + self.markSet = False + self.startTime = None + self.totalDownloadSize = self.totalDownloadedSoFar = 0 + self.totalDownloadSizeThisRun = self.totalDownloadedSoFarThisRun = 0 + self.timeRemaining.clear() + + def addToTotalDownloadSize(self, size): + self.totalDownloadSize += size + + def setOverallDownloadMark(self): + if not self.markSet: + self.markSet = True + self.totalDownloadSizeThisRun = self.totalDownloadSize - self.totalDownloadedSoFar + self.totalDownloadedSoFarThisRun = 0 + + self.startTime = time.time() + self.timeStatusBarUpdated = self.startTime + + self.timeMark = self.startTime + self.sizeMark = 0 + + def startOrResumeWorkers(self): + + # resume any paused workers + for w in workers.getPausedDownloadingWorkers(): + w.startStop() + self.timeRemaining.setTimeMark(w) + + #start any new workers + workers.startDownloadingWorkers() + + if is_beta and verbose: + workers.printWorkerStatus() + + + def updateOverallProgress(self, thread_id, imageSize, percentComplete): + """ + Updates progress bar and status bar text with time remaining + to download images + """ + + self.totalDownloadedSoFar += imageSize + self.totalDownloadedSoFarThisRun += imageSize + + fraction = self.totalDownloadedSoFar / float(self.totalDownloadSize) + + self.download_progressbar.set_fraction(fraction) + + if percentComplete == 100.0: + self.menu_clear.set_sensitive(True) + self.timeRemaining.remove(thread_id) + + if self.downloadComplete(): + # finished all downloads + self.rapid_statusbar.push(self.statusbar_context_id, "") + self.download_button_is_download = True + self._set_download_button() + self.setDownloadButtonSensitivity() + cmd_line(_("All downloads complete")) + if is_beta and verbose: + workers.printWorkerStatus() + + else: + now = time.time() + self.timeRemaining.update(thread_id, imageSize) + + if now > (self.downloadTimeGap + self.timeMark): + amtTime = now - self.timeMark + self.timeMark = now + amtDownloaded = self.totalDownloadedSoFarThisRun - self.sizeMark + self.sizeMark = self.totalDownloadedSoFarThisRun + amtToDownload = float(self.totalDownloadSizeThisRun) - self.totalDownloadedSoFarThisRun + downloadSpeed = "%1.1f" % (amtDownloaded / 1048576 / amtTime) +_("MB/s") + self.speed_label.set_text(downloadSpeed) + + timeRemaining = self.timeRemaining.timeRemaining() + if timeRemaining: + secs = int(timeRemaining) + + if secs == 0: + message = "" + elif secs == 1: + message = _("About 1 second remaining") + elif secs < 60: + message = _("About %i seconds remaining") % secs + elif secs == 60: + message = _("About 1 minute remaining") + else: + # Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed. + # '%(minutes)i' and '%(seconds)02i' should not be modified or left out. They are used to format and display the amount + # of time the download has remainging, e.g. 'About 5:36 minutes remaining' + message = _("About %(minutes)i:%(seconds)02i minutes remaining") % {'minutes': secs / 60, 'seconds': secs % 60} + + self.rapid_statusbar.push(self.statusbar_context_id, message) + + + def resetSequences(self): + if self.downloadComplete(): + sequences.reset(self.prefs.getDownloadsToday(), self.prefs.stored_sequence_no) + + def notifyUserAllDownloadsComplete(self): + """ Possibly notify the user all downloads are complete using libnotify + + Reset progress bar info""" + + if self.downloadComplete(): + if self.displayDownloadSummaryNotification: + message = _("All downloads complete") + message += "\n%s " % self.downloadStats.noImagesDownloaded + _("images downloaded") + if self.downloadStats.noImagesSkipped: + message = "%s\n%s " % (message, self.downloadStats.noImagesSkipped) + _("images skipped") + if self.downloadStats.noWarnings: + message = "%s\n%s " % (message, self.downloadStats.noWarnings) + _("warnings") + if self.downloadStats.noErrors: + message = "%s\n%s " % (message, self.downloadStats.noErrors) +_("errors") + n = pynotify.Notification(PROGRAM_NAME, message) + n.show() + self.displayDownloadSummaryNotification = False # don't show it again unless needed + self.downloadStats.clear() + self._resetDownloadInfo() + self.speed_label.set_text(' ') + + def exitOnDownloadComplete(self): + if self.downloadComplete(): + if self.prefs.auto_exit: + if not (self.downloadStats.noErrors or self.downloadStats.noWarnings): + self.quit() + + def downloadComplete(self): + return self.totalDownloadedSoFar == self.totalDownloadSize + + def setDownloadButtonSensitivity(self): + + isSensitive = workers.noReadyToDownloadWorkers() > 0 or workers.noDownloadingWorkers() > 0 + + if isSensitive: + self.download_button.props.sensitive = True + self.menu_download_pause.props.sensitive = True + else: + self.download_button.props.sensitive = False + self.menu_download_pause.props.sensitive = False + + return isSensitive + + + def on_rapidapp_destroy(self, widget): + """Called when the application is going to quit""" + workers.quitAllWorkers() + + self.flushevents() + + display_queue.close("w") + + + def on_menu_clear_activate(self, widget): + self.clearCompletedDownloads() + widget.set_sensitive(False) + + def on_menu_report_problem_activate(self, widget): + webbrowser.open("https://bugs.launchpad.net/rapid") + + def on_menu_get_help_online_activate(self, widget): + webbrowser.open("http://www.damonlynch.net/rapid/help.html") + + def on_menu_donate_activate(self, widget): + webbrowser.open("http://www.damonlynch.net/rapid/donate.html") + + def on_menu_translate_activate(self, widget): + webbrowser.open("http://www.damonlynch.net/rapid/translate.html") + + def on_menu_preferences_activate(self, widget): + """ Sets preferences for the application using dialog window """ + + PreferencesDialog(self) + + def on_menu_log_window_toggled(self, widget): + active = widget.get_active() + self.prefs.show_log_dialog = active + if active: + log_dialog.widget.show() + else: + log_dialog.widget.hide() + + def on_menu_display_thumbnails_toggled(self, check_button): + self.prefs.display_thumbnails = check_button.get_active() + + def on_menu_about_activate(self, widget): + """ Display about dialog box """ + + about = gtk.glade.XML(paths.share_dir(config.GLADE_FILE), "about").get_widget("about") + about.set_property("name", PROGRAM_NAME) + about.set_property("version", __version__) + about.run() + about.destroy() + + def _set_download_button(self): + """ + Sets download button to appropriate state + """ + if self.download_button_is_download: + # This text will be displayed to the user on the Download / Pause button. + # Please note the space at the end of the label - it is needed to meet the Gnome Human Interface Guidelines + self.download_button.set_label(_("_Download ")) + self.download_button.set_image(gtk.image_new_from_stock( + gtk.STOCK_CONVERT, + gtk.ICON_SIZE_BUTTON)) + else: + # button should indicate paused state + self.download_button.set_image(gtk.image_new_from_stock( + gtk.STOCK_MEDIA_PAUSE, + gtk.ICON_SIZE_BUTTON)) + # This text will be displayed to the user on the Download / Pause button. + self.download_button.set_label(_("_Pause") + " ") + + def on_menu_download_pause_activate(self, widget): + self.on_download_button_clicked(widget) + + def startScan(self): + if workers.noReadyToStartWorkers() > 0: + workers.startWorkers() + + + + def postStartDownloadTasks(self): + if workers.noDownloadingWorkers() > 1: + self.displayDownloadSummaryNotification = True + + # set button to display Pause + self.download_button_is_download = False + self._set_download_button() + + def startDownload(self): + self.startOrResumeWorkers() + self.postStartDownloadTasks() + + def pauseDownload(self): + for w in workers.getDownloadingWorkers(): + w.startStop() + # set button to display Download + if not self.download_button_is_download: + self.download_button_is_download = True + self._set_download_button() + + def on_download_button_clicked(self, widget): + """ + Handle download button click. + + Button is in one of two states: download, or pause. + + If download, a click indicates to start or resume a download run. + If pause, a click indicates to pause all running downloads. + """ + if self.download_button_is_download: + self.startDownload() + else: + self.pauseDownload() + + def on_preference_changed(self, key, value): + + if key == 'display_thumbnails': + self.set_display_thumbnails(value) + elif key == 'show_log_dialog': + self.menu_log_window.set_active(value) + elif key in ['device_autodetection', 'device_autodetection_psd', 'backup_images', 'device_location', + 'backup_device_autodetection', 'backup_location' ]: + if self.usingVolumeMonitor(): + self.startVolumeMonitor() + cmd_line("\n" + _("Preferences were changed.")) + self.setupAvailableImageAndBackupMedia(onStartup = False, onPreferenceChange = True, doNotAllowAutoStart = False) + if is_beta and verbose: + print "Current worker status:" + workers.printWorkerStatus() + + def on_error_eventbox_button_press_event(self, widget, event): + self.prefs.show_log_dialog = True + log_dialog.widget.show() + +class VMonitor: + """ Transistion to gvfs from gnomevfs""" + def __init__(self, app): + self.app = app + if using_gio: + self.vmonitor = gio.volume_monitor_get() + self.vmonitor.connect("mount-added", self.app.on_volume_mounted) + self.vmonitor.connect("mount-removed", self.app.on_volume_unmounted) + else: + self.vmonitor = gnomevfs.VolumeMonitor() + self.vmonitor.connect("volume-mounted", self.app.on_volume_mounted) + self.vmonitor.connect("volume-unmounted", self.app.on_volume_unmounted) + + + def get_mounts(self): + if using_gio: + return self.vmonitor.get_mounts() + else: + return self.vmonitor.get_mounted_volumes() + +class Volume: + """ Transistion to gvfs from gnomevfs""" + def __init__(self, volume): + self.volume = volume + + def get_name(self, limit=config.MAX_LENGTH_DEVICE_NAME): + if using_gio: + v = self.volume.get_name() + else: + v = self.volume.get_display_name() + + if limit: + if len(v) > limit: + v = v[:limit] + '...' + return v + + def get_path(self, avoid_gnomeVFS_bug = False): + if using_gio: + path = self.volume.get_root().get_path() + else: + uri = self.volume.get_activation_uri() + path = None + if avoid_gnomeVFS_bug: + # ugly hack to work around bug where gnomevfs.get_local_path_from_uri(uri) causes a crash + mediaLocation = "file://" + config.MEDIA_LOCATION + if uri.find(mediaLocation) == 0: + path = gnomevfs.get_local_path_from_uri(uri) + else: + path = gnomevfs.get_local_path_from_uri(uri) + return path + + def unmount(self, callback): + self.volume.unmount(callback) + +class DownloadStats: + def __init__(self): + self.clear() + + def adjust(self, size, noImagesDownloaded, noImagesSkipped, noWarnings, noErrors): + self.downloadSize += size + self.noImagesDownloaded += noImagesDownloaded + self.noImagesSkipped += noImagesSkipped + self.noWarnings += noWarnings + self.noErrors += noErrors + + def clear(self): + self.noImagesDownloaded = self.noImagesSkipped = 0 + self.downloadSize = 0 + self.noWarnings = self.noErrors = 0 + +class TimeForDownload: + pass + +class TimeRemaining: + gap = 2 + def __init__(self): + self.clear() + + def add(self, w, size): + if w not in self.times: + t = TimeForDownload() + t.timeRemaining = None + t.size = size + t.downloaded = 0 + t.sizeMark = 0 + t.timeMark = time.time() + self.times[w] = t + + def update(self, w, size): + if w in self.times: + self.times[w].downloaded += size + now = time.time() + tm = self.times[w].timeMark + amtTime = now - tm + if amtTime > self.gap: + self.times[w].timeMark = now + amtDownloaded = self.times[w].downloaded - self.times[w].sizeMark + self.times[w].sizeMark = self.times[w].downloaded + timefraction = amtDownloaded / amtTime + amtToDownload = float(self.times[w].size) - self.times[w].downloaded + self.times[w].timeRemaining = amtToDownload / timefraction + + def _timeEstimates(self): + for t in self.times: + yield self.times[t].timeRemaining + + def timeRemaining(self): + return max(self._timeEstimates()) + + def setTimeMark(self, w): + if w in self.times: + self.times[w].timeMark = time.time() + + def clear(self): + self.times = {} + + def remove(self, w): + if w in self.times: + del self.times[w] + +def programStatus(): + print _("Goodbye") + + +def start (): + global is_beta + is_beta = config.version.find('~b') > 0 + + parser = OptionParser(version= "%%prog %s" % config.version) + parser.set_defaults(verbose=is_beta, extensions=False) + # Translators: this text is displayed to the user when they request information on the command line options. + # The text %default should not be modified or left out. + parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help=_("display program information on the command line as the program runs (default: %default)")) + parser.add_option("-q", "--quiet", action="store_false", dest="verbose", help=_("only output errors to the command line")) + # image file extensions are recognized RAW files plus TIFF and JPG + parser.add_option("-e", "--extensions", action="store_true", dest="extensions", help=_("list image file extensions the program recognizes and exit")) + (options, args) = parser.parse_args() + global verbose + verbose = options.verbose + + if verbose: + atexit.register(programStatus) + + if options.extensions: + exts = config.RAW_FILE_EXTENSIONS + config.NON_RAW_IMAGE_FILE_EXTENSIONS + v = '' + for e in exts[:-1]: + v += '%s, ' % e.upper() + v = v[:-1] + ' '+ (_('and %s') % exts[-1].upper()) + print v + sys.exit(0) + + if using_gio: + cmd_line(_("Using") + " GIO") + else: + # Which volume management code is being used (GIO or GnomeVFS) + cmd_line(_("Using") + " GnomeVFS") + + gdk.threads_init() + display_queue.open("rw") + tube.tube_add_watch(display_queue, updateDisplay) + gdk.threads_enter() + + # run only a single instance of the application + bus = dbus.SessionBus () + request = bus.request_name (config.DBUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE) + if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS: + app = RapidApp (bus, '/', config.DBUS_NAME) + else: + # this application is already running + print _("%s is already running") % PROGRAM_NAME + object = bus.get_object (config.DBUS_NAME, "/") + app = dbus.Interface (object, config.DBUS_NAME) + + app.start() + gdk.threads_leave() + +if __name__ == "__main__": + start() diff --git a/rapid/renamesubfolderprefs.py b/rapid/renamesubfolderprefs.py new file mode 100644 index 0000000..9804c59 --- /dev/null +++ b/rapid/renamesubfolderprefs.py @@ -0,0 +1,1470 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007, 2008, 2009 Damon Lynch <damonlynch@gmail.com> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" Define and test preferences for use in PlusMinus tables. + +These are displayed to the user as a series of rows in the user +preferences dialog window. + +Preferences for subfolders and image renaming are defined below +in dictionaries and lists. This makes it easier for checking validity and +creating combo boxes. + +There are 3 levels: 0, 1 and 2, which specify the depth of the pref value. +Level 0 is the topmost level, and corresponds to the first entry in the +row of preferences the user sees in the preferences dialog window. + +Custom exceptions are defined to handle invalid preferences. + +The user's actual preferences, on the other hand, are stored in flat lists. +Each list has members which are a multiple of 3 in length. +Each group of 3 members is equal to one line of preferences in the plus minus +table. +""" +#needed for python 2.5, unneeded for python 2.6 +from __future__ import with_statement + +import string + +import os +import re +import sys + +import gtk.gdk as gdk + +try: + import pygtk + pygtk.require("2.0") +except: + pass +try: + import gtk +except: + sys.exit(1) + +from common import Configi18n +global _ +_ = Configi18n._ + +import datetime + +import ValidatedEntry +import config + +from common import pythonifyVersion + +# Special key in each dictionary which specifies the order of elements. +# It is very important to have a consistent and rational order when displaying +# these prefs to the user, and dictionaries are unsorted. + +ORDER_KEY = "__order__" + +# PLEASE NOTE: these values are duplicated in a dummy class whose function +# is to have them put into the translation template. If you change the values below +# then change the value in class i18TranslateMeThanks as well!! Thanks!! + +# *** Level 0 +DATE_TIME = 'Date time' +TEXT = 'Text' +FILENAME = 'Filename' +METADATA = 'Metadata' +SEQUENCES = 'Sequences' + +SEPARATOR = os.sep + +# *** Level 1 + +# Date time +IMAGE_DATE = 'Image date' +TODAY = 'Today' +YESTERDAY = 'Yesterday' + +# File name +NAME_EXTENSION = 'Name + extension' +NAME = 'Name' +EXTENSION = 'Extension' +IMAGE_NUMBER = 'Image number' + +# Metadata +APERTURE = 'Aperture' +ISO = 'ISO' +EXPOSURE_TIME = 'Exposure time' +FOCAL_LENGTH = 'Focal length' +CAMERA_MAKE = 'Camera make' +CAMERA_MODEL = 'Camera model' +SHORT_CAMERA_MODEL = 'Short camera model' +SHORT_CAMERA_MODEL_HYPHEN = 'Hyphenated short camera model' +SERIAL_NUMBER = 'Serial number' +SHUTTER_COUNT = 'Shutter count' +OWNER_NAME = 'Owner name' + +#Image sequences +DOWNLOAD_SEQ_NUMBER = 'Downloads today' +SESSION_SEQ_NUMBER = 'Session number' +SUBFOLDER_SEQ_NUMBER = 'Subfolder number' +STORED_SEQ_NUMBER = 'Stored number' + +SEQUENCE_LETTER = 'Sequence letter' + + + +# *** Level 2 + +# Image number +IMAGE_NUMBER_ALL = 'All digits' +IMAGE_NUMBER_1 = 'Last digit' +IMAGE_NUMBER_2 = 'Last 2 digits' +IMAGE_NUMBER_3 = 'Last 3 digits' +IMAGE_NUMBER_4 = 'Last 4 digits' + + +# Case +ORIGINAL_CASE = "Original Case" +UPPERCASE = "UPPERCASE" +LOWERCASE = "lowercase" + +# Sequence number +SEQUENCE_NUMBER_1 = "One digit" +SEQUENCE_NUMBER_2 = "Two digits" +SEQUENCE_NUMBER_3 = "Three digits" +SEQUENCE_NUMBER_4 = "Four digits" +SEQUENCE_NUMBER_5 = "Five digits" +SEQUENCE_NUMBER_6 = "Six digits" + + +# Now, define dictionaries and lists of valid combinations of preferences. + +# Level 2 + +# Date + +SUBSECONDS = 'Subseconds' + +# ****** note if changing LIST_DATE_TIME_L2, update the default subfolder preference below :D ***** +LIST_DATE_TIME_L2 = ['YYYYMMDD', 'YYYY-MM-DD','YYMMDD', 'YY-MM-DD', + 'MMDDYYYY', 'MMDDYY', 'MMDD', + 'DDMMYYYY', 'DDMMYY', 'YYYY', 'YY', + 'MM', 'DD', + 'HHMMSS', 'HHMM'] + +LIST_IMAGE_DATE_TIME_L2 = LIST_DATE_TIME_L2 + [SUBSECONDS] + +DEFAULT_SUBFOLDER_PREFS = [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[9], '/', '', '', DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]] + +class i18TranslateMeThanks: + """ this class is never used in actual running code + It's purpose is to have these values inserted into the program's i18n template file + + """ + def __init__(self): + _('Date time') + _('Text') + _('Filename') + _('Metadata') + _('Sequences') + _('Image date') + _('Today') + _('Yesterday') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Name + extension') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Name') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Extension') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Image number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Aperture') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('ISO') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Exposure time') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Focal length') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Camera make') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Camera model') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Short camera model') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Hyphenated short camera model') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Serial number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Shutter count') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamemetadata + _('Owner name') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Downloads today') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Session number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Subfolder number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequencenumbers + _('Stored number') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#sequenceletters + _('Sequence letter') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('All digits') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last digit') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last 2 digits') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last 3 digits') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamefilename + _('Last 4 digits') + # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization + _("Original Case") + # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization + _("UPPERCASE") + # Translators: please not the capitalization of this text, and keep it the same if your language features capitalization + _("lowercase") + _("One digit") + _("Two digits") + _("Three digits") + _("Four digits") + _("Five digits") + _("Six digits") + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('Subseconds') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYYYMMDD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYYY-MM-DD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYMMDD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YY-MM-DD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MMDDYYYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MMDDYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MMDD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('DDMMYYYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('DDMMYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YYYY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('YY') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('MM') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('DD') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HHMMSS') + # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime + _('HHMM') + + +# Convenience values for python datetime conversion using values in +# LIST_DATE_TIME_L2. Obviously the two must remain synchronized. + +DATE_TIME_CONVERT = ['%Y%m%d', '%Y-%m-%d','%y%m%d', '%y-%m-%d', + '%m%d%Y', '%m%d%y', '%m%d', + '%d%m%Y', '%d%m%y', '%Y', '%y', + '%m', '%d', + '%H%M%S', '%H%M'] + + +LIST_IMAGE_NUMBER_L2 = [IMAGE_NUMBER_ALL, IMAGE_NUMBER_1, IMAGE_NUMBER_2, + IMAGE_NUMBER_3, IMAGE_NUMBER_4] + + +LIST_CASE_L2 = [ORIGINAL_CASE, UPPERCASE, LOWERCASE] + +LIST_SEQUENCE_LETTER_L2 = [ + UPPERCASE, + LOWERCASE + ] + + + +LIST_SEQUENCE_NUMBERS_L2 = [ + SEQUENCE_NUMBER_1, + SEQUENCE_NUMBER_2, + SEQUENCE_NUMBER_3, + SEQUENCE_NUMBER_4, + SEQUENCE_NUMBER_5, + SEQUENCE_NUMBER_6, + ] + + + +LIST_SHUTTER_COUNT_L2 = [ + SEQUENCE_NUMBER_3, + SEQUENCE_NUMBER_4, + SEQUENCE_NUMBER_5, + SEQUENCE_NUMBER_6, + ] + +# Level 1 +LIST_DATE_TIME_L1 = [IMAGE_DATE, TODAY, YESTERDAY] + +DICT_DATE_TIME_L1 = { + IMAGE_DATE: LIST_IMAGE_DATE_TIME_L2, + TODAY: LIST_DATE_TIME_L2, + YESTERDAY: LIST_DATE_TIME_L2, + ORDER_KEY: LIST_DATE_TIME_L1 + } + + +LIST_FILENAME_L1 = [NAME_EXTENSION, NAME, EXTENSION, IMAGE_NUMBER] + +DICT_FILENAME_L1 = { + NAME_EXTENSION: LIST_CASE_L2, + NAME: LIST_CASE_L2, + EXTENSION: LIST_CASE_L2, + IMAGE_NUMBER: LIST_IMAGE_NUMBER_L2, + ORDER_KEY: LIST_FILENAME_L1 + } + + + +LIST_SUBFOLDER_FILENAME_L1 = [EXTENSION] + +DICT_SUBFOLDER_FILENAME_L1 = { + EXTENSION: LIST_CASE_L2, + ORDER_KEY: LIST_SUBFOLDER_FILENAME_L1 +} + +LIST_METADATA_L1 = [APERTURE, ISO, EXPOSURE_TIME, FOCAL_LENGTH, + CAMERA_MAKE, CAMERA_MODEL, + SHORT_CAMERA_MODEL, + SHORT_CAMERA_MODEL_HYPHEN, + SERIAL_NUMBER, + SHUTTER_COUNT, + OWNER_NAME] + +DICT_METADATA_L1 = { + APERTURE: None, + ISO: None, + EXPOSURE_TIME: None, + FOCAL_LENGTH: None, + CAMERA_MAKE: LIST_CASE_L2, + CAMERA_MODEL: LIST_CASE_L2, + SHORT_CAMERA_MODEL: LIST_CASE_L2, + SHORT_CAMERA_MODEL_HYPHEN: LIST_CASE_L2, + SERIAL_NUMBER: None, + SHUTTER_COUNT: LIST_SHUTTER_COUNT_L2, + OWNER_NAME: LIST_CASE_L2, + ORDER_KEY: LIST_METADATA_L1 + } + + +LIST_SEQUENCE_L1 = [ + DOWNLOAD_SEQ_NUMBER, + STORED_SEQ_NUMBER, + SESSION_SEQ_NUMBER, + SEQUENCE_LETTER + ] + +DICT_SEQUENCE_L1 = { + DOWNLOAD_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, + STORED_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, + SESSION_SEQ_NUMBER: LIST_SEQUENCE_NUMBERS_L2, + SEQUENCE_LETTER: LIST_SEQUENCE_LETTER_L2, + ORDER_KEY: LIST_SEQUENCE_L1 + } + + +# Level 0 + + +LIST_IMAGE_RENAME_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, + SEQUENCES] + + +DICT_IMAGE_RENAME_L0 = { + DATE_TIME: DICT_DATE_TIME_L1, + TEXT: None, + FILENAME: DICT_FILENAME_L1, + METADATA: DICT_METADATA_L1, + SEQUENCES: DICT_SEQUENCE_L1, + ORDER_KEY: LIST_IMAGE_RENAME_L0 + } + +LIST_SUBFOLDER_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, SEPARATOR] + +DICT_SUBFOLDER_L0 = { + DATE_TIME: DICT_DATE_TIME_L1, + TEXT: None, + FILENAME: DICT_SUBFOLDER_FILENAME_L1, + METADATA: DICT_METADATA_L1, + SEPARATOR: None, + ORDER_KEY: LIST_SUBFOLDER_L0 + } + +# preference elements that require metadata +# note there is no need to specify lower level elements if a higher level +# element is necessary for them to be present to begin with +METADATA_ELEMENTS = [METADATA, IMAGE_DATE] + +# preference elements that are sequence numbers or letters +SEQUENCE_ELEMENTS = [ + DOWNLOAD_SEQ_NUMBER, + SESSION_SEQ_NUMBER, + SUBFOLDER_SEQ_NUMBER, + STORED_SEQ_NUMBER, + SEQUENCE_LETTER] + +# preference elements that do not require metadata and are not fixed +# as above, there is no need to specify lower level elements if a higher level +# element is necessary for them to be present to begin with +DYNAMIC_NON_METADATA_ELEMENTS = [ + TODAY, YESTERDAY, + FILENAME] + SEQUENCE_ELEMENTS + + + +#the following is what the preferences looked in older versions of the program +#they are here for reference, and for checking the validity of preferences + +USER_INPUT = 'User' + +DOWNLOAD_SEQ_NUMBER_V_0_0_8_B7 = 'Downloads today' +SESSION_SEQ_NUMBER_V_0_0_8_B7 = 'Session sequence number' +SUBFOLDER_SEQ_NUMBER_V_0_0_8_B7 = 'Subfolder sequence number' +STORED_SEQ_NUMBER_V_0_0_8_B7 = 'Stored sequence number' +SEQUENCE_LETTER_V_0_0_8_B7 = 'Sequence letter' + +LIST_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7 = [ + SEQUENCE_NUMBER_1, + SEQUENCE_NUMBER_2, + SEQUENCE_NUMBER_3, + SEQUENCE_NUMBER_4, + SEQUENCE_NUMBER_5, + SEQUENCE_NUMBER_6, + ] + +DICT_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7 = { + SEQUENCE_NUMBER_1: None, + SEQUENCE_NUMBER_2: None, + SEQUENCE_NUMBER_3: None, + SEQUENCE_NUMBER_4: None, + SEQUENCE_NUMBER_5: None, + SEQUENCE_NUMBER_6: None, + ORDER_KEY: LIST_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7 + } + +LIST_SEQUENCE_L1_V_0_0_8_B7 = [USER_INPUT] + +DICT_SEQUENCE_L1_V_0_0_8_B7 = { + USER_INPUT: DICT_SEQUENCE_NUMBERS_L1_L2_V_0_0_8_B7, + ORDER_KEY: LIST_SEQUENCE_L1_V_0_0_8_B7 + } + +LIST_SEQUENCE_LETTER_L1_L1_V_0_0_8_B7 = [ + UPPERCASE, + LOWERCASE + ] + +DICT_SEQUENCE_LETTER_L1_V_0_0_8_B7 = { + UPPERCASE: None, + LOWERCASE: None, + ORDER_KEY: LIST_SEQUENCE_LETTER_L1_L1_V_0_0_8_B7 + } + +LIST_IMAGE_RENAME_L0_V_0_0_8_B7 = [DATE_TIME, TEXT, FILENAME, METADATA, + DOWNLOAD_SEQ_NUMBER_V_0_0_8_B7, + SESSION_SEQ_NUMBER_V_0_0_8_B7, + SEQUENCE_LETTER_V_0_0_8_B7] + +DICT_IMAGE_RENAME_L0_V_0_0_8_B7 = { + DATE_TIME: DICT_DATE_TIME_L1, + TEXT: None, + FILENAME: DICT_FILENAME_L1, + METADATA: DICT_METADATA_L1, + DOWNLOAD_SEQ_NUMBER_V_0_0_8_B7: None, + SESSION_SEQ_NUMBER_V_0_0_8_B7: None, + SEQUENCE_LETTER_V_0_0_8_B7: DICT_SEQUENCE_LETTER_L1_V_0_0_8_B7, + ORDER_KEY: LIST_IMAGE_RENAME_L0_V_0_0_8_B7 + } + +PREVIOUS_IMAGE_RENAME= { + '0.0.8~b7': DICT_IMAGE_RENAME_L0_V_0_0_8_B7, + } + + +# Functions to work with above data + +def _getPrevPrefs(oldDefs, currentDefs, previousVersion): + k = oldDefs.keys() + # if there were other defns, we'd need to figure out which one + # but currently, there are no others + # there will be in future, and this code wil be updated then + version_change = pythonifyVersion(k[0]) + if pythonifyVersion(previousVersion) <= version_change: + return oldDefs[k[0]] + else: + return currentDefs + +def _upgradePreferencesToCurrent(prefs, previousVersion): + """ checks to see if preferences should be upgraded + + returns True if they were upgraded, and the new prefs + + VERY IMPORTANT: the new prefs will be a new list, not an inplace + modification of the existing preferences! Otherwise, the check on + assignment in the prefs.py __setattr__ will not work as expected!! + """ + upgraded = False + # code to upgrade from <= 0.0.8~b7 to >= 0.0.8~b8 + p = [] + for i in range(0, len(prefs), 3): + if prefs[i] in [SEQUENCE_LETTER_V_0_0_8_B7, SESSION_SEQ_NUMBER_V_0_0_8_B7]: + upgraded = True + p.append(SEQUENCES) + if prefs[i] == SEQUENCE_LETTER_V_0_0_8_B7: + p.append(SEQUENCE_LETTER) + p.append(prefs[i+1]) + else: + p.append(SESSION_SEQ_NUMBER) + p.append(prefs[i+2]) + else: + p += prefs[i:i+3] + + assert(len(prefs)==len(p)) + return (upgraded, p) + + +def upgradePreferencesToCurrent(imageRenamePrefs, subfolderPrefs, previousVersion): + """Upgrades user preferences to current version + + returns True if the preferences were upgraded""" + + # only check image rename, for now.... + upgraded, imageRenamePrefs = _upgradePreferencesToCurrent(imageRenamePrefs, previousVersion) + return (upgraded, imageRenamePrefs , subfolderPrefs) + +def checkPreferencesForValidity(imageRenamePrefs, subfolderPrefs, version=config.version): + """Returns true if the passed in preferences are valid""" + + if version == config.version: + try: + checkPreferenceValid(DICT_SUBFOLDER_L0, subfolderPrefs) + checkPreferenceValid(DICT_IMAGE_RENAME_L0, imageRenamePrefs) + except: + return False + return True + else: + defn = _getPrevPrefs(PREVIOUS_IMAGE_RENAME, DICT_IMAGE_RENAME_L0, version) + try: + checkPreferenceValid(defn, imageRenamePrefs) + checkPreferenceValid(DICT_SUBFOLDER_L0, subfolderPrefs) + except: + return False + return True + +def checkPreferenceValid(prefDefinition, prefs, modulo=3): + """ + Checks to see if prefs are valid according to definition. + + prefs is a list of preferences. + prefDefinition is a Dict specifying what is valid. + modulo is how many list elements are equivalent to one line of preferences. + + Returns True if prefs match with prefDefinition, + else raises appropriate error. + """ + + if (len(prefs) % modulo <> 0) or not prefs: + raise PrefLengthError(prefs) + else: + for i in range(0, len(prefs), modulo): + _checkPreferenceValid(prefDefinition, prefs[i:i+modulo]) + + return True + +def _checkPreferenceValid(prefDefinition, prefs): + + key = prefs[0] + value = prefs[1] + + + if prefDefinition.has_key(key): + + nextPrefDefinition = prefDefinition[key] + + if value == None: + # value should never be None, at any time + raise PrefValueInvalidError((None, nextPrefDefinition)) + + if nextPrefDefinition and not value: + raise PrefValueInvalidError((value, nextPrefDefinition)) + + if type(nextPrefDefinition) == type({}): + return _checkPreferenceValid(nextPrefDefinition, prefs[1:]) + else: + if type(nextPrefDefinition) == type([]): + result = value in nextPrefDefinition + if not result: + raise PrefValueInvalidError((value, nextPrefDefinition)) + return True + elif not nextPrefDefinition: + return True + else: + result = nextPrefDefinition == value + if not result: + raise PrefKeyValue((value, nextPrefDefinition)) + return True + else: + raise PrefKeyError((key, prefDefinition[ORDER_KEY])) + +def filterSubfolderPreferences(prefList): + """ + Filters out extraneous preference choices + """ + prefs_changed = False + continueCheck = True + while continueCheck and prefList: + continueCheck = False + if prefList[0] == SEPARATOR: + # Subfolder preferences should not start with a / + prefList = prefList[3:] + prefs_changed = True + continueCheck = True + elif prefList[-3] == SEPARATOR: + # Subfolder preferences should not end with a / + prefList = prefList[:-3] + continueCheck = True + prefs_changed = True + else: + for i in range(0, len(prefList) - 3, 3): + if prefList[i] == SEPARATOR and prefList[i+3] == SEPARATOR: + # Subfolder preferences should not contain two /s side by side + continueCheck = True + prefs_changed = True + # note we are messing with the contents of the pref list, + # must exit loop and try again + prefList = prefList[:i] + prefList[i+3:] + break + + return (prefs_changed, prefList) + + +class PrefError(Exception): + """ base class """ + def unpackList(self, l): + s = '' + for i in l: + if i <> ORDER_KEY: + s += "'" + i + "', " + return s[:-2] + + def __str__(self): + return self.msg + +class PrefKeyError(PrefError): + def __init__(self, error): + value = error[0] + expectedValues = self.unpackList(error[1]) + self.msg = _("Preference key '%(key)s' is invalid.\nExpected one of %(value)s") % { + 'key': value, 'value': expectedValues} + + +class PrefValueInvalidError(PrefKeyError): + def __init__(self, error): + value = error[0] + self.msg = _("Preference value '%(value)s' is invalid") % {'value': value} + +class PrefLengthError(PrefError): + def __init__(self, error): + self.msg = _("These preferences are not well formed:") % self.unpackList(error) + "\n %s" + +class PrefValueKeyComboError(PrefError): + def __init__(self, error): + self.msg = error + + +def convertDateForStrftime(dateTimeUserChoice): + try: + return DATE_TIME_CONVERT[LIST_DATE_TIME_L2.index(dateTimeUserChoice)] + except: + raise PrefValueInvalidError(dateTimeUserChoice) + + +class Comboi18n(gtk.ComboBox): + """ very simple i18n version of the venerable combo box + with one column displayed to the user. + + This combo box has two columns: + 1. the first contains the actual value and is invisible + 2. the second contains the translation of the first column, and this is what + the users sees + """ + def __init__(self): + liststore = gtk.ListStore(str, str) + gtk.ComboBox.__init__(self, liststore) + cell = gtk.CellRendererText() + self.pack_start(cell, True) + self.add_attribute(cell, 'text', 1) + + def append_text(self, text): + model = self.get_model() + model.append((text, _(text))) + + def get_active_text(self): + model = self.get_model() + active = self.get_active() + if active < 0: + return None + return model[active][0] + +class ImageRenamePreferences: + def __init__(self, prefList, parent, fileSequenceLock=None, sequences=None): + """ + Exception raised if preferences are invalid. + + This should be caught by calling class.""" + + self.parent = parent + self.prefList = prefList + + # use variables for determining sequence numbers + # there are two possibilities: + # 1. this code is being called while being run from within a copy photos process + # 2. it's being called from within the preferences dialog window + + self.fileSequenceLock = fileSequenceLock + self.sequences = sequences + + # derived classes will have their own definitions, do not overwrite + if not hasattr(self, "prefsDefnL0"): + self.prefsDefnL0 = DICT_IMAGE_RENAME_L0 + self.defaultPrefs = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE] + self.defaultRow = self.defaultPrefs + self.stripForwardSlash = True + + + + def checkPrefsForValidity(self): + """ + Checks image preferences validity + """ + + return checkPreferenceValid(self.prefsDefnL0, self.prefList) + + def formatPreferencesForPrettyPrint(self): + """ returns a string useful for printing the preferences""" + + v = '' + + for i in range(0, len(self.prefList), 3): + s = "%s: " % self.prefList[i] + if self.prefList[i+1]: + s = "%s%s" % (s, self.prefList[i+1]) + if self.prefList[i+2]: + s = "%s (%s)" % (s, self.prefList[i+2]) + + v += s + "\n" + return v + + + def _getDateComponent(self): + """ + Returns portion of new image / subfolder name based on date time + """ + + problem = None + if self.L1 == IMAGE_DATE: + if self.L2 == SUBSECONDS: + d = self.photo.subSeconds() + problem = _("Subsecond metadata not present in image") + else: + d = self.photo.dateTime(missing=None) + problem = _("%s metadata is not present in image") % self.L1.lower() + elif self.L1 == TODAY: + d = datetime.datetime.now() + elif self.L1 == YESTERDAY: + delta = datetime.timedelta(days = 1) + d = datetime.datetime.now() - delta + else: + raise("Date options invalid") + + if d: + if self.L2 <> SUBSECONDS: + try: + return (d.strftime(convertDateForStrftime(self.L2)), None) + except: + v = '' + problem = _('Error in date time component. Value %s appears invalid') % d + return (v, problem) + else: + return (d, None) + else: + return ('', problem) + + def _getFilenameComponent(self): + """ + Returns portion of new image / subfolder name based on the file name + """ + + name, extension = os.path.splitext(self.existingFilename) + problem = None + + if self.L1 == NAME_EXTENSION: + filename = self.existingFilename + elif self.L1 == NAME: + filename = name + elif self.L1 == EXTENSION: + if extension: + if not self.stripInitialPeriodFromExtension: + # keep the period / dot of the extension, so the user does not + # need to manually specify it + filename = extension + else: + # having the period when this is used as a part of a subfolder name + # is a bad idea! + filename = extension[1:] + else: + filename = "" + problem = _("extension was specified but image name has no extension") + elif self.L1 == IMAGE_NUMBER: + n = re.search("(?P<image_number>[0-9]+$)", name) + if not n: + problem = _("image number was specified but image filename has no number") + else: + image_number = n.group("image_number") + + if self.L2 == IMAGE_NUMBER_ALL: + filename = image_number + elif self.L2 == IMAGE_NUMBER_1: + filename = image_number[-1] + elif self.L2 == IMAGE_NUMBER_2: + filename = image_number[-2:] + elif self.L2 == IMAGE_NUMBER_3: + filename = image_number[-3:] + elif self.L2 == IMAGE_NUMBER_4: + filename = image_number[-4:] + else: + raise TypeError("Incorrect filename option") + + if self.L2 == UPPERCASE: + filename = filename.upper() + elif self.L2 == LOWERCASE: + filename = filename.lower() + + return (filename, problem) + + def _getMetadataComponent(self): + """ + Returns portion of new image / subfolder name based on the metadata + + Note: date time metadata found in _getDateComponent() + """ + + problem = None + if self.L1 == APERTURE: + v = self.photo.aperture() + elif self.L1 == ISO: + v = self.photo.iso() + elif self.L1 == EXPOSURE_TIME: + v = self.photo.exposureTime(alternativeFormat=True) + elif self.L1 == FOCAL_LENGTH: + v = self.photo.focalLength() + elif self.L1 == CAMERA_MAKE: + v = self.photo.cameraMake() + elif self.L1 == CAMERA_MODEL: + v = self.photo.cameraModel() + elif self.L1 == SHORT_CAMERA_MODEL: + v = self.photo.shortCameraModel() + elif self.L1 == SHORT_CAMERA_MODEL_HYPHEN: + v = self.photo.shortCameraModel(includeCharacters = "\-") + elif self.L1 == SERIAL_NUMBER: + v = self.photo.cameraSerial() + elif self.L1 == SHUTTER_COUNT: + v = self.photo.shutterCount() + if v: + v = int(v) + padding = LIST_SHUTTER_COUNT_L2.index(self.L2) + 3 + formatter = '%0' + str(padding) + "i" + v = formatter % v + + elif self.L1 == OWNER_NAME: + v = self.photo.ownerName() + else: + raise TypeError("Invalid metadata option specified") + if self.L1 in [CAMERA_MAKE, CAMERA_MODEL, SHORT_CAMERA_MODEL, + SHORT_CAMERA_MODEL_HYPHEN, OWNER_NAME]: + if self.L2 == UPPERCASE: + v = v.upper() + elif self.L2 == LOWERCASE: + v = v.lower() + if not v: + if self.L1 <> ISO: + md = self.L1.lower() + else: + md = ISO + problem = _("%s metadata is not present in image") % md + return (v, problem) + + + def _formatSequenceNo(self, value, amountToPad): + padding = LIST_SEQUENCE_NUMBERS_L2.index(amountToPad) + 1 + formatter = '%0' + str(padding) + "i" + return formatter % value + + + def _calculateLetterSequence(self, sequence): + + def _letters(x): + """ + Adapted from algorithm at http://en.wikipedia.org/wiki/Hexavigesimal + """ + v = '' + while x > 25: + r = x % 26 + x= x / 26 - 1 + v = string.lowercase[r] + v + v = string.lowercase[x] + v + + return v + + + v = _letters(sequence) + if self.L2 == UPPERCASE: + v = v.upper() + + return v + + def _getSubfolderSequenceNo(self): + """ + Add a sequence number to the filename + + * Sequence numbering is per subfolder + * Assume the user might actually have a (perhaps odd) reason to have more + than one subfolder sequence number in the same file name + """ + + problem = None + self.subfolderSeqNoInstanceInFilename += 1 + + if self.downloadSubfolder: + subfolder = self.downloadSubfolder + str(self.subfolderSeqNoInstanceInFilename) + else: + subfolder = "__subfolder__" + str(self.subfolderSeqNoInstanceInFilename) + + if self.fileSequenceLock: + with self.fileSequenceLock: + v = self.sequenceNos.calculate(subfolder) + v = self.formatSequenceNo(v, self.L1) + else: + v = self.sequenceNos.calculate(subfolder) + v = self.formatSequenceNo(v, self.L1) + + return (v, problem) + + def _getSessionSequenceNo(self): + problem = None + v = self._formatSequenceNo(self.sequences.getSessionSequenceNoUsingCounter(self.sequenceCounter), self.L2) + return (v, problem) + + def _getDownloadsTodaySequenceNo(self): + problem = None + + v = self._formatSequenceNo(self.sequences.getDownloadsTodayUsingCounter(self.sequenceCounter), self.L2) + + return (v, problem) + + def _getStoredSequenceNo(self): + problem = None + v = self._formatSequenceNo(self.sequences.getStoredSequenceNoUsingCounter(self.sequenceCounter), self.L2) + + return (v, problem) + + def _getSequenceLetter(self): + + problem = None + v = self._calculateLetterSequence(self.sequences.getSequenceLetterUsingCounter(self.sequenceCounter)) + return (v, problem) + + def _getSequencesComponent(self): + problem = None + if self.L1 == DOWNLOAD_SEQ_NUMBER: + return self._getDownloadsTodaySequenceNo() + elif self.L1 == SESSION_SEQ_NUMBER: + return self._getSessionSequenceNo() + elif self.L1 == SUBFOLDER_SEQ_NUMBER: + return self._getSubfolderSequenceNo() + elif self.L1 == STORED_SEQ_NUMBER: + return self._getStoredSequenceNo() + elif self.L1 == SEQUENCE_LETTER: + return self._getSequenceLetter() + + def _getComponent(self): + try: + if self.L0 == DATE_TIME: + return self._getDateComponent() + elif self.L0 == TEXT: + return (self.L1, None) + elif self.L0 == FILENAME: + return self._getFilenameComponent() + elif self.L0 == METADATA: + return self._getMetadataComponent() + elif self.L0 == SEQUENCES: + return self._getSequencesComponent() + elif self.L0 == SEPARATOR: + return (os.sep, None) + except: + v = "" + problem = _("error generating name with component %s") % self.L2 + return (v, problem) + + def _getValuesFromList(self): + for i in range(0, len(self.prefList), 3): + yield (self.prefList[i], self.prefList[i+1], self.prefList[i+2]) + + + def _generateName(self, photo, existingFilename, stripCharacters, subfolder, stripInitialPeriodFromExtension, sequence): + self.photo = photo + self.existingFilename = existingFilename + self.stripInitialPeriodFromExtension = stripInitialPeriodFromExtension + + name = '' + problem = '' + + #the subfolder in which the image will be downloaded to + self.downloadSubfolder = subfolder + + self.sequenceCounter = sequence + + for self.L0, self.L1, self.L2 in self._getValuesFromList(): + v, p = self._getComponent() + if v: + name += v + if p: + problem += p + "; " + + if problem: + # remove final semicolon and space + problem = problem[:-2] + '.' + + if stripCharacters: + for c in r'\:*?"<>|': + name = name.replace(c, '') + + if self.stripForwardSlash: + name = name.replace('/', '') + + return (name, problem) + + def generateNameUsingPreferences(self, photo, existingFilename=None, + stripCharacters = False, subfolder=None, + stripInitialPeriodFromExtension=False, + sequencesPreliminary = True): + """ + Generate a filename for the photo in string format based on user prefs. + + Returns a tuple of two strings: + - the name + - any problems generating the name. If blank, there were no problems + """ + + if self.sequences: + if sequencesPreliminary: + sequence = self.sequences.getPrelimSequence() + else: + sequence = self.sequences.getFinalSequence() + else: + sequence = 0 + + return self._generateName(photo, existingFilename, stripCharacters, subfolder, + stripInitialPeriodFromExtension, sequence) + + def generateNameSequencePossibilities(self, photo, existingFilename, + stripCharacters=False, subfolder=None, + stripInitialPeriodFromExtension=False): + + """ Generates the possible image names using the sequence numbers / letter possibilities""" + + for sequence in self.sequences.getSequencePossibilities(): + yield self._generateName(photo, existingFilename, stripCharacters , subfolder, + stripInitialPeriodFromExtension, sequence) + + def filterPreferences(self): + """ + Filters out extraneous preference choices + Expected to be implemented in derived classes when needed + """ + pass + + def needImageMetaDataToCreateUniqueName(self): + """ + Returns True if an image's metadata is essential to properly generate a unique image name + + Image names should be unique. Some images may not have metadata. If + only non-dynamic components make up the rest of an image name + (e.g. text specified by the user), then relying on metadata will likely + produce duplicate names. + + File extensions are not considered dynamic. + + This is NOT a general test to see if unique filenames can be generated. It is a test + to see if an image's metadata is needed. + """ + hasMD = hasDynamic = False + + for e in METADATA_ELEMENTS: + if e in self.prefList: + hasMD = True + break + + if hasMD: + for e in DYNAMIC_NON_METADATA_ELEMENTS: + if e in self.prefList: + if e == FILENAME and (NAME_EXTENSION in self.prefList or + NAME in self.prefList or + IMAGE_NUMBER in self.prefList): + hasDynamic = True + break + + if hasMD and not hasDynamic: + return True + else: + return False + + def usesSequenceElements(self): + """ Returns true if any sequence numbers or letters are used to generate the filename """ + + for e in SEQUENCE_ELEMENTS: + if e in self.prefList: + return True + + return False + + def usesTheSequenceElement(self, e): + """ Returns true if a stored sequence number is used to generate the filename """ + return e in self.prefList + + + def _createCombo(self, choices): + combobox = Comboi18n() + for text in choices: + combobox.append_text(text) + return combobox + + def getDefaultRow(self): + """ + returns a list of default widgets + """ + return self.getWidgetsBasedOnUserSelection(self.defaultRow) + + def _getPreferenceWidgets(self, prefDefinition, prefs, widgets): + key = prefs[0] + value = prefs[1] + + # supply a default value if the user has not yet chosen a value! + if not key: + key = prefDefinition[ORDER_KEY][0] + + if not key in prefDefinition: + raise PrefKeyError((key, prefDefinition.keys())) + + + list0 = prefDefinition[ORDER_KEY] + + # the first widget will always be a combo box + widget0 = self._createCombo(list0) + widget0.set_active(list0.index(key)) + + widgets.append(widget0) + + if key == TEXT: + widget1 = gtk.Entry() + widget1.set_text(value) + + widgets.append(widget1) + widgets.append(None) + return + elif key == SEPARATOR: + widgets.append(None) + widgets.append(None) + return + else: + nextPrefDefinition = prefDefinition[key] + if type(nextPrefDefinition) == type({}): + return self._getPreferenceWidgets(nextPrefDefinition, + prefs[1:], + widgets) + else: + if type(nextPrefDefinition) == type([]): + widget1 = self._createCombo(nextPrefDefinition) + if not value: + value = nextPrefDefinition[0] + try: + widget1.set_active(nextPrefDefinition.index(value)) + except: + raise PrefValueInvalidError((value, nextPrefDefinition)) + + widgets.append(widget1) + else: + widgets.append(None) + + def getWidgetsBasedOnPreferences(self): + """ + Yields a list of widgets and their callbacks based on the users preferences. + + This list is equivalent to one row of preferences when presented to the + user in the Plus Minus Table. + """ + + for L0, L1, L2 in self._getValuesFromList(): + prefs = [L0, L1, L2] + widgets = [] + self._getPreferenceWidgets(self.prefsDefnL0, prefs, widgets) + yield widgets + + + def getWidgetsBasedOnUserSelection(self, selection): + """ + Returns a list of widgets and their callbacks based on what the user has selected. + + Selection is the values the user has chosen thus far in comboboxes. + It determines the contents of the widgets returned. + It should be a list of three values, with None for values not chosen. + For values which are None, the first value in the preferences + definition is chosen. + + """ + widgets = [] + + self._getPreferenceWidgets(self.prefsDefnL0, selection, widgets) + return widgets + +class SubfolderPreferences(ImageRenamePreferences): + def __init__(self, prefList, parent): + self.prefsDefnL0 = DICT_SUBFOLDER_L0 + self.defaultPrefs = DEFAULT_SUBFOLDER_PREFS + self.defaultRow = [DATE_TIME, IMAGE_DATE, LIST_DATE_TIME_L2[0]] + self.stripForwardSlash = False + ImageRenamePreferences.__init__(self, prefList, parent) + + def generateNameUsingPreferences(self, photo, existingFilename=None, + stripCharacters = False): + """ + Generate a filename for the photo in string format based on user prefs. + + Returns a tuple of two strings: + - the name + - any problems generating the name. If blank, there were no problems + """ + + subfolders, problem = ImageRenamePreferences.generateNameUsingPreferences( + self, photo, + existingFilename, stripCharacters, stripInitialPeriodFromExtension=True) + # subfolder value must never start with a separator, or else any + # os.path.join function call will fail to join a subfolder to its + # parent folder + if subfolders: + if subfolders[0] == os.sep: + subfolders = subfolders[1:] + + return (subfolders, problem) + + def filterPreferences(self): + filtered, prefList = filterSubfolderPreferences(self.prefList) + if filtered: + self.prefList = prefList + + def needMetaDataToCreateUniqueName(self): + """ + Returns True if metadata is essential to properly generate subfolders + + This will be the case if the only components are metadata and separators + """ + + for e in self.prefList: + if (not e) and ((e not in METADATA_ELEMENTS) or (e <> SEPARATOR)): + return True + + return False + + + + + def checkPrefsForValidity(self): + """ + Checks subfolder preferences validity above and beyond image name checks. + + See parent method for full description. + + Subfolders have additional requirments to that of image names. + """ + v = ImageRenamePreferences.checkPrefsForValidity(self) + if v: + # peform additional checks: + # 1. do not start with a separator + # 2. do not end with a separator + # 3. do not have two separators in a row + # these three rules will ensure something else other than a + # separator is specified + L1s = [] + for i in range(0, len(self.prefList), 3): + L1s.append(self.prefList[i]) + + if L1s[0] == SEPARATOR: + raise PrefValueKeyComboError(_("Subfolder preferences should not start with a %s") % os.sep) + elif L1s[-1] == SEPARATOR: + raise PrefValueKeyComboError(_("Subfolder preferences should not end with a %s") % os.sep) + else: + for i in range(len(L1s) - 1): + if L1s[i] == SEPARATOR and L1s[i+1] == SEPARATOR: + raise PrefValueKeyComboError(_("Subfolder preferences should not contain two %s one after the other") % os.sep) + return v + + +class Sequences: + """ Holds sequence numbers and letters used in generating filenames""" + def __init__(self, downloadsToday, storedSequenceNo): + + + self.subfolderSequenceNo = {} + self.sessionSequenceNo = 1 + self.sequenceLetter = 0 + + self.setUseOfSequenceElements(False, False) + + self.assignedSequenceCounter = 1 + self.reset(downloadsToday, storedSequenceNo) + + def setUseOfSequenceElements(self, usesSessionSequenceNo, usesSequenceLetter): + self.usesSessionSequenceNo = usesSessionSequenceNo + self.usesSequenceLetter = usesSequenceLetter + + def reset(self, downloadsToday, storedSequenceNo): + self.downloadsToday = downloadsToday + self.downloadsTodayOffset = 0 + self.storedSequenceNo = storedSequenceNo + if self.usesSessionSequenceNo: + self.sessionSequenceNo = self.sessionSequenceNo + self.assignedSequenceCounter - 1 + if self.usesSequenceLetter: + self.sequenceLetter = self.sequenceLetter + self.assignedSequenceCounter - 1 + self.doNotAddToPool = False + self.pool = [] + self.poolSequenceCounter = 0 + self.assignedSequenceCounter = 1 + + def getPrelimSequence(self): + if self.doNotAddToPool: + self.doNotAddToPool = False + else: + # increment pool sequence number + self.poolSequenceCounter += 1 + self.pool.append(self.poolSequenceCounter) + + return self.poolSequenceCounter + + def getFinalSequence(self): + # get oldest queue value + # remove from queue or flag it should be removed + + return self.assignedSequenceCounter + + def getSequencePossibilities(self): + for i in self.pool: + yield i + + def getSessionSequenceNo(self): + return self.sessionSequenceNo + self.assignedSequenceCounter - 1 + + def getSessionSequenceNoUsingCounter(self, counter): + return self.sessionSequenceNo + counter - 1 + + def setSessionSequenceNo(self, value): + self.sessionSequenceNo = value + + def setStoredSequenceNo(self, value): + self.storedSequenceNo = value + + def getDownloadsTodayUsingCounter(self, counter): + return self.downloadsToday + counter - self.downloadsTodayOffset + + def setDownloadsToday(self, value): + self.downloadsToday = value + self.downloadsTodayOffset = self.assignedSequenceCounter - 1 + + def getStoredSequenceNoUsingCounter(self, counter): + return self.storedSequenceNo + counter + + def getSequenceLetterUsingCounter(self, counter): + return self.sequenceLetter + counter - 1 + + def imageCopyFailed(self): + self.doNotAddToPool = True + + def imageCopySucceeded(self): + self.increment() + + def increment(self, subfolder=None): + assert(self.assignedSequenceCounter == self.pool[0]) + self.assignedSequenceCounter += 1 + self.pool = self.pool[1:] + + + + +if __name__ == '__main__': + import sys + import os.path + from metadata import MetaData + + if False: + if (len(sys.argv) != 2): + print 'Usage: ' + sys.argv[0] + ' path/to/photo/containing/metadata' + sys.exit(1) + else: + p0 = [FILENAME, NAME_EXTENSION, ORIGINAL_CASE] + p1 = [FILENAME, NAME_EXTENSION, LOWERCASE] + p2 = [METADATA, APERTURE, None] + p3 = [FILENAME, IMAGE_NUMBER, IMAGE_NUMBER_ALL] + p4 = [METADATA, CAMERA_MODEL, ORIGINAL_CASE] + p5 = [TEXT, '-', None] + p6 = [TEXT, 'Job', None] + + p = [p0, p1, p2, p3, p4] + p = [p6 + p5 + p2 + p5 + p3] + + d0 = [DATE_TIME, IMAGE_DATE, 'YYYYMMDD'] + d1 = [DATE_TIME, IMAGE_DATE, 'HHMMSS'] + d2 = [DATE_TIME, IMAGE_DATE, SUBSECONDS] + + d = [d0 + d1 + d2] + + fullpath = sys.argv[1] + path, filename = os.path.split(fullpath) + + m = MetaData(fullpath) + m.readMetadata() + + for pref in p: + i = ImageRenamePreferences(pref, None) + print i.generateNameUsingPreferences(m, filename) + + for pref in d: + i = ImageRenamePreferences(pref, None) + print i.generateNameUsingPreferences(m, filename) + else: + prefs = [SEQUENCES, SESSION_SEQ_NUMBER, SEQUENCE_NUMBER_3] +# prefs = ['Filename2', NAME_EXTENSION, UPPERCASE] + print checkPreferenceValid(DICT_IMAGE_RENAME_L0, prefs) diff --git a/rapid/renamesubfolderprefstest.py b/rapid/renamesubfolderprefstest.py new file mode 100644 index 0000000..a689c5c --- /dev/null +++ b/rapid/renamesubfolderprefstest.py @@ -0,0 +1,190 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007, 2008, 2009 Damon Lynch <damonlynch@gmail.com> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest + +from renamesubfolderprefs import * + +class PreferenceTest (unittest.TestCase): + image_test = ( [TEXT, '', ''], + [DATE_TIME, IMAGE_DATE, 'YYYYMMDD'], + [METADATA, APERTURE, ''], + [FILENAME, NAME_EXTENSION, UPPERCASE], + ) + subfolder_test = ( [TEXT, '', ''], + [DATE_TIME, IMAGE_DATE, 'HHMM'], + [METADATA, SHORT_CAMERA_MODEL_HYPHEN, + LOWERCASE], + [SEPARATOR, '', ''], + [FILENAME, EXTENSION, LOWERCASE] + ) + + trueMetadataTest = ([FILENAME, EXTENSION, LOWERCASE, TEXT, '', '', METADATA, APERTURE, ''], [METADATA, APERTURE, '', TEXT, '', '', FILENAME, EXTENSION, LOWERCASE], ) + + falseMetadataTest = ([FILENAME, EXTENSION, LOWERCASE, METADATA, APERTURE, '', FILENAME, NAME, LOWERCASE], + [FILENAME, NAME, LOWERCASE, FILENAME, EXTENSION, LOWERCASE], + [FILENAME, NAME_EXTENSION, LOWERCASE, FILENAME, EXTENSION, LOWERCASE], + [FILENAME, NAME, LOWERCASE, FILENAME, METADATA, EXPOSURE_TIME, '', IMAGE_NUMBER, IMAGE_NUMBER_ALL, FILENAME, EXTENSION, LOWERCASE], ) + + sequences_test = ([SEQUENCES, SESSION_SEQ_NUMBER, SEQUENCE_NUMBER_3], + [FILENAME, NAME, LOWERCASE, SEQUENCES, DOWNLOAD_SEQ_NUMBER, SEQUENCE_NUMBER_1, + FILENAME, EXTENSION, UPPERCASE], + [METADATA, APERTURE, '', SEQUENCES, STORED_SEQ_NUMBER, SEQUENCE_NUMBER_5, + FILENAME, EXTENSION, UPPERCASE], ) + + def testPrefImageList(self): + for pref in self.image_test: + result = checkPreferenceValid(DICT_IMAGE_RENAME_L0, pref) + self.assertEqual(result, True) + + def testSequencesList(self): + for pref in self.sequences_test: + result = checkPreferenceValid(DICT_IMAGE_RENAME_L0, pref) + self.assertEqual(result, True) + + def testNeedImageMetaDataToCreateUniqueName(self): + for i in self.trueMetadataTest: + p = ImageRenamePreferences(i, None) + result = p.needImageMetaDataToCreateUniqueName() + self.assertEqual(result, True) + + for i in self.falseMetadataTest: + p = ImageRenamePreferences(i, None) + result = p.needImageMetaDataToCreateUniqueName() + self.assertEqual(result, False) + + + + def testLargePrefList(self): + prefList = [] + for pref in self.image_test: + for l in pref: + prefList.append(l) + + result = checkPreferenceValid(DICT_IMAGE_RENAME_L0, prefList) + self.assertEqual(result, True) + + def testPrefSubfolderList(self): + for pref in self.subfolder_test: + result = checkPreferenceValid(DICT_SUBFOLDER_L0, pref) + self.assertEqual(result, True) + + def testDateTimeL2Length(self): + self.assertEqual(len(LIST_DATE_TIME_L2), len(DATE_TIME_CONVERT)) + + def testDateTimeL2Conversion(self): + self.assertEqual(convertDateForStrftime('YY'), '%y') + + + +class BadPreferences(unittest.TestCase): + bad_image_key = ( [TEXT, '', '', + DATE_TIME, IMAGE_DATE, 'YYYYMMDD', + METADATA, APERTURE, '', + FILENAME, NAME_EXTENSION, UPPERCASE, + 'Filename2', NAME_EXTENSION, UPPERCASE], + ) + bad_image_value = ( [DATE_TIME, TODAY, IMAGE_NUMBER_ALL], + [METADATA, CAMERA_MAKE, IMAGE_NUMBER_4], + [DATE_TIME, IMAGE_DATE, None], + [DATE_TIME, IMAGE_DATE, ''], + [DATE_TIME, None, None], + [DATE_TIME, '', ''], + ) + + bad_subfolder_key = ([FILENAME, NAME_EXTENSION, UPPERCASE],) + + bad_subfolder_key2 = ( [TEXT, '', '', + DATE_TIME, IMAGE_DATE, 'HHMM', + METADATA, SHORT_CAMERA_MODEL_HYPHEN, + LOWERCASE, + SEPARATOR, '', '', + 'Filename-bad', EXTENSION, LOWERCASE], + ) + + bad_subfolder_value = ( [FILENAME, None, None], + [FILENAME, '', ''],) + + bad_length = ([], [DATE_TIME, TODAY], [DATE_TIME]) + + bad_dt_conversion = ('HHYY', 'YYSS') + + bad_subfolder_combos = ([SEPARATOR, '', ''], + [FILENAME, EXTENSION, UPPERCASE, + SEPARATOR, '', ''], + [FILENAME, EXTENSION, UPPERCASE, + SEPARATOR, '', '', + SEPARATOR, '', '', + FILENAME, EXTENSION, UPPERCASE + ], + [SEPARATOR, '', '', + SEPARATOR, '', '', + SEPARATOR, '', '', + SEPARATOR, '', '' + ] + ) + + def testBadImageKey(self): + for pref in self.bad_image_key: + self.assertRaises(PrefKeyError, checkPreferenceValid, + DICT_IMAGE_RENAME_L0, + pref) + + def testBadImageValue(self): + for pref in self.bad_image_value: + self.assertRaises(PrefValueInvalidError, checkPreferenceValid, + DICT_IMAGE_RENAME_L0, + pref) + + def testBadSubfolderKey(self): + for pref in self.bad_subfolder_key: + self.assertRaises(PrefKeyError, checkPreferenceValid, + DICT_SUBFOLDER_L0, + pref) + + for pref in self.bad_subfolder_key2: + self.assertRaises(PrefKeyError, checkPreferenceValid, + DICT_SUBFOLDER_L0, + pref) + + + def testBadSubfolderValue(self): + for pref in self.bad_subfolder_value: + self.assertRaises(PrefValueInvalidError, checkPreferenceValid, + DICT_SUBFOLDER_L0, + pref) + + def testBadLength(self): + for pref in self.bad_length: + self.assertRaises(PrefLengthError, checkPreferenceValid, + DICT_IMAGE_RENAME_L0, + pref) + def testBadDTConversion(self): + for pref in self.bad_dt_conversion: + self.assertRaises(PrefValueInvalidError, convertDateForStrftime, + pref) + + def testBadSubfolderCombo(self): + + for pref in self.bad_subfolder_combos: + s = SubfolderPreferences(pref, self) + self.assertRaises(PrefValueKeyComboError, s.checkPrefsForValidity) + +if __name__ == "__main__": + unittest.main() diff --git a/rapid/tableplusminus.py b/rapid/tableplusminus.py new file mode 100644 index 0000000..ab7e7c1 --- /dev/null +++ b/rapid/tableplusminus.py @@ -0,0 +1,284 @@ +#!/usr/bin/python +# -*- coding: latin1 -*- + +### Copyright (C) 2007, 2008, 2009 Damon Lynch <damonlynch@gmail.com> + +### This program is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. + +### This program is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. + +### You should have received a copy of the GNU General Public License +### along with this program; if not, write to the Free Software +### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# import gtk.gdk as gdk +import sys +try: + import pygtk + pygtk.require("2.0") +except: + pass +try: + import gtk +except: + sys.exit(1) + +import higdefaults as hd + +class TablePlusMinus(gtk.Table): + """ + A regular gtk table which allows users to add and delete rows to the table. + + Users add and delete rows by using plus and minus buttons. + The buttons (minus first) are in the two rightmost colums. + The user can never delete a table so it has no rows. + """ + + debug = False # if True, then debugging info for the developer is displayed + def __init__(self, rows=1, columns=1, homogeneous=False): + if not self.debug: + gtk.Table.__init__(self, rows, columns + 2, homogeneous) + self.extraCols = 2 # representing minus and plus buttons + else: + gtk.Table.__init__(self, rows, columns + 3, homogeneous) + self.extraCols = 3 # representing minus and plus buttons, and info label + + # no of columns NOT including the + and - buttons + self.pm_noColumns = columns + # how many rows there are in the gtk.Table + self.pm_noRows = rows + # list of widgets in the gtk.Table + self.pm_rows = [] + # dict of callback ids for minus and plus buttons + self.pm_callbacks = {} + + #spacing of controls + for i in range(columns-1): + self.set_col_spacing(i, hd.CONTROL_IN_TABLE_SPACE) + self.set_col_spacing(columns-1, hd.CONTROL_IN_TABLE_SPACE*2) + self.set_col_spacing(columns, hd.CONTROL_IN_TABLE_SPACE) + if self.debug: + self.set_col_spacing(columns+1, hd.CONTROL_IN_TABLE_SPACE) + self.set_row_spacings(hd.CONTROL_IN_TABLE_SPACE) + + def _setMinusButtonSensitivity(self): + button = self.pm_rows[0][self.pm_noColumns] + if len(self.pm_rows) == 1: + button.set_sensitive(False) + else: + button.set_sensitive(True) + + def _createMinusPlusButtons(self, rowPosition): + plus_button = gtk.Button() + plus_button.set_image(gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_MENU)) + self._createCallback(plus_button, rowPosition, 'clicked', self.on_plus_button_clicked) + minus_button = gtk.Button() + minus_button.set_image(gtk.image_new_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_MENU)) + self._createCallback(minus_button, rowPosition, 'clicked', self.on_minus_button_clicked) + + return minus_button, plus_button + + + + def append(self, row): + self.insertAfter(len(self.pm_rows)-1, row) + + + def _getMinusAndPlusButtonsForRow(self, rowPosition): + """ + Return as a tuple minus and plus buttons for the row specified by rowPosition + """ + return (self.pm_rows[rowPosition][self.pm_noColumns], self.pm_rows[rowPosition][self.pm_noColumns+1]) + + def removeRow(self, rowPosition): + # remove widgets from table + for col in range(self.pm_noColumns + self.extraCols): + widget = self.pm_rows[rowPosition][col] + if widget: + self.remove(widget) + if self.pm_callbacks.has_key(widget): + widget.disconnect(self.pm_callbacks[widget]) + del self.pm_callbacks[widget] + + + # reposition existing rows in gtk.Table + self._moveRows(-1, rowPosition + 1) + # remove row from list of rows + del self.pm_rows[rowPosition] + + self._setMinusButtonSensitivity() + self.pm_noRows -= 1 + self.resize(self.pm_noRows, self.pm_noColumns + self.extraCols) + self._printDebugInfo() + + + def _createCallback(self, widget, rowPosition, callbackType = None, callbackMethod=None): + if callbackType: + self.pm_callbacks[widget] = widget.connect(callbackType, callbackMethod, rowPosition) + else: + name = widget.get_name() + if name == 'GtkComboBox': + self.pm_callbacks[widget] = widget.connect("changed", self.on_combobox_changed, rowPosition) + elif name == 'GtkEntry': + self.pm_callbacks[widget] = widget.connect("changed", self.on_entry_changed, rowPosition) + + + def _moveRows(self, adjustment, startRow, endRow = -1): + """ + Moves gtk.Table rows up or down according to adjustment (which MUST be -1 or 1). + + Starts at row startRow and ends at row endRow. If endRow == -1, then goes to last row in table. + Readjusts callbacks. + """ + if endRow == -1: + endRow = len(self.pm_rows) + for r in range(startRow, endRow): + if self.debug: + print "Row %s becomes row %s" % (self.pm_rows[r][self.pm_noColumns + 2].get_label(), r + adjustment) + self.pm_rows[r][self.pm_noColumns + 2].set_label(str(r + adjustment)) + + for col in range(self.pm_noColumns + self.extraCols): + widget = self.pm_rows[r][col] + if widget: + self.remove(widget) + widget.disconnect(self.pm_callbacks[widget]) + self.attach(widget, col, col+1, r + adjustment, r + adjustment + 1) + if col == self.pm_noColumns: + self._createCallback(widget, r + adjustment, 'clicked', self.on_minus_button_clicked) + elif col == self.pm_noColumns + 1: + self._createCallback(widget, r + adjustment, 'clicked', self.on_plus_button_clicked) + else: + self._createCallback(widget, r + adjustment) + + + def _printDebugInfo(self): + if self.debug: + print "\nRows in internal list: %s\nTable rows: %s" % \ + (len(self.pm_rows), self.pm_noRows) + + if len(self.pm_rows) <> self.pm_noRows: + print "|\n\\\n --> Unequal no. of rows" + + + + def attach(self, child, left_attach, right_attach, top_attach, bottom_attach, xoptions=gtk.EXPAND|gtk.FILL, + yoptions=gtk.SHRINK, xpadding=0, ypadding=0): + """ + Override base class attach method, to allow automatic shrinking of minus and plus buttons + """ + if left_attach >= self.pm_noColumns and left_attach <= self.pm_noColumns + 1: + # since we are adding plus or minus button, shrink the button + gtk.Table.attach(self, child, left_attach, right_attach, top_attach, bottom_attach, gtk.SHRINK, gtk.SHRINK, xpadding, ypadding) + else: + gtk.Table.attach(self, child, left_attach, right_attach, top_attach, bottom_attach, xoptions, yoptions, xpadding, ypadding) + + + def insertAfter(self, rowPosition, row): + """ + Inserts row into the table at row following rowPosition + """ + + + #is table big enough? + self.checkTableRowsAndAdjust() + + #move (reattach) other widgets & readjust connect + self._moveRows(1, rowPosition + 1) + + # insert row + for col in range(self.pm_noColumns): + widget = row[col] + if widget: + self._createCallback(widget, rowPosition+1) + self.attach(widget, col, col+1, rowPosition+1, rowPosition+2) + + minus_button, plus_button = self._createMinusPlusButtons(rowPosition+1) + + row.append(minus_button) + row.append(plus_button) + self.attach(minus_button, self.pm_noColumns, self.pm_noColumns+1, rowPosition+1, rowPosition+2) + self.attach(plus_button, self.pm_noColumns+1, self.pm_noColumns+2, rowPosition+1, rowPosition+2) + + if self.debug: + label = gtk.Label(str(rowPosition+1)) + self.attach(label, self.pm_noColumns+2, self.pm_noColumns+3, rowPosition+1, rowPosition+2) + row.append(label) + + + for widget in row: + if widget: + widget.show() + + #adjust internal reference table + + self.pm_rows.insert(rowPosition + 1, row) + + self._setMinusButtonSensitivity() + + self._printDebugInfo() + + def checkTableRowsAndAdjust(self, noRowsToAdd=1, adjustRows=True): + noRowsOk = True + if len(self.pm_rows) + noRowsToAdd > self.pm_noRows: + if adjustRows: + extraRowsToAdd = len(self.pm_rows) + noRowsToAdd - self.pm_noRows + self.pm_noRows += extraRowsToAdd + self.resize(self.pm_noRows, self.pm_noColumns + self.extraCols) + else: + noRowsOk = False + return noRowsOk + + def getDefaultRow(self): + """ + Returns a list of default widgets to insert as a row into the table. + + Expected to be implemented in derived class. + """ + + return [None] * self.pm_noColumns + + def on_combobox_changed(self, widget, rowPosition): + """ + Callback for combobox that is expected to be implemented in derived class + """ + pass + + def on_entry_changed(self, widget, rowPosition): + """ + Callback for entry that is expected to be implemented in derived class + """ + pass + + def _debugButtonPressed(self, buttonText, rowPosition): + if self.debug: + t = datetime.datetime.now().strftime("%H:%M:%S") + print "\n****\n%s\n\n%s clicked at %s" %(t, buttonText, rowPosition) + + def on_minus_button_clicked(self, widget, rowPosition): + self._debugButtonPressed("Minus", rowPosition) + self.removeRow(rowPosition) + self.on_rowDeleted(rowPosition) + + def on_plus_button_clicked(self, widget, rowPosition): + self._debugButtonPressed("Plus", rowPosition) + self.insertAfter(rowPosition, self.getDefaultRow()) + self.on_rowAdded(rowPosition) + + def on_rowAdded(self, rowPosition): + """ + Expected to be implemented in derived class + """ + pass + + def on_rowDeleted(self, rowPosition): + """ + Expected to be implemented in derived class + """ + pass + |