Codebase list mirage / 6d7aa27
Initial commit Import from Mirage 0.9.5.2 source code Thomas Ross 3 years ago
33 changed file(s) with 20866 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 v0.9.5.2 - February 13, 2011
1 + Added ukrainian translation
2 + Bug: Screenshot did not work correctly
3 + Bug: Fixed a bug in the use of keymodifiers
4
5 v0.9.5.1 - July 23, 2010
6 + Added dutch translation
7 + Bug: Mirage showed verbose output by default
8 + Bug: Misplaced parenthesis in Open Remote Image dialog code
9 + Changes: Fixed translations again...
10
11 v0.9.5 - July 20, 2010
12 + Added a windows icon
13 + Added new shortcuts PgUP/PgDN for previous/next image
14 + Added option to disable the backgroundcolor in windowed mode
15 + Bug: & in custom actions caused an error when mirage tried to open custom
16 action dialog (bug #17329)
17 + Bug: Save image didn't work. (bug #17330)
18 + Bug: Fixed gettext files not found if installed into /usr/local (bug #17359)
19 + Changes: now uses $XDG_CONFIG_HOME/mirage as config directory if possible
20 (patch taken from debian)
21 + Changes: Switched zooming directions for Ctrl+Mousewheel up/down.
22 + Changes: Small interface changes
23 + Changes: Updated mnemonics (bug #17350 and patch #3020)
24 + Changes: Improvements to localized strings
25
26 v0.9.4 - June 26, 2010
27 + Mirage now requires PyGTK 2.12.0
28 + Added the ability to set the quality for saving images
29 + Added windows support
30 + Fixed DeprecationWarnings for old tooltip API in pygtk
31 + Fixed DeprecationWarnings for md5
32 + Prevent toolbar from getting focus on keypresses
33 + Bug: Fix returning from fullscreen mode with no images displayed
34 + Bug: delete_image() could cause some thumbnails to not get loaded
35 + Bug: If a image modified image was deleted mirage would ask to save the image
36 + Bug: Hopefully fixed image preloading
37 + Bug: Fixed wrong tooltip for hidden images
38 + Bug: Some menu entries was visible while they shouldnt have been
39 + Bug: Fixed parsing of custom commands
40 + Changes: Properties now show "file modified" instead of "file date"
41 + Changes: Slideshow delay is choosen with a spinner
42 + Changes: Fixed DeprecationWarnings for old tooltip API in pygtk
43 + Changes: Fixed DeprecationWarnings for md5
44 + Feature request: Show filename in statusbar
45 + Translation: Added Portuguese translation (Danilo)
46 + Translation: Added Chinese translation (Jayden)
47 + Translation: Updated the po-files
48
49 v0.9.3 - March 26, 2008
50 + Added shortcuts for consistency with gqview/eog (Benjamin)
51 + Added Hungarian translation (Sandor)
52 + Added Czech translation (Petr)
53
54 v0.9.2 - January 28, 2008
55 + Bug: Fix typo preventing thumbnails from being reused (Lane)
56
57 v0.9.1 - January 9, 2008
58 + Screenshot support
59 + Use png attributes to be more fully thumbnail spec compliant
60 + Allow running without compiled C modules (certain features disabled)
61 + Reuse generated thumbnails for open dialog preview if they exist
62 + Bug: Fix freeze involving modified image and navigation
63 + Bug: Prevent possible crash on invalid image files
64 + Bug: Fix occasional wrong height of thumbnails (and gobject assertion)
65 + Bug: Wrong image displayed if discarding modified app and using preload
66 + Bug: Ctrl+mousewheel doesn't zoom when caps/num lock is on
67 + Bug: Fix horizontal centering of image
68 + Bug: Fix .desktop file category
69 + Bug: Create thumbnails dir if it doesn't exist
70
71 v0.9 - October 18, 2007
72 + Thumbnails pane (freedesktop spec compliant using ~/.thumbnails)
73 + Ability to open remote images (http and ftp)
74 + Register viewed images with gtk's recent documents (ongardie)
75 + Preference for opening hidden files
76 + Add up/down keybindings to go to previous/next image
77 + Bug: Filenames in custom actions aren't correctly escaped (Andrew)
78 + Bug: Can't open properties on animated images
79 + Bug: Editing a custom action creates a new action
80 + Bug: Fast image traversal overruns image list and becomes stuck
81
82 v0.8.3 - January 17, 2007
83 + Use F11 for fullscreen like most applications
84 + Use 'kfmclient' to launch browser in kde
85 + Added Italian translation (Daniele Maggio)
86 + Bug: Save As doesn't work
87
88 v0.8.2 - December 7, 2006
89 + Ability to modify image saturation
90 + Recent file/list menu
91 + Save/load accel_map in case user edits any gtk accelerators
92 + Bug: Fullscreen controls can be on wrong screen with Xinerama
93 + Bug: Scrollbars don't appear correctly on window resizes
94 + Bug: Panning images doesn't always work properly
95 + Bug: Alpha channel is lost when resizing image with transparency
96 + Bug: Allow saving files when file extension isn't lowercase
97 + Bug: Specified image scaling method isn't properly used
98
99 v0.8.1 - October 19, 2006
100 + Add list of image names to custom actions (%L)
101 + Improved preloading/navigation handling
102 + Menu improvements; added Custom Actions to EditMenu
103 + Try gnome-open/exo-open first for opening help, then fallback
104 + Bug: Next/brevious buttons block with mouse over it
105 + Bug: Ctrl-0/1 shortcuts don't work from numpad with numlock on
106 + Bug: Custom actions don't execute when using --verbose
107 + Bug: Cannot install when X server isn't running
108 + Updated icon (William Rea)
109
110 v0.8 - September 4, 2006
111 + Editing capabilities: resize, crop, save, delete, rename
112 + Pref to preload images for faster navigation (enabled by default)
113 + Implemented 'custom actions' framework, including batch actions
114 + --onload or -o argument for performing actions on image loading
115 + Added File > Properties to display image properties
116 + Show "Scanning..." in statusbar when scanning for images; Escape to abort
117 + Reduce GTK+ requirement to 2.6.0
118 + Bug: Opening with 'load all images in dir' gives expected result
119 + Bug: Cannot open folder while search process is active
120 + Added French translation (Mike Massonnet)
121 + Added Polish translation (Tomasz Dominikowski)
122 + Added Russian translation (mavka)
123
124 v0.7.3 - August 2, 2006
125 + Fix crash when loading images in certain circumstances
126
127 v0.7.2 - August 1, 2006
128 + Preference to automatically start the slideshow in fullscreen mode
129 + Background image search process to load image faster
130 + Added cli argument: -s or --slideshow to start in slideshow mode
131 + Added cli argument: -f or --fullscreen to start in fullscreen mode
132 + Additional shortcuts: +/- for zooming in/out
133 + Fixed .desktop mimetypes
134 + Added German translation (Bjoern Martensen)
135 + Added Spanish translation (Isidro Arribas)
136
137 v0.7.1 - July 9, 2006
138 + Toolbar style follows gnome preference (optional gnome-python dependency)
139 + Skip hidden files/dirs when loading images
140 + Bug: Caps-lock breaks some shortcuts
141 + Bug fixes
142
143 v0.7 - June 20, 2006
144 + Slideshow mode (delay time, randomize order, disable screensaver preferences)
145 + Display animated images
146 + Edit > Open in Editor
147 + Image wrapping options: yes/no/prompt
148 + Cleaned up menubar, preferences
149 + Set go navigation items' sensitivities based on context
150 + Bug: 1:1 images are sometimes incorrectly set to fit
151 + Many bugfixes, interface tweaks
152
153 v0.6 - May 29, 2006
154 + In fullscreen mode, hide mouse cursor after 2 seconds of no movement
155 + Option to wrap around imagelist
156 + When in zoom-to-fit mode, fit the image when it's rotated
157 + Show broken icon if image not found (i.e. deleted)
158 + Bug: Ensure that scrollbars don't show up unless needed
159 + Bug: Don't resize an image if it's at 100% zoom
160 + Other small bugfixes
161
162 v0.5 - May 1, 2006
163 + Make open image mode configurable: smart, fit, 1:1, last
164 + Option to navigate imagelist via mousewheel (default: on)
165 + Add maximum/minimum zoom levels
166 + Reduce border around image for fit mode
167 + Have Go>Random not show images twice until all images have been shown once
168 + Allow numpad keys as shortcuts
169 + Allow panning image additionally with left-click/drag
170 + Revert retaining state of images in list introduced in v0.4
171 + Add hourglass feedback for loading images
172 + Bug: Small images don't zoom-to-fit
173 + Bug: Some non-image files are thought to be images
174 + Bug: Files/folders with spaces don't open from file manager
175 + Bug: Some shortcuts don't work with numslock enabled
176
177 v0.4 - April 25, 2006
178 + Removed <Alt> from navigational shortcuts
179 + Default to alphabetically populating the image list
180 + Ability to go to first (Home) and last (End) image in list
181 + Retain zoom/flip/rotate state of images in list
182 + Additional preferences:
183 - Open all images in current directory
184 - Default directory for 'Open' dialog
185 + Fullscreen hides menu/tool/status bars
186
187 v0.3.1 - April 4, 2006
188 + Fixed inability to open a file from a file manager.
189
190 v0.3 - April 4, 2006
191 + Speed improvements for zooming and rotating images
192 + Allow drag-and-drop of images/folders from file managers
193 + Flip images vertically/horizontally
194 + Go > Random Image
195 + Ability to recursively load images found in all subdirectories (mirage -R or
196 checkbox in Open Folder dialog)
197 + Right-click menu on image
198 + "mirage --help" information.
199 + Fixed .desktop bug that caused multiple images to open in separate windows
200 when launched via a file manager.
201
202 v0.2 - March 29, 2006
203 + Allow multiple files/folders to be passed as arguments or opened directly
204 + MiddleMouseButton-Drag to pan the image (like GIMP)
205 + Add Tools > Options for scaling quality, background color
206 + Retain center of user-view when zooming
207 + Display a broken image in the window instead of an error popup message
208 + Improve handling of which images Mirage can successfully display
209
210 v0.1 - March 22, 2006 (Initial release)
211 + Fast scaling/rotating of images
212 + Displays 'checkerboard' for transparency
213 + Ctrl-MouseScrollDown to zoom out, Ctrl-MouseScrollUp to zoom in (like GIMP)
0 GNU GENERAL PUBLIC LICENSE
1 Version 3, 29 June 2007
2
3 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
4 Everyone is permitted to copy and distribute verbatim copies
5 of this license document, but changing it is not allowed.
6
7 Preamble
8
9 The GNU General Public License is a free, copyleft license for
10 software and other kinds of works.
11
12 The licenses for most software and other practical works are designed
13 to take away your freedom to share and change the works. By contrast,
14 the GNU General Public License is intended to guarantee your freedom to
15 share and change all versions of a program--to make sure it remains free
16 software for all its users. We, the Free Software Foundation, use the
17 GNU General Public License for most of our software; it applies also to
18 any other work released this way by its authors. You can apply it to
19 your programs, too.
20
21 When we speak of free software, we are referring to freedom, not
22 price. Our General Public Licenses are designed to make sure that you
23 have the freedom to distribute copies of free software (and charge for
24 them if you wish), that you receive source code or can get it if you
25 want it, that you can change the software or use pieces of it in new
26 free programs, and that you know you can do these things.
27
28 To protect your rights, we need to prevent others from denying you
29 these rights or asking you to surrender the rights. Therefore, you have
30 certain responsibilities if you distribute copies of the software, or if
31 you modify it: responsibilities to respect the freedom of others.
32
33 For example, if you distribute copies of such a program, whether
34 gratis or for a fee, you must pass on to the recipients the same
35 freedoms that you received. You must make sure that they, too, receive
36 or can get the source code. And you must show them these terms so they
37 know their rights.
38
39 Developers that use the GNU GPL protect your rights with two steps:
40 (1) assert copyright on the software, and (2) offer you this License
41 giving you legal permission to copy, distribute and/or modify it.
42
43 For the developers' and authors' protection, the GPL clearly explains
44 that there is no warranty for this free software. For both users' and
45 authors' sake, the GPL requires that modified versions be marked as
46 changed, so that their problems will not be attributed erroneously to
47 authors of previous versions.
48
49 Some devices are designed to deny users access to install or run
50 modified versions of the software inside them, although the manufacturer
51 can do so. This is fundamentally incompatible with the aim of
52 protecting users' freedom to change the software. The systematic
53 pattern of such abuse occurs in the area of products for individuals to
54 use, which is precisely where it is most unacceptable. Therefore, we
55 have designed this version of the GPL to prohibit the practice for those
56 products. If such problems arise substantially in other domains, we
57 stand ready to extend this provision to those domains in future versions
58 of the GPL, as needed to protect the freedom of users.
59
60 Finally, every program is threatened constantly by software patents.
61 States should not allow patents to restrict development and use of
62 software on general-purpose computers, but in those that do, we wish to
63 avoid the special danger that patents applied to a free program could
64 make it effectively proprietary. To prevent this, the GPL assures that
65 patents cannot be used to render the program non-free.
66
67 The precise terms and conditions for copying, distribution and
68 modification follow.
69
70 TERMS AND CONDITIONS
71
72 0. Definitions.
73
74 "This License" refers to version 3 of the GNU General Public License.
75
76 "Copyright" also means copyright-like laws that apply to other kinds of
77 works, such as semiconductor masks.
78
79 "The Program" refers to any copyrightable work licensed under this
80 License. Each licensee is addressed as "you". "Licensees" and
81 "recipients" may be individuals or organizations.
82
83 To "modify" a work means to copy from or adapt all or part of the work
84 in a fashion requiring copyright permission, other than the making of an
85 exact copy. The resulting work is called a "modified version" of the
86 earlier work or a work "based on" the earlier work.
87
88 A "covered work" means either the unmodified Program or a work based
89 on the Program.
90
91 To "propagate" a work means to do anything with it that, without
92 permission, would make you directly or secondarily liable for
93 infringement under applicable copyright law, except executing it on a
94 computer or modifying a private copy. Propagation includes copying,
95 distribution (with or without modification), making available to the
96 public, and in some countries other activities as well.
97
98 To "convey" a work means any kind of propagation that enables other
99 parties to make or receive copies. Mere interaction with a user through
100 a computer network, with no transfer of a copy, is not conveying.
101
102 An interactive user interface displays "Appropriate Legal Notices"
103 to the extent that it includes a convenient and prominently visible
104 feature that (1) displays an appropriate copyright notice, and (2)
105 tells the user that there is no warranty for the work (except to the
106 extent that warranties are provided), that licensees may convey the
107 work under this License, and how to view a copy of this License. If
108 the interface presents a list of user commands or options, such as a
109 menu, a prominent item in the list meets this criterion.
110
111 1. Source Code.
112
113 The "source code" for a work means the preferred form of the work
114 for making modifications to it. "Object code" means any non-source
115 form of a work.
116
117 A "Standard Interface" means an interface that either is an official
118 standard defined by a recognized standards body, or, in the case of
119 interfaces specified for a particular programming language, one that
120 is widely used among developers working in that language.
121
122 The "System Libraries" of an executable work include anything, other
123 than the work as a whole, that (a) is included in the normal form of
124 packaging a Major Component, but which is not part of that Major
125 Component, and (b) serves only to enable use of the work with that
126 Major Component, or to implement a Standard Interface for which an
127 implementation is available to the public in source code form. A
128 "Major Component", in this context, means a major essential component
129 (kernel, window system, and so on) of the specific operating system
130 (if any) on which the executable work runs, or a compiler used to
131 produce the work, or an object code interpreter used to run it.
132
133 The "Corresponding Source" for a work in object code form means all
134 the source code needed to generate, install, and (for an executable
135 work) run the object code and to modify the work, including scripts to
136 control those activities. However, it does not include the work's
137 System Libraries, or general-purpose tools or generally available free
138 programs which are used unmodified in performing those activities but
139 which are not part of the work. For example, Corresponding Source
140 includes interface definition files associated with source files for
141 the work, and the source code for shared libraries and dynamically
142 linked subprograms that the work is specifically designed to require,
143 such as by intimate data communication or control flow between those
144 subprograms and other parts of the work.
145
146 The Corresponding Source need not include anything that users
147 can regenerate automatically from other parts of the Corresponding
148 Source.
149
150 The Corresponding Source for a work in source code form is that
151 same work.
152
153 2. Basic Permissions.
154
155 All rights granted under this License are granted for the term of
156 copyright on the Program, and are irrevocable provided the stated
157 conditions are met. This License explicitly affirms your unlimited
158 permission to run the unmodified Program. The output from running a
159 covered work is covered by this License only if the output, given its
160 content, constitutes a covered work. This License acknowledges your
161 rights of fair use or other equivalent, as provided by copyright law.
162
163 You may make, run and propagate covered works that you do not
164 convey, without conditions so long as your license otherwise remains
165 in force. You may convey covered works to others for the sole purpose
166 of having them make modifications exclusively for you, or provide you
167 with facilities for running those works, provided that you comply with
168 the terms of this License in conveying all material for which you do
169 not control copyright. Those thus making or running the covered works
170 for you must do so exclusively on your behalf, under your direction
171 and control, on terms that prohibit them from making any copies of
172 your copyrighted material outside their relationship with you.
173
174 Conveying under any other circumstances is permitted solely under
175 the conditions stated below. Sublicensing is not allowed; section 10
176 makes it unnecessary.
177
178 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
179
180 No covered work shall be deemed part of an effective technological
181 measure under any applicable law fulfilling obligations under article
182 11 of the WIPO copyright treaty adopted on 20 December 1996, or
183 similar laws prohibiting or restricting circumvention of such
184 measures.
185
186 When you convey a covered work, you waive any legal power to forbid
187 circumvention of technological measures to the extent such circumvention
188 is effected by exercising rights under this License with respect to
189 the covered work, and you disclaim any intention to limit operation or
190 modification of the work as a means of enforcing, against the work's
191 users, your or third parties' legal rights to forbid circumvention of
192 technological measures.
193
194 4. Conveying Verbatim Copies.
195
196 You may convey verbatim copies of the Program's source code as you
197 receive it, in any medium, provided that you conspicuously and
198 appropriately publish on each copy an appropriate copyright notice;
199 keep intact all notices stating that this License and any
200 non-permissive terms added in accord with section 7 apply to the code;
201 keep intact all notices of the absence of any warranty; and give all
202 recipients a copy of this License along with the Program.
203
204 You may charge any price or no price for each copy that you convey,
205 and you may offer support or warranty protection for a fee.
206
207 5. Conveying Modified Source Versions.
208
209 You may convey a work based on the Program, or the modifications to
210 produce it from the Program, in the form of source code under the
211 terms of section 4, provided that you also meet all of these conditions:
212
213 a) The work must carry prominent notices stating that you modified
214 it, and giving a relevant date.
215
216 b) The work must carry prominent notices stating that it is
217 released under this License and any conditions added under section
218 7. This requirement modifies the requirement in section 4 to
219 "keep intact all notices".
220
221 c) You must license the entire work, as a whole, under this
222 License to anyone who comes into possession of a copy. This
223 License will therefore apply, along with any applicable section 7
224 additional terms, to the whole of the work, and all its parts,
225 regardless of how they are packaged. This License gives no
226 permission to license the work in any other way, but it does not
227 invalidate such permission if you have separately received it.
228
229 d) If the work has interactive user interfaces, each must display
230 Appropriate Legal Notices; however, if the Program has interactive
231 interfaces that do not display Appropriate Legal Notices, your
232 work need not make them do so.
233
234 A compilation of a covered work with other separate and independent
235 works, which are not by their nature extensions of the covered work,
236 and which are not combined with it such as to form a larger program,
237 in or on a volume of a storage or distribution medium, is called an
238 "aggregate" if the compilation and its resulting copyright are not
239 used to limit the access or legal rights of the compilation's users
240 beyond what the individual works permit. Inclusion of a covered work
241 in an aggregate does not cause this License to apply to the other
242 parts of the aggregate.
243
244 6. Conveying Non-Source Forms.
245
246 You may convey a covered work in object code form under the terms
247 of sections 4 and 5, provided that you also convey the
248 machine-readable Corresponding Source under the terms of this License,
249 in one of these ways:
250
251 a) Convey the object code in, or embodied in, a physical product
252 (including a physical distribution medium), accompanied by the
253 Corresponding Source fixed on a durable physical medium
254 customarily used for software interchange.
255
256 b) Convey the object code in, or embodied in, a physical product
257 (including a physical distribution medium), accompanied by a
258 written offer, valid for at least three years and valid for as
259 long as you offer spare parts or customer support for that product
260 model, to give anyone who possesses the object code either (1) a
261 copy of the Corresponding Source for all the software in the
262 product that is covered by this License, on a durable physical
263 medium customarily used for software interchange, for a price no
264 more than your reasonable cost of physically performing this
265 conveying of source, or (2) access to copy the
266 Corresponding Source from a network server at no charge.
267
268 c) Convey individual copies of the object code with a copy of the
269 written offer to provide the Corresponding Source. This
270 alternative is allowed only occasionally and noncommercially, and
271 only if you received the object code with such an offer, in accord
272 with subsection 6b.
273
274 d) Convey the object code by offering access from a designated
275 place (gratis or for a charge), and offer equivalent access to the
276 Corresponding Source in the same way through the same place at no
277 further charge. You need not require recipients to copy the
278 Corresponding Source along with the object code. If the place to
279 copy the object code is a network server, the Corresponding Source
280 may be on a different server (operated by you or a third party)
281 that supports equivalent copying facilities, provided you maintain
282 clear directions next to the object code saying where to find the
283 Corresponding Source. Regardless of what server hosts the
284 Corresponding Source, you remain obligated to ensure that it is
285 available for as long as needed to satisfy these requirements.
286
287 e) Convey the object code using peer-to-peer transmission, provided
288 you inform other peers where the object code and Corresponding
289 Source of the work are being offered to the general public at no
290 charge under subsection 6d.
291
292 A separable portion of the object code, whose source code is excluded
293 from the Corresponding Source as a System Library, need not be
294 included in conveying the object code work.
295
296 A "User Product" is either (1) a "consumer product", which means any
297 tangible personal property which is normally used for personal, family,
298 or household purposes, or (2) anything designed or sold for incorporation
299 into a dwelling. In determining whether a product is a consumer product,
300 doubtful cases shall be resolved in favor of coverage. For a particular
301 product received by a particular user, "normally used" refers to a
302 typical or common use of that class of product, regardless of the status
303 of the particular user or of the way in which the particular user
304 actually uses, or expects or is expected to use, the product. A product
305 is a consumer product regardless of whether the product has substantial
306 commercial, industrial or non-consumer uses, unless such uses represent
307 the only significant mode of use of the product.
308
309 "Installation Information" for a User Product means any methods,
310 procedures, authorization keys, or other information required to install
311 and execute modified versions of a covered work in that User Product from
312 a modified version of its Corresponding Source. The information must
313 suffice to ensure that the continued functioning of the modified object
314 code is in no case prevented or interfered with solely because
315 modification has been made.
316
317 If you convey an object code work under this section in, or with, or
318 specifically for use in, a User Product, and the conveying occurs as
319 part of a transaction in which the right of possession and use of the
320 User Product is transferred to the recipient in perpetuity or for a
321 fixed term (regardless of how the transaction is characterized), the
322 Corresponding Source conveyed under this section must be accompanied
323 by the Installation Information. But this requirement does not apply
324 if neither you nor any third party retains the ability to install
325 modified object code on the User Product (for example, the work has
326 been installed in ROM).
327
328 The requirement to provide Installation Information does not include a
329 requirement to continue to provide support service, warranty, or updates
330 for a work that has been modified or installed by the recipient, or for
331 the User Product in which it has been modified or installed. Access to a
332 network may be denied when the modification itself materially and
333 adversely affects the operation of the network or violates the rules and
334 protocols for communication across the network.
335
336 Corresponding Source conveyed, and Installation Information provided,
337 in accord with this section must be in a format that is publicly
338 documented (and with an implementation available to the public in
339 source code form), and must require no special password or key for
340 unpacking, reading or copying.
341
342 7. Additional Terms.
343
344 "Additional permissions" are terms that supplement the terms of this
345 License by making exceptions from one or more of its conditions.
346 Additional permissions that are applicable to the entire Program shall
347 be treated as though they were included in this License, to the extent
348 that they are valid under applicable law. If additional permissions
349 apply only to part of the Program, that part may be used separately
350 under those permissions, but the entire Program remains governed by
351 this License without regard to the additional permissions.
352
353 When you convey a copy of a covered work, you may at your option
354 remove any additional permissions from that copy, or from any part of
355 it. (Additional permissions may be written to require their own
356 removal in certain cases when you modify the work.) You may place
357 additional permissions on material, added by you to a covered work,
358 for which you have or can give appropriate copyright permission.
359
360 Notwithstanding any other provision of this License, for material you
361 add to a covered work, you may (if authorized by the copyright holders of
362 that material) supplement the terms of this License with terms:
363
364 a) Disclaiming warranty or limiting liability differently from the
365 terms of sections 15 and 16 of this License; or
366
367 b) Requiring preservation of specified reasonable legal notices or
368 author attributions in that material or in the Appropriate Legal
369 Notices displayed by works containing it; or
370
371 c) Prohibiting misrepresentation of the origin of that material, or
372 requiring that modified versions of such material be marked in
373 reasonable ways as different from the original version; or
374
375 d) Limiting the use for publicity purposes of names of licensors or
376 authors of the material; or
377
378 e) Declining to grant rights under trademark law for use of some
379 trade names, trademarks, or service marks; or
380
381 f) Requiring indemnification of licensors and authors of that
382 material by anyone who conveys the material (or modified versions of
383 it) with contractual assumptions of liability to the recipient, for
384 any liability that these contractual assumptions directly impose on
385 those licensors and authors.
386
387 All other non-permissive additional terms are considered "further
388 restrictions" within the meaning of section 10. If the Program as you
389 received it, or any part of it, contains a notice stating that it is
390 governed by this License along with a term that is a further
391 restriction, you may remove that term. If a license document contains
392 a further restriction but permits relicensing or conveying under this
393 License, you may add to a covered work material governed by the terms
394 of that license document, provided that the further restriction does
395 not survive such relicensing or conveying.
396
397 If you add terms to a covered work in accord with this section, you
398 must place, in the relevant source files, a statement of the
399 additional terms that apply to those files, or a notice indicating
400 where to find the applicable terms.
401
402 Additional terms, permissive or non-permissive, may be stated in the
403 form of a separately written license, or stated as exceptions;
404 the above requirements apply either way.
405
406 8. Termination.
407
408 You may not propagate or modify a covered work except as expressly
409 provided under this License. Any attempt otherwise to propagate or
410 modify it is void, and will automatically terminate your rights under
411 this License (including any patent licenses granted under the third
412 paragraph of section 11).
413
414 However, if you cease all violation of this License, then your
415 license from a particular copyright holder is reinstated (a)
416 provisionally, unless and until the copyright holder explicitly and
417 finally terminates your license, and (b) permanently, if the copyright
418 holder fails to notify you of the violation by some reasonable means
419 prior to 60 days after the cessation.
420
421 Moreover, your license from a particular copyright holder is
422 reinstated permanently if the copyright holder notifies you of the
423 violation by some reasonable means, this is the first time you have
424 received notice of violation of this License (for any work) from that
425 copyright holder, and you cure the violation prior to 30 days after
426 your receipt of the notice.
427
428 Termination of your rights under this section does not terminate the
429 licenses of parties who have received copies or rights from you under
430 this License. If your rights have been terminated and not permanently
431 reinstated, you do not qualify to receive new licenses for the same
432 material under section 10.
433
434 9. Acceptance Not Required for Having Copies.
435
436 You are not required to accept this License in order to receive or
437 run a copy of the Program. Ancillary propagation of a covered work
438 occurring solely as a consequence of using peer-to-peer transmission
439 to receive a copy likewise does not require acceptance. However,
440 nothing other than this License grants you permission to propagate or
441 modify any covered work. These actions infringe copyright if you do
442 not accept this License. Therefore, by modifying or propagating a
443 covered work, you indicate your acceptance of this License to do so.
444
445 10. Automatic Licensing of Downstream Recipients.
446
447 Each time you convey a covered work, the recipient automatically
448 receives a license from the original licensors, to run, modify and
449 propagate that work, subject to this License. You are not responsible
450 for enforcing compliance by third parties with this License.
451
452 An "entity transaction" is a transaction transferring control of an
453 organization, or substantially all assets of one, or subdividing an
454 organization, or merging organizations. If propagation of a covered
455 work results from an entity transaction, each party to that
456 transaction who receives a copy of the work also receives whatever
457 licenses to the work the party's predecessor in interest had or could
458 give under the previous paragraph, plus a right to possession of the
459 Corresponding Source of the work from the predecessor in interest, if
460 the predecessor has it or can get it with reasonable efforts.
461
462 You may not impose any further restrictions on the exercise of the
463 rights granted or affirmed under this License. For example, you may
464 not impose a license fee, royalty, or other charge for exercise of
465 rights granted under this License, and you may not initiate litigation
466 (including a cross-claim or counterclaim in a lawsuit) alleging that
467 any patent claim is infringed by making, using, selling, offering for
468 sale, or importing the Program or any portion of it.
469
470 11. Patents.
471
472 A "contributor" is a copyright holder who authorizes use under this
473 License of the Program or a work on which the Program is based. The
474 work thus licensed is called the contributor's "contributor version".
475
476 A contributor's "essential patent claims" are all patent claims
477 owned or controlled by the contributor, whether already acquired or
478 hereafter acquired, that would be infringed by some manner, permitted
479 by this License, of making, using, or selling its contributor version,
480 but do not include claims that would be infringed only as a
481 consequence of further modification of the contributor version. For
482 purposes of this definition, "control" includes the right to grant
483 patent sublicenses in a manner consistent with the requirements of
484 this License.
485
486 Each contributor grants you a non-exclusive, worldwide, royalty-free
487 patent license under the contributor's essential patent claims, to
488 make, use, sell, offer for sale, import and otherwise run, modify and
489 propagate the contents of its contributor version.
490
491 In the following three paragraphs, a "patent license" is any express
492 agreement or commitment, however denominated, not to enforce a patent
493 (such as an express permission to practice a patent or covenant not to
494 sue for patent infringement). To "grant" such a patent license to a
495 party means to make such an agreement or commitment not to enforce a
496 patent against the party.
497
498 If you convey a covered work, knowingly relying on a patent license,
499 and the Corresponding Source of the work is not available for anyone
500 to copy, free of charge and under the terms of this License, through a
501 publicly available network server or other readily accessible means,
502 then you must either (1) cause the Corresponding Source to be so
503 available, or (2) arrange to deprive yourself of the benefit of the
504 patent license for this particular work, or (3) arrange, in a manner
505 consistent with the requirements of this License, to extend the patent
506 license to downstream recipients. "Knowingly relying" means you have
507 actual knowledge that, but for the patent license, your conveying the
508 covered work in a country, or your recipient's use of the covered work
509 in a country, would infringe one or more identifiable patents in that
510 country that you have reason to believe are valid.
511
512 If, pursuant to or in connection with a single transaction or
513 arrangement, you convey, or propagate by procuring conveyance of, a
514 covered work, and grant a patent license to some of the parties
515 receiving the covered work authorizing them to use, propagate, modify
516 or convey a specific copy of the covered work, then the patent license
517 you grant is automatically extended to all recipients of the covered
518 work and works based on it.
519
520 A patent license is "discriminatory" if it does not include within
521 the scope of its coverage, prohibits the exercise of, or is
522 conditioned on the non-exercise of one or more of the rights that are
523 specifically granted under this License. You may not convey a covered
524 work if you are a party to an arrangement with a third party that is
525 in the business of distributing software, under which you make payment
526 to the third party based on the extent of your activity of conveying
527 the work, and under which the third party grants, to any of the
528 parties who would receive the covered work from you, a discriminatory
529 patent license (a) in connection with copies of the covered work
530 conveyed by you (or copies made from those copies), or (b) primarily
531 for and in connection with specific products or compilations that
532 contain the covered work, unless you entered into that arrangement,
533 or that patent license was granted, prior to 28 March 2007.
534
535 Nothing in this License shall be construed as excluding or limiting
536 any implied license or other defenses to infringement that may
537 otherwise be available to you under applicable patent law.
538
539 12. No Surrender of Others' Freedom.
540
541 If conditions are imposed on you (whether by court order, agreement or
542 otherwise) that contradict the conditions of this License, they do not
543 excuse you from the conditions of this License. If you cannot convey a
544 covered work so as to satisfy simultaneously your obligations under this
545 License and any other pertinent obligations, then as a consequence you may
546 not convey it at all. For example, if you agree to terms that obligate you
547 to collect a royalty for further conveying from those to whom you convey
548 the Program, the only way you could satisfy both those terms and this
549 License would be to refrain entirely from conveying the Program.
550
551 13. Use with the GNU Affero General Public License.
552
553 Notwithstanding any other provision of this License, you have
554 permission to link or combine any covered work with a work licensed
555 under version 3 of the GNU Affero General Public License into a single
556 combined work, and to convey the resulting work. The terms of this
557 License will continue to apply to the part which is the covered work,
558 but the special requirements of the GNU Affero General Public License,
559 section 13, concerning interaction through a network will apply to the
560 combination as such.
561
562 14. Revised Versions of this License.
563
564 The Free Software Foundation may publish revised and/or new versions of
565 the GNU General Public License from time to time. Such new versions will
566 be similar in spirit to the present version, but may differ in detail to
567 address new problems or concerns.
568
569 Each version is given a distinguishing version number. If the
570 Program specifies that a certain numbered version of the GNU General
571 Public License "or any later version" applies to it, you have the
572 option of following the terms and conditions either of that numbered
573 version or of any later version published by the Free Software
574 Foundation. If the Program does not specify a version number of the
575 GNU General Public License, you may choose any version ever published
576 by the Free Software Foundation.
577
578 If the Program specifies that a proxy can decide which future
579 versions of the GNU General Public License can be used, that proxy's
580 public statement of acceptance of a version permanently authorizes you
581 to choose that version for the Program.
582
583 Later license versions may give you additional or different
584 permissions. However, no additional obligations are imposed on any
585 author or copyright holder as a result of your choosing to follow a
586 later version.
587
588 15. Disclaimer of Warranty.
589
590 THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
591 APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
592 HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
593 OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
594 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
595 PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
596 IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
597 ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
598
599 16. Limitation of Liability.
600
601 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
602 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
603 THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
604 GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
605 USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
606 DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
607 PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
608 EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
609 SUCH DAMAGES.
610
611 17. Interpretation of Sections 15 and 16.
612
613 If the disclaimer of warranty and limitation of liability provided
614 above cannot be given local legal effect according to their terms,
615 reviewing courts shall apply local law that most closely approximates
616 an absolute waiver of all civil liability in connection with the
617 Program, unless a warranty or assumption of liability accompanies a
618 copy of the Program in return for a fee.
619
620 END OF TERMS AND CONDITIONS
621
622 How to Apply These Terms to Your New Programs
623
624 If you develop a new program, and you want it to be of the greatest
625 possible use to the public, the best way to achieve this is to make it
626 free software which everyone can redistribute and change under these terms.
627
628 To do so, attach the following notices to the program. It is safest
629 to attach them to the start of each source file to most effectively
630 state the exclusion of warranty; and each file should have at least
631 the "copyright" line and a pointer to where the full notice is found.
632
633 <one line to give the program's name and a brief idea of what it does.>
634 Copyright (C) <year> <name of author>
635
636 This program is free software: you can redistribute it and/or modify
637 it under the terms of the GNU General Public License as published by
638 the Free Software Foundation, either version 3 of the License, or
639 (at your option) any later version.
640
641 This program is distributed in the hope that it will be useful,
642 but WITHOUT ANY WARRANTY; without even the implied warranty of
643 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
644 GNU General Public License for more details.
645
646 You should have received a copy of the GNU General Public License
647 along with this program. If not, see <http://www.gnu.org/licenses/>.
648
649 Also add information on how to contact you by electronic and paper mail.
650
651 If the program does terminal interaction, make it output a short
652 notice like this when it starts in an interactive mode:
653
654 <program> Copyright (C) <year> <name of author>
655 This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
656 This is free software, and you are welcome to redistribute it
657 under certain conditions; type `show c' for details.
658
659 The hypothetical commands `show w' and `show c' should show the appropriate
660 parts of the General Public License. Of course, your program's commands
661 might be different; for a GUI interface, you would use an "about box".
662
663 You should also get your employer (if you work as a programmer) or school,
664 if any, to sign a "copyright disclaimer" for the program, if necessary.
665 For more information on this, and how to apply and follow the GNU GPL, see
666 <http://www.gnu.org/licenses/>.
667
668 The GNU General Public License does not permit incorporating your program
669 into proprietary programs. If your program is a subroutine library, you
670 may consider it more useful to permit linking proprietary applications with
671 the library. If this is what you want to do, use the GNU Lesser General
672 Public License instead of this License. But first, please read
673 <http://www.gnu.org/philosophy/why-not-lgpl.html>.
0 Mirage, a fast and simple GTK+ Image Viewer
1 Copyright 2007 Scott Horowitz <stonecrest@gmail.com>
2
3 REQUIREMENTS:
4 (Required) PyGTK 2.12.0 or newer
5 (Required) GTK 2.10.0 or newer
6 (Optional) Gnome-python if you want Mirage's toolbar to follow your Gnome setting
7 (Building) GCC
8 (Building) python-dev and libX11-dev (on some distros)
9
10 INSTALLATION:
11 Just run 'python setup.py install' as root.
12
13 USAGE:
14 Simply run mirage to open a blank new window. Note that you can select multiple
15 files/folders from the 'Open' dialogs. 'mirage --help' is also available.
16
17 FEATURES:
18 + Supports png, jpg, svg, xpm, gif, bmp, tiff, and others
19 + Cycling through multiple images (with preloading)
20 + Thumbnail pane for quick navigation
21 + Slideshow and fullscreen modes
22 + Rotating, zooming, flipping, resizing, cropping
23 + Saving, deleting, renaming
24 + Screenshot support
25 + Custom actions
26 + Command-line access
27 + Configurable interface
28 + Available in many languages
0 + Pluralized words using gettext
1
2 1.0
3 + Allow zooming in crop dialog (default to same view as main window so that
4 the user can essentially 'crop visible' by clicking ok right away?)
5 + why is light/dark grey no longer used for transparency? (argh)
6 + Printing support (requires gtk 2.10, pygtk) (Bjoern Martensen)
7 + Try to highlight selected region of cropped image (or darken unselected region)
8 + remote images - load in separate thread to prevent blocking...
9 + code cleanup... (preload_*, animtest stuff, rename funcs)
10 + Location of thumbpane?
11
12 Future
13 + Ability to specify save compression level?
14 + Test on Windows/Mac? (requires new pygtk)
15 + Try to make zooming smoother (is this even possible with pygtk?)
16 + UPnP support?
17 + mirage --add to add image to current instance instead of opening
18 new window
0 Mirage provides a quick and easy way of creating a translation for
1 your language. If you find this software useful and are capable of
2 adding a language that has not yet been translated, it would be
3 greatly appreciated.
4
5 1. Download the latest messages.po file. Please use the SVN
6 version, which can be found at:
7 http://svn.berlios.de/viewvc/mirageiv/trunk/po/messages.po
8
9 2. Use poedit (http://www.poedit.org/) to create translations for
10 all of the strings.
11
12 3. Join the mirageiv-translators mailing list:
13 https://lists.berlios.de/mailman/listinfo/mirageiv-translations
14
15 4. Submit your po file and keep it updated for future releases ;-)
0 #include "Python.h"
1 /**
2 * copy length chars from source to dest
3 */
4 void copy(char *dest, const char *source, const int length)
5 {
6 int i;
7 for(i=0; i< length; i++, dest++, source++)
8 *dest = *source;
9 }
10
11 /* Wrapper methods */
12 PyObject *rotate_right(PyObject *self, PyObject *args)
13 {
14 char *a1;
15 char *a2;
16 int length;
17 int w1, w2;
18 int h1, h2;
19 int rws1, rws2;
20 int psz;
21 int i1, i2;
22 int x1, y1;
23 PyObject *ret;
24
25 /* Get Python Arguments */
26 if(!PyArg_ParseTuple(args, "z#iiii", &a1, &length, &w1, &h1, &rws1, &psz))
27 {
28 return NULL;
29 }
30
31 /* Do the mirroring */
32 w2 = h1;
33 h2 = w1;
34
35 if(w2 % 4 != 0)
36 rws2 = ((w2/4 + 1) * 4) * psz;
37 else
38 rws2 = w2 * psz;
39
40 length = rws2 * h2;
41 a2 = malloc(length);
42
43 for(x1=0; x1<w1; x1++)
44 {
45 for(y1=0; y1<h1; y1++)
46 {
47 i1 = y1 * rws1 + x1 * psz;
48 i2 = (h1 - 1 - y1) * psz + rws2 * x1;
49 copy(a2 + i2, a1 + i1, psz);
50 }
51 }
52
53 ret = Py_BuildValue("z#iii", a2, length, w2, h2, rws2);
54 free(a2);
55
56 return ret;
57 }
58
59 PyObject *rotate_left(PyObject *self, PyObject *args)
60 {
61 char *a1;
62 char *a2;
63 int length;
64 int w1, w2;
65 int h1, h2;
66 int rws1, rws2;
67 int psz;
68 int i1, i2;
69 int x1, y1;
70 PyObject *ret;
71
72 /* Get Python Arguments */
73 if(!PyArg_ParseTuple(args, "z#iiii", &a1, &length, &w1, &h1, &rws1, &psz))
74 {
75 return NULL;
76 }
77
78 /* Do the mirroring */
79 w2 = h1;
80 h2 = w1;
81
82 if(w2 % 4 != 0)
83 rws2 = ((w2/4 + 1) * 4) * psz;
84 else
85 rws2 = w2 * psz;
86
87 length = rws2 * h2;
88 a2 = malloc(length);
89
90 for(x1=0; x1<w1; x1++)
91 {
92 for(y1=0; y1<h1; y1++)
93 {
94 i1 = y1 * rws1 + x1 * psz;
95 i2 = y1 * psz + rws2 * (w1 - 1 - x1);
96 copy(a2 + i2, a1 + i1, psz);
97 }
98 }
99
100 ret = Py_BuildValue("z#iii", a2, length, w2, h2, rws2);
101 free(a2);
102
103 return ret;
104 }
105
106 PyObject *rotate_mirror(PyObject *self, PyObject *args)
107 {
108 char *a1;
109 char *a2;
110 int length;
111 int w1, w2;
112 int h1, h2;
113 int rws1, rws2;
114 int psz;
115 int i1, i2;
116 int x1, y1;
117 PyObject *ret;
118
119 /* Get Python Arguments */
120 if(!PyArg_ParseTuple(args, "z#iiii", &a1, &length, &w1, &h1, &rws1, &psz))
121 {
122 return NULL;
123 }
124
125 /* Do the mirroring */
126 w2 = w1;
127 h2 = h1;
128 rws2 = rws1;
129
130 length = rws2 * h2;
131 a2 = malloc(length);
132
133 for(x1=0; x1<w1; x1++)
134 {
135 for(y1=0; y1<h1; y1++)
136 {
137 i1 = y1 * rws1 + x1 * psz;
138 i2 = (w1 - 1 - x1) * psz + rws2 * (h1 - 1 - y1);
139 copy(a2 + i2, a1 + i1, psz);
140 }
141 }
142
143 ret = Py_BuildValue("z#iii", a2, length, w2, h2, rws2);
144 free(a2);
145
146 return ret;
147 }
148
149 PyObject *flip_vert(PyObject *self, PyObject *args)
150 {
151 char *a1;
152 char *a2;
153 int length;
154 int w1, w2;
155 int h1, h2;
156 int rws1, rws2;
157 int psz;
158 int i1, i2;
159 int x1, y1;
160 PyObject *ret;
161
162 /* Get Python Arguments */
163 if(!PyArg_ParseTuple(args, "z#iiii", &a1, &length, &w1, &h1, &rws1, &psz))
164 {
165 return NULL;
166 }
167
168 /* Do the mirroring */
169 w2 = w1;
170 h2 = h1;
171 rws2 = rws1;
172
173 length = rws2 * h2;
174 a2 = malloc(length);
175
176 for(x1=0; x1<w1; x1++)
177 {
178 for(y1=0; y1<h1; y1++)
179 {
180 i1 = y1 * rws1 + x1 * psz;
181 i2 = x1 * psz + rws2 * (h1 - 1 - y1);
182 copy(a2 + i2, a1 + i1, psz);
183 }
184 }
185
186 ret = Py_BuildValue("z#iii", a2, length, w2, h2, rws2);
187 free(a2);
188
189 return ret;
190 }
191
192 PyObject *flip_horiz(PyObject *self, PyObject *args)
193 {
194 char *a1;
195 char *a2;
196 int length;
197 int w1, w2;
198 int h1, h2;
199 int rws1, rws2;
200 int psz;
201 int i1, i2;
202 int x1, y1;
203 PyObject *ret;
204
205 /* Get Python Arguments */
206 if(!PyArg_ParseTuple(args, "z#iiii", &a1, &length, &w1, &h1, &rws1, &psz))
207 {
208 return NULL;
209 }
210
211 /* Do the mirroring */
212 w2 = w1;
213 h2 = h1;
214 rws2 = rws1;
215
216 length = rws2 * h2;
217 a2 = malloc(length);
218
219 for(x1=0; x1<w1; x1++)
220 {
221 for(y1=0; y1<h1; y1++)
222 {
223 i1 = y1 * rws1 + x1 * psz;
224 i2 = (w1 - 1 - x1) * psz + rws2 * y1;
225 copy(a2 + i2, a1 + i1, psz);
226 }
227 }
228
229 ret = Py_BuildValue("z#iii", a2, length, w2, h2, rws2);
230 free(a2);
231
232 return ret;
233 }
234
235 /* Method table mapping names to wrappers */
236 static PyMethodDef imgfuncs_methods[] = {
237 {"left", rotate_left, METH_VARARGS},
238 {"right", rotate_right, METH_VARARGS},
239 {"mirror", rotate_mirror, METH_VARARGS},
240 {"vert", flip_vert, METH_VARARGS},
241 {"horiz", flip_horiz, METH_VARARGS},
242 {NULL, NULL, 0}
243 };
244
245 /* Module initialization function */
246 void initimgfuncs(void)
247 {
248 Py_InitModule("imgfuncs", imgfuncs_methods);
249 }
0 #!/usr/bin/env python
1 """Mirage is a fast GTK+ Image Viewer
2 """
3
4 __author__ = "Scott Horowitz"
5 __email__ = "stonecrest@gmail.com"
6 __license__ = """
7 Mirage, a simple GTK+ Image Viewer
8 Copyright 2007 Scott Horowitz <stonecrest@gmail.com>
9
10 This file is part of Mirage.
11
12 Mirage is free software; you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation; either version 3 of the License, or
15 (at your option) any later version.
16
17 Mirage is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
21
22 You should have received a copy of the GNU General Public License
23 along with this program. If not, see <http://www.gnu.org/licenses/>.
24 """
25
26 import mirage
27
28 if __name__ == "__main__":
29 app = mirage.Base()
30 try:
31 app.main()
32 except KeyboardInterrupt:
33 pass
0 [Desktop Entry]
1 Name=Mirage
2 Comment=A fast GTK+ Image Viewer
3 Exec=mirage %f
4 Terminal=false
5 Type=Application
6 Icon=mirage
7 Categories=GTK;Graphics;2DGraphics;Viewer;
8 MimeType=image/bmp;image/gif;image/jpeg;image/jpg;image/pjpeg;image/png;image/tiff;image/x-bmp;image/x-pcx;image/x-png;image/x-portable-anymap;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/x-sun-raster;image/x-tga;image/x-xbitmap;image/x-xpixmap;image/svg+xml;
Binary diff not shown
Binary diff not shown
0 # $HeadURL: http://svn.berlios.de/svnroot/repos/mirageiv/branches/mirage-0.9.x/mirage.py $
1 # $Id: mirage.py 337 2011-02-13 22:40:05Z fredricj $
2
3 __version__ = "0.9.5.2"
4
5 __license__ = """
6 Mirage, a fast GTK+ Image Viewer
7 Copyright 2007 Scott Horowitz <stonecrest@gmail.com>
8
9 This file is part of Mirage.
10
11 Mirage is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation; either version 3 of the License, or
14 (at your option) any later version.
15
16 Mirage is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
20
21 You should have received a copy of the GNU General Public License
22 along with this program. If not, see <http://www.gnu.org/licenses/>.
23 """
24
25 import pygtk
26 pygtk.require('2.0')
27 import gtk
28 import os, sys, getopt, ConfigParser, string, gc
29 import random, urllib, gobject, gettext, locale
30 import stat, time, subprocess, shutil, filecmp
31 import tempfile, socket, threading
32 try:
33 import hashlib
34 HAS_HASHLIB = True
35 except:
36 HAS_HASHLIB= False
37 import md5
38 try:
39 import imgfuncs
40 HAS_IMGFUNCS = True
41 except:
42 HAS_IMGFUNCS = False
43 print "imgfuncs.so module not found, rotating/flipping images will be disabled."
44 try:
45 import xmouse
46 HAS_XMOUSE = True
47 except:
48 HAS_XMOUSE = False
49 print "xmouse.so module not found, some screenshot capabilities will be disabled."
50 try:
51 import gconf
52 except:
53 pass
54
55 if gtk.gtk_version < (2, 10, 0):
56 sys.stderr.write("Mirage requires GTK+ 2.10.0 or newer..\n")
57 sys.exit(1)
58 if gtk.pygtk_version < (2, 12, 0):
59 sys.stderr.write("Mirage requires PyGTK 2.12.0 or newer.\n")
60 sys.exit(1)
61
62 def valid_int(inputstring):
63 try:
64 x = int(inputstring)
65 return True
66 except:
67 return False
68
69 class Base:
70
71 def __init__(self):
72
73 gtk.gdk.threads_init()
74
75 # FIX THIS! Does not work on windows and what happens if mo-files exists
76 # in both dirs?
77 gettext.install('mirage', '/usr/share/locale', unicode=1)
78 gettext.install('mirage', '/usr/local/share/locale', unicode=1)
79
80 # Constants
81 self.open_mode_smart = 0
82 self.open_mode_fit = 1
83 self.open_mode_1to1 = 2
84 self.open_mode_last = 3
85 self.min_zoomratio = 0.02
86
87 # Initialize vars:
88 width=600
89 height=400
90 bgcolor_found = False
91 self.simple_bgcolor = False
92 # Current image:
93 self.curr_img_in_list = 0
94 self.currimg_name = ""
95 self.currimg_width = 0
96 self.currimg_height = 0
97 self.currimg_pixbuf = None
98 self.currimg_pixbuf_original = None
99 self.currimg_zoomratio = 1
100 self.currimg_is_animation = False
101 # This is the actual pixbuf that is loaded in Mirage. This will
102 # usually be the same as self.curr_img_in_list except for scenarios
103 # like when the user presses 'next image' multiple times in a row.
104 # In this case, self.curr_img_in_list will increment while
105 # self.loaded_img_in_list will retain the current loaded image.
106 self.loaded_img_in_list = 0
107 # Next preloaded image:
108 self.preloadimg_next_in_list = -1
109 self.preloadimg_next_name = ""
110 self.preloadimg_next_width = 0
111 self.preloadimg_next_height = 0
112 self.preloadimg_next_pixbuf = None
113 self.preloadimg_next_pixbuf_original = None
114 self.preloadimg_next_zoomratio = 1
115 self.preloadimg_next_is_animation = False
116 # Previous preloaded image:
117 self.preloadimg_prev_in_list = -1
118 self.preloadimg_prev_name = ""
119 self.preloadimg_prev_width = 0
120 self.preloadimg_prev_height = 0
121 self.preloadimg_prev_pixbuf = None
122 self.preloadimg_prev_pixbuf_original = None
123 self.preloadimg_prev_zoomratio = 1
124 self.preloadimg_prev_is_animation = False
125 # Settings, misc:
126 self.toolbar_show = True
127 self.thumbpane_show = True
128 self.statusbar_show = True
129 self.fullscreen_mode = False
130 self.opendialogpath = ""
131 self.zoom_quality = gtk.gdk.INTERP_BILINEAR
132 self.recursive = False
133 self.verbose = False
134 self.image_loaded = False
135 self.open_all_images = True # open all images in the directory(ies)
136 self.use_last_dir = True
137 self.last_dir = os.path.expanduser("~")
138 self.fixed_dir = os.path.expanduser("~")
139 self.image_list = []
140 self.open_mode = self.open_mode_smart
141 self.last_mode = self.open_mode_smart
142 self.listwrap_mode = 0 # 0=no, 1=yes, 2=ask
143 self.user_prompt_visible = False # the "wrap?" prompt
144 self.slideshow_delay = 1 # seconds
145 self.slideshow_mode = False
146 self.slideshow_random = False
147 self.slideshow_controls_visible = False # fullscreen slideshow controls
148 self.controls_moving = False
149 self.zoomvalue = 2
150 self.quality_save = 90
151 self.updating_adjustments = False
152 self.disable_screensaver = False
153 self.slideshow_in_fullscreen = False
154 self.closing_app = False
155 self.confirm_delete = True
156 self.preloading_images = True
157 self.action_names = ["Open in GIMP", "Create Thumbnail", "Create Thumbnails", "Move to Favorites"]
158 self.action_shortcuts = ["<Control>e", "<Alt>t", "<Control><Alt>t", "<Control><Alt>f"]
159 self.action_commands = ["gimp-remote-2.4 %F", "convert %F -thumbnail 150x150 %Pt_%N.jpg", "convert %F -thumbnail 150x150 %Pt_%N.jpg", "mkdir -p ~/mirage-favs; mv %F ~/mirage-favs; [NEXT]"]
160 self.action_batch = [False, False, True, False]
161 self.onload_cmd = None
162 self.searching_for_images = False
163 self.preserve_aspect = True
164 self.ignore_preserve_aspect_callback = False
165 self.savemode = 2
166 self.image_modified = False
167 self.image_zoomed = False
168 self.start_in_fullscreen = False
169 self.running_custom_actions = False
170 self.merge_id = None
171 self.actionGroupCustom = None
172 self.merge_id_recent = None
173 self.actionGroupRecent = None
174 self.open_hidden_files = False
175 self.thumbnail_sizes = ["128", "96", "72", "64", "48", "32"]
176 self.thumbnail_size = 128 # Default to 128 x 128
177 self.thumbnail_loaded = []
178 self.thumbpane_updating = False
179 self.recentfiles = ["", "", "", "", ""]
180 self.screenshot_delay = 2
181 self.thumbpane_bottom_coord_loaded = 0
182
183 # Read any passed options/arguments:
184 try:
185 opts, args = getopt.getopt(sys.argv[1:], "hRvVsfo:", ["help", "version", "recursive", "verbose", "slideshow", "fullscreen", "onload="])
186 except getopt.GetoptError:
187 # print help information and exit:
188 self.print_usage()
189 sys.exit(2)
190 # If options were passed, perform action on them.
191 if opts != []:
192 for o, a in opts:
193 if o in ("-v", "--version"):
194 self.print_version()
195 sys.exit(2)
196 elif o in ("-h", "--help"):
197 self.print_usage()
198 sys.exit(2)
199 elif o in ("-R", "--recursive"):
200 self.recursive = True
201 elif o in ("-V", "--verbose"):
202 self.verbose = True
203 elif o in ("-s", "--slideshow", "-f", "--fullscreen"):
204 #This will be handled later
205 None
206 elif o in ("-o", "--onload"):
207 self.onload_cmd = a
208 else:
209 self.print_usage()
210 sys.exit(2)
211
212
213 # Determine config dir, first try the environment variable XDG_CONFIG_HOME
214 # according to XDG specification and as a fallback use ~/.config/mirage
215 self.config_dir = (os.getenv('XDG_CONFIG_HOME') or os.path.expanduser('~/.config')) + '/mirage'
216 # Load config from disk:
217 conf = ConfigParser.ConfigParser()
218 if os.path.isfile(self.config_dir + '/miragerc'):
219 conf.read(self.config_dir + '/miragerc')
220 if conf.has_option('window', 'w'):
221 width = conf.getint('window', 'w')
222 if conf.has_option('window', 'h'):
223 height = conf.getint('window', 'h')
224 if conf.has_option('window', 'toolbar'):
225 self.toolbar_show = conf.getboolean('window', 'toolbar')
226 if conf.has_option('window', 'statusbar'):
227 self.statusbar_show = conf.getboolean('window', 'statusbar')
228 if conf.has_option('window', 'thumbpane'):
229 self.thumbpane_show = conf.getboolean('window', 'thumbpane')
230 if conf.has_option('prefs', 'simple-bgcolor'):
231 self.simple_bgcolor = conf.getboolean('prefs', 'simple-bgcolor')
232 if conf.has_option('prefs', 'bgcolor-red'):
233 bgr = conf.getint('prefs', 'bgcolor-red')
234 bgg = conf.getint('prefs', 'bgcolor-green')
235 bgb = conf.getint('prefs', 'bgcolor-blue')
236 bgcolor_found = True
237 self.bgcolor = gtk.gdk.Color(red=bgr, green=bgg, blue=bgb)
238 if conf.has_option('prefs', 'use_last_dir'):
239 self.use_last_dir = conf.getboolean('prefs', 'use_last_dir')
240 if conf.has_option('prefs', 'last_dir'):
241 self.last_dir = conf.get('prefs', 'last_dir')
242 if conf.has_option('prefs', 'fixed_dir'):
243 self.fixed_dir = conf.get('prefs', 'fixed_dir')
244 if conf.has_option('prefs', 'open_all'):
245 self.open_all_images = conf.getboolean('prefs', 'open_all')
246 if conf.has_option('prefs', 'hidden'):
247 self.open_hidden_files = conf.getboolean('prefs', 'hidden')
248 if conf.has_option('prefs', 'open_mode'):
249 self.open_mode = conf.getint('prefs', 'open_mode')
250 if conf.has_option('prefs', 'last_mode'):
251 self.last_mode = conf.getint('prefs', 'last_mode')
252 if conf.has_option('prefs', 'listwrap_mode'):
253 self.listwrap_mode = conf.getint('prefs', 'listwrap_mode')
254 if conf.has_option('prefs', 'slideshow_delay'):
255 self.slideshow_delay = conf.getint('prefs', 'slideshow_delay')
256 if conf.has_option('prefs', 'slideshow_random'):
257 self.slideshow_random = conf.getboolean('prefs', 'slideshow_random')
258 if conf.has_option('prefs', 'zoomquality'):
259 self.zoomvalue = conf.getint('prefs', 'zoomquality')
260 if int(round(self.zoomvalue, 0)) == 0:
261 self.zoom_quality = gtk.gdk.INTERP_NEAREST
262 elif int(round(self.zoomvalue, 0)) == 1:
263 self.zoom_quality = gtk.gdk.INTERP_TILES
264 elif int(round(self.zoomvalue, 0)) == 2:
265 self.zoom_quality = gtk.gdk.INTERP_BILINEAR
266 elif int(round(self.zoomvalue, 0)) == 3:
267 self.zoom_quality = gtk.gdk.INTERP_HYPER
268 if conf.has_option('prefs', 'quality_save'):
269 self.quality_save = conf.getint('prefs', 'quality_save')
270 if conf.has_option('prefs', 'disable_screensaver'):
271 self.disable_screensaver = conf.getboolean('prefs', 'disable_screensaver')
272 if conf.has_option('prefs', 'slideshow_in_fullscreen'):
273 self.slideshow_in_fullscreen = conf.getboolean('prefs', 'slideshow_in_fullscreen')
274 if conf.has_option('prefs', 'preloading_images'):
275 self.preloading_images = conf.getboolean('prefs', 'preloading_images')
276 if conf.has_option('prefs', 'thumbsize'):
277 self.thumbnail_size = conf.getint('prefs', 'thumbsize')
278 if conf.has_option('prefs', 'screenshot_delay'):
279 self.screenshot_delay = conf.getint('prefs', 'screenshot_delay')
280 if conf.has_option('actions', 'num_actions'):
281 num_actions = conf.getint('actions', 'num_actions')
282 self.action_names = []
283 self.action_commands = []
284 self.action_shortcuts = []
285 self.action_batch = []
286 for i in range(num_actions):
287 if conf.has_option('actions', 'names[' + str(i) + ']') and conf.has_option('actions', 'commands[' + str(i) + ']') and conf.has_option('actions', 'shortcuts[' + str(i) + ']') and conf.has_option('actions', 'batch[' + str(i) + ']'):
288 self.action_names.append(conf.get('actions', 'names[' + str(i) + ']'))
289 self.action_commands.append(conf.get('actions', 'commands[' + str(i) + ']'))
290 self.action_shortcuts.append(conf.get('actions', 'shortcuts[' + str(i) + ']'))
291 self.action_batch.append(conf.getboolean('actions', 'batch[' + str(i) + ']'))
292 if conf.has_option('prefs', 'savemode'):
293 self.savemode = conf.getint('prefs', 'savemode')
294 if conf.has_option('prefs', 'start_in_fullscreen'):
295 self.start_in_fullscreen = conf.getboolean('prefs', 'start_in_fullscreen')
296 if conf.has_option('prefs', 'confirm_delete'):
297 self.confirm_delete = conf.getboolean('prefs', 'confirm_delete')
298 self.recentfiles = []
299 if conf.has_option('recent', 'num_recent'):
300 num_recent = conf.getint('recent', 'num_recent')
301 for i in range(num_recent):
302 self.recentfiles.append('')
303 if conf.has_option('recent', 'urls[' + str(i) + ',0]'):
304 self.recentfiles[i] = conf.get('recent', 'urls[' + str(i) + ',0]')
305 # slideshow_delay is the user's preference, whereas curr_slideshow_delay is
306 # the current delay (which can be changed without affecting the 'default')
307 self.curr_slideshow_delay = self.slideshow_delay
308 # Same for randomization:
309 self.curr_slideshow_random = self.slideshow_random
310
311 # Read accel_map file, if it exists
312 if os.path.isfile(self.config_dir + '/accel_map'):
313 gtk.accel_map_load(self.config_dir + '/accel_map')
314
315 # Directory/ies in which to find application images/pixmaps
316 self.resource_path_list = False
317
318 self.blank_image = gtk.gdk.pixbuf_new_from_file(self.find_path("mirage_blank.png"))
319
320 # Define the main menubar and toolbar:
321 factory = gtk.IconFactory()
322 iconname = 'stock_leave-fullscreen.png'
323 iconname2 = 'stock_fullscreen.png'
324 leave_fullscreen_icon_path = self.find_path(iconname)
325 pixbuf = gtk.gdk.pixbuf_new_from_file(leave_fullscreen_icon_path)
326 iconset = gtk.IconSet(pixbuf)
327 factory.add('leave-fullscreen', iconset)
328 factory.add_default()
329 fullscreen_icon_path = self.find_path(iconname2)
330 pixbuf = gtk.gdk.pixbuf_new_from_file(fullscreen_icon_path)
331 iconset = gtk.IconSet(pixbuf)
332 factory.add('fullscreen', iconset)
333 factory.add_default()
334 try:
335 test = gtk.Button("", gtk.STOCK_LEAVE_FULLSCREEN)
336 leave_fullscreen_icon = gtk.STOCK_LEAVE_FULLSCREEN
337 fullscreen_icon = gtk.STOCK_FULLSCREEN
338 except:
339 # This will allow gtk 2.6 users to run Mirage
340 leave_fullscreen_icon = 'leave-fullscreen'
341 fullscreen_icon = 'fullscreen'
342 actions = (
343 ('FileMenu', None, _('_File')),
344 ('EditMenu', None, _('_Edit')),
345 ('ViewMenu', None, _('_View')),
346 ('GoMenu', None, _('_Go')),
347 ('HelpMenu', None, _('_Help')),
348 ('ActionSubMenu', None, _('Custom _Actions')),
349 ('Open Image', gtk.STOCK_FILE, _('_Open Image...'), '<Ctrl>O', _('Open Image'), self.open_file),
350 ('Open Remote Image', gtk.STOCK_NETWORK, _('Open _Remote image...'), None, _('Open Remote Image'), self.open_file_remote),
351 ('Open Folder', gtk.STOCK_DIRECTORY, _('Open _Folder...'), '<Ctrl>F', _('Open Folder'), self.open_folder),
352 ('Save', gtk.STOCK_SAVE, _('_Save Image'), '<Ctrl>S', _('Save Image'), self.save_image),
353 ('Save As', gtk.STOCK_SAVE, _('Save Image _As...'), '<Shift><Ctrl>S', _('Save Image As'), self.save_image_as),
354 ('Crop', None, _('_Crop...'), None, _('Crop Image'), self.crop_image),
355 ('Resize', None, _('R_esize...'), None, _('Resize Image'), self.resize_image),
356 ('Saturation', None, _('_Saturation...'), None, _('Modify saturation'), self.saturation),
357 ('Quit', gtk.STOCK_QUIT, _('_Quit'), '<Ctrl>Q', _('Quit'), self.exit_app),
358 ('Previous Image', gtk.STOCK_GO_BACK, _('_Previous Image'), 'Left', _('Previous Image'), self.goto_prev_image),
359 ('Next Image', gtk.STOCK_GO_FORWARD, _('_Next Image'), 'Right', _('Next Image'), self.goto_next_image),
360 ('Previous2', gtk.STOCK_GO_BACK, _('_Previous'), 'Left', _('Previous'), self.goto_prev_image),
361 ('Next2', gtk.STOCK_GO_FORWARD, _('_Next'), 'Right', _('Next'), self.goto_next_image),
362 ('Random Image', None, _('_Random Image'), 'R', _('Random Image'), self.goto_random_image),
363 ('First Image', gtk.STOCK_GOTO_FIRST, _('_First Image'), 'Home', _('First Image'), self.goto_first_image),
364 ('Last Image', gtk.STOCK_GOTO_LAST, _('_Last Image'), 'End', _('Last Image'), self.goto_last_image),
365 ('In', gtk.STOCK_ZOOM_IN, _('Zoom _In'), '<Ctrl>Up', _('Zoom In'), self.zoom_in),
366 ('Out', gtk.STOCK_ZOOM_OUT, _('Zoom _Out'), '<Ctrl>Down', _('Zoom Out'), self.zoom_out),
367 ('Fit', gtk.STOCK_ZOOM_FIT, _('Zoom To _Fit'), '<Ctrl>0', _('Fit'), self.zoom_to_fit_window_action),
368 ('1:1', gtk.STOCK_ZOOM_100, _('_1:1'), '<Ctrl>1', _('1:1'), self.zoom_1_to_1_action),
369 ('Rotate Left', None, _('Rotate _Left'), '<Ctrl>Left', _('Rotate Left'), self.rotate_left),
370 ('Rotate Right', None, _('Rotate _Right'), '<Ctrl>Right', _('Rotate Right'), self.rotate_right),
371 ('Flip Vertically', None, _('Flip _Vertically'), '<Ctrl>V', _('Flip Vertically'), self.flip_image_vert),
372 ('Flip Horizontally', None, _('Flip _Horizontally'), '<Ctrl>H', _('Flip Horizontally'), self.flip_image_horiz),
373 ('About', gtk.STOCK_ABOUT, _('_About'), None, _('About'), self.show_about),
374 ('Contents', gtk.STOCK_HELP, _('_Contents'), 'F1', _('Contents'), self.show_help),
375 ('Preferences', gtk.STOCK_PREFERENCES, _('_Preferences...'), '<Ctrl>P', _('Preferences'), self.show_prefs),
376 ('Full Screen', fullscreen_icon, _('_Full Screen'), 'F11', _('Full Screen'), self.enter_fullscreen),
377 ('Exit Full Screen', leave_fullscreen_icon, _('E_xit Full Screen'), None, _('Exit Full Screen'), self.leave_fullscreen),
378 ('Start Slideshow', gtk.STOCK_MEDIA_PLAY, _('_Start Slideshow'), 'F5', _('Start Slideshow'), self.toggle_slideshow),
379 ('Stop Slideshow', gtk.STOCK_MEDIA_STOP, _('_Stop Slideshow'), 'F5', _('Stop Slideshow'), self.toggle_slideshow),
380 ('Delete Image', gtk.STOCK_DELETE, _('_Delete...'), 'Delete', _('Delete Image'), self.delete_image),
381 ('Rename Image', None, _('Re_name...'), 'F2', _('Rename Image'), self.rename_image),
382 ('Take Screenshot', None, _('_Take Screenshot...'), None, _('Take Screenshot'), self.screenshot),
383 ('Properties', gtk.STOCK_PROPERTIES, _('_Properties...'), None, _('Properties'), self.show_properties),
384 ('Custom Actions', None, _('_Configure...'), None, _('Custom Actions'), self.show_custom_actions),
385 ('MiscKeysMenuHidden', None, 'Keys'),
386 ('Escape', None, '', 'Escape', _('Exit Full Screen'), self.leave_fullscreen),
387 ('Minus', None, '', 'minus', _('Zoom Out'), self.zoom_out),
388 ('Plus', None, '', 'plus', _('Zoom In'), self.zoom_in),
389 ('Equal', None, '', 'equal', _('Zoom In'), self.zoom_in),
390 ('Space', None, '', 'space', _('Next Image'), self.goto_next_image),
391 ('Ctrl-KP_Insert', None, '', '<Ctrl>KP_Insert', _('Fit'), self.zoom_to_fit_window_action),
392 ('Ctrl-KP_End', None, '', '<Ctrl>KP_End', _('1:1'), self.zoom_1_to_1_action),
393 ('Ctrl-KP_Subtract', None, '', '<Ctrl>KP_Subtract', _('Zoom Out'), self.zoom_out),
394 ('Ctrl-KP_Add', None, '', '<Ctrl>KP_Add', _('Zoom In'), self.zoom_in),
395 ('Ctrl-KP_0', None, '', '<Ctrl>KP_0', _('Fit'), self.zoom_to_fit_window_action),
396 ('Ctrl-KP_1', None, '', '<Ctrl>KP_1', _('1:1'), self.zoom_1_to_1_action),
397 ('Full Screen Key', None, '', '<Shift>Return', None, self.enter_fullscreen),
398 ('Prev', None, '', 'Up', _('Previous Image'), self.goto_prev_image),
399 ('Next', None, '', 'Down', _('Next Image'), self.goto_next_image),
400 ('PgUp', None, '', 'Page_Up', _('Previous Image'), self.goto_prev_image),
401 ('PgDn', None, '', 'Page_Down', _('Next Image'), self.goto_next_image),
402 ('BackSpace', None, '', 'BackSpace', _('Previous Image'), self.goto_prev_image),
403 ('OriginalSize', None, '', '1', _('1:1'), self.zoom_1_to_1_action),
404 ('ZoomIn', None, '', 'KP_Add', _('Zoom In'), self.zoom_in),
405 ('ZoomOut', None, '', 'KP_Subtract', _('Zoom Out'), self.zoom_out)
406 )
407 toggle_actions = (
408 ('Status Bar', None, _('_Status Bar'), None, _('Status Bar'), self.toggle_status_bar, self.statusbar_show),
409 ('Toolbar', None, _('_Toolbar'), None, _('Toolbar'), self.toggle_toolbar, self.toolbar_show),
410 ('Thumbnails Pane', None, _('Thumbnails _Pane'), None, _('Thumbnails Pane'), self.toggle_thumbpane, self.thumbpane_show)
411 )
412
413 # Populate keys[]:
414 self.keys=[]
415 for i in range(len(actions)):
416 if len(actions[i]) > 3:
417 if actions[i][3] != None:
418 self.keys.append([actions[i][4], actions[i][3]])
419
420 uiDescription = """
421 <ui>
422 <popup name="Popup">
423 <menuitem action="Next Image"/>
424 <menuitem action="Previous Image"/>
425 <separator name="FM1"/>
426 <menuitem action="Out"/>
427 <menuitem action="In"/>
428 <menuitem action="1:1"/>
429 <menuitem action="Fit"/>
430 <separator name="FM4"/>
431 <menuitem action="Start Slideshow"/>
432 <menuitem action="Stop Slideshow"/>
433 <separator name="FM3"/>
434 <menuitem action="Exit Full Screen"/>
435 <menuitem action="Full Screen"/>
436 </popup>
437 <menubar name="MainMenu">
438 <menu action="FileMenu">
439 <menuitem action="Open Image"/>
440 <menuitem action="Open Folder"/>
441 <menuitem action="Open Remote Image"/>
442 <separator name="FM1"/>
443 <menuitem action="Save"/>
444 <menuitem action="Save As"/>
445 <separator name="FM2"/>
446 <menuitem action="Take Screenshot"/>
447 <separator name="FM3"/>
448 <menuitem action="Properties"/>
449 <separator name="FM4"/>
450 <placeholder name="Recent Files">
451 </placeholder>
452 <separator name="FM5"/>
453 <menuitem action="Quit"/>
454 </menu>
455 <menu action="EditMenu">
456 <menuitem action="Rotate Left"/>
457 <menuitem action="Rotate Right"/>
458 <menuitem action="Flip Vertically"/>
459 <menuitem action="Flip Horizontally"/>
460 <separator name="FM1"/>
461 <menuitem action="Crop"/>
462 <menuitem action="Resize"/>
463 <menuitem action="Saturation"/>
464 <separator name="FM2"/>
465 <menuitem action="Rename Image"/>
466 <menuitem action="Delete Image"/>
467 <separator name="FM3"/>
468 <menu action="ActionSubMenu">
469 <separator name="FM4" position="bot"/>
470 <menuitem action="Custom Actions" position="bot"/>
471 </menu>
472 <menuitem action="Preferences"/>
473 </menu>
474 <menu action="ViewMenu">
475 <menuitem action="Out"/>
476 <menuitem action="In"/>
477 <menuitem action="1:1"/>
478 <menuitem action="Fit"/>
479 <separator name="FM2"/>
480 <menuitem action="Toolbar"/>
481 <menuitem action="Thumbnails Pane"/>
482 <menuitem action="Status Bar"/>
483 <separator name="FM1"/>
484 <menuitem action="Full Screen"/>
485 </menu>
486 <menu action="GoMenu">
487 <menuitem action="Next Image"/>
488 <menuitem action="Previous Image"/>
489 <menuitem action="Random Image"/>
490 <separator name="FM1"/>
491 <menuitem action="First Image"/>
492 <menuitem action="Last Image"/>
493 <separator name="FM2"/>
494 <menuitem action="Start Slideshow"/>
495 <menuitem action="Stop Slideshow"/>
496 </menu>
497 <menu action="HelpMenu">
498 <menuitem action="Contents"/>
499 <menuitem action="About"/>
500 </menu>
501 <menu action="MiscKeysMenuHidden">
502 <menuitem action="Minus"/>
503 <menuitem action="Escape"/>
504 <menuitem action="Plus"/>
505 <menuitem action="Equal"/>
506 <menuitem action="Space"/>
507 <menuitem action="Ctrl-KP_Insert"/>
508 <menuitem action="Ctrl-KP_End"/>
509 <menuitem action="Ctrl-KP_Subtract"/>
510 <menuitem action="Ctrl-KP_Add"/>
511 <menuitem action="Ctrl-KP_0"/>
512 <menuitem action="Ctrl-KP_1"/>
513 <menuitem action="Full Screen Key"/>
514 <menuitem action="Prev"/>
515 <menuitem action="Next"/>
516 <menuitem action="PgUp"/>
517 <menuitem action="PgDn"/>
518 <menuitem action="OriginalSize"/>
519 <menuitem action="BackSpace"/>
520 <menuitem action="ZoomIn"/>
521 <menuitem action="ZoomOut"/>
522 </menu>
523 </menubar>
524 <toolbar name="MainToolbar">
525 <toolitem action="Open Image"/>
526 <separator name="FM1"/>
527 <toolitem action="Previous2"/>
528 <toolitem action="Next2"/>
529 <separator name="FM2"/>
530 <toolitem action="Out"/>
531 <toolitem action="In"/>
532 <toolitem action="1:1"/>
533 <toolitem action="Fit"/>
534 </toolbar>
535 </ui>
536 """
537
538 # Create interface
539 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
540 self.update_title()
541 icon_path = self.find_path('mirage.png')
542 try:
543 gtk.window_set_default_icon_from_file(icon_path)
544 except:
545 pass
546 vbox = gtk.VBox(False, 0)
547 self.UIManager = gtk.UIManager()
548 actionGroup = gtk.ActionGroup('Actions')
549 actionGroup.add_actions(actions)
550 actionGroup.add_toggle_actions(toggle_actions)
551 self.UIManager.insert_action_group(actionGroup, 0)
552 self.UIManager.add_ui_from_string(uiDescription)
553 self.refresh_custom_actions_menu()
554 self.refresh_recent_files_menu()
555 self.window.add_accel_group(self.UIManager.get_accel_group())
556 self.menubar = self.UIManager.get_widget('/MainMenu')
557 vbox.pack_start(self.menubar, False, False, 0)
558 self.toolbar = self.UIManager.get_widget('/MainToolbar')
559 vbox.pack_start(self.toolbar, False, False, 0)
560 self.layout = gtk.Layout()
561 self.vscroll = gtk.VScrollbar(None)
562 self.vscroll.set_adjustment(self.layout.get_vadjustment())
563 self.hscroll = gtk.HScrollbar(None)
564 self.hscroll.set_adjustment(self.layout.get_hadjustment())
565 self.table = gtk.Table(3, 2, False)
566
567 self.thumblist = gtk.ListStore(gtk.gdk.Pixbuf)
568 self.thumbpane = gtk.TreeView(self.thumblist)
569 self.thumbcolumn = gtk.TreeViewColumn(None)
570 self.thumbcell = gtk.CellRendererPixbuf()
571 self.thumbcolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
572 self.thumbpane_set_size()
573 self.thumbpane.append_column(self.thumbcolumn)
574 self.thumbcolumn.pack_start(self.thumbcell, True)
575 self.thumbcolumn.set_attributes(self.thumbcell, pixbuf=0)
576 self.thumbpane.get_selection().set_mode(gtk.SELECTION_SINGLE)
577 self.thumbpane.set_headers_visible(False)
578 self.thumbpane.set_property('can-focus', False)
579 self.thumbscroll = gtk.ScrolledWindow()
580 self.thumbscroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
581 self.thumbscroll.add(self.thumbpane)
582
583 self.table.attach(self.thumbscroll, 0, 1, 0, 1, 0, gtk.FILL|gtk.EXPAND, 0, 0)
584 self.table.attach(self.layout, 1, 2, 0, 1, gtk.FILL|gtk.EXPAND, gtk.FILL|gtk.EXPAND, 0, 0)
585 self.table.attach(self.hscroll, 1, 2, 1, 2, gtk.FILL|gtk.SHRINK, gtk.FILL|gtk.SHRINK, 0, 0)
586 self.table.attach(self.vscroll, 2, 3, 0, 1, gtk.FILL|gtk.SHRINK, gtk.FILL|gtk.SHRINK, 0, 0)
587 vbox.pack_start(self.table, True, True, 0)
588 if not bgcolor_found:
589 self.bgcolor = gtk.gdk.Color(0, 0, 0) # Default to black
590 if self.simple_bgcolor:
591 self.layout.modify_bg(gtk.STATE_NORMAL, None)
592 else:
593 self.layout.modify_bg(gtk.STATE_NORMAL, self.bgcolor)
594 self.imageview = gtk.Image()
595 self.layout.add(self.imageview)
596
597 self.statusbar = gtk.Statusbar()
598 self.statusbar2 = gtk.Statusbar()
599 self.statusbar.set_has_resize_grip(False)
600 self.statusbar2.set_has_resize_grip(True)
601 self.statusbar2.set_size_request(200, -1)
602 hbox_statusbar = gtk.HBox()
603 hbox_statusbar.pack_start(self.statusbar, expand=True)
604 hbox_statusbar.pack_start(self.statusbar2, expand=False)
605 vbox.pack_start(hbox_statusbar, False, False, 0)
606 self.window.add(vbox)
607 self.window.set_property('allow-shrink', False)
608 self.window.set_default_size(width,height)
609
610 # Slideshow control:
611 self.slideshow_window = gtk.Window(gtk.WINDOW_POPUP)
612 self.slideshow_controls = gtk.HBox()
613 self.ss_back = gtk.Button()
614 self.ss_back.add(gtk.image_new_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_BUTTON))
615 self.ss_back.set_property('can-focus', False)
616 self.ss_back.connect('clicked', self.goto_prev_image)
617 self.ss_start = gtk.Button("", gtk.STOCK_MEDIA_PLAY)
618 self.ss_start.get_child().get_child().get_children()[1].set_text('')
619 self.ss_start.set_property('can-focus', False)
620 self.ss_start.connect('clicked', self.toggle_slideshow)
621 self.ss_stop = gtk.Button("", gtk.STOCK_MEDIA_STOP)
622 self.ss_stop.get_child().get_child().get_children()[1].set_text('')
623 self.ss_stop.set_property('can-focus', False)
624 self.ss_stop.connect('clicked', self.toggle_slideshow)
625 self.ss_forward = gtk.Button("", gtk.STOCK_GO_FORWARD)
626 self.ss_forward.get_child().get_child().get_children()[1].set_text('')
627 self.ss_forward.set_property('can-focus', False)
628 self.ss_forward.connect('clicked', self.goto_next_image)
629 self.slideshow_controls.pack_start(self.ss_back, False, False, 0)
630 self.slideshow_controls.pack_start(self.ss_start, False, False, 0)
631 self.slideshow_controls.pack_start(self.ss_stop, False, False, 0)
632 self.slideshow_controls.pack_start(self.ss_forward, False, False, 0)
633 self.slideshow_window.add(self.slideshow_controls)
634 if self.simple_bgcolor:
635 self.slideshow_window.modify_bg(gtk.STATE_NORMAL, None)
636 else:
637 self.slideshow_window.modify_bg(gtk.STATE_NORMAL, self.bgcolor)
638 self.slideshow_window2 = gtk.Window(gtk.WINDOW_POPUP)
639 self.slideshow_controls2 = gtk.HBox()
640 try:
641 self.ss_exit = gtk.Button("", gtk.STOCK_LEAVE_FULLSCREEN)
642 self.ss_exit.get_child().get_child().get_children()[1].set_text('')
643 except:
644 self.ss_exit = gtk.Button()
645 self.ss_exit.set_image(gtk.image_new_from_stock('leave-fullscreen', gtk.ICON_SIZE_MENU))
646 self.ss_exit.set_property('can-focus', False)
647 self.ss_exit.connect('clicked', self.leave_fullscreen)
648 self.ss_randomize = gtk.ToggleButton()
649 icon_path = self.find_path('stock_shuffle.png')
650 try:
651 pixbuf = gtk.gdk.pixbuf_new_from_file(icon_path)
652 iconset = gtk.IconSet(pixbuf)
653 factory.add('stock-shuffle', iconset)
654 factory.add_default()
655 self.ss_randomize.set_image(gtk.image_new_from_stock('stock-shuffle', gtk.ICON_SIZE_MENU))
656 except:
657 self.ss_randomize.set_label("Rand")
658 self.ss_randomize.connect('toggled', self.random_changed)
659
660 spin_adj = gtk.Adjustment(self.slideshow_delay, 0, 50000, 1,100, 0)
661 self.ss_delayspin = gtk.SpinButton(spin_adj, 1.0, 0)
662 self.ss_delayspin.set_numeric(True)
663 self.ss_delayspin.connect('changed', self.delay_changed)
664 self.slideshow_controls2.pack_start(self.ss_randomize, False, False, 0)
665 self.slideshow_controls2.pack_start(self.ss_delayspin, False, False, 0)
666 self.slideshow_controls2.pack_start(self.ss_exit, False, False, 0)
667 self.slideshow_window2.add(self.slideshow_controls2)
668 if self.simple_bgcolor:
669 self.slideshow_window2.modify_bg(gtk.STATE_NORMAL, None)
670 else:
671 self.slideshow_window2.modify_bg(gtk.STATE_NORMAL, self.bgcolor)
672
673 # Connect signals
674 self.window.connect("delete_event", self.delete_event)
675 self.window.connect("destroy", self.destroy)
676 self.window.connect("size-allocate", self.window_resized)
677 self.window.connect('key-press-event', self.topwindow_keypress)
678 self.toolbar.connect('focus', self.toolbar_focused)
679 self.layout.drag_dest_set(gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP, [("text/uri-list", 0, 80)], gtk.gdk.ACTION_DEFAULT)
680 self.layout.connect('drag_motion', self.motion_cb)
681 self.layout.connect('drag_data_received', self.drop_cb)
682 self.layout.add_events(gtk.gdk.KEY_PRESS_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_MOTION_MASK | gtk.gdk.SCROLL_MASK)
683 self.layout.connect("scroll-event", self.mousewheel_scrolled)
684 self.layout.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.KEY_PRESS_MASK)
685 self.layout.connect("button_press_event", self.button_pressed)
686 self.layout.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
687 self.layout.connect("motion-notify-event", self.mouse_moved)
688 self.layout.connect("button-release-event", self.button_released)
689 self.imageview.connect("expose-event", self.expose_event)
690 self.thumb_sel_handler = self.thumbpane.get_selection().connect('changed', self.thumbpane_selection_changed)
691 self.thumb_scroll_handler = self.thumbscroll.get_vscrollbar().connect("value-changed", self.thumbpane_scrolled)
692
693 # Since GNOME does its own thing for the toolbar style...
694 # Requires gnome-python installed to work (but optional)
695 try:
696 client = gconf.client_get_default()
697 style = client.get_string('/desktop/gnome/interface/toolbar_style')
698 if style == "both":
699 self.toolbar.set_style(gtk.TOOLBAR_BOTH)
700 elif style == "both-horiz":
701 self.toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
702 elif style == "icons":
703 self.toolbar.set_style(gtk.TOOLBAR_ICONS)
704 elif style == "text":
705 self.toolbar.set_style(gtk.TOOLBAR_TEXT)
706 client.add_dir("/desktop/gnome/interface", gconf.CLIENT_PRELOAD_NONE)
707 client.notify_add("/desktop/gnome/interface/toolbar_style", self.gconf_key_changed)
708 except:
709 pass
710
711 # Show GUI:
712 if not self.toolbar_show:
713 self.toolbar.set_property('visible', False)
714 self.toolbar.set_no_show_all(True)
715 if not self.statusbar_show:
716 self.statusbar.set_property('visible', False)
717 self.statusbar.set_no_show_all(True)
718 self.statusbar2.set_property('visible', False)
719 self.statusbar2.set_no_show_all(True)
720 if not self.thumbpane_show:
721 self.thumbscroll.set_property('visible', False)
722 self.thumbscroll.set_no_show_all(True)
723 self.hscroll.set_no_show_all(True)
724 self.vscroll.set_no_show_all(True)
725 go_into_fullscreen = False
726 if opts != []:
727 for o, a in opts:
728 if (o in ("-f", "--fullscreen")) or ((o in ("-s", "--slideshow")) and self.slideshow_in_fullscreen):
729 go_into_fullscreen = True
730 if go_into_fullscreen or self.start_in_fullscreen:
731 self.enter_fullscreen(None)
732 self.statusbar.set_no_show_all(True)
733 self.statusbar2.set_no_show_all(True)
734 self.toolbar.set_no_show_all(True)
735 self.menubar.set_no_show_all(True)
736 self.thumbscroll.set_no_show_all(True)
737 self.window.show_all()
738 self.ss_exit.set_size_request(self.ss_start.size_request()[0], self.ss_stop.size_request()[1])
739 self.ss_randomize.set_size_request(self.ss_start.size_request()[0], -1)
740 self.ss_start.set_size_request(self.ss_start.size_request()[0]*2, -1)
741 self.ss_stop.set_size_request(self.ss_stop.size_request()[0]*2, -1)
742 self.UIManager.get_widget('/Popup/Exit Full Screen').hide()
743 self.layout.set_flags(gtk.CAN_FOCUS)
744 self.window.set_focus(self.layout)
745
746 #sets the visibility of some menu entries
747 self.set_slideshow_sensitivities()
748 self.UIManager.get_widget('/MainMenu/MiscKeysMenuHidden').set_property('visible', False)
749 if opts != []:
750 for o, a in opts:
751 if o in ("-f", "--fullscreen"):
752 self.UIManager.get_widget('/Popup/Exit Full Screen').show()
753
754 # If arguments (filenames) were passed, try to open them:
755 self.image_list = []
756 if args != []:
757 for i in range(len(args)):
758 args[i] = urllib.url2pathname(args[i])
759 self.expand_filelist_and_load_image(args)
760 else:
761 self.set_go_sensitivities(False)
762 self.set_image_sensitivities(False)
763
764 if opts != []:
765 for o, a in opts:
766 if o in ("-s", "--slideshow"):
767 self.toggle_slideshow(None)
768
769 def refresh_recent_files_menu(self):
770 if self.merge_id_recent:
771 self.UIManager.remove_ui(self.merge_id_recent)
772 if self.actionGroupRecent:
773 self.UIManager.remove_action_group(self.actionGroupRecent)
774 self.actionGroupRecent = None
775 self.actionGroupRecent = gtk.ActionGroup('RecentFiles')
776 self.UIManager.ensure_update()
777 for i in range(len(self.recentfiles)):
778 if len(self.recentfiles[i]) > 0:
779 filename = self.recentfiles[i].split("/")[-1]
780 if len(filename) > 0:
781 if len(filename) > 27:
782 # Replace end of file name (excluding extension) with ..
783 try:
784 menu_name = filename[:25] + '..' + os.path.splitext(filename)[1]
785 except:
786 menu_name = filename[0]
787 else:
788 menu_name = filename
789 menu_name = menu_name.replace('_','__')
790 action = [(str(i), None, menu_name, '<Alt>' + str(i+1), None, self.recent_action_click)]
791 self.actionGroupRecent.add_actions(action)
792 uiDescription = """
793 <ui>
794 <menubar name="MainMenu">
795 <menu action="FileMenu">
796 <placeholder name="Recent Files">
797 """
798 for i in range(len(self.recentfiles)):
799 if len(self.recentfiles[i]) > 0:
800 uiDescription = uiDescription + """<menuitem action=\"""" + str(i) + """\"/>"""
801 uiDescription = uiDescription + """</placeholder></menu></menubar></ui>"""
802 self.merge_id_recent = self.UIManager.add_ui_from_string(uiDescription)
803 self.UIManager.insert_action_group(self.actionGroupRecent, 0)
804 self.UIManager.get_widget('/MainMenu/MiscKeysMenuHidden').set_property('visible', False)
805
806 def refresh_custom_actions_menu(self):
807 if self.merge_id:
808 self.UIManager.remove_ui(self.merge_id)
809 if self.actionGroupCustom:
810 self.UIManager.remove_action_group(self.actionGroupCustom)
811 self.actionGroupCustom = None
812 self.actionGroupCustom = gtk.ActionGroup('CustomActions')
813 self.UIManager.ensure_update()
814 for i in range(len(self.action_names)):
815 action = [(self.action_names[i], None, self.action_names[i], self.action_shortcuts[i], None, self.custom_action_click)]
816 self.actionGroupCustom.add_actions(action)
817 uiDescription = """
818 <ui>
819 <menubar name="MainMenu">
820 <menu action="EditMenu">
821 <menu action="ActionSubMenu">
822 """
823 for i in range(len(self.action_names)):
824 uiDescription = uiDescription + """<menuitem action=\"""" + self.action_names[len(self.action_names)-i-1].replace('&','&amp;') + """\" position="top"/>"""
825 uiDescription = uiDescription + """</menu></menu></menubar></ui>"""
826 self.merge_id = self.UIManager.add_ui_from_string(uiDescription)
827 self.UIManager.insert_action_group(self.actionGroupCustom, 0)
828 self.UIManager.get_widget('/MainMenu/MiscKeysMenuHidden').set_property('visible', False)
829
830 def thumbpane_update_images(self, clear_first=False, force_upto_imgnum=-1):
831 self.stop_now = False
832 # When first populating the thumbpane, make sure we go up to at least
833 # force_upto_imgnum so that we can show this image selected:
834 if clear_first:
835 self.thumbpane_clear_list()
836 # Load all images up to the bottom ofo the visible thumbpane rect:
837 rect = self.thumbpane.get_visible_rect()
838 bottom_coord = rect.y + rect.height + self.thumbnail_size
839 if bottom_coord > self.thumbpane_bottom_coord_loaded:
840 self.thumbpane_bottom_coord_loaded = bottom_coord
841 # update images:
842 if not self.thumbpane_updating:
843 thread = threading.Thread(target=self.thumbpane_update_pending_images, args=(force_upto_imgnum, None))
844 thread.setDaemon(True)
845 thread.start()
846
847 def thumbpane_create_dir(self):
848 if not os.path.exists(os.path.expanduser('~/.thumbnails/')):
849 os.mkdir(os.path.expanduser('~/.thumbnails/'))
850 if not os.path.exists(os.path.expanduser('~/.thumbnails/normal/')):
851 os.mkdir(os.path.expanduser('~/.thumbnails/normal/'))
852
853 def thumbpane_update_pending_images(self, force_upto_imgnum, foo):
854 self.thumbpane_updating = True
855 self.thumbpane_create_dir()
856 # Check to see if any images need their thumbnails generated.
857 curr_coord = 0
858 imgnum = 0
859 while curr_coord < self.thumbpane_bottom_coord_loaded or imgnum <= force_upto_imgnum:
860 if self.closing_app or self.stop_now or not self.thumbpane_show:
861 break
862 if imgnum >= len(self.image_list):
863 break
864 self.thumbpane_set_image(self.image_list[imgnum], imgnum)
865 curr_coord += self.thumbpane.get_background_area((imgnum,),self.thumbcolumn).height
866 if force_upto_imgnum == imgnum:
867 # Verify that the user hasn't switched images while we're loading thumbnails:
868 if force_upto_imgnum == self.curr_img_in_list:
869 gobject.idle_add(self.thumbpane_select, force_upto_imgnum)
870 imgnum += 1
871 self.thumbpane_updating = False
872
873 def thumbpane_clear_list(self):
874 self.thumbpane_bottom_coord_loaded = 0
875 self.thumbscroll.get_vscrollbar().handler_block(self.thumb_scroll_handler)
876 self.thumblist.clear()
877 self.thumbscroll.get_vscrollbar().handler_unblock(self.thumb_scroll_handler)
878 for image in self.image_list:
879 blank_pix = self.get_blank_pix_for_image(image)
880 self.thumblist.append([blank_pix])
881 self.thumbnail_loaded = [False]*len(self.image_list)
882
883 def thumbpane_set_image(self, image_name, imgnum, force_update=False):
884 if self.thumbpane_show:
885 if not self.thumbnail_loaded[imgnum] or force_update:
886 filename, thumbfile = self.thumbnail_get_name(image_name)
887 pix = self.thumbpane_get_pixbuf(thumbfile, filename, force_update)
888 if pix:
889 if self.thumbnail_size != 128:
890 # 128 is the size of the saved thumbnail, so convert if different:
891 pix, image_width, image_height = self.get_pixbuf_of_size(pix, self.thumbnail_size, gtk.gdk.INTERP_TILES)
892 self.thumbnail_loaded[imgnum] = True
893 self.thumbscroll.get_vscrollbar().handler_block(self.thumb_scroll_handler)
894 pix = self.pixbuf_add_border(pix)
895 try:
896 self.thumblist[imgnum] = [pix]
897 except:
898 pass
899 self.thumbscroll.get_vscrollbar().handler_unblock(self.thumb_scroll_handler)
900
901 def thumbnail_get_name(self, image_name):
902 filename = os.path.expanduser('file://' + image_name)
903 uriname = os.path.expanduser('file://' + urllib.pathname2url(image_name))
904 if HAS_HASHLIB:
905 m = hashlib.md5()
906 else:
907 m = md5.new()
908 m.update(uriname)
909 mhex = m.hexdigest()
910 mhex_filename = os.path.expanduser('~/.thumbnails/normal/' + mhex + '.png')
911 return filename, mhex_filename
912
913 def thumbpane_get_pixbuf(self, thumb_url, image_url, force_generation):
914 # Returns a valid pixbuf or None if a pixbuf cannot be generated. Tries to re-use
915 # a thumbnail from ~/.thumbails/normal/, otherwise generates one with the
916 # XDG filename: md5(file:///full/path/to/image).png
917 imgfile = image_url
918 if imgfile[:7] == 'file://':
919 imgfile = imgfile[7:]
920 try:
921 if os.path.exists(thumb_url) and not force_generation:
922 pix = gtk.gdk.pixbuf_new_from_file(thumb_url)
923 pix_mtime = pix.get_option('tEXt::Thumb::MTime')
924 if pix_mtime:
925 st = os.stat(imgfile)
926 file_mtime = str(st[stat.ST_MTIME])
927 # If the mtimes match, we're good. if not, regenerate the thumbnail..
928 if pix_mtime == file_mtime:
929 return pix
930 # Create the 128x128 thumbnail:
931 uri = 'file://' + urllib.pathname2url(imgfile)
932 pix = gtk.gdk.pixbuf_new_from_file(imgfile)
933 pix, image_width, image_height = self.get_pixbuf_of_size(pix, 128, gtk.gdk.INTERP_TILES)
934 st = os.stat(imgfile)
935 file_mtime = str(st[stat.ST_MTIME])
936 # Save image to .thumbnails:
937 pix.save(thumb_url, "png", {'tEXt::Thumb::URI':uri, 'tEXt::Thumb::MTime':file_mtime, 'tEXt::Software':'Mirage' + __version__})
938 return pix
939 except:
940 return None
941
942 def thumbpane_load_image(self, treeview, imgnum):
943 if imgnum != self.curr_img_in_list:
944 gobject.idle_add(self.goto_image, str(imgnum), None)
945
946 def thumbpane_selection_changed(self, treeview):
947 cancel = self.autosave_image()
948 if cancel:
949 # Revert selection...
950 gobject.idle_add(self.thumbpane_select, self.curr_img_in_list)
951 return True
952 try:
953 model, paths = self.thumbpane.get_selection().get_selected_rows()
954 imgnum = paths[0][0]
955 if not self.thumbnail_loaded[imgnum]:
956 self.thumbpane_set_image(self.image_list[imgnum], imgnum)
957 gobject.idle_add(self.thumbpane_load_image, treeview, imgnum)
958 except:
959 pass
960
961 def thumbpane_select(self, imgnum):
962 if self.thumbpane_show:
963 self.thumbpane.get_selection().handler_block(self.thumb_sel_handler)
964 try:
965 self.thumbpane.get_selection().select_path((imgnum,))
966 self.thumbpane.scroll_to_cell((imgnum,))
967 except:
968 pass
969 self.thumbpane.get_selection().handler_unblock(self.thumb_sel_handler)
970
971 def thumbpane_set_size(self):
972 self.thumbcolumn.set_fixed_width(self.thumbpane_get_size())
973 self.window_resized(None, self.window.allocation, True)
974
975 def thumbpane_get_size(self):
976 return int(self.thumbnail_size * 1.3)
977
978 def thumbpane_scrolled(self, range):
979 self.thumbpane_update_images()
980
981 def get_blank_pix_for_image(self, image):
982 # Sizes the "blank image" icon for the thumbpane. This will ensure that we don't
983 # load a humongous icon for a small pix, for example, and will keep the thumbnails
984 # from shifting around when they are actually loaded.
985 try:
986 info = gtk.gdk.pixbuf_get_file_info(image)
987 imgwidth = float(info[1])
988 imgheight = float(info[2])
989 if imgheight > self.thumbnail_size:
990 if imgheight > imgwidth:
991 imgheight = self.thumbnail_size
992 else:
993 imgheight = imgheight/imgwidth * self.thumbnail_size
994 imgheight = 2 + int(imgheight) # Account for border that will be added to thumbnails..
995 imgwidth = self.thumbnail_size
996 except:
997 imgheight = 2 + self.thumbnail_size
998 imgwidth = self.thumbnail_size
999 blank_pix = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, imgwidth, imgheight)
1000 blank_pix.fill(0x00000000)
1001 imgwidth2 = int(imgheight*0.8)
1002 imgheight2 = int(imgheight*0.8)
1003 composite_pix = self.blank_image.scale_simple(imgwidth2, imgheight2, gtk.gdk.INTERP_BILINEAR)
1004 leftcoord = int((imgwidth - imgwidth2)/2)
1005 topcoord = int((imgheight - imgheight2)/2)
1006 composite_pix.copy_area(0, 0, imgwidth2, imgheight2, blank_pix, leftcoord, topcoord)
1007 return blank_pix
1008
1009 def find_path(self, filename, exit_on_fail=True):
1010 """ Find a pixmap or icon by looking through standard dirs.
1011 If the image isn't found exit with error status 1 unless
1012 exit_on_fail is set to False, then return None """
1013 if not self.resource_path_list:
1014 #If executed from mirage in bin this points to the basedir
1015 basedir_mirage = os.path.split(sys.path[0])[0]
1016 #If executed from mirage.py module in python lib this points to the basedir
1017 f0 = os.path.split(__file__)[0].split('/lib')[0]
1018 self.resource_path_list = list(set(filter(os.path.isdir, [
1019 os.path.join(basedir_mirage, 'share', 'mirage'),
1020 os.path.join(basedir_mirage, 'share', 'pixmaps'),
1021 os.path.join(sys.prefix, 'share', 'mirage'),
1022 os.path.join(sys.prefix, 'share', 'pixmaps'),
1023 os.path.join(sys.prefix, 'local', 'share', 'mirage'),
1024 os.path.join(sys.prefix, 'local', 'share', 'pixmaps'),
1025 sys.path[0], #If it's run non-installed
1026 os.path.join(f0, 'share', 'mirage'),
1027 os.path.join(f0, 'share', 'pixmaps'),
1028 ])))
1029 for path in self.resource_path_list:
1030 pix = os.path.join(path, filename)
1031 if os.path.exists(pix):
1032 return pix
1033 # If we reached here, we didn't find the pixmap
1034 if exit_on_fail:
1035 print _("Couldn't find the image %s. Please check your installation.") % filename
1036 sys.exit(1)
1037 else:
1038 return None
1039
1040 def gconf_key_changed(self, client, cnxn_id, entry, label):
1041 if entry.value.type == gconf.VALUE_STRING:
1042 style = entry.value.to_string()
1043 if style == "both":
1044 self.toolbar.set_style(gtk.TOOLBAR_BOTH)
1045 elif style == "both-horiz":
1046 self.toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
1047 elif style == "icons":
1048 self.toolbar.set_style(gtk.TOOLBAR_ICONS)
1049 elif style == "text":
1050 self.toolbar.set_style(gtk.TOOLBAR_TEXT)
1051 if self.image_loaded and self.last_image_action_was_fit:
1052 if self.last_image_action_was_smart_fit:
1053 self.zoom_to_fit_or_1_to_1(None, False, False)
1054 else:
1055 self.zoom_to_fit_window(None, False, False)
1056
1057 def toolbar_focused(self, widget, direction):
1058 self.layout.grab_focus()
1059 return True
1060
1061 def topwindow_keypress(self, widget, event):
1062 # For whatever reason, 'Left' and 'Right' cannot be used as menu
1063 # accelerators so we will manually check for them here:
1064 if (not (event.state & gtk.gdk.SHIFT_MASK)) and not (event.state & gtk.gdk.CONTROL_MASK) and not (event.state & gtk.gdk.MOD1_MASK) and not (event.state & gtk.gdk.MOD2_MASK) and not (event.state & gtk.gdk.CONTROL_MASK):
1065 if event.keyval == gtk.gdk.keyval_from_name('Left') or event.keyval == gtk.gdk.keyval_from_name('Up'):
1066 self.goto_prev_image(None)
1067 return
1068 elif event.keyval == gtk.gdk.keyval_from_name('Right') or event.keyval == gtk.gdk.keyval_from_name('Down'):
1069 self.goto_next_image(None)
1070 return
1071 shortcut = gtk.accelerator_name(event.keyval, event.state)
1072 if "Escape" in shortcut:
1073 self.stop_now = True
1074 self.searching_for_images = False
1075 while gtk.events_pending():
1076 gtk.main_iteration()
1077 self.update_title()
1078 return
1079
1080 def parse_action_command(self, command, batchmode):
1081 self.running_custom_actions = True
1082 self.change_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
1083 while gtk.events_pending():
1084 gtk.main_iteration()
1085 self.curr_custom_action = 0
1086 if batchmode:
1087 self.num_custom_actions = len(self.image_list)
1088 for i in range(self.num_custom_actions):
1089 self.curr_custom_action += 1
1090 self.update_statusbar()
1091 while gtk.events_pending():
1092 gtk.main_iteration()
1093 imagename = self.image_list[i]
1094 self.parse_action_command2(command, imagename)
1095 else:
1096 self.num_custom_actions = 1
1097 self.curr_custom_action = 1
1098 self.update_statusbar()
1099 while gtk.events_pending():
1100 gtk.main_iteration()
1101 self.parse_action_command2(command, self.currimg_name)
1102 gc.collect()
1103 self.change_cursor(None)
1104 # Refresh the current image or any preloaded needed if they have changed:
1105 if not os.path.exists(self.currimg_name):
1106 self.currimg_pixbuf_original = None
1107 self.image_load_failed(False)
1108 else:
1109 animtest = gtk.gdk.PixbufAnimation(self.currimg_name)
1110 if animtest.is_static_image():
1111 if self.images_are_different(animtest.get_static_image(), self.currimg_pixbuf_original):
1112 self.load_new_image2(False, False, True, False)
1113 else:
1114 if self.images_are_different(animtest, self.currimg_pixbuf_original):
1115 self.load_new_image2(False, False, True, False)
1116 self.running_custom_actions = False
1117 self.update_statusbar()
1118 while gtk.events_pending():
1119 gtk.main_iteration()
1120 if not os.path.exists(self.preloadimg_prev_name):
1121 self.preloadimg_prev_in_list = -1
1122 else:
1123 animtest = gtk.gdk.PixbufAnimation(self.preloadimg_prev_name)
1124 if animtest.is_static_image():
1125 if self.images_are_different(animtest.get_static_image(), self.preloadimg_prev_pixbuf_original):
1126 self.preloadimg_prev_in_list = -1
1127 self.preload_when_idle = gobject.idle_add(self.preload_prev_image, False)
1128 else:
1129 if self.images_are_different(animtest, self.preloadimg_prev_pixbuf_original):
1130 self.preloadimg_prev_in_list = -1
1131 self.preload_when_idle = gobject.idle_add(self.preload_prev_image, False)
1132 if not os.path.exists(self.preloadimg_next_name):
1133 self.preloadimg_next_in_list = -1
1134 else:
1135 animtest = gtk.gdk.PixbufAnimation(self.preloadimg_next_name)
1136 if animtest.is_static_image():
1137 if self.images_are_different(animtest.get_static_image(), self.preloadimg_next_pixbuf_original):
1138 self.preloadimg_next_in_list = -1
1139 self.preload_when_idle = gobject.idle_add(self.preload_next_image, False)
1140 else:
1141 if self.images_are_different(animtest, self.preloadimg_next_pixbuf_original):
1142 self.preloadimg_next_in_list = -1
1143 self.preload_when_idle = gobject.idle_add(self.preload_next_image, False)
1144 self.stop_now = False
1145 if batchmode:
1146 # Update all thumbnails:
1147 gobject.idle_add(self.thumbpane_update_images, True, self.curr_img_in_list)
1148 else:
1149 # Update only the current thumbnail:
1150 gobject.idle_add(self.thumbpane_set_image, self.image_list[self.curr_img_in_list], self.curr_img_in_list, True)
1151
1152 def images_are_different(self, pixbuf1, pixbuf2):
1153 if pixbuf1.get_pixels() == pixbuf2.get_pixels():
1154 return False
1155 else:
1156 return True
1157
1158 def recent_action_click(self, action):
1159 self.stop_now = True
1160 while gtk.events_pending():
1161 gtk.main_iteration()
1162 cancel = self.autosave_image()
1163 if cancel:
1164 return
1165 index = int(action.get_name())
1166 if os.path.isfile(self.recentfiles[index]) or os.path.exists(self.recentfiles[index]) or self.recentfiles[index].startswith('http://') or self.recentfiles[index].startswith('ftp://'):
1167 self.expand_filelist_and_load_image([self.recentfiles[index]])
1168 else:
1169 self.image_list = []
1170 self.curr_img_in_list = 0
1171 self.image_list.append(self.recentfiles[index])
1172 self.image_load_failed(False)
1173 self.recent_file_remove_and_refresh(index)
1174
1175 def recent_file_remove_and_refresh_name(self, rmfile):
1176 index_num = 0
1177 for imgfile in self.recentfiles:
1178 if imgfile == rmfile:
1179 self.recent_file_remove_and_refresh(index_num)
1180 break
1181 index_num += index_num
1182
1183 def recent_file_remove_and_refresh(self, index_num):
1184 i = index_num
1185 while i < len(self.recentfiles)-1:
1186 self.recentfiles[i] = self.recentfiles[i+1]
1187 i = i + 1
1188 # Set last item empty:
1189 self.recentfiles[len(self.recentfiles)-1] = ''
1190 self.refresh_recent_files_menu()
1191
1192 def recent_file_add_and_refresh(self, addfile):
1193 # First check if the filename is already in the list:
1194 for i in range(len(self.recentfiles)):
1195 if len(self.recentfiles[i]) > 0:
1196 if addfile == self.recentfiles[i]:
1197 # If found in list, put to position 1 and decrement the rest:
1198 j = i
1199 while j > 0:
1200 self.recentfiles[j] = self.recentfiles[j-1]
1201 j = j - 1
1202 self.recentfiles[0] = addfile
1203 self.refresh_recent_files_menu()
1204 return
1205 # If not found, put to position 1, decrement the rest:
1206 j = len(self.recentfiles)-1
1207 while j > 0:
1208 self.recentfiles[j] = self.recentfiles[j-1]
1209 j = j - 1
1210 if len(self.recentfiles) > 0:
1211 self.recentfiles[0] = addfile
1212 self.refresh_recent_files_menu()
1213
1214 def custom_action_click(self, action):
1215 if self.UIManager.get_widget('/MainMenu/EditMenu/ActionSubMenu/' + action.get_name()).get_property('sensitive'):
1216 for i in range(len(self.action_shortcuts)):
1217 try:
1218 if action.get_name() == self.action_names[i]:
1219 self.parse_action_command(self.action_commands[i], self.action_batch[i])
1220 except:
1221 pass
1222
1223
1224 def parse_action_command2(self, cmd, imagename):
1225 # Executes the given command using ``os.system``, substituting "%"-macros approprately.
1226 def sh_esc(s):
1227 import re
1228 return re.sub(r'[^/._a-zA-Z0-9-]', lambda c: '\\'+c.group(), s)
1229 cmd = cmd.strip()
1230 # [NEXT] and [PREV] are only valid alone or at the end of the command
1231 if cmd == "[NEXT]":
1232 self.goto_next_image(None)
1233 return
1234 elif cmd == "[PREV]":
1235 self.goto_prev_image(None)
1236 return
1237 # -1=go to previous, 1=go to next, 0=don't change
1238 prev_or_next=0
1239 if cmd[-6:] == "[NEXT]":
1240 prev_or_next=1
1241 cmd = cmd[:-6]
1242 elif cmd[-6:] == "[PREV]":
1243 prev_or_next=-1
1244 cmd = cmd[:-6]
1245 if "%F" in cmd:
1246 cmd = cmd.replace("%F", sh_esc(imagename))
1247 if "%N" in cmd:
1248 cmd = cmd.replace("%N", sh_esc(os.path.splitext(os.path.basename(imagename))[0]))
1249 if "%P" in cmd:
1250 cmd = cmd.replace("%P", sh_esc(os.path.dirname(imagename) + "/"))
1251 if "%E" in cmd:
1252 cmd = cmd.replace("%E", sh_esc(os.path.splitext(os.path.basename(imagename))[1]))
1253 if "%L" in cmd:
1254 cmd = cmd.replace("%L", " ".join([sh_esc(s) for s in self.image_list]))
1255 if self.verbose:
1256 print _("Action: %s") % cmd
1257 shell_rc = os.system(cmd) >> 8
1258 if self.verbose:
1259 print _("Action return code: %s") % shell_rc
1260 if shell_rc != 0:
1261 msg = _('Unable to launch \"%s\". Please specify a valid command from Edit > Custom Actions.') % cmd
1262 error_dialog = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, msg)
1263 error_dialog.set_title(_("Invalid Custom Action"))
1264 error_dialog.run()
1265 error_dialog.destroy()
1266 elif prev_or_next == 1:
1267 self.goto_next_image(None)
1268 elif prev_or_next == -1:
1269 self.goto_prev_image(None)
1270 self.running_custom_actions = False
1271
1272 def set_go_sensitivities(self, enable):
1273 self.UIManager.get_widget('/MainMenu/GoMenu/Previous Image').set_sensitive(enable)
1274 self.UIManager.get_widget('/MainMenu/GoMenu/Next Image').set_sensitive(enable)
1275 self.UIManager.get_widget('/MainMenu/GoMenu/Random Image').set_sensitive(enable)
1276 self.UIManager.get_widget('/MainMenu/GoMenu/First Image').set_sensitive(enable)
1277 self.UIManager.get_widget('/MainMenu/GoMenu/Last Image').set_sensitive(enable)
1278 self.UIManager.get_widget('/Popup/Previous Image').set_sensitive(enable)
1279 self.UIManager.get_widget('/Popup/Next Image').set_sensitive(enable)
1280 self.UIManager.get_widget('/MainToolbar/Previous2').set_sensitive(enable)
1281 self.UIManager.get_widget('/MainToolbar/Next2').set_sensitive(enable)
1282 self.ss_forward.set_sensitive(enable)
1283 self.ss_back.set_sensitive(enable)
1284
1285 def set_image_sensitivities(self, enable):
1286 self.set_zoom_in_sensitivities(enable)
1287 self.set_zoom_out_sensitivities(enable)
1288 self.UIManager.get_widget('/MainMenu/ViewMenu/1:1').set_sensitive(enable)
1289 self.UIManager.get_widget('/MainMenu/ViewMenu/Fit').set_sensitive(enable)
1290 self.UIManager.get_widget('/MainMenu/EditMenu/Delete Image').set_sensitive(enable)
1291 self.UIManager.get_widget('/MainMenu/EditMenu/Rename Image').set_sensitive(enable)
1292 self.UIManager.get_widget('/MainMenu/EditMenu/Crop').set_sensitive(enable)
1293 self.UIManager.get_widget('/MainMenu/EditMenu/Resize').set_sensitive(enable)
1294 self.UIManager.get_widget('/MainMenu/EditMenu/Saturation').set_sensitive(enable)
1295 self.UIManager.get_widget('/MainToolbar/1:1').set_sensitive(enable)
1296 self.UIManager.get_widget('/MainToolbar/Fit').set_sensitive(enable)
1297 self.UIManager.get_widget('/Popup/1:1').set_sensitive(enable)
1298 self.UIManager.get_widget('/Popup/Fit').set_sensitive(enable)
1299 self.UIManager.get_widget('/MainMenu/FileMenu/Save As').set_sensitive(enable)
1300 self.UIManager.get_widget('/MainMenu/FileMenu/Save').set_sensitive(False)
1301 self.UIManager.get_widget('/MainMenu/FileMenu/Properties').set_sensitive(False)
1302 # Only jpeg, png, and bmp images are currently supported for saving
1303 if len(self.image_list) > 0:
1304 try:
1305 filetype = gtk.gdk.pixbuf_get_file_info(self.currimg_name)[0]['name']
1306 self.UIManager.get_widget('/MainMenu/FileMenu/Properties').set_sensitive(True)
1307 if self.filetype_is_writable(filetype):
1308 self.UIManager.get_widget('/MainMenu/FileMenu/Save').set_sensitive(enable)
1309 except:
1310 self.UIManager.get_widget('/MainMenu/FileMenu/Save').set_sensitive(False)
1311 if self.actionGroupCustom:
1312 for action in self.action_names:
1313 self.UIManager.get_widget('/MainMenu/EditMenu/ActionSubMenu/' + action).set_sensitive(enable)
1314 if not HAS_IMGFUNCS:
1315 enable = False
1316 self.UIManager.get_widget('/MainMenu/EditMenu/Rotate Left').set_sensitive(enable)
1317 self.UIManager.get_widget('/MainMenu/EditMenu/Rotate Right').set_sensitive(enable)
1318 self.UIManager.get_widget('/MainMenu/EditMenu/Flip Vertically').set_sensitive(enable)
1319 self.UIManager.get_widget('/MainMenu/EditMenu/Flip Horizontally').set_sensitive(enable)
1320
1321 def set_zoom_in_sensitivities(self, enable):
1322 self.UIManager.get_widget('/MainMenu/ViewMenu/In').set_sensitive(enable)
1323 self.UIManager.get_widget('/MainToolbar/In').set_sensitive(enable)
1324 self.UIManager.get_widget('/Popup/In').set_sensitive(enable)
1325
1326 def set_zoom_out_sensitivities(self, enable):
1327 self.UIManager.get_widget('/MainMenu/ViewMenu/Out').set_sensitive(enable)
1328 self.UIManager.get_widget('/MainToolbar/Out').set_sensitive(enable)
1329 self.UIManager.get_widget('/Popup/Out').set_sensitive(enable)
1330
1331 def set_next_image_sensitivities(self, enable):
1332 self.UIManager.get_widget('/MainToolbar/Next2').set_sensitive(enable)
1333 self.UIManager.get_widget('/MainMenu/GoMenu/Next Image').set_sensitive(enable)
1334 self.UIManager.get_widget('/Popup/Next Image').set_sensitive(enable)
1335 self.ss_forward.set_sensitive(enable)
1336
1337 def set_previous_image_sensitivities(self, enable):
1338 self.UIManager.get_widget('/MainToolbar/Previous2').set_sensitive(enable)
1339 self.UIManager.get_widget('/MainMenu/GoMenu/Previous Image').set_sensitive(enable)
1340 self.UIManager.get_widget('/Popup/Previous Image').set_sensitive(enable)
1341 self.ss_back.set_sensitive(enable)
1342
1343 def set_first_image_sensitivities(self, enable):
1344 self.UIManager.get_widget('/MainMenu/GoMenu/First Image').set_sensitive(enable)
1345
1346 def set_last_image_sensitivities(self, enable):
1347 self.UIManager.get_widget('/MainMenu/GoMenu/Last Image').set_sensitive(enable)
1348
1349 def set_random_image_sensitivities(self, enable):
1350 self.UIManager.get_widget('/MainMenu/GoMenu/Random Image').set_sensitive(enable)
1351
1352 def set_slideshow_sensitivities(self):
1353 if len(self.image_list) <=1:
1354 self.UIManager.get_widget('/MainMenu/GoMenu/Start Slideshow').show()
1355 self.UIManager.get_widget('/MainMenu/GoMenu/Start Slideshow').set_sensitive(False)
1356 self.UIManager.get_widget('/MainMenu/GoMenu/Stop Slideshow').hide()
1357 self.UIManager.get_widget('/MainMenu/GoMenu/Stop Slideshow').set_sensitive(False)
1358 elif self.slideshow_mode:
1359 self.UIManager.get_widget('/MainMenu/GoMenu/Start Slideshow').hide()
1360 self.UIManager.get_widget('/MainMenu/GoMenu/Start Slideshow').set_sensitive(False)
1361 self.UIManager.get_widget('/MainMenu/GoMenu/Stop Slideshow').show()
1362 self.UIManager.get_widget('/MainMenu/GoMenu/Stop Slideshow').set_sensitive(True)
1363 else:
1364 self.UIManager.get_widget('/MainMenu/GoMenu/Start Slideshow').show()
1365 self.UIManager.get_widget('/MainMenu/GoMenu/Start Slideshow').set_sensitive(True)
1366 self.UIManager.get_widget('/MainMenu/GoMenu/Stop Slideshow').hide()
1367 self.UIManager.get_widget('/MainMenu/GoMenu/Stop Slideshow').set_sensitive(False)
1368 if self.slideshow_mode:
1369 self.UIManager.get_widget('/Popup/Start Slideshow').hide()
1370 self.UIManager.get_widget('/Popup/Stop Slideshow').show()
1371 else:
1372 self.UIManager.get_widget('/Popup/Start Slideshow').show()
1373 self.UIManager.get_widget('/Popup/Stop Slideshow').hide()
1374 if len(self.image_list) <=1:
1375 self.UIManager.get_widget('/Popup/Start Slideshow').set_sensitive(False)
1376 else:
1377 self.UIManager.get_widget('/Popup/Start Slideshow').set_sensitive(True)
1378
1379 def set_zoom_sensitivities(self):
1380 if not self.currimg_is_animation:
1381 self.set_zoom_out_sensitivities(True)
1382 self.set_zoom_in_sensitivities(True)
1383 else:
1384 self.set_zoom_out_sensitivities(False)
1385 self.set_zoom_in_sensitivities(False)
1386
1387 def print_version(self):
1388 print _("Version: Mirage"), __version__
1389 print _("Website: http://mirageiv.berlios.de")
1390
1391 def print_usage(self):
1392 self.print_version()
1393 print ""
1394 print _("Usage: mirage [OPTION]... FILES|FOLDERS...")
1395 print ""
1396 print _("Options") + ":"
1397 print " -h, --help " + _("Show this help and exit")
1398 print " -v, --version " + _("Show version information and exit")
1399 print " -V, --verbose " + _("Show more detailed information")
1400 print " -R, --recursive " + _("Recursively include all images found in")
1401 print " " + _("subdirectories of FOLDERS")
1402 print " -s, --slideshow " + _("Start in slideshow mode")
1403 print " -f, --fullscreen " + _("Start in fullscreen mode")
1404 print " -o, --onload 'cmd' " + _("Execute 'cmd' when an image is loaded")
1405 print " " + _("uses same syntax as custom actions,\n")
1406 print " " + _("i.e. mirage -o 'echo file is %F'")
1407
1408 def delay_changed(self, action):
1409 self.curr_slideshow_delay = self.ss_delayspin.get_value()
1410 if self.slideshow_mode:
1411 gobject.source_remove(self.timer_delay)
1412 if self.curr_slideshow_random:
1413 self.timer_delay = gobject.timeout_add(int(self.curr_slideshow_delay*1000), self.goto_random_image, "ss")
1414 else:
1415 self.timer_delay = gobject.timeout_add((self.curr_slideshow_delay*1000), self.goto_next_image, "ss")
1416 self.window.set_focus(self.layout)
1417
1418 def random_changed(self, action):
1419 self.curr_slideshow_random = self.ss_randomize.get_active()
1420
1421 def motion_cb(self, widget, context, x, y, time):
1422 context.drag_status(gtk.gdk.ACTION_COPY, time)
1423 return True
1424
1425 def drop_cb(self, widget, context, x, y, selection, info, time):
1426 uri = selection.data.strip()
1427 path = urllib.url2pathname(uri)
1428 paths = path.rsplit('\n')
1429 for i, path in enumerate(paths):
1430 paths[i] = path.rstrip('\r')
1431 self.expand_filelist_and_load_image(paths)
1432
1433 def put_error_image_to_window(self):
1434 self.imageview.set_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_LARGE_TOOLBAR)
1435 self.currimg_width = self.imageview.size_request()[0]
1436 self.currimg_height = self.imageview.size_request()[1]
1437 self.center_image()
1438 self.set_go_sensitivities(False)
1439 self.set_image_sensitivities(False)
1440 self.update_statusbar()
1441 self.loaded_img_in_list = -1
1442 return
1443
1444 def expose_event(self, widget, event):
1445 if self.updating_adjustments:
1446 return
1447 self.updating_adjustments = True
1448 if self.hscroll.get_property('visible'):
1449 try:
1450 zoomratio = float(self.currimg_width)/self.previmg_width
1451 newvalue = abs(self.layout.get_hadjustment().get_value() * zoomratio + (self.available_image_width()) * (zoomratio - 1) / 2)
1452 if newvalue >= self.layout.get_hadjustment().lower and newvalue <= (self.layout.get_hadjustment().upper - self.layout.get_hadjustment().page_size):
1453 self.layout.get_hadjustment().set_value(newvalue)
1454 except:
1455 pass
1456 if self.vscroll.get_property('visible'):
1457 try:
1458 newvalue = abs(self.layout.get_vadjustment().get_value() * zoomratio + (self.available_image_height()) * (zoomratio - 1) / 2)
1459 if newvalue >= self.layout.get_vadjustment().lower and newvalue <= (self.layout.get_vadjustment().upper - self.layout.get_vadjustment().page_size):
1460 self.layout.get_vadjustment().set_value(newvalue)
1461 self.previmg_width = self.currimg_width
1462 except:
1463 pass
1464 self.updating_adjustments = False
1465
1466 def window_resized(self, widget, allocation, force_update=False):
1467 # Update the image size on window resize if the current image was last fit:
1468 if self.image_loaded:
1469 if force_update or allocation.width != self.prevwinwidth or allocation.height != self.prevwinheight:
1470 if self.last_image_action_was_fit:
1471 if self.last_image_action_was_smart_fit:
1472 self.zoom_to_fit_or_1_to_1(None, False, False)
1473 else:
1474 self.zoom_to_fit_window(None, False, False)
1475 else:
1476 self.center_image()
1477 self.load_new_image_stop_now()
1478 self.show_scrollbars_if_needed()
1479 # Also, regenerate preloaded image for new window size:
1480 self.preload_when_idle = gobject.idle_add(self.preload_next_image, True)
1481 self.preload_when_idle2 = gobject.idle_add(self.preload_prev_image, True)
1482 self.prevwinwidth = allocation.width
1483 self.prevwinheight = allocation.height
1484 return
1485
1486 def save_settings(self):
1487 conf = ConfigParser.ConfigParser()
1488 conf.add_section('window')
1489 conf.set('window', 'w', self.window.get_allocation().width)
1490 conf.set('window', 'h', self.window.get_allocation().height)
1491 conf.set('window', 'toolbar', self.toolbar_show)
1492 conf.set('window', 'statusbar', self.statusbar_show)
1493 conf.set('window', 'thumbpane', self.thumbpane_show)
1494 conf.add_section('prefs')
1495 conf.set('prefs', 'simple-bgcolor', self.simple_bgcolor)
1496 conf.set('prefs', 'bgcolor-red', self.bgcolor.red)
1497 conf.set('prefs', 'bgcolor-green', self.bgcolor.green)
1498 conf.set('prefs', 'bgcolor-blue', self.bgcolor.blue)
1499 conf.set('prefs', 'open_all', self.open_all_images)
1500 conf.set('prefs', 'hidden', self.open_hidden_files)
1501 conf.set('prefs', 'use_last_dir', self.use_last_dir)
1502 conf.set('prefs', 'last_dir', self.last_dir)
1503 conf.set('prefs', 'fixed_dir', self.fixed_dir)
1504 conf.set('prefs', 'open_mode', self.open_mode)
1505 conf.set('prefs', 'last_mode', self.last_mode)
1506 conf.set('prefs', 'listwrap_mode', self.listwrap_mode)
1507 conf.set('prefs', 'slideshow_delay', int(self.slideshow_delay))
1508 conf.set('prefs', 'slideshow_random', self.slideshow_random)
1509 conf.set('prefs', 'zoomquality', self.zoomvalue)
1510 conf.set('prefs', 'quality_save', int(self.quality_save))
1511 conf.set('prefs', 'disable_screensaver', self.disable_screensaver)
1512 conf.set('prefs', 'slideshow_in_fullscreen', self.slideshow_in_fullscreen)
1513 conf.set('prefs', 'confirm_delete', self.confirm_delete)
1514 conf.set('prefs', 'preloading_images', self.preloading_images)
1515 conf.set('prefs', 'savemode', self.savemode)
1516 conf.set('prefs', 'start_in_fullscreen', self.start_in_fullscreen)
1517 conf.set('prefs', 'thumbsize', self.thumbnail_size)
1518 conf.set('prefs', 'screenshot_delay', self.screenshot_delay)
1519 conf.add_section('actions')
1520 conf.set('actions', 'num_actions', len(self.action_names))
1521 for i in range(len(self.action_names)):
1522 conf.set('actions', 'names[' + str(i) + ']', self.action_names[i])
1523 conf.set('actions', 'commands[' + str(i) + ']', self.action_commands[i])
1524 conf.set('actions', 'shortcuts[' + str(i) + ']', self.action_shortcuts[i])
1525 conf.set('actions', 'batch[' + str(i) + ']', self.action_batch[i])
1526 conf.add_section('recent')
1527 conf.set('recent', 'num_recent', len(self.recentfiles))
1528 for i in range(len(self.recentfiles)):
1529 conf.set('recent', 'num[' + str(i) + ']', len(self.recentfiles[i]))
1530 conf.set('recent', 'urls[' + str(i) + ',0]', self.recentfiles[i])
1531 if not os.path.exists(self.config_dir):
1532 os.makedirs(self.config_dir)
1533 conf.write(file(self.config_dir + '/miragerc', 'w'))
1534
1535 # Also, save accel_map:
1536 gtk.accel_map_save(self.config_dir + '/accel_map')
1537
1538 return
1539
1540 def delete_event(self, widget, event, data=None):
1541 cancel = self.autosave_image()
1542 if cancel:
1543 return True
1544 self.stop_now = True
1545 self.closing_app = True
1546 self.save_settings()
1547 sys.exit(0)
1548
1549 def destroy(self, event, data=None):
1550 cancel = self.autosave_image()
1551 if cancel:
1552 return True
1553 self.stop_now = True
1554 self.closing_app = True
1555 self.save_settings()
1556
1557 def exit_app(self, action):
1558 cancel = self.autosave_image()
1559 if cancel:
1560 return True
1561 self.stop_now = True
1562 self.closing_app = True
1563 self.save_settings()
1564 sys.exit(0)
1565
1566 def put_zoom_image_to_window(self, currimg_preloaded):
1567 self.window.window.freeze_updates()
1568 if not currimg_preloaded:
1569 # Always start with the original image to preserve quality!
1570 # Calculate image size:
1571 finalimg_width = int(self.currimg_pixbuf_original.get_width() * self.currimg_zoomratio)
1572 finalimg_height = int(self.currimg_pixbuf_original.get_height() * self.currimg_zoomratio)
1573 if not self.currimg_is_animation:
1574 # Scale image:
1575 if not self.currimg_pixbuf_original.get_has_alpha():
1576 self.currimg_pixbuf = self.currimg_pixbuf_original.scale_simple(finalimg_width, finalimg_height, self.zoom_quality)
1577 else:
1578 colormap = self.imageview.get_colormap()
1579 light_grey = colormap.alloc_color('#666666', True, True)
1580 dark_grey = colormap.alloc_color('#999999', True, True)
1581 self.currimg_pixbuf = self.currimg_pixbuf_original.composite_color_simple(finalimg_width, finalimg_height, self.zoom_quality, 255, 8, light_grey.pixel, dark_grey.pixel)
1582 else:
1583 self.currimg_pixbuf = self.currimg_pixbuf_original
1584 self.currimg_width, self.currimg_height = finalimg_width, finalimg_height
1585 self.layout.set_size(self.currimg_width, self.currimg_height)
1586 self.center_image()
1587 self.show_scrollbars_if_needed()
1588 if not self.currimg_is_animation:
1589 self.imageview.set_from_pixbuf(self.currimg_pixbuf)
1590 self.previmage_is_animation = False
1591 else:
1592 self.imageview.set_from_animation(self.currimg_pixbuf)
1593 self.previmage_is_animation = True
1594 # Clean up (free memory) because I'm lazy
1595 gc.collect()
1596 self.window.window.thaw_updates()
1597 self.loaded_img_in_list = self.curr_img_in_list
1598
1599 def show_scrollbars_if_needed(self):
1600 if self.currimg_width > self.available_image_width():
1601 self.hscroll.show()
1602 else:
1603 self.hscroll.hide()
1604 if self.currimg_height > self.available_image_height():
1605 self.vscroll.show()
1606 else:
1607 self.vscroll.hide()
1608
1609 def center_image(self):
1610 x_shift = int((self.available_image_width() - self.currimg_width)/2)
1611 if x_shift < 0:
1612 x_shift = 0
1613 y_shift = int((self.available_image_height() - self.currimg_height)/2)
1614 if y_shift < 0:
1615 y_shift = 0
1616 self.layout.move(self.imageview, x_shift, y_shift)
1617
1618 def available_image_width(self):
1619 width = self.window.get_size()[0]
1620 if not self.fullscreen_mode:
1621 if self.thumbpane_show:
1622 width -= self.thumbscroll.size_request()[0]
1623 return width
1624
1625 def available_image_height(self):
1626 height = self.window.get_size()[1]
1627 if not self.fullscreen_mode:
1628 height -= self.menubar.size_request()[1]
1629 if self.toolbar_show:
1630 height -= self.toolbar.size_request()[1]
1631 if self.statusbar_show:
1632 height -= self.statusbar.size_request()[1]
1633 return height
1634
1635 def save_image(self, action):
1636 if self.UIManager.get_widget('/MainMenu/FileMenu/Save').get_property('sensitive'):
1637 self.save_image_now(self.currimg_name, gtk.gdk.pixbuf_get_file_info(self.currimg_name)[0]['name'])
1638
1639 def save_image_as(self, action):
1640 dialog = gtk.FileChooserDialog(title=_("Save As"),action=gtk.FILE_CHOOSER_ACTION_SAVE,buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK))
1641 dialog.set_default_response(gtk.RESPONSE_OK)
1642 filename = os.path.basename(self.currimg_name)
1643 filetype = None
1644 dialog.set_current_folder(os.path.dirname(self.currimg_name))
1645 dialog.set_current_name(filename)
1646 dialog.set_do_overwrite_confirmation(True)
1647 response = dialog.run()
1648 if response == gtk.RESPONSE_OK:
1649 prev_name = self.currimg_name
1650 filename = dialog.get_filename()
1651 dialog.destroy()
1652 fileext = os.path.splitext(os.path.basename(filename))[1].lower()
1653 if len(fileext) > 0:
1654 fileext = fileext[1:]
1655 # Override filetype if user typed a filename with a different extension:
1656 for i in gtk.gdk.pixbuf_get_formats():
1657 if fileext in i['extensions']:
1658 filetype = i['name']
1659 self.save_image_now(filename, filetype)
1660 self.register_file_with_recent_docs(filename)
1661 else:
1662 dialog.destroy()
1663
1664 def save_image_now(self, dest_name, filetype):
1665 try:
1666 self.change_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
1667 while gtk.events_pending():
1668 gtk.main_iteration()
1669 if filetype == None:
1670 filetype = gtk.gdk.pixbuf_get_file_info(self.currimg_name)[0]['name']
1671 if self.filetype_is_writable(filetype):
1672 self.currimg_pixbuf_original.save(dest_name, filetype, {'quality': str(self.quality_save)})
1673 self.currimg_name = dest_name
1674 self.image_list[self.curr_img_in_list] = dest_name
1675 self.update_title()
1676 self.update_statusbar()
1677 # Update thumbnail:
1678 gobject.idle_add(self.thumbpane_set_image, dest_name, self.curr_img_in_list, True)
1679 self.image_modified = False
1680 else:
1681 error_dialog = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_WARNING, gtk.BUTTONS_YES_NO, _('The %s format is not supported for saving. Do you wish to save the file in a different format?') % filetype)
1682 error_dialog.set_title(_("Save"))
1683 response = error_dialog.run()
1684 if response == gtk.RESPONSE_YES:
1685 error_dialog.destroy()
1686 while gtk.events_pending():
1687 gtk.main_iteration()
1688 self.save_image_as(None)
1689 else:
1690 error_dialog.destroy()
1691 except:
1692 error_dialog = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _('Unable to save %s') % dest_name)
1693 error_dialog.set_title(_("Save"))
1694 error_dialog.run()
1695 error_dialog.destroy()
1696 self.change_cursor(None)
1697
1698 def autosave_image(self):
1699 # Returns True if the user has canceled out of the dialog
1700 # Never call this function from an idle or timeout loop! That will cause
1701 # the app to freeze.
1702 if self.image_modified:
1703 if self.savemode == 1:
1704 temp = self.UIManager.get_widget('/MainMenu/FileMenu/Save').get_property('sensitive')
1705 self.UIManager.get_widget('/MainMenu/FileMenu/Save').set_property('sensitive', True)
1706 self.save_image(None)
1707 self.UIManager.get_widget('/MainMenu/FileMenu/Save').set_property('sensitive', temp)
1708 elif self.savemode == 2:
1709 dialog = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE, _("The current image has been modified. Save changes?"))
1710 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1711 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
1712 dialog.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_YES)
1713 dialog.set_title(_("Save?"))
1714 dialog.set_default_response(gtk.RESPONSE_YES)
1715 response = dialog.run()
1716 dialog.destroy()
1717 if response == gtk.RESPONSE_YES:
1718 temp = self.UIManager.get_widget('/MainMenu/FileMenu/Save').get_property('sensitive')
1719 self.UIManager.get_widget('/MainMenu/FileMenu/Save').set_property('sensitive', True)
1720 self.save_image(None)
1721 self.UIManager.get_widget('/MainMenu/FileMenu/Save').set_property('sensitive', temp)
1722 self.image_modified = False
1723 elif response == gtk.RESPONSE_NO:
1724 self.image_modified = False
1725 # Ensures that we don't use the current pixbuf for any preload pixbufs if we are in
1726 # the process of loading the previous or next image in the list:
1727 self.currimg_pixbuf = self.currimg_pixbuf_original
1728 self.preloadimg_next_in_list = -1
1729 self.preloadimg_prev_in_list = -1
1730 self.loaded_img_in_list = -1
1731 else:
1732 return True
1733
1734 def filetype_is_writable(self, filetype):
1735 # Determine if filetype is a writable format
1736 filetype_is_writable = True
1737 for i in gtk.gdk.pixbuf_get_formats():
1738 if filetype in i['extensions']:
1739 if i['is_writable']:
1740 return True
1741 return False
1742
1743 def open_file(self, action):
1744 self.stop_now = True
1745 while gtk.events_pending():
1746 gtk.main_iteration()
1747 self.open_file_or_folder(action, True)
1748
1749 def open_file_remote(self, action):
1750