summaryrefslogtreecommitdiff
path: root/rapid
diff options
context:
space:
mode:
Diffstat (limited to 'rapid')
-rw-r--r--rapid/AUTHORS1
-rw-r--r--rapid/COPYING339
-rw-r--r--rapid/ChangeLog462
-rw-r--r--rapid/INSTALL36
-rw-r--r--rapid/TODO26
-rw-r--r--rapid/ValidatedEntry.py383
-rw-r--r--rapid/__init__.py1
-rw-r--r--rapid/common.py181
-rw-r--r--rapid/config.py58
-rw-r--r--rapid/glade3/image-missing.svg94
-rw-r--r--rapid/glade3/rapid-photo-downloader-about.pngbin0 -> 8084 bytes
-rw-r--r--rapid/glade3/rapid.glade2905
-rw-r--r--rapid/gnomeglade.py166
-rw-r--r--rapid/higdefaults.py8
-rw-r--r--rapid/idletube.py205
-rwxr-xr-xrapid/media.py180
-rwxr-xr-xrapid/metadata.py403
-rw-r--r--rapid/misc.py45
-rw-r--r--rapid/paths.py40
-rw-r--r--rapid/prefs.py182
-rwxr-xr-xrapid/rapid.py3512
-rw-r--r--rapid/renamesubfolderprefs.py1524
-rw-r--r--rapid/renamesubfolderprefstest.py190
-rw-r--r--rapid/tableplusminus.py284
24 files changed, 11225 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..3a0914d
--- /dev/null
+++ b/rapid/ChangeLog
@@ -0,0 +1,462 @@
+Version 0.1.0 beta 2
+--------------------
+
+2009-11-22
+
+New feature: when detection of portable storage devices is selected, the program
+will prompt you whether or not to download from each device it automatically
+detects. You can choose whether the program should remember the choice you make
+every time it runs. This fixes bug #376020.
+
+Fixed bug #484432: error in adding job codes via the preferences dialog.
+
+Fixed bug #486886: Job code prompt can appear multiple times.
+
+Updated Hungarian and French translations.
+
+
+Version 0.1.0 beta 1
+--------------------
+
+2009-11-14
+
+This code is ready for full release, but given the magnitude of changes, a beta
+seems like a good idea, simply to catch any undetected bugs.
+
+Added a "Job codes" option. Like the "text" option in image and subfolder name
+generation, this allows you to specify text that will be placed into the file
+and subfolder names. However, unlike the "text" option, which requires that the
+text be directly entered via the program preferences, when using the "Job code"
+option, the program will prompt for it each time a download begins.
+
+Made Download button the default button. Hitting enter while the main window
+has focus will now start the download.
+
+Fixed bug #387002: added dependency in Ubuntu packages for librsvg2-common.
+Thanks go to user hasp for this fix.
+
+Fixed bug #478620: problem with corrupted image files. Thanks go to user Katrin
+Krieger for tracking this one down.
+
+Fixed bug #479424: some camera model names do not have numbers, but it still
+makes sense to return a shortened name. Thanks go to user Wesley Harp for
+highlighting this problem.
+
+Fixed bug #482831: program no longer crashes when auto-download is off, and a
+device is inserted before another download has completed.
+
+Added Czech translation by Tomas Novak.
+
+Added French translation by Julien Valroff, Michel Ange, and Cenwen.
+
+Added Hungarian translation by Balazs Oveges and Andras Lorincz.
+
+Added Slovak translation by Tomas Novak.
+
+Added Swedish translation by Ulf Urden and Michal Predotka.
+
+Added dependency on gnome-icon-theme in Ubuntu packages.
+
+Added additional hour, minute and second options in image renaming and subfolder
+creation. Thanks to Art Zemon for the patch.
+
+Malformed image date time exif values have are minimally checked to see if they
+can still be used for subfolder and image renaming. Some software programs seem
+to make a mess of them.
+
+Updated man page, including a bug fix by Julien Valroff.
+
+
+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..7972a01
--- /dev/null
+++ b/rapid/INSTALL
@@ -0,0 +1,36 @@
+Rapid Photo Downloader depends on the following software:
+
+- GNOME 2.18 or higher
+- GTK+ 2.10 or higher
+- Python 2.5 or 2.6
+- pygtk 2.10 or higher
+- python-gconf 2.18 or higher
+- python-glade2 2.10 or higher
+- gnome-python 2.10 or higher
+- libexiv2 0.15 or higher
+- pyexiv2 0.1.1 or higher
+
+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 = ("&&amp;", "<&lt;", ">&gt;")
+ 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..c970977
--- /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.1.0~b2'
+
+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
new file mode 100644
index 0000000..0aefb1d
--- /dev/null
+++ b/rapid/glade3/rapid-photo-downloader-about.png
Binary files differ
diff --git a/rapid/glade3/rapid.glade b/rapid/glade3/rapid.glade
new file mode 100644
index 0000000..860a54b
--- /dev/null
+++ b/rapid/glade3/rapid.glade
@@ -0,0 +1,2905 @@
+<?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">&lt;span weight="bold" size="x-large"&gt;Download Folder&lt;/span&gt;</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">&lt;i&gt;Example: /home/user/photos&lt;/i&gt;</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">&lt;b&gt;Download Subfolders&lt;/b&gt;</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">&lt;b&gt;Download Folder&lt;/b&gt;</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">&lt;span weight="bold" size="x-large"&gt;Image Rename&lt;/span&gt; </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">&lt;b&gt;Image Rename&lt;/b&gt;</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">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">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">&lt;i&gt;New:&lt;/i&gt;</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">&lt;i&gt;Original:&lt;/i&gt;</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">&lt;b&gt;Example&lt;/b&gt;</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">&lt;span weight="bold" size="x-large"&gt;Rename Options&lt;/span&gt;</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="reame_options_vbox">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkLabel" id="sequence_number_label">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="xpad">12</property>
+ <property name="label" translatable="yes">&lt;b&gt;Sequence Numbers&lt;/b&gt;</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="sequence_number_hbox">
+ <property name="visible">True</property>
+ <property name="spacing">12</property>
+ <child>
+ <widget class="GtkLabel" id="spacer_seq_label">
+ <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="seq_vbox">
+ <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 &lt;i&gt;Downloads today&lt;/i&gt; 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="compatibility_label">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="xpad">12</property>
+ <property name="label" translatable="yes">&lt;b&gt;Compatibility with Other Operating Systems&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkHBox" id="compatibility_hbox">
+ <property name="visible">True</property>
+ <property name="spacing">12</property>
+ <child>
+ <widget class="GtkLabel" id="compatibility_spacer_label">
+ <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">2</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>
+ </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">4</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="job_codes_tab">
+ <property name="visible">True</property>
+ <property name="spacing">12</property>
+ <child>
+ <widget class="GtkVBox" id="job_codes_header_vbox">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkHBox" id="hbox188">
+ <property name="visible">True</property>
+ <property name="spacing">6</property>
+ <child>
+ <widget class="GtkImage" id="image77">
+ <property name="visible">True</property>
+ <property name="icon_name">emblem-photos</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label1340">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">&lt;span weight="bold" size="x-large"&gt;Job Codes&lt;/span&gt;</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="hseparator44">
+ <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="job_codes_vbox">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkVBox" id="job_code_vbox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <widget class="GtkLabel" id="job_code_label">
+ <property name="xalign">0</property>
+ <property name="xpad">12</property>
+ <property name="label" translatable="yes">&lt;b&gt;Job Codes&lt;/b&gt;</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="job_code_hbox">
+ <property name="visible">True</property>
+ <property name="spacing">12</property>
+ <child>
+ <widget class="GtkLabel" id="job_code_spacer_label">
+ <property name="visible">True</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkScrolledWindow" id="job_code_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="shadow_type">in</property>
+ <child>
+ <widget class="GtkTreeView" id="job_code_treeview">
+ <property name="width_request">250</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">False</property>
+ <property name="rubber_banding">True</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkHBox" id="job_code_button_hbox">
+ <property name="visible">True</property>
+ <child>
+ <widget class="GtkVButtonBox" id="job_code_vbuttonbox">
+ <property name="visible">True</property>
+ <property name="spacing">12</property>
+ <property name="layout_style">start</property>
+ <child>
+ <widget class="GtkButton" id="add_job_code_button">
+ <property name="label">gtk-add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_add_job_code_button_clicked"/>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="remove_job_code_button">
+ <property name="label">gtk-remove</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="on_remove_job_code_button_clicked"/>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkButton" id="clear_job_code_button">
+ <property name="label" translatable="yes" comments="The underscore after the C signifies that the l is the accelerator key. This is the standard 'Clear' button, but I needed to change the accelerator from the standard 'c' to 'l' because the close button also used 'c'">C_lear</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ <signal name="clicked" handler="on_clear_job_code_button_clicked"/>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="job_codes_tab_label">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Job Codes</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="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">&lt;span weight="bold" size="x-large"&gt;Image Devices&lt;/span&gt;</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">&lt;b&gt;Image Devices&lt;/b&gt;</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.
+
+&lt;i&gt;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.&lt;/i&gt;</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">4</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">4</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">&lt;span weight="bold" size="x-large"&gt;Backup&lt;/span&gt; </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">&lt;b&gt;Backup&lt;/b&gt;</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.
+
+&lt;i&gt;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.&lt;/i&gt;</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">&lt;i&gt;Example:&lt;/i&gt;</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">&lt;i&gt;/media/externaldrive/Photos&lt;/i&gt;</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">5</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">5</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">&lt;span weight="bold" size="x-large"&gt;Automation&lt;/span&gt;</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">&lt;b&gt;Program Automation&lt;/b&gt;</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">6</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">6</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">&lt;span weight="bold" size="x-large"&gt;Error Handling&lt;/span&gt;</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">&lt;b&gt;Image Name Conflicts&lt;/b&gt;</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">&lt;b&gt;Missing Backup Devices&lt;/b&gt;</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">7</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">7</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 &lt;damonlynch@gmail.com&gt;</property>
+ <property name="translator_credits" comments="TRANSLATORS: Replace this string with your names, one name per line.">Michel Ange &lt;michelange@wanadoo.fr&gt;
+Martin Egger &lt;martin.egger@gmx.net&gt;
+L&#x151;rincz Andr&#xE1;s &lt;level.andrasnak@gmail.com&gt;
+Jose Luis Navarro &lt;jlnavarro111@gmail.com&gt;
+Tomas Novak &lt;kuvaly@seznam.cz&gt;
+Abel O'Rian &lt;abel.orian@gmail.com&gt;
+Balazs Oveges
+Daniel Paessler &lt;daniel@paessler.org&gt;
+Michal Predotka &lt;mpredotka@googlemail.com&gt;
+Luca Reverberi &lt;thereve@gmail.com&gt;
+Mikko Ruohola &lt;polarfox@polarfox.net&gt;
+Sergei Sedov &lt;sedov@webmail.perm.ru&gt;
+Marco Solari &lt;marcosolari@gmail.com&gt;
+Ulf Urd&#xE9;n &lt;ulf.urden@purplescout.com&gt;
+Julien Valroff &lt;julien@kirya.net&gt;
+
+</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="GtkHBox" id="hbox2">
+ <property name="visible">True</property>
+ <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">0</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="GtkVSeparator" id="warning_vseparator">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ </widget>
+ <packing>
+ <property name="expand">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>
+ <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">2</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <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>
+ <property name="orientation">vertical</property>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">4</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">5</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..def73e6
--- /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 or gio volume, see class Volume in rapid.py
+ """
+
+ 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 or gio volume, see class Volume in rapid.py
+ """
+ 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..1d4b2df
--- /dev/null
+++ b/rapid/metadata.py
@@ -0,0 +1,403 @@
+#!/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 occurrence of a digit in the
+ camera model, including all alphaNumeric characters before and after
+ that digit up till a non-alphanumeric character, but with these interventions:
+
+ 1. Canon "Mark" designations are shortened prior to conversion.
+ 2. Names like "Canon EOS DIGITAL REBEL XSi" do not have a number and must
+ and treated differently (see below)
+
+ 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
+ Canon EOS DIGITAL REBEL XSi -> XSi
+ Canon EOS Digital Rebel XS -> XS
+ Canon EOS Digital Rebel XTi -> XTi
+ Canon EOS Kiss Digital X -> Digital
+ Canon EOS Digital Rebel XT -> XT
+ EOS Kiss Digital -> Digital
+ Canon Digital IXUS Wireless -> Wireless
+ Canon Digital IXUS i zoom -> zoom
+ Canon EOS Kiss Digital N -> N
+ Canon Digital IXUS IIs -> IIs
+ IXY Digital L -> L
+ Digital IXUS i -> i
+ IXY Digital -> Digital
+ Digital IXUS -> IXUS
+
+ 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 last word 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:
+ head, space, model = m.strip().rpartition(' ')
+ return model
+ 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..e270a6a
--- /dev/null
+++ b/rapid/rapid.py
@@ -0,0 +1,3512 @@
+#!/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
+
+job_code = None
+need_job_code = False
+
+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 getWaitingForJobCodeWorkers(self):
+ for w in self._workers:
+ if w.waitingForJobCode:
+ 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),
+ "device_whitelist": prefs.ListValue(prefs.STRING_LIST, ['']),
+ "device_blacklist": prefs.ListValue(prefs.STRING_LIST, ['']),
+ "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),
+ "job_codes": prefs.ListValue(prefs.STRING_LIST, [_('New York'),
+ _('Manila'), _('Prague'), _('Helsinki'), _('Wellington'),
+ _('Tehran'), _('Kampala'), _('Paris'), _('Berlin'), _('Sydney'),
+ _('Budapest'), _('Rome'), _('Moscow'), _('Delhi'), _('Warsaw'),
+ _('Jakarta'), _('Madrid'), _('Stockholm')])
+ }
+
+ 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
+
+ def getSampleJobCode(self):
+ if self.job_codes:
+ return self.job_codes[0]
+ else:
+ return ''
+
+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 updateExampleJobCode(self):
+ job_code = self.parentApp.prefs.getSampleJobCode()
+ if not job_code:
+ job_code = _('Job code')
+ self.prefsFactory.setJobCode(job_code)
+
+ 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._setupJobCodeTab()
+ 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):
+
+ # sequence numbers
+ 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))
+
+ #compatibility
+ self.strip_characters_checkbutton.set_active(
+ self.prefs.strip_characters)
+
+ def _setupJobCodeTab(self):
+ self.job_code_liststore = gtk.ListStore(str)
+ column = gtk.TreeViewColumn()
+ rentext = gtk.CellRendererText()
+ rentext.connect('edited', self.on_job_code_edited)
+ rentext .set_property('editable', True)
+
+ column.pack_start(rentext, expand=0)
+ column.set_attributes(rentext, text=0)
+ self.job_code_treeview_column = column
+ self.job_code_treeview.append_column(column)
+ self.job_code_treeview.props.model = self.job_code_liststore
+ for code in self.prefs.job_codes:
+ self.job_code_liststore.append((code, ))
+
+ # set multiple selections
+ self.job_code_treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
+
+ self.clear_job_code_button.set_image(gtk.image_new_from_stock(
+ gtk.STOCK_CLEAR,
+ gtk.ICON_SIZE_BUTTON))
+ 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'):
+ self.rename_table.updateExampleJobCode()
+ 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 metadata 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'):
+ self.subfolder_table.updateExampleJobCode()
+ 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 metadata 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_add_job_code_button_clicked(self, button):
+ j = JobCodeDialog(self.widget, self.prefs.job_codes, None, self.add_job_code, False)
+
+
+ def add_job_code(self, dialog, userChoseCode, job_code, autoStart):
+ dialog.destroy()
+ if userChoseCode:
+ if job_code and job_code not in self.prefs.job_codes:
+ self.job_code_liststore.prepend((job_code, ))
+ self.update_job_codes()
+ selection = self.job_code_treeview.get_selection()
+ selection.unselect_all()
+ selection.select_path((0, ))
+ #scroll to the top
+ adjustment = self.job_code_scrolledwindow.get_vadjustment()
+ adjustment.set_value(adjustment.lower)
+
+ def on_remove_job_code_button_clicked(self, button):
+ """ remove selected job codes (can be multiple selection)"""
+ selection = self.job_code_treeview.get_selection()
+ model, selected = selection.get_selected_rows()
+ iters = [model.get_iter(path) for path in selected]
+ # only delete if a jobe code is selected
+ if iters:
+ no = len(iters)
+ path = None
+ for i in range(0, no):
+ iter = iters[i]
+ if i == no - 1:
+ path = model.get_path(iter)
+ model.remove(iter)
+
+ # now that we removed the selection, play nice with
+ # the user and select the next item
+ selection.select_path(path)
+
+ # if there was no selection that meant the user
+ # removed the last entry, so we try to select the
+ # last item
+ if not selection.path_is_selected(path):
+ row = path[0]-1
+ # test case for empty lists
+ if row >= 0:
+ selection.select_path((row,))
+
+ self.update_job_codes()
+ self.updateImageRenameExample()
+ self.updateDownloadFolderExample()
+
+ def on_clear_job_code_button_clicked(self, button):
+ self.job_code_liststore.clear()
+ self.update_job_codes()
+ self.updateImageRenameExample()
+ self.updateDownloadFolderExample()
+
+ def on_job_code_edited(self, widget, path, new_text):
+ iter = self.job_code_liststore.get_iter(path)
+ self.job_code_liststore.set_value(iter, 0, new_text)
+ self.update_job_codes()
+ self.updateImageRenameExample()
+ self.updateDownloadFolderExample()
+
+ def update_job_codes(self):
+ """ update preferences with list of job codes"""
+ job_codes = []
+ for row in self.job_code_liststore:
+ job_codes.append(row[0])
+ self.prefs.job_codes = job_codes
+
+ 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.waitingForJobCode = 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:
+ try:
+ # this step can fail if the source image is corrupt
+ imageMetadata.readMetadata()
+ except:
+ skipImage = True
+
+ if not skipImage:
+ if not imageMetadata.exifKeys() and (needMetaDataToCreateUniqueSubfolderName or
+ (needMetaDataToCreateUniqueImageName and
+ not addUniqueIdentifier)):
+ skipImage = True
+
+ if skipImage:
+ logError(config.SERIOUS_ERROR, _("Image has no metadata"),
+ _("Metadata is essential for generating subfolders / image names.\nSource: %s") % image,
+ IMAGE_SKIPPED)
+ 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 self.autoStart and need_job_code:
+ if job_code == None:
+ self.waitingForJobCode = True
+ display_queue.put((self.parentApp.getJobCode, ()))
+ self.running = False
+ self.lock.acquire()
+ self.running = True
+ self.waitingForJobCode = False
+ 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))
+
+ if need_job_code and job_code == None:
+ sys.stderr.write(str(self.thread_id ) + ": job code should never be None\n")
+ self.imageRenamePrefsFactory.setJobCode('unknown-job-code')
+ self.subfolderPrefsFactory.setJobCode('unknown-job-code')
+ else:
+ self.imageRenamePrefsFactory.setJobCode(job_code)
+ self.subfolderPrefsFactory.setJobCode(job_code)
+
+ # 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(str(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 UseDeviceDialog(gtk.Dialog):
+ def __init__(self, parent_window, path, volume, autostart, postChoiceCB):
+ gtk.Dialog.__init__(self, _('Device Detected'), None,
+ gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+ (gtk.STOCK_NO, gtk.RESPONSE_CANCEL,
+ gtk.STOCK_YES, gtk.RESPONSE_OK))
+
+ self.postChoiceCB = postChoiceCB
+
+ self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader-about.png'))
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt
+ prompt_label = gtk.Label(_('Should this device or partition be used to download images from?'))
+ prompt_label.set_line_wrap(True)
+ device_label = gtk.Label()
+ device_label.set_markup("<b>%s</b>" % volume.get_name(limit=0))
+ device_hbox = gtk.HBox()
+ device_hbox.pack_start(device_label, False, False)
+ path_label = gtk.Label()
+ path_label.set_markup("<i>%s</i>" % path)
+ path_hbox = gtk.HBox()
+ path_hbox.pack_start(path_label, False, False)
+
+ icon = volume.get_icon_pixbuf(36)
+ if icon:
+ image = gtk.Image()
+ image.set_from_pixbuf(icon)
+
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt
+ self.always_checkbutton = gtk.CheckButton(_('_Remember this choice'), True)
+
+ if icon:
+ device_hbox_icon = gtk.HBox(homogeneous=False, spacing=6)
+ device_hbox_icon.pack_start(image, False, False, padding = 6)
+ device_vbox = gtk.VBox(homogeneous=True, spacing=6)
+ device_vbox.pack_start(device_hbox, False, False)
+ device_vbox.pack_start(path_hbox, False, False)
+ device_hbox_icon.pack_start(device_vbox, False, False)
+ self.vbox.pack_start(device_hbox_icon, padding = 6)
+ else:
+ self.vbox.pack_start(device_hbox, padding=6)
+ self.vbox.pack_start(path_hbox, padding = 6)
+
+ self.vbox.pack_start(prompt_label, padding=6)
+ self.vbox.pack_start(self.always_checkbutton, padding=6)
+
+ self.set_border_width(6)
+ self.set_has_separator(False)
+
+ self.set_default_response(gtk.RESPONSE_OK)
+
+
+ self.set_transient_for(parent_window)
+ self.show_all()
+ self.path = path
+ self.volume = volume
+ self.autostart = autostart
+
+ self.connect('response', self.on_response)
+
+ def on_response(self, device_dialog, response):
+ userSelected = False
+ permanent_choice = self.always_checkbutton.get_active()
+ if response == gtk.RESPONSE_OK:
+ userSelected = True
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt
+ cmd_line(_("%s selected for downloading from" % self.volume.get_name(limit=0)))
+ if permanent_choice:
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt
+ cmd_line(_("This device or partition will always be used to download from"))
+ else:
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt
+ cmd_line(_("%s rejected as a download device" % self.volume.get_name(limit=0)))
+ if permanent_choice:
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#usedeviceprompt
+ cmd_line(_("This device or partition will never be used to download from"))
+
+ self.postChoiceCB(self, userSelected, permanent_choice, self.path,
+ self.volume, self.autostart)
+
+class JobCodeDialog(gtk.Dialog):
+ """ Dialog prompting for a job code"""
+
+ def __init__(self, parent_window, job_codes, default_job_code, postJobCodeEntryCB, autoStart):
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode
+ gtk.Dialog.__init__(self, _('Enter a Job Code'), None,
+ gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+ (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+ gtk.STOCK_OK, gtk.RESPONSE_OK))
+
+
+ self.set_icon_from_file(paths.share_dir('glade3/rapid-photo-downloader-about.png'))
+ self.postJobCodeEntryCB = postJobCodeEntryCB
+ self.autoStart = autoStart
+
+ self.combobox = gtk.combo_box_entry_new_text()
+ for text in job_codes:
+ self.combobox.append_text(text)
+
+ self.job_code_hbox = gtk.HBox(homogeneous = False)
+
+ if len(job_codes):
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode
+ task_label = gtk.Label(_('Enter a new job code, or select a previous one.'))
+ else:
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode
+ task_label = gtk.Label(_('Enter a new job code.'))
+ task_label.set_line_wrap(True)
+ task_hbox = gtk.HBox()
+ task_hbox.pack_start(task_label, False, False, padding=6)
+
+ label = gtk.Label(_('Job Code:'))
+ self.job_code_hbox.pack_start(label, False, False, padding=6)
+ self.job_code_hbox.pack_start(self.combobox, True, True, padding=6)
+
+ self.set_border_width(6)
+ self.set_has_separator(False)
+
+ # make entry box have entry completion
+ self.entry = self.combobox.child
+
+ completion = gtk.EntryCompletion()
+ completion.set_match_func(self.match_func)
+ completion.connect("match-selected",
+ self.on_completion_match)
+ completion.set_model(self.combobox.get_model())
+ completion.set_text_column(0)
+ self.entry.set_completion(completion)
+
+ # when user hits enter, close the dialog window
+ self.set_default_response(gtk.RESPONSE_OK)
+ self.entry.set_activates_default(True)
+
+ if default_job_code:
+ self.entry.set_text(default_job_code)
+
+ self.vbox.pack_start(task_hbox, False, False, padding = 6)
+ self.vbox.pack_start(self.job_code_hbox, False, False, padding=12)
+
+ self.set_transient_for(parent_window)
+ self.show_all()
+ self.connect('response', self.on_job_code_resp)
+
+ def match_func(self, completion, key, iter):
+ model = completion.get_model()
+ return model[iter][0].startswith(self.entry.get_text())
+
+ def on_completion_match(self, completion, model, iter):
+ self.entry.set_text(model[iter][0])
+ self.entry.set_position(-1)
+
+ def get_job_code(self):
+ return self.combobox.child.get_text()
+
+ def on_job_code_resp(self, jc_dialog, response):
+ userChoseCode = False
+ if response == gtk.RESPONSE_OK:
+ userChoseCode = True
+ cmd_line(_("Job Code entered"))
+ else:
+ cmd_line(_("Job Code not entered"))
+ self.postJobCodeEntryCB(self, userChoseCode, self.get_job_code(), self.autoStart)
+
+
+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()
+ self.parentApp.warning_vseparator.show()
+
+ iter = self.textbuffer.get_end_iter()
+ self.textbuffer.insert_with_tags(iter, problem +"\n", self.problemTag)
+ if details:
+ 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.warning_vseparator.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")
+
+ # hide display of warning and error symbols in the taskbar until they are needed
+ self.error_image.hide()
+ self.warning_image.hide()
+ self.warning_vseparator.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
+
+ # whether we need to prompt for a job code
+ global need_job_code
+
+ 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 to display image devices and download status
+ 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_display_thumbnails.set_active(self.prefs.display_thumbnails)
+ self.menu_clear.set_sensitive(False)
+
+ #job code initialization
+ need_job_code = self.needJobCode()
+ self.last_chosen_job_code = None
+ self.prompting_for_job_code = False
+
+ #setup download and backup mediums, initiating scans
+ 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_default()
+ # for some reason, the grab focus command is not working... unsure why
+ 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 needJobCode(self):
+ return rn.usesJobCode(self.prefs.image_rename) or rn.usesJobCode(self.prefs.subfolder)
+
+ def assignJobCode(self, code):
+ """ assign job code (which may be empty) to global variable and update user preferences
+
+ Update preferences only if code is not empty. Do not duplicate job code.
+ """
+ global job_code
+ if code == None:
+ code = ''
+ job_code = code
+
+ if job_code:
+ #add this value to job codes preferences
+ #delete any existing value which is the same
+ #(this way it comes to the front, which is where it should be)
+ #never modify self.prefs.job_codes in place! (or prefs become screwed up)
+
+ jcs = self.prefs.job_codes
+ while code in jcs:
+ jcs.remove(code)
+
+ self.prefs.job_codes = [code] + jcs
+
+
+ def getUseDevice(self, path, volume, autostart):
+ """ Prompt user whether or not to download from this device """
+
+ cmd_line(_("Prompting whether to use %s" % volume.get_name(limit=0)))
+ d = UseDeviceDialog(self.widget, path, volume, autostart, self.gotUseDevice)
+
+ def gotUseDevice(self, dialog, userSelected, permanent_choice, path, volume, autostart):
+ """ User has chosen whether or not to use a device to download from """
+ dialog.destroy()
+
+ if userSelected:
+ if permanent_choice and path not in self.prefs.device_whitelist:
+ # do not do a list append operation here without the assignment, or the preferences will not be updated!
+ if len(self.prefs.device_whitelist):
+ self.prefs.device_whitelist = self.prefs.device_whitelist + [path]
+ else:
+ self.prefs.device_whitelist = [path]
+ self.initiateScan(path, volume, autostart)
+
+ elif permanent_choice and path not in self.prefs.device_blacklist:
+ # do not do a list append operation here without the assignment, or the preferences will not be updated!
+ if len(self.prefs.device_blacklist):
+ self.prefs.device_blacklist = self.prefs.device_blacklist + [path]
+ else:
+ self.prefs.device_blacklist = [path]
+
+ def _getJobCode(self, postJobCodeEntryCB, autoStart):
+ """ prompt for a job code """
+
+
+ if not self.prompting_for_job_code:
+ cmd_line(_("Prompting for Job Code"))
+ self.prompting_for_job_code = True
+ j = JobCodeDialog(self.widget, self.prefs.job_codes, self.last_chosen_job_code, postJobCodeEntryCB, autoStart)
+ else:
+ cmd_line(_("Already prompting for Job Code, do not prompt again"))
+
+ def getJobCode(self, autoStart=True):
+ """ called from the copyphotos thread"""
+
+ self._getJobCode(self.gotJobCode, autoStart)
+
+ def gotJobCode(self, dialog, userChoseCode, code, autoStart):
+ dialog.destroy()
+ self.prompting_for_job_code = False
+
+ if userChoseCode:
+ self.assignJobCode(code)
+ self.last_chosen_job_code = code
+ if autoStart:
+ cmd_line(_("Starting downloads that have been waiting for a Job Code"))
+ for w in workers.getWaitingForJobCodeWorkers():
+ w.startStop()
+ else:
+ cmd_line(_("Starting downloads"))
+ self.startDownload()
+
+
+ # FIXME: what happens to these workers that are waiting? How will the user start their download?
+ # check if need to add code to start button
+
+ 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 gio or 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 path in self.prefs.device_blacklist and self.searchForPsd():
+ cmd_line(_("Device %(device)s (%(path)s) ignored") % {
+ 'device': volume.get_name(limit=0), 'path': path})
+ else:
+ 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():
+ if self.searchForPsd() and path not in self.prefs.device_whitelist:
+ # prompt user if device should be used or not
+ self.getUseDevice(path, volume, self.prefs.auto_download_upon_device_insertion)
+ else:
+ self._printAutoStart(self.prefs.auto_download_upon_device_insertion)
+ self.initiateScan(path, volume, self.prefs.auto_download_upon_device_insertion)
+
+ def initiateScan(self, path, volume, autostart):
+ """ initiates scan of image device"""
+ cardMedia = CardMedia(path, volume, True)
+ i = workers.getNextThread_id()
+
+ workers.append(CopyPhotos(i, self, self.fileRenameLock, self.fileSequenceLock, self.statsLock,
+ self.downloadStats, autostart,
+ 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):
+ """
+ Sets up volumes for downloading from and backing up to
+
+ 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()
+
+ volumeList = []
+ 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 path in self.prefs.device_blacklist and self.searchForPsd():
+ cmd_line(_("Device %(device)s (%(path)s) ignored") % {
+ 'device': volume.get_name(limit=0),
+ 'path': path})
+ else:
+ 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()):
+ volumeList.append((path, volume))
+
+
+ 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)
+ volumeList.append((path, None))
+
+ 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
+
+ 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(len(volumeList)):
+ path, volume = volumeList[i]
+ if self.searchForPsd() and path not in self.prefs.device_whitelist:
+ # prompt user to see if device should be used or not
+ self.getUseDevice(path, volume, autoStart)
+ else:
+ self.initiateScan(path, volume, autoStart)
+
+ 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.download_button.set_flags(gtk.CAN_DEFAULT)
+ 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
+ # there is no need to clear self.timeRemaining, as when each thread is completed, it removes itself
+
+ global job_code
+ job_code = None
+
+ 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:
+ if need_job_code and job_code == None and not self.prompting_for_job_code:
+ self.getJobCode(autoStart=False)
+ else:
+ self.startDownload()
+ else:
+ self.pauseDownload()
+
+ def on_preference_changed(self, key, value):
+ """
+ Called when user changes the program's preferences
+ """
+
+ 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()
+ elif key in ['subfolder', 'image_rename']:
+ global need_job_code
+ need_job_code = self.needJobCode()
+
+
+
+ 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 get_icon_pixbuf(self, size):
+ """ returns icon for the volume, or None if not available"""
+
+ icontheme = gtk.icon_theme_get_default()
+
+ if using_gio:
+ gicon = self.volume.get_icon()
+ f = None
+ if isinstance(gicon, gio.ThemedIcon):
+ iconinfo = icontheme.choose_icon(gicon.get_names(), size, gtk.ICON_LOOKUP_USE_BUILTIN)
+ f = iconinfo.get_filename()
+ try:
+ v = gtk.gdk.pixbuf_new_from_file_at_size(f, size, size)
+ except:
+ f = None
+ if not f:
+ v = icontheme.load_icon('gtk-harddisk', size, gtk.ICON_LOOKUP_USE_BUILTIN)
+ else:
+ gicon = self.volume.get_icon()
+ v = icontheme.load_icon(gicon, size, gtk.ICON_LOOKUP_USE_BUILTIN)
+ return v
+
+ 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..f28e068
--- /dev/null
+++ b/rapid/renamesubfolderprefs.py
@@ -0,0 +1,1524 @@
+#!/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'
+JOB_CODE = 'Job code'
+
+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', 'HH-MM-SS', 'HH-MM', 'HH', 'MM', 'SS']
+
+
+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')
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#jobcode
+ _('Job code')
+ _('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')
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
+ _('HH-MM-SS')
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
+ _('HH-MM')
+ # Translators: for an explanation of what this means, see http://damonlynch.net/rapid/documentation/index.html#renamedateandtime
+ _('HH')
+ # 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
+ _('SS')
+
+
+# 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', '%H-%M-%S', '%H-%M',
+ '%H', '%M', '%S']
+
+
+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, JOB_CODE]
+
+
+DICT_IMAGE_RENAME_L0 = {
+ DATE_TIME: DICT_DATE_TIME_L1,
+ TEXT: None,
+ FILENAME: DICT_FILENAME_L1,
+ METADATA: DICT_METADATA_L1,
+ SEQUENCES: DICT_SEQUENCE_L1,
+ JOB_CODE: None,
+ ORDER_KEY: LIST_IMAGE_RENAME_L0
+ }
+
+LIST_SUBFOLDER_L0 = [DATE_TIME, TEXT, FILENAME, METADATA, JOB_CODE, SEPARATOR]
+
+DICT_SUBFOLDER_L0 = {
+ DATE_TIME: DICT_DATE_TIME_L1,
+ TEXT: None,
+ FILENAME: DICT_SUBFOLDER_FILENAME_L1,
+ METADATA: DICT_METADATA_L1,
+ JOB_CODE: None,
+ 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 usesJobCode(prefs):
+ """ Returns True if the preferences contain a job code, else returns False"""
+ for i in range(0, len(prefs), 3):
+ if prefs[i] == JOB_CODE:
+ return True
+ return False
+
+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
+
+ self.job_code = ''
+
+ # 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):
+ if (self.prefList[i+1] or self.prefList[i+2]):
+ c = ':'
+ else:
+ c = ''
+ s = "%s%s " % (self.prefList[i], c)
+
+ 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 setJobCode(self, job_code):
+ self.job_code = job_code
+
+ 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:
+
+ if type(d) == type('string'):
+ # will be a string only if the date time could not be converted in the datetime type
+ # try to massage badly formed date / times into a valid value
+ _datetime = d.strip()
+ # remove any weird characters at the end of the string
+ while _datetime and not _datetime[-1].isdigit():
+ _datetime = _datetime[:-1]
+ _date, _time = _datetime.split(' ')
+ _datetime = "%s %s" % (_date.replace(":", "-") , _time.replace("-", ":"))
+ try:
+ d = datetime.datetime.strptime(_datetime, '%Y-%m-%d %H:%M:%S')
+ except:
+ v = ''
+ problem = _('Error in date time component. Value %s appears invalid') % ''
+ return (v, problem)
+
+ 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 == JOB_CODE:
+ return (self.job_code, None)
+ 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 in [SEPARATOR, JOB_CODE]:
+ 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
+