Codebase list mirage / 1746d7e
Move source files to the "mirage" package Thomas Ross 3 years ago
11 changed file(s) with 6936 addition(s) and 6935 deletion(s). Raw diff Collapse all Expand all
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
-58
fullscreen_controls.py less more
0 from gi.repository import Gtk
1
2
3 @Gtk.Template(resource_path="/io/thomasross/mirage/fullscreen-controls.ui")
4 class FullscreenControls(Gtk.Widget):
5 __gtype_name__ = "FullscreenControls"
6
7 slideshow_delay_adjustment = Gtk.Template.Child()
8
9 left_window = Gtk.Template.Child()
10 slideshow_start_button = Gtk.Template.Child()
11 slideshow_stop_button = Gtk.Template.Child()
12
13 right_window = Gtk.Template.Child()
14
15 def insert_action_group(self, *args, **kwargs):
16 self.left_window.insert_action_group(*args, **kwargs)
17 self.right_window.insert_action_group(*args, **kwargs)
18
19 def set_screen(self, *args, **kwargs):
20 self.left_window.set_screen(*args, **kwargs)
21 self.right_window.set_screen(*args, **kwargs)
22
23 def show_all(self, *args, **kwargs):
24 self.left_window.show_all(*args, **kwargs)
25 self.right_window.show_all(*args, **kwargs)
26
27 def hide(self, *args, **kwargs):
28 self.left_window.hide(*args, **kwargs)
29 self.right_window.hide(*args, **kwargs)
30
31 def modify_bg(self, *args, **kwargs):
32 self.left_window.modify_bg(*args, **kwargs)
33 self.right_window.modify_bg(*args, **kwargs)
34
35 def position(self, width, height, x, y):
36 left_window_height = self.left_window.get_allocation().height
37 right_window_width = self.right_window.get_allocation().width
38 right_window_height = self.right_window.get_allocation().height
39 self.left_window.move(2 + x, int(height - left_window_height - 2))
40 self.right_window.move(
41 width - right_window_width - 2 + x, int(height - right_window_height - 2)
42 )
43
44 def set_slideshow_delay(self, delay):
45 self.slideshow_delay_adjustment.set_value(delay)
46
47 def set_slideshow_playing(self, playing):
48 if playing:
49 self.slideshow_start_button.hide()
50 self.slideshow_start_button.set_no_show_all(True)
51 self.slideshow_stop_button.show()
52 self.slideshow_stop_button.set_no_show_all(False)
53 else:
54 self.slideshow_start_button.show()
55 self.slideshow_start_button.set_no_show_all(False)
56 self.slideshow_stop_button.hide()
57 self.slideshow_stop_button.set_no_show_all(True)
+0
-250
imgfuncs.c less more
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
-34
mirage less more
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 __version__ = "0.9.5.2"
1
2 __license__ = """
3 Mirage, a fast GTK+ Image Viewer
4 Copyright 2007 Scott Horowitz <stonecrest@gmail.com>
5
6 This file is part of Mirage.
7
8 Mirage is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 3 of the License, or
11 (at your option) any later version.
12
13 Mirage is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program. If not, see <http://www.gnu.org/licenses/>.
20 """
21
22 import gi
23
24
25 gi.require_version("Gtk", "3.0")
26 from gi.repository import Gtk, Gdk, GdkPixbuf, GObject, GLib, Gio
27 import os, sys, getopt, configparser, string, gc
28 import random, urllib.request, gettext, locale
29 import stat, time, subprocess, shutil, filecmp
30 import tempfile, socket, threading
31
32 resources = Gio.resource_load("mirage.gresource")
33 Gio.resources_register(resources)
34
35 from .fullscreen_controls import FullscreenControls
36
37 try:
38 import hashlib
39
40 HAS_HASHLIB = True
41 except:
42 HAS_HASHLIB = False
43 import md5
44
45 try:
46 import imgfuncs
47
48 HAS_IMGFUNCS = True
49 except:
50 HAS_IMGFUNCS = False
51 print("imgfuncs.so module not found, rotating/flipping images will be disabled.")
52
53 try:
54 import xmouse
55
56 HAS_XMOUSE = True
57 except:
58 HAS_XMOUSE = False
59 print("xmouse.so module not found, some screenshot capabilities will be disabled.")
60
61 try:
62 from gi.repository import GConf
63 except:
64 pass
65
66
67 def valid_int(inputstring):
68 try:
69 x = int(inputstring)
70 return True
71 except:
72 return False
73
74
75 class Base(Gtk.Application):
76 def __init__(self, *args, **kwargs):
77 super().__init__(
78 *args,
79 application_id="io.thomasross.mirage",
80 flags=Gio.ApplicationFlags.NON_UNIQUE
81 )
82
83 def do_activate(self):
84 Gdk.threads_init()
85
86 # FIX THIS! Does not work on windows and what happens if mo-files exists
87 # in both dirs?
88 gettext.install("mirage", "/usr/share/locale")
89 gettext.install("mirage", "/usr/local/share/locale")
90
91 # Constants
92 self.open_mode_smart = 0
93 self.open_mode_fit = 1
94 self.open_mode_1to1 = 2
95 self.open_mode_last = 3
96 self.min_zoomratio = 0.02
97
98 # Initialize vars:
99 width = 600
100 height = 400
101 bgcolor_found = False
102 self.simple_bgcolor = False
103 # Current image:
104 self.curr_img_in_list = 0
105 self.currimg_name = ""
106 self.currimg_width = 0
107 self.currimg_height = 0
108 self.currimg_pixbuf = None
109 self.currimg_pixbuf_original = None
110 self.currimg_zoomratio = 1
111 self.currimg_is_animation = False
112 # This is the actual pixbuf that is loaded in Mirage. This will
113 # usually be the same as self.curr_img_in_list except for scenarios
114 # like when the user presses 'next image' multiple times in a row.
115 # In this case, self.curr_img_in_list will increment while
116 # self.loaded_img_in_list will retain the current loaded image.
117 self.loaded_img_in_list = 0
118 # Next preloaded image:
119 self.preloadimg_next_in_list = -1
120 self.preloadimg_next_name = ""
121 self.preloadimg_next_width = 0
122 self.preloadimg_next_height = 0
123 self.preloadimg_next_pixbuf = None
124 self.preloadimg_next_pixbuf_original = None
125 self.preloadimg_next_zoomratio = 1
126 self.preloadimg_next_is_animation = False
127 # Previous preloaded image:
128 self.preloadimg_prev_in_list = -1
129 self.preloadimg_prev_name = ""
130 self.preloadimg_prev_width = 0
131 self.preloadimg_prev_height = 0
132 self.preloadimg_prev_pixbuf = None
133 self.preloadimg_prev_pixbuf_original = None
134 self.preloadimg_prev_zoomratio = 1
135 self.preloadimg_prev_is_animation = False
136 # Settings, misc:
137 self.toolbar_show = True
138 self.thumbpane_show = True
139 self.statusbar_show = True
140 self.fullscreen_mode = False
141 self.opendialogpath = ""
142 self.zoom_quality = GdkPixbuf.InterpType.BILINEAR
143 self.recursive = False
144 self.verbose = False
145 self.image_loaded = False
146 self.open_all_images = True # open all images in the directory(ies)
147 self.use_last_dir = True
148 self.last_dir = os.path.expanduser("~")
149 self.fixed_dir = os.path.expanduser("~")
150 self.image_list = []
151 self.open_mode = self.open_mode_smart
152 self.last_mode = self.open_mode_smart
153 self.listwrap_mode = 0 # 0=no, 1=yes, 2=ask
154 self.user_prompt_visible = False # the "wrap?" prompt
155 self.slideshow_delay = 1 # seconds
156 self.slideshow_mode = False
157 self.slideshow_random = False
158 self.slideshow_controls_visible = False # fullscreen slideshow controls
159 self.zoomvalue = 2
160 self.quality_save = 90
161 self.updating_adjustments = False
162 self.disable_screensaver = False
163 self.slideshow_in_fullscreen = False
164 self.closing_app = False
165 self.confirm_delete = True
166 self.preloading_images = True
167 self.action_names = [
168 "Open in GIMP",
169 "Create Thumbnail",
170 "Create Thumbnails",
171 "Move to Favorites",
172 ]
173 self.action_hashes = [
174 "d47444d623dfd3d943b50ce5f43f3a0937dc25d6",
175 "2984214616cbf75e6035c9a1374f6419e40719b1",
176 "b80d24c876c479486571a37ad2ef90f897f5cfff",
177 "d497998eee206191ffc6d1045b118038e3ab83c6",
178 ]
179 self.action_shortcuts = [
180 "<Control>e",
181 "<Alt>t",
182 "<Control><Alt>t",
183 "<Control><Alt>f",
184 ]
185 self.action_commands = [
186 "gimp-remote-2.4 %F",
187 "convert %F -thumbnail 150x150 %Pt_%N.jpg",
188 "convert %F -thumbnail 150x150 %Pt_%N.jpg",
189 "mkdir -p ~/mirage-favs; mv %F ~/mirage-favs; [NEXT]",
190 ]
191 self.action_batch = [False, False, True, False]
192 self.onload_cmd = None
193 self.searching_for_images = False
194 self.preserve_aspect = True
195 self.ignore_preserve_aspect_callback = False
196 self.savemode = 2
197 self.image_modified = False
198 self.image_zoomed = False
199 self.start_in_fullscreen = False
200 self.running_custom_actions = False
201 self.merge_id_recent = None
202 self.actionGroupRecent = None
203 self.open_hidden_files = False
204 self.thumbnail_sizes = ["128", "96", "72", "64", "48", "32"]
205 self.thumbnail_size = 128 # Default to 128 x 128
206 self.thumbnail_loaded = []
207 self.thumbpane_updating = False
208 self.recentfiles = ["", "", "", "", ""]
209 self.screenshot_delay = 2
210 self.thumbpane_bottom_coord_loaded = 0
211
212 # Read any passed options/arguments:
213 try:
214 opts, args = getopt.getopt(
215 sys.argv[1:],
216 "hRvVsfo:",
217 [
218 "help",
219 "version",
220 "recursive",
221 "verbose",
222 "slideshow",
223 "fullscreen",
224 "onload=",
225 ],
226 )
227 except getopt.GetoptError:
228 # print help information and exit:
229 self.print_usage()
230 sys.exit(2)
231 # If options were passed, perform action on them.
232 if opts != []:
233 for o, a in opts:
234 if o in ("-v", "--version"):
235 self.print_version()
236 sys.exit(2)
237 elif o in ("-h", "--help"):
238 self.print_usage()
239 sys.exit(2)
240 elif o in ("-R", "--recursive"):
241 self.recursive = True
242 elif o in ("-V", "--verbose"):
243 self.verbose = True
244 elif o in ("-s", "--slideshow", "-f", "--fullscreen"):
245 # This will be handled later
246 None
247 elif o in ("-o", "--onload"):
248 self.onload_cmd = a
249 else:
250 self.print_usage()
251 sys.exit(2)
252
253 # Determine config dir, first try the environment variable XDG_CONFIG_HOME
254 # according to XDG specification and as a fallback use ~/.config/mirage
255 self.config_dir = (
256 os.getenv("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
257 ) + "/mirage"
258 # Load config from disk:
259 conf = configparser.ConfigParser(interpolation=None)
260 if os.path.isfile(self.config_dir + "/miragerc"):
261 conf.read(self.config_dir + "/miragerc")
262 if conf.has_option("window", "w"):
263 width = conf.getint("window", "w")
264 if conf.has_option("window", "h"):
265 height = conf.getint("window", "h")
266 if conf.has_option("window", "toolbar"):
267 self.toolbar_show = conf.getboolean("window", "toolbar")
268 if conf.has_option("window", "statusbar"):
269 self.statusbar_show = conf.getboolean("window", "statusbar")
270 if conf.has_option("window", "thumbpane"):
271 self.thumbpane_show = conf.getboolean("window", "thumbpane")
272 if conf.has_option("prefs", "simple-bgcolor"):
273 self.simple_bgcolor = conf.getboolean("prefs", "simple-bgcolor")
274 if conf.has_option("prefs", "bgcolor-red"):
275 bgr = conf.getint("prefs", "bgcolor-red")
276 bgg = conf.getint("prefs", "bgcolor-green")
277 bgb = conf.getint("prefs", "bgcolor-blue")
278 bgcolor_found = True
279 self.bgcolor = Gdk.Color(red=bgr, green=bgg, blue=bgb)
280 if conf.has_option("prefs", "use_last_dir"):
281 self.use_last_dir = conf.getboolean("prefs", "use_last_dir")
282 if conf.has_option("prefs", "last_dir"):
283 self.last_dir = conf.get("prefs", "last_dir")
284 if conf.has_option("prefs", "fixed_dir"):
285 self.fixed_dir = conf.get("prefs", "fixed_dir")
286 if conf.has_option("prefs", "open_all"):
287 self.open_all_images = conf.getboolean("prefs", "open_all")
288 if conf.has_option("prefs", "hidden"):
289 self.open_hidden_files = conf.getboolean("prefs", "hidden")
290 if conf.has_option("prefs", "open_mode"):
291 self.open_mode = conf.getint("prefs", "open_mode")
292 if conf.has_option("prefs", "last_mode"):
293 self.last_mode = conf.getint("prefs", "last_mode")
294 if conf.has_option("prefs", "listwrap_mode"):
295 self.listwrap_mode = conf.getint("prefs", "listwrap_mode")
296 if conf.has_option("prefs", "slideshow_delay"):
297 self.slideshow_delay = conf.getint("prefs", "slideshow_delay")
298 if conf.has_option("prefs", "slideshow_random"):
299 self.slideshow_random = conf.getboolean("prefs", "slideshow_random")
300 if conf.has_option("prefs", "zoomquality"):
301 self.zoomvalue = conf.getint("prefs", "zoomquality")
302 if int(round(self.zoomvalue, 0)) == 0:
303 self.zoom_quality = GdkPixbuf.InterpType.NEAREST
304 elif int(round(self.zoomvalue, 0)) == 1:
305 self.zoom_quality = GdkPixbuf.InterpType.TILES
306 elif int(round(self.zoomvalue, 0)) == 2:
307 self.zoom_quality = GdkPixbuf.InterpType.BILINEAR
308 elif int(round(self.zoomvalue, 0)) == 3:
309 self.zoom_quality = GdkPixbuf.InterpType.HYPER
310 if conf.has_option("prefs", "quality_save"):
311 self.quality_save = conf.getint("prefs", "quality_save")
312 if conf.has_option("prefs", "disable_screensaver"):
313 self.disable_screensaver = conf.getboolean("prefs", "disable_screensaver")
314 if conf.has_option("prefs", "slideshow_in_fullscreen"):
315 self.slideshow_in_fullscreen = conf.getboolean(
316 "prefs", "slideshow_in_fullscreen"
317 )
318 if conf.has_option("prefs", "preloading_images"):
319 self.preloading_images = conf.getboolean("prefs", "preloading_images")
320 if conf.has_option("prefs", "thumbsize"):
321 self.thumbnail_size = conf.getint("prefs", "thumbsize")
322 if conf.has_option("prefs", "screenshot_delay"):
323 self.screenshot_delay = conf.getint("prefs", "screenshot_delay")
324 if conf.has_option("actions", "num_actions"):
325 num_actions = conf.getint("actions", "num_actions")
326 self.action_names = []
327 self.action_commands = []
328 self.action_shortcuts = []
329 self.action_batch = []
330 for i in range(num_actions):
331 if (
332 conf.has_option("actions", "names[" + str(i) + "]")
333 and conf.has_option("actions", "commands[" + str(i) + "]")
334 and conf.has_option("actions", "shortcuts[" + str(i) + "]")
335 and conf.has_option("actions", "batch[" + str(i) + "]")
336 ):
337 name = conf.get("actions", "names[" + str(i) + "]")
338 self.action_names.append(name)
339 self.action_hashes.append(
340 hashlib.sha1(name.encode("utf8")).hexdigest()
341 )
342 self.action_commands.append(
343 conf.get("actions", "commands[" + str(i) + "]")
344 )
345 self.action_shortcuts.append(
346 conf.get("actions", "shortcuts[" + str(i) + "]")
347 )
348 self.action_batch.append(
349 conf.getboolean("actions", "batch[" + str(i) + "]")
350 )
351 if conf.has_option("prefs", "savemode"):
352 self.savemode = conf.getint("prefs", "savemode")
353 if conf.has_option("prefs", "start_in_fullscreen"):
354 self.start_in_fullscreen = conf.getboolean("prefs", "start_in_fullscreen")
355 if conf.has_option("prefs", "confirm_delete"):
356 self.confirm_delete = conf.getboolean("prefs", "confirm_delete")
357 self.recentfiles = []
358 if conf.has_option("recent", "num_recent"):
359 num_recent = conf.getint("recent", "num_recent")
360 for i in range(num_recent):
361 self.recentfiles.append("")
362 if conf.has_option("recent", "urls[" + str(i) + ",0]"):
363 self.recentfiles[i] = conf.get("recent", "urls[" + str(i) + ",0]")
364 # slideshow_delay is the user's preference, whereas curr_slideshow_delay is
365 # the current delay (which can be changed without affecting the 'default')
366 self.curr_slideshow_delay = self.slideshow_delay
367 # Same for randomization:
368 self.curr_slideshow_random = self.slideshow_random
369
370 # Read accel_map file, if it exists
371 if os.path.isfile(self.config_dir + "/accel_map"):
372 Gtk.AccelMap.load(self.config_dir + "/accel_map")
373
374 # Directory/ies in which to find application images/pixmaps
375 self.resource_path_list = False
376
377 self.blank_image = GdkPixbuf.Pixbuf.new_from_file(
378 self.find_path("mirage_blank.png")
379 )
380
381 # Define the actions
382 self.action_group = Gio.SimpleActionGroup()
383
384 actions = [
385 ## Main Menubar
386 # File
387 ("open-file", self.open_file),
388 ("open-folder", self.open_folder),
389 ("open-file-remote", self.open_file_remote),
390 ("save-image", self.save_image),
391 ("save-image-as", self.save_image_as),
392 ("screenshot", self.screenshot),
393 ("show-properties", self.show_properties),
394 ("exit-app", self.exit_app),
395 # Edit
396 ("rotate-left", self.rotate_left),
397 ("rotate-right", self.rotate_right),
398 ("flip-image-vert", self.flip_image_vert),
399 ("flip-image-horiz", self.flip_image_horiz),
400 ("crop-image", self.crop_image),
401 ("resize-image", self.resize_image),
402 ("saturation", self.saturation),
403 ("rename-image", self.rename_image),
404 ("delete-image", self.delete_image),
405 ("show-custom-actions", self.show_custom_actions),
406 ("show-prefs", self.show_prefs),
407 # View
408 ("zoom-out", self.zoom_out),
409 ("zoom-in", self.zoom_in),
410 ("zoom-1-to-1", self.zoom_1_to_1_action),
411 ("zoom-to-fit-window", self.zoom_to_fit_window_action),
412 ("toggle-toolbar", None, None, "true", self.toggle_toolbar),
413 ("toggle-thumbpane", None, None, "true", self.toggle_thumbpane),
414 ("toggle-status-bar", None, None, "true", self.toggle_status_bar),
415 ("enter-fullscreen", self.enter_fullscreen),
416 # Go
417 ("goto-next-image", self.goto_next_image),
418 ("goto-prev-image", self.goto_prev_image),
419 ("goto-random-image", self.goto_random_image),
420 ("goto-first-image", self.goto_first_image),
421 ("goto-last-image", self.goto_last_image),
422 ("start-slideshow", self.toggle_slideshow),
423 ("stop-slideshow", self.toggle_slideshow),
424 # Help
425 ("show-help", self.show_help),
426 ("show-about", self.show_about),
427 # Other
428 (
429 "toggle-slideshow-shuffle",
430 None,
431 None,
432 "false",
433 self.toggle_slideshow_shuffle,
434 ),
435 ]
436 self.action_group.add_action_entries(actions)
437
438 # Populate keys[]:
439 self.keys = []
440 for i in range(len(actions)):
441 if len(actions[i]) > 3:
442 if actions[i][3] != None:
443 self.keys.append([actions[i][4], actions[i][3]])
444
445 # Create interface
446 self.window = Gtk.ApplicationWindow.new(self)
447 self.window.set_has_resize_grip(True)
448 self.window.insert_action_group("app", self.action_group)
449 self.popup_menu.insert_action_group("app", self.action_group)
450 self.fullscreen_controls.insert_action_group("app", self.action_group)
451 self.update_title()
452 icon_path = self.find_path("mirage.png")
453 try:
454 Gtk.Window.set_default_icon_from_file(icon_path)
455 except:
456 pass
457 vbox = Gtk.VBox(False, 0)
458
459 # Hidden hotkeys
460 hotkeys = (
461 ("app.zoom-out", "<Ctrl>KP_Subtract"),
462 ("app.zoom-out", "minus"),
463 ("app.zoom-out", "KP_Subtract"),
464 ("app.zoom-in", "plus"),
465 ("app.zoom-in", "equal"),
466 ("app.zoom-in", "KP_Add"),
467 ("app.zoom-in", "<Ctrl>KP_Add"),
468 ("app.zoom-1-to-1", "1"),
469 ("app.zoom-1-to-1", "<Ctrl>KP_End"),
470 ("app.zoom-1-to-1", "<Ctrl>KP_1"),
471 ("app.zoom-to-fit-window", "<Ctrl>KP_Insert"),
472 ("app.zoom-to-fit-window", "<Ctrl>KP_0"),
473 ("app.enter-fullscreen", "<Shift>Return"),
474 ("app.leave-fullscreen", "Escape"),
475 ("app.goto-next-image", "space"),
476 ("app.goto-next-image", "Down"),
477 ("app.goto-next-image", "Page_Down"),
478 ("app.goto-prev-image", "BackSpace"),
479 ("app.goto-prev-image", "Up"),
480 ("app.goto-prev-image", "Page_Up"),
481 )
482 for action_name, hotkey in hotkeys:
483 self.set_accels_for_action(
484 action_name, self.get_accels_for_action(action_name) + [hotkey]
485 )
486
487 self.refresh_custom_actions_menu()
488 self.refresh_recent_files_menu()
489
490 vbox.pack_start(self.toolbar, False, False, 0)
491
492 self.layout = Gtk.Layout()
493 self.vscroll = Gtk.VScrollbar(None)
494 self.vscroll.set_adjustment(self.layout.get_vadjustment())
495 self.hscroll = Gtk.HScrollbar(None)
496 self.hscroll.set_adjustment(self.layout.get_hadjustment())
497 self.table = Gtk.Table(3, 2, False)
498
499 self.thumblist = Gtk.ListStore(GdkPixbuf.Pixbuf)
500 self.thumbpane = Gtk.TreeView(self.thumblist)
501 self.thumbcolumn = Gtk.TreeViewColumn(None)
502 self.thumbcell = Gtk.CellRendererPixbuf()
503 self.thumbcolumn.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
504 self.thumbpane_set_size()
505 self.thumbpane.append_column(self.thumbcolumn)
506 self.thumbcolumn.pack_start(self.thumbcell, True)
507 self.thumbcolumn.set_attributes(self.thumbcell, pixbuf=0)
508 self.thumbpane.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
509 self.thumbpane.set_headers_visible(False)
510 self.thumbpane.set_property("can-focus", False)
511 self.thumbscroll = Gtk.ScrolledWindow()
512 self.thumbscroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS)
513 self.thumbscroll.add(self.thumbpane)
514
515 self.table.attach(
516 self.thumbscroll,
517 0,
518 1,
519 0,
520 1,
521 0,
522 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
523 0,
524 0,
525 )
526 self.table.attach(
527 self.layout,
528 1,
529 2,
530 0,
531 1,
532 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
533 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
534 0,
535 0,
536 )
537 self.table.attach(
538 self.hscroll,
539 1,
540 2,
541 1,
542 2,
543 Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK,
544 Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK,
545 0,
546 0,
547 )
548 self.table.attach(
549 self.vscroll,
550 2,
551 3,
552 0,
553 1,
554 Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK,
555 Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK,
556 0,
557 0,
558 )
559 vbox.pack_start(self.table, True, True, 0)
560 if not bgcolor_found:
561 self.bgcolor = Gdk.Color(0, 0, 0) # Default to black
562 if self.simple_bgcolor:
563 self.layout.modify_bg(Gtk.StateType.NORMAL, None)
564 else:
565 self.layout.modify_bg(Gtk.StateType.NORMAL, self.bgcolor)
566 self.imageview = Gtk.Image()
567 self.layout.add(self.imageview)
568
569 self.statusbar = Gtk.Statusbar()
570 self.statusbar2 = Gtk.Statusbar()
571 self.statusbar2.set_size_request(200, -1)
572 hbox_statusbar = Gtk.HBox()
573 hbox_statusbar.pack_start(self.statusbar, True, True, 0)
574 hbox_statusbar.pack_start(self.statusbar2, False, True, 0)
575 vbox.pack_start(hbox_statusbar, False, False, 0)
576 self.window.add(vbox)
577 self.window.set_size_request(width, height)
578
579 if self.simple_bgcolor:
580 self.fullscreen_controls.modify_bg(Gtk.StateType.NORMAL, None)
581 else:
582 self.fullscreen_controls.modify_bg(Gtk.StateType.NORMAL, self.bgcolor)
583
584 # Connect signals
585 self.window.connect("delete_event", self.delete_event)
586 self.window.connect("destroy", self.destroy)
587 self.window.connect("size-allocate", self.window_resized)
588 self.window.connect("key-press-event", self.topwindow_keypress)
589 self.toolbar.connect("focus", self.toolbar_focused)
590 self.layout.drag_dest_set(
591 Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP,
592 [Gtk.TargetEntry.new("text/uri-list", 0, 80)],
593 Gdk.DragAction.DEFAULT,
594 )
595 self.layout.connect("drag_motion", self.motion_cb)
596 self.layout.connect("drag_data_received", self.drop_cb)
597 self.layout.add_events(
598 Gdk.EventMask.KEY_PRESS_MASK
599 | Gdk.EventMask.POINTER_MOTION_MASK
600 | Gdk.EventMask.BUTTON_PRESS_MASK
601 | Gdk.EventMask.BUTTON_MOTION_MASK
602 | Gdk.EventMask.SCROLL_MASK
603 )
604 self.layout.connect("scroll-event", self.mousewheel_scrolled)
605 self.layout.add_events(
606 Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.KEY_PRESS_MASK
607 )
608 self.layout.connect("button_press_event", self.button_pressed)
609 self.layout.add_events(
610 Gdk.EventMask.POINTER_MOTION_MASK
611 | Gdk.EventMask.POINTER_MOTION_HINT_MASK
612 | Gdk.EventMask.BUTTON_RELEASE_MASK
613 )
614 self.layout.connect("motion-notify-event", self.mouse_moved)
615 self.layout.connect("button-release-event", self.button_released)
616 # self.imageview.connect("expose-event", self.expose_event) TODO
617 self.thumb_sel_handler = self.thumbpane.get_selection().connect(
618 "changed", self.thumbpane_selection_changed
619 )
620 self.thumb_scroll_handler = self.thumbscroll.get_vscrollbar().connect(
621 "value-changed", self.thumbpane_scrolled
622 )
623 self.fullscreen_controls.slideshow_delay_adjustment.connect(
624 "value-changed", self.slideshow_delay_changed
625 )
626
627 # Since GNOME does its own thing for the toolbar style...
628 # Requires gnome-python installed to work (but optional)
629 try:
630 client = GConf.Client.get_default()
631 style = client.get_string("/desktop/gnome/interface/toolbar_style")
632 if style == "both":
633 self.toolbar.set_style(Gtk.ToolbarStyle.BOTH)
634 elif style == "both-horiz":
635 self.toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)
636 elif style == "icons":
637 self.toolbar.set_style(Gtk.ToolbarStyle.ICONS)
638 elif style == "text":
639 self.toolbar.set_style(Gtk.ToolbarStyle.TEXT)
640 client.add_dir(
641 "/desktop/gnome/interface", GConf.ClientPreloadType.PRELOAD_NONE
642 )
643 client.notify_add(
644 "/desktop/gnome/interface/toolbar_style", self.gconf_key_changed
645 )
646 except:
647 pass
648
649 # Show GUI:
650 if not self.toolbar_show:
651 self.toolbar.set_property("visible", False)
652 self.toolbar.set_no_show_all(True)
653 if not self.statusbar_show:
654 self.statusbar.set_property("visible", False)
655 self.statusbar.set_no_show_all(True)
656 self.statusbar2.set_property("visible", False)
657 self.statusbar2.set_no_show_all(True)
658 if not self.thumbpane_show:
659 self.thumbscroll.set_property("visible", False)
660 self.thumbscroll.set_no_show_all(True)
661 self.hscroll.set_no_show_all(True)
662 self.vscroll.set_no_show_all(True)
663 go_into_fullscreen = False
664 if opts != []:
665 for o, a in opts:
666 if (o in ("-f", "--fullscreen")) or (
667 (o in ("-s", "--slideshow")) and self.slideshow_in_fullscreen
668 ):
669 go_into_fullscreen = True
670 if go_into_fullscreen or self.start_in_fullscreen:
671 self.enter_fullscreen(None, None, None)
672 self.statusbar.set_no_show_all(True)
673 self.statusbar2.set_no_show_all(True)
674 self.toolbar.set_no_show_all(True)
675 self.thumbscroll.set_no_show_all(True)
676 self.window.show_all()
677 self.set_leave_fullscreen_visible(False)
678 self.layout.set_can_focus(True)
679 self.window.set_focus(self.layout)
680
681 self.set_slideshow_sensitivities()
682
683 # If arguments (filenames) were passed, try to open them:
684 self.image_list = []
685 if args != []:
686 for i in range(len(args)):
687 args[i] = urllib.request.url2pathname(args[i])
688 self.expand_filelist_and_load_image(args)
689 else:
690 self.set_go_sensitivities(False)
691 self.set_image_sensitivities(False)
692
693 if opts != []:
694 for o, a in opts:
695 if o in ("-s", "--slideshow"):
696 self.toggle_slideshow(None, None, None)
697
698 def do_startup(self):
699 Gtk.Application.do_startup(self)
700
701 toolbar_builder = Gtk.Builder.new_from_resource(
702 "/io/thomasross/mirage/toolbar.ui"
703 )
704 self.toolbar = toolbar_builder.get_object("toolbar")
705 self.fullscreen_controls = FullscreenControls()
706 self.popup_menu = Gtk.Menu.new_from_model(self.get_menu_by_id("popup-menu"))
707
708 def add_remove_action(self, action_name, activate, visible):
709 if visible and not self.action_group.lookup_action(action_name):
710 action = Gio.SimpleAction.new(action_name, None)
711 action.connect("activate", activate, None)
712 self.action_group.add_action(action)
713 elif not visible:
714 self.action_group.remove_action(action_name)
715
716 def get_pointer(self, window):
717 display = window.get_display()
718 seat = display.get_default_seat()
719 device = seat.get_pointer()
720
721 return window.get_device_position(device)[1:]
722
723 def refresh_recent_files_menu(self):
724 recent_files_section = self.get_menu_by_id("recent-files-section")
725
726 # Clear the old recent files
727 recent_files_section.remove_all()
728 for action in self.action_group.list_actions():
729 if action.startswith("recent-file-{}"):
730 self.action_group.remove_action(action)
731
732 for i, recent_file in enumerate(self.recentfiles):
733 filename = os.path.basename(recent_file)
734 if not filename:
735 continue
736
737 if len(filename) > 27:
738 filename = "{}..{}".format(filename[:25], os.path.splitext(filename)[1])
739
740 action_name = "recent-file-{}".format(
741 hashlib.sha1(recent_file.encode("utf8")).hexdigest()
742 )
743 action = Gio.SimpleAction.new(action_name, None)
744 action.connect("activate", self.recent_action_click, recent_file)
745 self.action_group.add_action(action)
746 self.set_accels_for_action("app." + action_name, ["<Alt>{}".format(i + 1)])
747
748 menu_item = Gio.MenuItem.new(filename, "app." + action_name)
749 recent_files_section.insert_item(i, menu_item)
750
751 def refresh_custom_actions_menu(self):
752 custom_actions_section = self.get_menu_by_id("custom-actions-section")
753
754 # Clear the old actions
755 custom_actions_section.remove_all()
756 for action in self.action_group.list_actions():
757 if action.startswith("custom-"):
758 self.action_group.remove_action(action)
759
760 for i, name in enumerate(self.action_names):
761 action_name = "custom-{}".format(self.action_hashes[i])
762
763 action = Gio.SimpleAction.new(action_name, None)
764 action.connect("activate", self.custom_action_click, self.action_hashes[i])
765 self.action_group.add_action(action)
766 self.set_accels_for_action("app." + action_name, [self.action_shortcuts[i]])
767
768 menu_item = Gio.MenuItem.new(name, "app." + action_name)
769 custom_actions_section.insert_item(i, menu_item)
770
771 def thumbpane_update_images(self, clear_first=False, force_upto_imgnum=-1):
772 self.stop_now = False
773 # When first populating the thumbpane, make sure we go up to at least
774 # force_upto_imgnum so that we can show this image selected:
775 if clear_first:
776 self.thumbpane_clear_list()
777 # Load all images up to the bottom ofo the visible thumbpane rect:
778 rect = self.thumbpane.get_visible_rect()
779 bottom_coord = rect.y + rect.height + self.thumbnail_size
780 if bottom_coord > self.thumbpane_bottom_coord_loaded:
781 self.thumbpane_bottom_coord_loaded = bottom_coord
782 # update images:
783 if not self.thumbpane_updating:
784 thread = threading.Thread(
785 target=self.thumbpane_update_pending_images,
786 args=(force_upto_imgnum, None),
787 )
788 thread.setDaemon(True)
789 thread.start()
790
791 def thumbpane_create_dir(self):
792 if not os.path.exists(os.path.expanduser("~/.thumbnails/")):
793 os.mkdir(os.path.expanduser("~/.thumbnails/"))
794 if not os.path.exists(os.path.expanduser("~/.thumbnails/normal/")):
795 os.mkdir(os.path.expanduser("~/.thumbnails/normal/"))
796
797 def thumbpane_update_pending_images(self, force_upto_imgnum, foo):
798 self.thumbpane_updating = True
799 self.thumbpane_create_dir()
800 # Check to see if any images need their thumbnails generated.
801 curr_coord = 0
802 imgnum = 0
803 while (
804 curr_coord < self.thumbpane_bottom_coord_loaded
805 or imgnum <= force_upto_imgnum
806 ):
807 if self.closing_app or self.stop_now or not self.thumbpane_show:
808 break
809 if imgnum >= len(self.image_list):
810 break
811 self.thumbpane_set_image(self.image_list[imgnum], imgnum)
812 curr_coord += self.thumbpane.get_background_area(
813 (imgnum,), self.thumbcolumn
814 ).height
815 if force_upto_imgnum == imgnum:
816 # Verify that the user hasn't switched images while we're loading thumbnails:
817 if force_upto_imgnum == self.curr_img_in_list:
818 GObject.idle_add(self.thumbpane_select, force_upto_imgnum)
819 imgnum += 1
820 self.thumbpane_updating = False
821
822 def thumbpane_clear_list(self):
823 self.thumbpane_bottom_coord_loaded = 0
824 self.thumbscroll.get_vscrollbar().handler_block(self.thumb_scroll_handler)
825 self.thumblist.clear()
826 self.thumbscroll.get_vscrollbar().handler_unblock(self.thumb_scroll_handler)
827 for image in self.image_list:
828 blank_pix = self.get_blank_pix_for_image(image)
829 self.thumblist.append([blank_pix])
830 self.thumbnail_loaded = [False] * len(self.image_list)
831
832 def thumbpane_set_image(self, image_name, imgnum, force_update=False):
833 if self.thumbpane_show:
834 if not self.thumbnail_loaded[imgnum] or force_update:
835 filename, thumbfile = self.thumbnail_get_name(image_name)
836 pix = self.thumbpane_get_pixbuf(thumbfile, filename, force_update)
837 if pix:
838 if self.thumbnail_size != 128:
839 # 128 is the size of the saved thumbnail, so convert if different:
840 pix, image_width, image_height = self.get_pixbuf_of_size(
841 pix, self.thumbnail_size, GdkPixbuf.InterpType.TILES
842 )
843 self.thumbnail_loaded[imgnum] = True
844 self.thumbscroll.get_vscrollbar().handler_block(
845 self.thumb_scroll_handler
846 )
847 pix = self.pixbuf_add_border(pix)
848 try:
849 self.thumblist[imgnum] = [pix]
850 except:
851 pass
852 self.thumbscroll.get_vscrollbar().handler_unblock(
853 self.thumb_scroll_handler
854 )
855
856 def thumbnail_get_name(self, image_name):
857 filename = os.path.expanduser("file://" + image_name)
858 uriname = os.path.expanduser(
859 "file://" + urllib.request.pathname2url(image_name)
860 )
861 if HAS_HASHLIB:
862 m = hashlib.md5()
863 else:
864 m = md5.new()
865 m.update(uriname)
866 mhex = m.hexdigest()
867 mhex_filename = os.path.expanduser("~/.thumbnails/normal/" + mhex + ".png")
868 return filename, mhex_filename
869
870 def thumbpane_get_pixbuf(self, thumb_url, image_url, force_generation):
871 # Returns a valid pixbuf or None if a pixbuf cannot be generated. Tries to re-use
872 # a thumbnail from ~/.thumbails/normal/, otherwise generates one with the
873 # XDG filename: md5(file:///full/path/to/image).png
874 imgfile = image_url
875 if imgfile[:7] == "file://":
876 imgfile = imgfile[7:]
877 try:
878 if os.path.exists(thumb_url) and not force_generation:
879 pix = GdkPixbuf.Pixbuf.new_from_file(thumb_url)
880 pix_mtime = pix.get_option("tEXt::Thumb::MTime")
881 if pix_mtime:
882 st = os.stat(imgfile)
883 file_mtime = str(st[stat.ST_MTIME])
884 # If the mtimes match, we're good. if not, regenerate the thumbnail..
885 if pix_mtime == file_mtime:
886 return pix
887 # Create the 128x128 thumbnail:
888 uri = "file://" + urllib.request.pathname2url(imgfile)
889 pix = GdkPixbuf.Pixbuf.new_from_file(imgfile)
890 pix, image_width, image_height = self.get_pixbuf_of_size(
891 pix, 128, GdkPixbuf.InterpType.TILES
892 )
893 st = os.stat(imgfile)
894 file_mtime = str(st[stat.ST_MTIME])
895 # Save image to .thumbnails:
896 pix.save(
897 thumb_url,
898 "png",
899 {
900 "tEXt::Thumb::URI": uri,
901 "tEXt::Thumb::MTime": file_mtime,
902 "tEXt::Software": "Mirage" + __version__,
903 },
904 )
905 return pix
906 except:
907 return None
908
909 def thumbpane_load_image(self, treeview, imgnum):
910 if imgnum != self.curr_img_in_list:
911 GObject.idle_add(self.goto_image, str(imgnum), None)
912
913 def thumbpane_selection_changed(self, treeview):
914 cancel = self.autosave_image()
915 if cancel:
916 # Revert selection...
917 GObject.idle_add(self.thumbpane_select, self.curr_img_in_list)
918 return True
919 try:
920 model, paths = self.thumbpane.get_selection().get_selected_rows()
921 imgnum = paths[0][0]
922 if not self.thumbnail_loaded[imgnum]:
923 self.thumbpane_set_image(self.image_list[imgnum], imgnum)
924 GObject.idle_add(self.thumbpane_load_image, treeview, imgnum)
925 except:
926 pass
927
928 def thumbpane_select(self, imgnum):
929 if self.thumbpane_show:
930 self.thumbpane.get_selection().handler_block(self.thumb_sel_handler)
931 try:
932 self.thumbpane.get_selection().select_path((imgnum,))
933 self.thumbpane.scroll_to_cell((imgnum,))
934 except:
935 pass
936 self.thumbpane.get_selection().handler_unblock(self.thumb_sel_handler)
937
938 def thumbpane_set_size(self):
939 self.thumbcolumn.set_fixed_width(self.thumbpane_get_size())
940 self.window_resized(None, self.window.get_allocation(), True)
941
942 def thumbpane_get_size(self):
943 return int(self.thumbnail_size * 1.3)
944
945 def thumbpane_scrolled(self, range):
946 self.thumbpane_update_images()
947
948 def get_blank_pix_for_image(self, image):
949 # Sizes the "blank image" icon for the thumbpane. This will ensure that we don't
950 # load a humongous icon for a small pix, for example, and will keep the thumbnails
951 # from shifting around when they are actually loaded.
952 try:
953 info = GdkPixbuf.Pixbuf.get_file_info(image)
954 imgwidth = float(info[1])
955 imgheight = float(info[2])
956 if imgheight > self.thumbnail_size:
957 if imgheight > imgwidth:
958 imgheight = self.thumbnail_size
959 else:
960 imgheight = (imgheight // imgwidth) * self.thumbnail_size
961 imgheight = 2 + int(
962 imgheight
963 ) # Account for border that will be added to thumbnails..
964 imgwidth = self.thumbnail_size
965 except:
966 imgheight = 2 + self.thumbnail_size
967 imgwidth = self.thumbnail_size
968 blank_pix = GdkPixbuf.Pixbuf(
969 GdkPixbuf.Colorspace.RGB, True, 8, imgwidth, imgheight
970 )
971 blank_pix.fill(0x00000000)
972 imgwidth2 = int(imgheight * 0.8)
973 imgheight2 = int(imgheight * 0.8)
974 composite_pix = self.blank_image.scale_simple(
975 imgwidth2, imgheight2, GdkPixbuf.InterpType.BILINEAR
976 )
977 leftcoord = int((imgwidth - imgwidth2) // 2)
978 topcoord = int((imgheight - imgheight2) // 2)
979 composite_pix.copy_area(
980 0, 0, imgwidth2, imgheight2, blank_pix, leftcoord, topcoord
981 )
982 return blank_pix
983
984 def find_path(self, filename, exit_on_fail=True):
985 """ Find a pixmap or icon by looking through standard dirs.
986 If the image isn't found exit with error status 1 unless
987 exit_on_fail is set to False, then return None """
988 if not self.resource_path_list:
989 # If executed from mirage in bin this points to the basedir
990 basedir_mirage = os.path.split(sys.path[0])[0]
991 # If executed from mirage.py module in python lib this points to the basedir
992 f0 = os.path.split(__file__)[0].split("/lib")[0]
993 self.resource_path_list = list(
994 set(
995 filter(
996 os.path.isdir,
997 [
998 os.path.join(basedir_mirage, "share", "mirage"),
999 os.path.join(basedir_mirage, "share", "pixmaps"),
1000 os.path.join(sys.prefix, "share", "mirage"),
1001 os.path.join(sys.prefix, "share", "pixmaps"),
1002 os.path.join(sys.prefix, "local", "share", "mirage"),
1003 os.path.join(sys.prefix, "local", "share", "pixmaps"),
1004 sys.path[0], # If it's run non-installed
1005 os.path.join(f0, "share", "mirage"),
1006 os.path.join(f0, "share", "pixmaps"),
1007 ],
1008 )
1009 )
1010 )
1011 for path in self.resource_path_list:
1012 pix = os.path.join(path, filename)
1013 if os.path.exists(pix):
1014 return pix
1015 # If we reached here, we didn't find the pixmap
1016 if exit_on_fail:
1017 print(
1018 _("Couldn't find the image %s. Please check your installation.")
1019 % filename
1020 )
1021 sys.exit(1)
1022 else:
1023 return None
1024
1025 def gconf_key_changed(self, client, cnxn_id, entry, label):
1026 if entry.value.type == GConf.ValueType.STRING:
1027 style = entry.value.to_string()
1028 if style == "both":
1029 self.toolbar.set_style(Gtk.ToolbarStyle.BOTH)
1030 elif style == "both-horiz":
1031 self.toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)
1032 elif style == "icons":
1033 self.toolbar.set_style(Gtk.ToolbarStyle.ICONS)
1034 elif style == "text":
1035 self.toolbar.set_style(Gtk.ToolbarStyle.TEXT)
1036 if self.image_loaded and self.last_image_action_was_fit:
1037 if self.last_image_action_was_smart_fit:
1038 self.zoom_to_fit_or_1_to_1(None, False, False)
1039 else:
1040 self.zoom_to_fit_window(None, False, False)
1041
1042 def toolbar_focused(self, widget, direction):
1043 self.layout.grab_focus()
1044 return True
1045
1046 def topwindow_keypress(self, widget, event):
1047 # For whatever reason, 'Left' and 'Right' cannot be used as menu
1048 # accelerators so we will manually check for them here:
1049 if (
1050 (not (event.get_state() & Gdk.ModifierType.SHIFT_MASK))
1051 and not (event.get_state() & Gdk.ModifierType.CONTROL_MASK)
1052 and not (event.get_state() & Gdk.ModifierType.MOD1_MASK)
1053 and not (event.get_state() & Gdk.ModifierType.MOD2_MASK)
1054 and not (event.get_state() & Gdk.ModifierType.CONTROL_MASK)
1055 ):
1056 if event.keyval == Gdk.keyval_from_name(
1057 "Left"
1058 ) or event.keyval == Gdk.keyval_from_name("Up"):
1059 self.goto_prev_image(None, None, None)
1060 return
1061 elif event.keyval == Gdk.keyval_from_name(
1062 "Right"
1063 ) or event.keyval == Gdk.keyval_from_name("Down"):
1064 self.goto_next_image(None, None, None)
1065 return
1066 shortcut = Gtk.accelerator_name(event.keyval, event.get_state())
1067 if "Escape" in shortcut:
1068 self.stop_now = True
1069 self.searching_for_images = False
1070 while Gtk.events_pending():
1071 Gtk.main_iteration()
1072 self.update_title()
1073 return
1074
1075 def parse_action_command(self, command, batchmode):
1076 self.running_custom_actions = True
1077 self.change_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))
1078 while Gtk.events_pending():
1079 Gtk.main_iteration()
1080 self.curr_custom_action = 0
1081 if batchmode:
1082 self.num_custom_actions = len(self.image_list)
1083 for i in range(self.num_custom_actions):
1084 self.curr_custom_action += 1
1085 self.update_statusbar()
1086 while Gtk.events_pending():
1087 Gtk.main_iteration()
1088 imagename = self.image_list[i]
1089 self.parse_action_command2(command, imagename)
1090 else:
1091 self.num_custom_actions = 1
1092 self.curr_custom_action = 1
1093 self.update_statusbar()
1094 while Gtk.events_pending():
1095 Gtk.main_iteration()
1096 self.parse_action_command2(command, self.currimg_name)
1097 gc.collect()
1098 self.change_cursor(None)
1099 # Refresh the current image or any preloaded needed if they have changed:
1100 if not os.path.exists(self.currimg_name):
1101 self.currimg_pixbuf_original = None
1102 self.image_load_failed(False)
1103 else:
1104 animtest = GdkPixbuf.PixbufAnimation(self.currimg_name)
1105 if animtest.is_static_image():
1106 if self.images_are_different(
1107 animtest.get_static_image(), self.currimg_pixbuf_original
1108 ):
1109 self.load_new_image2(False, False, True, False)
1110 else:
1111 if self.images_are_different(animtest, self.currimg_pixbuf_original):
1112 self.load_new_image2(False, False, True, False)
1113 self.running_custom_actions = False
1114 self.update_statusbar()
1115 while Gtk.events_pending():
1116 Gtk.main_iteration()
1117 if not os.path.exists(self.preloadimg_prev_name):
1118 self.preloadimg_prev_in_list = -1
1119 else:
1120 animtest = GdkPixbuf.PixbufAnimation(self.preloadimg_prev_name)
1121 if animtest.is_static_image():
1122 if self.images_are_different(
1123 animtest.get_static_image(), self.preloadimg_prev_pixbuf_original
1124 ):
1125 self.preloadimg_prev_in_list = -1
1126 self.preload_when_idle = GObject.idle_add(
1127 self.preload_prev_image, False
1128 )
1129 else:
1130 if self.images_are_different(
1131 animtest, self.preloadimg_prev_pixbuf_original
1132 ):
1133 self.preloadimg_prev_in_list = -1
1134 self.preload_when_idle = GObject.idle_add(
1135 self.preload_prev_image, False
1136 )
1137 if not os.path.exists(self.preloadimg_next_name):
1138 self.preloadimg_next_in_list = -1
1139 else:
1140 animtest = GdkPixbuf.PixbufAnimation(self.preloadimg_next_name)
1141 if animtest.is_static_image():
1142 if self.images_are_different(
1143 animtest.get_static_image(), self.preloadimg_next_pixbuf_original
1144 ):
1145 self.preloadimg_next_in_list = -1
1146 self.preload_when_idle = GObject.idle_add(
1147 self.preload_next_image, False
1148 )
1149 else:
1150 if self.images_are_different(
1151 animtest, self.preloadimg_next_pixbuf_original
1152 ):
1153 self.preloadimg_next_in_list = -1
1154 self.preload_when_idle = GObject.idle_add(
1155 self.preload_next_image, False
1156 )
1157 self.stop_now = False
1158 if batchmode:
1159 # Update all thumbnails:
1160 GObject.idle_add(self.thumbpane_update_images, True, self.curr_img_in_list)
1161 else:
1162 # Update only the current thumbnail:
1163 GObject.idle_add(
1164 self.thumbpane_set_image,
1165 self.image_list[self.curr_img_in_list],
1166 self.curr_img_in_list,
1167 True,
1168 )
1169
1170 def images_are_different(self, pixbuf1, pixbuf2):
1171 if pixbuf1.get_pixels() == pixbuf2.get_pixels():
1172 return False
1173 else:
1174 return True
1175
1176 def recent_action_click(self, action, parameter, filename):
1177 self.stop_now = True
1178 while Gtk.events_pending():
1179 Gtk.main_iteration()
1180 cancel = self.autosave_image()
1181 if cancel:
1182 return
1183 if (
1184 os.path.isfile(filename)
1185 or os.path.exists(filename)
1186 or filename.startswith("http://")
1187 or filename.startswith("ftp://")
1188 ):
1189 self.expand_filelist_and_load_image([filename])
1190 else:
1191 self.image_list = []
1192 self.curr_img_in_list = 0
1193 self.image_list.append(filename)
1194 self.image_load_failed(False)
1195 self.recent_file_remove_and_refresh(filename)
1196
1197 def recent_file_remove_and_refresh(self, rmfile):
1198 try:
1199 self.recentfiles.remove(rmfile)
1200 except ValueError:
1201 pass
1202
1203 self.refresh_recent_files_menu()
1204
1205 def recent_file_add_and_refresh(self, addfile):
1206 try:
1207 self.recentfiles.remove(addfile)
1208 except ValueError:
1209 pass
1210
1211 self.recentfiles.insert(0, addfile)
1212 self.refresh_recent_files_menu()
1213
1214 def custom_action_click(self, action, parameter, action_hash):
1215 for i, hash in enumerate(self.action_hashes):
1216 if hash == action_hash:
1217 try:
1218 self.parse_action_command(
1219 self.action_commands[i], self.action_batch[i]
1220 )
1221 except:
1222 pass
1223
1224 break
1225
1226 def parse_action_command2(self, cmd, imagename):
1227 # Executes the given command using ``os.system``, substituting "%"-macros approprately.
1228 def sh_esc(s):
1229 import re
1230
1231 return re.sub(r"[^/._a-zA-Z0-9-]", lambda c: "\\" + c.group(), s)
1232
1233 cmd = cmd.strip()
1234 # [NEXT] and [PREV] are only valid alone or at the end of the command
1235 if cmd == "[NEXT]":
1236 self.goto_next_image(None, None, None)
1237 return
1238 elif cmd == "[PREV]":
1239 self.goto_prev_image(None, None, None)
1240 return
1241 # -1=go to previous, 1=go to next, 0=don't change
1242 prev_or_next = 0
1243 if cmd[-6:] == "[NEXT]":
1244 prev_or_next = 1
1245 cmd = cmd[:-6]
1246 elif cmd[-6:] == "[PREV]":
1247 prev_or_next = -1
1248 cmd = cmd[:-6]
1249 if "%F" in cmd:
1250 cmd = cmd.replace("%F", sh_esc(imagename))
1251 if "%N" in cmd:
1252 cmd = cmd.replace(
1253 "%N", sh_esc(os.path.splitext(os.path.basename(imagename))[0])
1254 )
1255 if "%P" in cmd:
1256 cmd = cmd.replace("%P", sh_esc(os.path.dirname(imagename) + "/"))
1257 if "%E" in cmd:
1258 cmd = cmd.replace(
1259 "%E", sh_esc(os.path.splitext(os.path.basename(imagename))[1])
1260 )
1261 if "%L" in cmd:
1262 cmd = cmd.replace("%L", " ".join([sh_esc(s) for s in self.image_list]))
1263 if self.verbose:
1264 print(_("Action: %s") % cmd)
1265 shell_rc = os.system(cmd) >> 8
1266 if self.verbose:
1267 print(_("Action return code: %s") % shell_rc)
1268 if shell_rc != 0:
1269 msg = (
1270 _(
1271 'Unable to launch "%s". Please specify a valid command from Edit > Custom Actions.'
1272 )
1273 % cmd
1274 )
1275 error_dialog = Gtk.MessageDialog(
1276 self.window,
1277 Gtk.DialogFlags.MODAL,
1278 Gtk.MessageType.WARNING,
1279 Gtk.ButtonsType.CLOSE,
1280 msg,
1281 )
1282 error_dialog.set_title(_("Invalid Custom Action"))
1283 error_dialog.run()
1284 error_dialog.destroy()
1285 elif prev_or_next == 1:
1286 self.goto_next_image(None, None, None)
1287 elif prev_or_next == -1:
1288 self.goto_prev_image(None, None, None)
1289 self.running_custom_actions = False
1290
1291 def set_go_sensitivities(self, enable):
1292 self.action_group.lookup_action("goto-next-image").set_enabled(enable)
1293 self.action_group.lookup_action("goto-prev-image").set_enabled(enable)
1294 self.action_group.lookup_action("goto-random-image").set_enabled(enable)
1295 self.action_group.lookup_action("goto-first-image").set_enabled(enable)
1296 self.action_group.lookup_action("goto-last-image").set_enabled(enable)
1297
1298 def set_image_sensitivities(self, enable):
1299 self.set_zoom_in_sensitivities(enable)
1300 self.set_zoom_out_sensitivities(enable)
1301 self.action_group.lookup_action("save-image").set_enabled(enable)
1302 self.action_group.lookup_action("save-image-as").set_enabled(enable)
1303 self.action_group.lookup_action("show-properties").set_enabled(enable)
1304
1305 self.action_group.lookup_action("crop-image").set_enabled(enable)
1306 self.action_group.lookup_action("resize-image").set_enabled(enable)
1307 self.action_group.lookup_action("saturation").set_enabled(enable)
1308 self.action_group.lookup_action("rename-image").set_enabled(enable)
1309 self.action_group.lookup_action("delete-image").set_enabled(enable)
1310
1311 self.action_group.lookup_action("zoom-1-to-1").set_enabled(enable)
1312 self.action_group.lookup_action("zoom-to-fit-window").set_enabled(enable)
1313
1314 # Only jpeg, png, and bmp images are currently supported for saving
1315 if len(self.image_list) > 0:
1316 self.action_group.lookup_action("save-image").set_enabled(False)
1317 try:
1318 filetype = GdkPixbuf.Pixbuf.get_file_info(self.currimg_name)[0]["name"]
1319 if self.filetype_is_writable(filetype):
1320 self.action_group.lookup_action("save-image").set_enabled(enable)
1321 except:
1322 pass
1323
1324 for action in self.action_group.list_actions():
1325 if action.startswith("custom-"):
1326 self.action_group.lookup_action(action).set_enabled(enable)
1327
1328 if not HAS_IMGFUNCS:
1329 enable = False
1330
1331 self.action_group.lookup_action("rotate-left").set_enabled(enable)
1332 self.action_group.lookup_action("rotate-right").set_enabled(enable)
1333 self.action_group.lookup_action("flip-image-vert").set_enabled(enable)
1334 self.action_group.lookup_action("flip-image-horiz").set_enabled(enable)
1335
1336 def set_zoom_in_sensitivities(self, enable):
1337 self.action_group.lookup_action("zoom-in").set_enabled(enable)
1338
1339 def set_zoom_out_sensitivities(self, enable):
1340 self.action_group.lookup_action("zoom-out").set_enabled(enable)
1341
1342 def set_next_image_sensitivities(self, enable):
1343 self.action_group.lookup_action("goto-next-image").set_enabled(enable)
1344
1345 def set_previous_image_sensitivities(self, enable):
1346 self.action_group.lookup_action("goto-prev-image").set_enabled(enable)
1347
1348 def set_first_image_sensitivities(self, enable):
1349 self.action_group.lookup_action("goto-first-image").set_enabled(enable)
1350
1351 def set_last_image_sensitivities(self, enable):
1352 self.action_group.lookup_action("goto-last-image").set_enabled(enable)
1353
1354 def set_random_image_sensitivities(self, enable):
1355 self.action_group.lookup_action("goto-random-image").set_enabled(enable)
1356
1357 def set_start_slideshow_visible(self, visible):
1358 self.add_remove_action("start-slideshow", self.toggle_slideshow, visible)
1359
1360 def set_stop_slideshow_visible(self, visible):
1361 self.add_remove_action("stop-slideshow", self.toggle_slideshow, visible)
1362
1363 def set_slideshow_sensitivities(self):
1364 if len(self.image_list) <= 1:
1365 self.set_start_slideshow_visible(True)
1366 self.action_group.lookup_action("start-slideshow").set_enabled(False)
1367
1368 self.set_stop_slideshow_visible(False)
1369 elif self.slideshow_mode:
1370 self.set_start_slideshow_visible(False)
1371
1372 self.set_stop_slideshow_visible(True)
1373 self.action_group.lookup_action("stop-slideshow").set_enabled(True)
1374 else:
1375 self.set_start_slideshow_visible(True)
1376 self.action_group.lookup_action("start-slideshow").set_enabled(True)
1377
1378 self.set_stop_slideshow_visible(False)
1379
1380 def set_zoom_sensitivities(self):
1381 if not self.currimg_is_animation:
1382 self.set_zoom_out_sensitivities(True)
1383 self.set_zoom_in_sensitivities(True)
1384 else:
1385 self.set_zoom_out_sensitivities(False)
1386 self.set_zoom_in_sensitivities(False)
1387
1388 def print_version(self):
1389 print(_("Version: Mirage"), __version__)
1390 print(_("Website: http://mirageiv.berlios.de"))
1391
1392 def print_usage(self):
1393 self.print_version()
1394 print("")
1395 print(_("Usage: mirage [OPTION]... FILES|FOLDERS..."))
1396 print("")
1397 print(_("Options") + ":")
1398 print(" -h, --help " + _("Show this help and exit"))
1399 print(" -v, --version " + _("Show version information and exit"))
1400 print(" -V, --verbose " + _("Show more detailed information"))
1401 print(" -R, --recursive " + _("Recursively include all images found in"))
1402 print(" " + _("subdirectories of FOLDERS"))
1403 print(" -s, --slideshow " + _("Start in slideshow mode"))
1404 print(" -f, --fullscreen " + _("Start in fullscreen mode"))
1405 print(" -o, --onload 'cmd' " + _("Execute 'cmd' when an image is loaded"))
1406 print(" " + _("uses same syntax as custom actions,\n"))
1407 print(" " + _("i.e. mirage -o 'echo file is %F'"))
1408
1409 def slideshow_delay_changed(self, action):
1410 self.curr_slideshow_delay = (
1411 self.fullscreen_controls.slideshow_delay_adjustment.get_value()
1412 )
1413 if self.slideshow_mode:
1414 GObject.source_remove(self.timer_delay)
1415 if self.curr_slideshow_random:
1416 self.timer_delay = GObject.timeout_add(
1417 int(self.curr_slideshow_delay * 1000),
1418 self.goto_random_image,
1419 None,
1420 None,
1421 "ss",
1422 )
1423 else:
1424 self.timer_delay = GObject.timeout_add(
1425 (self.curr_slideshow_delay * 1000),
1426 self.goto_next_image,
1427 None,
1428 None,
1429 "ss",
1430 )
1431 self.window.set_focus(self.layout)
1432
1433 def toggle_slideshow_shuffle(self, action, value, data):
1434 action.set_state(value)
1435 self.curr_slideshow_random = value.get_boolean()
1436
1437 def motion_cb(self, widget, context, x, y, time):
1438 context.drag_status(Gdk.DragAction.COPY, time)
1439 return True
1440
1441 def drop_cb(self, widget, context, x, y, selection, info, time):
1442 uri = selection.data.strip()
1443 path = urllib.request.url2pathname(uri)
1444 paths = path.rsplit("\n")
1445 for i, path in enumerate(paths):
1446 paths[i] = path.rstrip("\r")
1447 self.expand_filelist_and_load_image(paths)
1448
1449 def put_error_image_to_window(self):
1450 self.imageview.set_from_stock(
1451 Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.LARGE_TOOLBAR
1452 )
1453 self.currimg_width = self.imageview.size_request()[0]
1454 self.currimg_height = self.imageview.size_request()[1]
1455 self.center_image()
1456 self.set_go_sensitivities(False)
1457 self.set_image_sensitivities(False)
1458 self.update_statusbar()
1459 self.loaded_img_in_list = -1
1460
1461 def expose_event(self, widget, event):
1462 if self.updating_adjustments:
1463 return
1464 self.updating_adjustments = True
1465 if self.hscroll.get_property("visible"):
1466 try:
1467 zoomratio = float(self.currimg_width) / self.previmg_width
1468 newvalue = abs(
1469 self.layout.get_hadjustment().get_value() * zoomratio
1470 + ((self.available_image_width()) * (zoomratio - 1)) // 2
1471 )
1472 if newvalue >= self.layout.get_hadjustment().lower and newvalue <= (
1473 self.layout.get_hadjustment().upper
1474 - self.layout.get_hadjustment().page_size
1475 ):
1476 self.layout.get_hadjustment().set_value(newvalue)
1477 except:
1478 pass
1479 if self.vscroll.get_property("visible"):
1480 try:
1481 newvalue = abs(
1482 self.layout.get_vadjustment().get_value() * zoomratio
1483 + ((self.available_image_height()) * (zoomratio - 1)) // 2
1484 )
1485 if newvalue >= self.layout.get_vadjustment().lower and newvalue <= (
1486 self.layout.get_vadjustment().upper
1487 - self.layout.get_vadjustment().page_size
1488 ):
1489 self.layout.get_vadjustment().set_value(newvalue)
1490 self.previmg_width = self.currimg_width
1491 except:
1492 pass
1493 self.updating_adjustments = False
1494
1495 def window_resized(self, widget, allocation, force_update=False):
1496 # Update the image size on window resize if the current image was last fit:
1497 if self.image_loaded:
1498 if (
1499 force_update
1500 or allocation.width != self.prevwinwidth
1501 or allocation.height != self.prevwinheight
1502 ):
1503 if self.last_image_action_was_fit:
1504 if self.last_image_action_was_smart_fit:
1505 self.zoom_to_fit_or_1_to_1(None, False, False)
1506 else:
1507 self.zoom_to_fit_window(None, False, False)
1508 else:
1509 self.center_image()
1510 self.load_new_image_stop_now()
1511 self.show_scrollbars_if_needed()
1512 # Also, regenerate preloaded image for new window size:
1513 self.preload_when_idle = GObject.idle_add(self.preload_next_image, True)
1514 self.preload_when_idle2 = GObject.idle_add(
1515 self.preload_prev_image, True
1516 )
1517 self.prevwinwidth = allocation.width
1518 self.prevwinheight = allocation.height
1519 return
1520
1521 def save_settings(self):
1522 conf = configparser.ConfigParser(interpolation=None)
1523 conf.add_section("window")
1524 conf.set("window", "w", self.window.get_allocation().width)
1525 conf.set("window", "h", self.window.get_allocation().height)
1526 conf.set("window", "toolbar", self.toolbar_show)
1527 conf.set("window", "statusbar", self.statusbar_show)
1528 conf.set("window", "thumbpane", self.thumbpane_show)
1529 conf.add_section("prefs")
1530 conf.set("prefs", "simple-bgcolor", self.simple_bgcolor)
1531 conf.set("prefs", "bgcolor-red", self.bgcolor.red)
1532 conf.set("prefs", "bgcolor-green", self.bgcolor.green)
1533 conf.set("prefs", "bgcolor-blue", self.bgcolor.blue)
1534 conf.set("prefs", "open_all", self.open_all_images)
1535 conf.set("prefs", "hidden", self.open_hidden_files)
1536 conf.set("prefs", "use_last_dir", self.use_last_dir)
1537 conf.set("prefs", "last_dir", self.last_dir)
1538 conf.set("prefs", "fixed_dir", self.fixed_dir)
1539 conf.set("prefs", "open_mode", self.open_mode)
1540 conf.set("prefs", "last_mode", self.last_mode)
1541 conf.set("prefs", "listwrap_mode", self.listwrap_mode)
1542 conf.set("prefs", "slideshow_delay", int(self.slideshow_delay))
1543 conf.set("prefs", "slideshow_random", self.slideshow_random)
1544 conf.set("prefs", "zoomquality", self.zoomvalue)
1545 conf.set("prefs", "quality_save", int(self.quality_save))
1546 conf.set("prefs", "disable_screensaver", self.disable_screensaver)
1547 conf.set("prefs", "slideshow_in_fullscreen", self.slideshow_in_fullscreen)
1548 conf.set("prefs", "confirm_delete", self.confirm_delete)
1549 conf.set("prefs", "preloading_images", self.preloading_images)
1550 conf.set("prefs", "savemode", self.savemode)
1551 conf.set("prefs", "start_in_fullscreen", self.start_in_fullscreen)
1552 conf.set("prefs", "thumbsize", self.thumbnail_size)
1553 conf.set("prefs", "screenshot_delay", self.screenshot_delay)
1554 conf.add_section("actions")
1555 conf.set("actions", "num_actions", len(self.action_names))
1556 for i in range(len(self.action_names)):
1557 conf.set("actions", "names[" + str(i) + "]", self.action_names[i])
1558 conf.set("actions", "commands[" + str(i) + "]", self.action_commands[i])
1559 conf.set("actions", "shortcuts[" + str(i) + "]", self.action_shortcuts[i])
1560 conf.set("actions", "batch[" + str(i) + "]", self.action_batch[i])
1561 conf.add_section("recent")
1562 conf.set("recent", "num_recent", len(self.recentfiles))
1563 for i in range(len(self.recentfiles)):
1564 conf.set("recent", "num[" + str(i) + "]", len(self.recentfiles[i]))
1565 conf.set("recent", "urls[" + str(i) + ",0]", self.recentfiles[i])
1566 if not os.path.exists(self.config_dir):
1567 os.makedirs(self.config_dir)
1568 with open(self.config_dir + "/miragerc", "w") as f:
1569 conf.write(f)
1570
1571 # Also, save accel_map:
1572 Gtk.AccelMap.save(self.config_dir + "/accel_map")
1573
1574 return
1575
1576 def delete_event(self, widget, event, data=None):
1577 cancel = self.autosave_image()
1578 if cancel:
1579 return True
1580 self.stop_now = True
1581 self.closing_app = True
1582 self.save_settings()
1583 sys.exit(0)
1584
1585 def destroy(self, event, data=None):
1586 cancel = self.autosave_image()
1587 if cancel:
1588 return True
1589 self.stop_now = True
1590 self.closing_app = True
1591 self.save_settings()
1592
1593 def exit_app(self, action, parameter, data):
1594 cancel = self.autosave_image()
1595 if cancel:
1596 return True
1597 self.stop_now = True
1598 self.closing_app = True
1599 self.save_settings()
1600 sys.exit(0)
1601
1602 def put_zoom_image_to_window(self, currimg_preloaded):
1603 self.window.window.freeze_updates()
1604 if not currimg_preloaded:
1605 # Always start with the original image to preserve quality!
1606 # Calculate image size:
1607 finalimg_width = int(
1608 self.currimg_pixbuf_original.get_width() * self.currimg_zoomratio
1609 )
1610 finalimg_height = int(
1611 self.currimg_pixbuf_original.get_height() * self.currimg_zoomratio
1612 )
1613 if not self.currimg_is_animation:
1614 # Scale image:
1615 if not self.currimg_pixbuf_original.get_has_alpha():
1616 self.currimg_pixbuf = self.currimg_pixbuf_original.scale_simple(
1617 finalimg_width, finalimg_height, self.zoom_quality
1618 )
1619 else:
1620 colormap = self.imageview.get_colormap()
1621 light_grey = colormap.alloc_color("#666666", True, True)
1622 dark_grey = colormap.alloc_color("#999999", True, True)
1623 self.currimg_pixbuf = self.currimg_pixbuf_original.composite_color_simple(
1624 finalimg_width,
1625 finalimg_height,
1626 self.zoom_quality,
1627 255,
1628 8,
1629 light_grey.pixel,
1630 dark_grey.pixel,
1631 )
1632 else:
1633 self.currimg_pixbuf = self.currimg_pixbuf_original
1634 self.currimg_width, self.currimg_height = finalimg_width, finalimg_height
1635 self.layout.set_size(self.currimg_width, self.currimg_height)
1636 self.center_image()
1637 self.show_scrollbars_if_needed()
1638 if not self.currimg_is_animation:
1639 self.imageview.set_from_pixbuf(self.currimg_pixbuf)
1640 self.previmage_is_animation = False
1641 else:
1642 self.imageview.set_from_animation(self.currimg_pixbuf)
1643 self.previmage_is_animation = True
1644 # Clean up (free memory) because I'm lazy
1645 gc.collect()
1646 self.window.window.thaw_updates()
1647 self.loaded_img_in_list = self.curr_img_in_list
1648
1649 def show_scrollbars_if_needed(self):
1650 if self.currimg_width > self.available_image_width():
1651 self.hscroll.show()
1652 else:
1653 self.hscroll.hide()
1654 if self.currimg_height > self.available_image_height():
1655 self.vscroll.show()
1656 else:
1657 self.vscroll.hide()
1658
1659 def center_image(self):
1660 x_shift = int((self.available_image_width() - self.currimg_width) // 2)
1661 if x_shift < 0:
1662 x_shift = 0
1663 y_shift = int((self.available_image_height() - self.currimg_height) // 2)
1664 if y_shift < 0:
1665 y_shift = 0
1666 self.layout.move(self.imageview, x_shift, y_shift)
1667
1668 def available_image_width(self):
1669 width = self.window.get_size()[0]
1670 if not self.fullscreen_mode:
1671 if self.thumbpane_show:
1672 width -= self.thumbscroll.size_request()[0]
1673 return width
1674
1675 def available_image_height(self):
1676 height = self.window.get_size()[1]
1677 if not self.fullscreen_mode:
1678 height -= self.menubar.size_request()[1] # TODO
1679 if self.toolbar_show:
1680 height -= self.toolbar.size_request()[1]
1681 if self.statusbar_show:
1682 height -= self.statusbar.size_request()[1]
1683 return height
1684
1685 def save_image(self, action, parameter, data):
1686 if self.action_group.lookup_action("save-image").get_enabled():
1687 self.save_image_now(
1688 self.currimg_name,
1689 GdkPixbuf.Pixbuf.get_file_info(self.currimg_name)[0]["name"],
1690 )
1691
1692 def save_image_as(self, action, parameter, data):
1693 dialog = Gtk.FileChooserDialog(
1694 title=_("Save As"),
1695 action=Gtk.FileChooserAction.SAVE,
1696 buttons=(
1697 Gtk.STOCK_CANCEL,
1698 Gtk.ResponseType.CANCEL,
1699 Gtk.STOCK_SAVE,
1700 Gtk.ResponseType.OK,
1701 ),
1702 )
1703 dialog.set_default_response(Gtk.ResponseType.OK)
1704 filename = os.path.basename(self.currimg_name)
1705 filetype = None
1706 dialog.set_current_folder(os.path.dirname(self.currimg_name))
1707 dialog.set_current_name(filename)
1708 dialog.set_do_overwrite_confirmation(True)
1709 response = dialog.run()
1710 if response == Gtk.ResponseType.OK:
1711 prev_name = self.currimg_name
1712 filename = dialog.get_filename()
1713 dialog.destroy()
1714 fileext = os.path.splitext(os.path.basename(filename))[1].lower()
1715 if len(fileext) > 0:
1716 fileext = fileext[1:]
1717 # Override filetype if user typed a filename with a different extension:
1718 for i in GdkPixbuf.Pixbuf.get_formats():
1719 if fileext in i["extensions"]:
1720 filetype = i["name"]
1721 self.save_image_now(filename, filetype)
1722 self.register_file_with_recent_docs(filename)
1723 else:
1724 dialog.destroy()
1725
1726 def save_image_now(self, dest_name, filetype):
1727 try:
1728 self.change_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))
1729 while Gtk.events_pending():
1730 Gtk.main_iteration()
1731 if filetype == None:
1732 filetype = GdkPixbuf.Pixbuf.get_file_info(self.currimg_name)[0]["name"]
1733 if self.filetype_is_writable(filetype):
1734 self.currimg_pixbuf_original.save(
1735 dest_name, filetype, {"quality": str(self.quality_save)}
1736 )
1737 self.currimg_name = dest_name
1738 self.image_list[self.curr_img_in_list] = dest_name
1739 self.update_title()
1740 self.update_statusbar()
1741 # Update thumbnail:
1742 GObject.idle_add(
1743 self.thumbpane_set_image, dest_name, self.curr_img_in_list, True
1744 )
1745 self.image_modified = False
1746 else:
1747 error_dialog = Gtk.MessageDialog(
1748 self.window,
1749 Gtk.DialogFlags.MODAL,
1750 Gtk.MessageType.WARNING,
1751 Gtk.ButtonsType.YES_NO,
1752 _(
1753 "The %s format is not supported for saving. Do you wish to save the file in a different format?"
1754 )
1755 % filetype,
1756 )
1757 error_dialog.set_title(_("Save"))
1758 response = error_dialog.run()
1759 if response == Gtk.ResponseType.YES:
1760 error_dialog.destroy()
1761 while Gtk.events_pending():
1762 Gtk.main_iteration()
1763 self.save_image_as(None, None, None)
1764 else:
1765 error_dialog.destroy()
1766 except:
1767 error_dialog = Gtk.MessageDialog(
1768 self.window,
1769 Gtk.DialogFlags.MODAL,
1770 Gtk.MessageType.WARNING,
1771 Gtk.ButtonsType.CLOSE,
1772 _("Unable to save %s") % dest_name,
1773 )
1774 error_dialog.set_title(_("Save"))
1775 error_dialog.run()
1776 error_dialog.destroy()
1777 self.change_cursor(None)
1778
1779 def autosave_image(self):
1780 # Returns True if the user has canceled out of the dialog
1781 # Never call this function from an idle or timeout loop! That will cause
1782 # the app to freeze.
1783 if self.image_modified:
1784 if self.savemode == 1:
1785 action = self.action_group.lookup_action("save-image")
1786 temp = action.get_enabled()
1787 action.set_enabled(True)
1788 self.save_image(None, None, None)
1789 action.set_enabled(temp)
1790 elif self.savemode == 2:
1791 dialog = Gtk.MessageDialog(
1792 self.window,
1793 Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
1794 Gtk.MessageType.QUESTION,
1795 Gtk.ButtonsType.NONE,
1796 _("The current image has been modified. Save changes?"),
1797 )
1798 dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
1799 dialog.add_button(Gtk.STOCK_NO, Gtk.ResponseType.NO)
1800 dialog.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.YES)
1801 dialog.set_title(_("Save?"))
1802 dialog.set_default_response(Gtk.ResponseType.YES)
1803 response = dialog.run()
1804 dialog.destroy()
1805 if response == Gtk.ResponseType.YES:
1806 action = self.action_group.lookup_action("save-image")
1807 temp = action.get_enabled()
1808 action.set_enabled(True)
1809 self.save_image(None, None, None)
1810 action.set_enabled(temp)
1811 self.image_modified = False
1812 elif response == Gtk.ResponseType.NO:
1813 self.image_modified = False
1814 # Ensures that we don't use the current pixbuf for any preload pixbufs if we are in
1815 # the process of loading the previous or next image in the list:
1816 self.currimg_pixbuf = self.currimg_pixbuf_original
1817 self.preloadimg_next_in_list = -1
1818 self.preloadimg_prev_in_list = -1
1819 self.loaded_img_in_list = -1
1820 else:
1821 return True
1822
1823 def filetype_is_writable(self, filetype):
1824 # Determine if filetype is a writable format
1825 filetype_is_writable = True
1826 for i in GdkPixbuf.Pixbuf.get_formats():
1827 if filetype in i["extensions"]:
1828 if i["is_writable"]:
1829 return True
1830 return False
1831
1832 def open_file(self, action, parameter, data):
1833 self.stop_now = True
1834 while Gtk.events_pending():
1835 Gtk.main_iteration()
1836 self.open_file_or_folder(True)
1837
1838 def open_file_remote(self, action, parameter, data):
1839 # Prompt user for the url:
1840 dialog = Gtk.Dialog(
1841 _("Open Remote"),
1842 self.window,
1843 Gtk.DialogFlags.MODAL,
1844 buttons=(
1845 Gtk.STOCK_CANCEL,
1846 Gtk.ResponseType.CANCEL,
1847 Gtk.STOCK_OPEN,
1848 Gtk.ResponseType.OK,
1849 ),
1850 )
1851 location = Gtk.Entry()
1852 location.set_size_request(300, -1)
1853 location.set_activates_default(True)
1854 hbox = Gtk.HBox()
1855 hbox.pack_start(Gtk.Label(_("Image Location (URL):")), False, False, 5)
1856 hbox.pack_start(location, True, True, 5)
1857 dialog.vbox.pack_start(hbox, True, True, 10)
1858 dialog.set_default_response(Gtk.ResponseType.OK)
1859 dialog.vbox.show_all()
1860 dialog.connect("response", self.open_file_remote_response, location)
1861 response = dialog.show()
1862
1863 def open_file_remote_response(self, dialog, response, location):
1864 if response == Gtk.ResponseType.OK:
1865 filenames = []
1866 filenames.append(location.get_text())
1867 dialog.destroy()
1868 while Gtk.events_pending():
1869 Gtk.main_iteration()
1870 self.expand_filelist_and_load_image(filenames)
1871 else:
1872 dialog.destroy()
1873
1874 def open_folder(self, action, parameter, data):
1875 self.stop_now = True
1876 while Gtk.events_pending():
1877 Gtk.main_iteration()
1878 self.open_file_or_folder(False)
1879
1880 def open_file_or_folder(self, isfile):
1881 self.thumbpane_create_dir()
1882 cancel = self.autosave_image()
1883 if cancel:
1884 return
1885 # If isfile = True, file; If isfile = False, folder
1886 dialog = Gtk.FileChooserDialog(
1887 title=_("Open"),
1888 action=Gtk.FileChooserAction.OPEN,
1889 buttons=(
1890 Gtk.STOCK_CANCEL,
1891 Gtk.ResponseType.CANCEL,
1892 Gtk.STOCK_OPEN,
1893 Gtk.ResponseType.OK,
1894 ),
1895 )
1896 if isfile:
1897 filter = Gtk.FileFilter()
1898 filter.set_name(_("Images"))
1899 filter.add_pixbuf_formats()
1900 dialog.add_filter(filter)
1901 filter = Gtk.FileFilter()
1902 filter.set_name(_("All files"))
1903 filter.add_pattern("*")
1904 dialog.add_filter(filter)
1905 preview = Gtk.Image()
1906 dialog.set_preview_widget(preview)
1907 dialog.set_use_preview_label(False)
1908 dialog.connect("update-preview", self.update_preview, preview)
1909 recursivebutton = None
1910 else:
1911 dialog.set_action(Gtk.FileChooserAction.SELECT_FOLDER)
1912 recursivebutton = Gtk.CheckButton(
1913 label=_("Include images in subdirectories")
1914 )
1915 dialog.set_extra_widget(recursivebutton)
1916 dialog.set_default_response(Gtk.ResponseType.OK)
1917 dialog.set_select_multiple(True)
1918 if self.use_last_dir:
1919 if self.last_dir != None:
1920 dialog.set_current_folder(self.last_dir)
1921 else:
1922 if self.fixed_dir != None:
1923 dialog.set_current_folder(self.fixed_dir)
1924 dialog.connect(
1925 "response", self.open_file_or_folder_response, isfile, recursivebutton
1926 )
1927 response = dialog.show()
1928
1929 def open_file_or_folder_response(self, dialog, response, isfile, recursivebutton):
1930 if response == Gtk.ResponseType.OK:
1931 if self.use_last_dir:
1932 self.last_dir = dialog.get_current_folder()
1933 if not isfile and recursivebutton.get_property("active"):
1934 self.recursive = True
1935 filenames = dialog.get_filenames()
1936 dialog.destroy()
1937 while Gtk.events_pending():
1938 Gtk.main_iteration()
1939 self.expand_filelist_and_load_image(filenames)
1940 else:
1941 dialog.destroy()
1942
1943 def update_preview(self, file_chooser, preview):
1944 filename = file_chooser.get_preview_filename()
1945 if not filename:
1946 return
1947 filename, thumbfile = self.thumbnail_get_name(filename)
1948 pixbuf = self.thumbpane_get_pixbuf(thumbfile, filename, False)
1949 if pixbuf:
1950 preview.set_from_pixbuf(pixbuf)
1951 else:
1952 pixbuf = GdkPixbuf.Pixbuf(GdkPixbuf.Colorspace.RGB, 1, 8, 128, 128)
1953 pixbuf.fill(0x00000000)
1954 preview.set_from_pixbuf(pixbuf)
1955 have_preview = True
1956 file_chooser.set_preview_widget_active(have_preview)
1957 del pixbuf
1958 gc.collect()
1959
1960 def hide_cursor(self):
1961 if (
1962 self.fullscreen_mode
1963 and not self.user_prompt_visible
1964 and not self.slideshow_controls_visible
1965 ):
1966 invisible = Gdk.Cursor.new_for_display(
1967 self.window.get_window().get_display(), Gdk.CursorType.BLANK_CURSOR
1968 )
1969 self.change_cursor(invisible)
1970
1971 return False
1972
1973 def set_enter_fullscreen_visible(self, visible):
1974 self.add_remove_action("enter-fullscreen", self.enter_fullscreen, visible)
1975
1976 def set_leave_fullscreen_visible(self, visible):
1977 self.add_remove_action("leave-fullscreen", self.leave_fullscreen, visible)
1978
1979 def enter_fullscreen(self, action, parameter, data):
1980 if not self.fullscreen_mode:
1981 self.fullscreen_mode = True
1982 self.set_enter_fullscreen_visible(False)
1983 self.set_leave_fullscreen_visible(True)
1984 self.statusbar.hide()
1985 self.statusbar2.hide()
1986 self.toolbar.hide()
1987 self.window.set_show_menubar(False)
1988 self.thumbscroll.hide()
1989 self.thumbpane.hide()
1990 self.window.fullscreen()
1991 self.timer_id = GObject.timeout_add(2000, self.hide_cursor)
1992 self.set_slideshow_sensitivities()
1993 if self.simple_bgcolor:
1994 self.layout.modify_bg(Gtk.StateType.NORMAL, self.bgcolor)
1995 else:
1996 if self.simple_bgcolor:
1997 self.layout.modify_bg(Gtk.StateType.NORMAL, None)
1998 self.leave_fullscreen(action)
1999
2000 def leave_fullscreen(self, action, parameter, data):
2001 if self.fullscreen_mode:
2002 self.slideshow_controls_visible = False
2003 self.fullscreen_controls.hide()
2004 self.fullscreen_mode = False
2005 self.set_enter_fullscreen_visible(True)
2006 self.set_leave_fullscreen_visible(False)
2007 if self.toolbar_show:
2008 self.toolbar.show()
2009 self.window.set_show_menubar(True)
2010 if self.statusbar_show:
2011 self.statusbar.show()
2012 self.statusbar2.show()
2013 if self.thumbpane_show:
2014 self.thumbscroll.show()
2015 self.thumbpane.show()
2016 self.thumbpane_update_images(False, self.curr_img_in_list)
2017 self.window.unfullscreen()
2018 self.change_cursor(None)
2019 self.set_slideshow_sensitivities()
2020 if self.simple_bgcolor:
2021 self.layout.modify_bg(Gtk.StateType.NORMAL, None)
2022
2023 def toggle_status_bar(self, action, value, data):
2024 action.set_state(value)
2025 if not value.get_boolean():
2026 self.statusbar.hide()
2027 self.statusbar2.hide()
2028 self.statusbar_show = False
2029 else:
2030 self.statusbar.show()
2031 self.statusbar2.show()
2032 self.statusbar_show = True
2033 if self.image_loaded and self.last_image_action_was_fit:
2034 if self.last_image_action_was_smart_fit:
2035 self.zoom_to_fit_or_1_to_1(None, False, False)
2036 else:
2037 self.zoom_to_fit_window(None, False, False)
2038
2039 def toggle_thumbpane(self, action, value, data):
2040 action.set_state(value)
2041 if not value.get_boolean():
2042 self.thumbscroll.hide()
2043 self.thumbpane.hide()
2044 self.thumbpane_show = False
2045 else:
2046 self.thumbscroll.show()
2047 self.thumbpane.show()
2048 self.thumbpane_show = True
2049 self.stop_now = False
2050 GObject.idle_add(self.thumbpane_update_images, True, self.curr_img_in_list)
2051 if self.image_loaded and self.last_image_action_was_fit:
2052 if self.last_image_action_was_smart_fit:
2053 self.zoom_to_fit_or_1_to_1(None, False, False)
2054 else:
2055 self.zoom_to_fit_window(None, False, False)
2056
2057 def toggle_toolbar(self, action, value, data):
2058 action.set_state(value)
2059 if not value.get_boolean():
2060 self.toolbar.hide()
2061 self.toolbar_show = False
2062 else:
2063 self.toolbar.show()
2064 self.toolbar_show = True
2065 if self.image_loaded and self.last_image_action_was_fit:
2066 if self.last_image_action_was_smart_fit:
2067 self.zoom_to_fit_or_1_to_1(None, False, False)
2068 else:
2069 self.zoom_to_fit_window(None, False, False)
2070
2071 def update_statusbar(self):
2072 # Update status bar:
2073 try:
2074 st = os.stat(self.currimg_name)
2075 filesize = st[stat.ST_SIZE] // 1000
2076 ratio = int(100 * self.currimg_zoomratio)
2077 status_text = (
2078 os.path.basename(self.currimg_name)
2079 + ": "
2080 + str(self.currimg_pixbuf_original.get_width())
2081 + "x"
2082 + str(self.currimg_pixbuf_original.get_height())
2083 + " "
2084 + str(filesize)
2085 + "KB "
2086 + str(ratio)
2087 + "% "
2088 )
2089 except:
2090 status_text = _("Cannot load image.")
2091 self.statusbar.push(self.statusbar.get_context_id(""), status_text)
2092 status_text = ""
2093 if self.running_custom_actions:
2094 status_text = _("Custom actions: %(current)i of %(total)i") % {
2095 "current": self.curr_custom_action,
2096 "total": self.num_custom_actions,
2097 }
2098 elif self.searching_for_images:
2099 status_text = _("Scanning...")
2100 self.statusbar2.push(self.statusbar2.get_context_id(""), status_text)
2101
2102 def show_custom_actions(self, action, parameter, data):
2103 self.actions_dialog = Gtk.Dialog(
2104 title=_("Configure Custom Actions"), parent=self.window
2105 )
2106 self.actions_dialog.set_has_separator(False)
2107 self.actions_dialog.set_resizable(False)
2108 table_actions = Gtk.Table(13, 2, False)
2109 table_actions.attach(
2110 Gtk.Label(),
2111 1,
2112 2,
2113 1,
2114 2,
2115 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2116 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2117 0,
2118 0,
2119 )
2120 actionscrollwindow = Gtk.ScrolledWindow()
2121 self.actionstore = Gtk.ListStore(str, str, str)
2122 self.actionwidget = Gtk.TreeView()
2123 self.actionwidget.set_enable_search(False)
2124 self.actionwidget.set_rules_hint(True)
2125 self.actionwidget.connect("row-activated", self.edit_custom_action2)
2126 actionscrollwindow.add(self.actionwidget)
2127 actionscrollwindow.set_shadow_type(Gtk.ShadowType.IN)
2128 actionscrollwindow.set_policy(
2129 Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC
2130 )
2131 actionscrollwindow.set_size_request(500, 200)
2132 self.actionwidget.set_model(self.actionstore)
2133 self.cell = Gtk.CellRendererText()
2134 self.cellbool = Gtk.CellRendererPixbuf()
2135 self.tvcolumn0 = Gtk.TreeViewColumn(_("Batch"))
2136 self.tvcolumn1 = Gtk.TreeViewColumn(_("Action"), self.cell, markup=0)
2137 self.tvcolumn2 = Gtk.TreeViewColumn(_("Shortcut"))
2138 self.tvcolumn1.set_max_width(
2139 self.actionwidget.size_request()[0]
2140 - self.tvcolumn0.get_width()
2141 - self.tvcolumn2.get_width()
2142 )
2143 self.actionwidget.append_column(self.tvcolumn0)
2144 self.actionwidget.append_column(self.tvcolumn1)
2145 self.actionwidget.append_column(self.tvcolumn2)
2146 self.populate_treeview()
2147 if len(self.action_names) > 0:
2148 self.actionwidget.get_selection().select_path(0)
2149 vbox_actions = Gtk.VBox()
2150 addbutton = Gtk.Button("", Gtk.STOCK_ADD)
2151 addbutton.get_child().get_child().get_children()[1].set_text("")
2152 addbutton.connect("clicked", self.add_custom_action, self.actionwidget)
2153 addbutton.set_tooltip_text(_("Add action"))
2154 editbutton = Gtk.Button("", Gtk.STOCK_EDIT)
2155 editbutton.get_child().get_child().get_children()[1].set_text("")
2156 editbutton.connect("clicked", self.edit_custom_action, self.actionwidget)
2157 editbutton.set_tooltip_text(_("Edit selected action."))
2158 removebutton = Gtk.Button("", Gtk.STOCK_REMOVE)
2159 removebutton.get_child().get_child().get_children()[1].set_text("")
2160 removebutton.connect("clicked", self.remove_custom_action)
2161 removebutton.set_tooltip_text(_("Remove selected action."))
2162 upbutton = Gtk.Button("", Gtk.STOCK_GO_UP)
2163 upbutton.get_child().get_child().get_children()[1].set_text("")
2164 upbutton.connect("clicked", self.custom_action_move_up, self.actionwidget)
2165 upbutton.set_tooltip_text(_("Move selected action up."))
2166 downbutton = Gtk.Button("", Gtk.STOCK_GO_DOWN)
2167 downbutton.get_child().get_child().get_children()[1].set_text("")
2168 downbutton.connect("clicked", self.custom_action_move_down, self.actionwidget)
2169 downbutton.set_tooltip_text(_("Move selected action down."))
2170 vbox_buttons = Gtk.VBox()
2171 propertyinfo = Gtk.Label()
2172 propertyinfo.set_markup(
2173 "<small>"
2174 + _("Parameters")
2175 + ':\n<span font_family="Monospace">%F</span> - '
2176 + _("File path, name, and extension")
2177 + '\n<span font_family="Monospace">%P</span> - '
2178 + _("File path")
2179 + '\n<span font_family="Monospace">%N</span> - '
2180 + _("File name without file extension")
2181 + '\n<span font_family="Monospace">%E</span> - '
2182 + _('File extension (i.e. ".png")')
2183 + '\n<span font_family="Monospace">%L</span> - '
2184 + _("List of files, space-separated")
2185 + "</small>"
2186 )
2187 propertyinfo.set_alignment(0, 0)
2188 actioninfo = Gtk.Label()
2189 actioninfo.set_markup(
2190 "<small>"
2191 + _("Operations")
2192 + ':\n<span font_family="Monospace">[NEXT]</span> - '
2193 + _("Go to next image")
2194 + '\n<span font_family="Monospace">[PREV]</span> - '
2195 + _("Go to previous image")
2196 + "</small>"
2197 )
2198 actioninfo.set_alignment(0, 0)
2199 hbox_info = Gtk.HBox()
2200 hbox_info.pack_start(propertyinfo, False, False, 15)
2201 hbox_info.pack_start(actioninfo, False, False, 15)
2202 vbox_buttons.pack_start(addbutton, False, False, 5)
2203 vbox_buttons.pack_start(editbutton, False, False, 5)
2204 vbox_buttons.pack_start(removebutton, False, False, 5)
2205 vbox_buttons.pack_start(upbutton, False, False, 5)
2206 vbox_buttons.pack_start(downbutton, False, False, 0)
2207 hbox_top = Gtk.HBox()
2208 hbox_top.pack_start(actionscrollwindow, True, True, 5)
2209 hbox_top.pack_start(vbox_buttons, False, False, 5)
2210 vbox_actions.pack_start(hbox_top, True, True, 5)
2211 vbox_actions.pack_start(hbox_info, False, False, 5)
2212 hbox_instructions = Gtk.HBox()
2213 info_image = Gtk.Image()
2214 info_image.set_from_stock(Gtk.STOCK_DIALOG_INFO, Gtk.IconSize.BUTTON)
2215 hbox_instructions.pack_start(info_image, False, False, 5)
2216 instructions = Gtk.Label(
2217 label=_(
2218 "Here you can define custom actions with shortcuts. Actions use the built-in parameters and operations listed below and can have multiple statements separated by a semicolon. Batch actions apply to all images in the list."
2219 )
2220 )
2221 instructions.set_line_wrap(True)
2222 instructions.set_alignment(0, 0.5)
2223 hbox_instructions.pack_start(instructions, False, False, 5)
2224 table_actions.attach(
2225 hbox_instructions,
2226 1,
2227 3,
2228 2,
2229 3,
2230 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2231 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2232 5,
2233 0,
2234 )
2235 table_actions.attach(
2236 Gtk.Label(),
2237 1,
2238 3,
2239 3,
2240 4,
2241 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2242 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2243 15,
2244 0,
2245 )
2246 table_actions.attach(
2247 vbox_actions,
2248 1,
2249 3,
2250 4,
2251 12,
2252 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2253 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2254 15,
2255 0,
2256 )
2257 table_actions.attach(
2258 Gtk.Label(),
2259 1,
2260 3,
2261 12,
2262 13,
2263 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2264 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2265 15,
2266 0,
2267 )
2268 self.actions_dialog.vbox.pack_start(table_actions, False, False, 0)
2269 # Show dialog:
2270 self.actions_dialog.vbox.show_all()
2271 instructions.set_size_request(self.actions_dialog.size_request()[0] - 50, -1)
2272 close_button = self.actions_dialog.add_button(
2273 Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE
2274 )
2275 close_button.grab_focus()
2276 self.actions_dialog.run()
2277 self.refresh_custom_actions_menu()
2278 while Gtk.events_pending():
2279 Gtk.main_iteration()
2280 if len(self.image_list) == 0:
2281 self.set_image_sensitivities(False)
2282 self.actions_dialog.destroy()
2283
2284 def add_custom_action(self, button, treeview):
2285 self.open_custom_action_dialog(True, "", "", "None", False, treeview)
2286
2287 def edit_custom_action2(self, treeview, path, view_column):
2288 self.edit_custom_action(None, treeview)
2289
2290 def edit_custom_action(self, button, treeview):
2291 (model, iter) = self.actionwidget.get_selection().get_selected()
2292 if iter != None:
2293 (row,) = self.actionstore.get_path(iter)
2294 self.open_custom_action_dialog(
2295 False,
2296 self.action_names[row],
2297 self.action_commands[row],
2298 self.action_shortcuts[row],
2299 self.action_batch[row],
2300 treeview,
2301 )
2302
2303 def open_custom_action_dialog(
2304 self, add_call, name, command, shortcut, batch, treeview
2305 ):
2306 if add_call:
2307 self.dialog_name = Gtk.Dialog(
2308 _("Add Custom Action"),
2309 self.actions_dialog,
2310 Gtk.DialogFlags.MODAL,
2311 (
2312 Gtk.STOCK_CANCEL,
2313 Gtk.ResponseType.REJECT,
2314 Gtk.STOCK_OK,
2315 Gtk.ResponseType.ACCEPT,
2316 ),
2317 )
2318 else:
2319 self.dialog_name = Gtk.Dialog(
2320 _("Edit Custom Action"),
2321 self.actions_dialog,
2322 Gtk.DialogFlags.MODAL,
2323 (
2324 Gtk.STOCK_CANCEL,
2325 Gtk.ResponseType.REJECT,
2326 Gtk.STOCK_OK,
2327 Gtk.ResponseType.ACCEPT,
2328 ),
2329 )
2330 self.dialog_name.set_modal(True)
2331 table = Gtk.Table(2, 4, False)
2332 action_name_label = Gtk.Label(label=_("Action Name:"))
2333 action_name_label.set_alignment(0, 0.5)
2334 action_command_label = Gtk.Label(label=_("Command:"))
2335 action_command_label.set_alignment(0, 0.5)
2336 shortcut_label = Gtk.Label(label=_("Shortcut:"))
2337 shortcut_label.set_alignment(0, 0.5)
2338 table.attach(
2339 action_name_label,
2340 0,
2341 1,
2342 0,
2343 1,
2344 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2345 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2346 15,
2347 0,
2348 )
2349 table.attach(
2350 action_command_label,
2351 0,
2352 1,
2353 1,
2354 2,
2355 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2356 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2357 15,
2358 0,
2359 )
2360 table.attach(
2361 shortcut_label,
2362 0,
2363 1,
2364 2,
2365 3,
2366 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2367 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2368 15,
2369 0,
2370 )
2371 action_name = Gtk.Entry()
2372 action_name.set_text(name)
2373 action_command = Gtk.Entry()
2374 action_command.set_text(command)
2375 table.attach(
2376 action_name,
2377 1,
2378 2,
2379 0,
2380 1,
2381 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2382 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2383 15,
2384 0,
2385 )
2386 table.attach(
2387 action_command,
2388 1,
2389 2,
2390 1,
2391 2,
2392 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2393 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2394 15,
2395 0,
2396 )
2397 self.shortcut = Gtk.Button(shortcut)
2398 self.shortcut.connect("clicked", self.shortcut_clicked)
2399 table.attach(
2400 self.shortcut,
2401 1,
2402 2,
2403 2,
2404 3,
2405 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2406 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2407 15,
2408 0,
2409 )
2410 batchmode = Gtk.CheckButton(_("Perform action on all images (Batch)"))
2411 batchmode.set_active(batch)
2412 table.attach(
2413 batchmode,
2414 0,
2415 2,
2416 3,
2417 4,
2418 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2419 Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
2420 15,
2421 0,
2422 )
2423 self.dialog_name.vbox.pack_start(table, False, False, 5)
2424 self.dialog_name.vbox.show_all()
2425 self.dialog_name.connect(
2426 "response",
2427 self.dialog_name_response,
2428 add_call,
2429 action_name,
2430 action_command,
2431 self.shortcut,
2432 batchmode,
2433 treeview,
2434 )
2435 self.dialog_name.run()
2436
2437 def dialog_name_response(
2438 self,
2439 dialog,
2440 response,
2441 add_call,
2442 action_name,
2443 action_command,
2444 shortcut,
2445 batchmode,
2446 treeview,
2447 ):
2448 if response == Gtk.ResponseType.ACCEPT:
2449 if not (
2450 action_command.get_text() == ""
2451 or action_name.get_text() == ""
2452 or self.shortcut.get_label() == "None"
2453 ):
2454 name = action_name.get_text()
2455 command = action_command.get_text()
2456 if (
2457 ("[NEXT]" in command.strip()) and command.strip()[-6:] != "[NEXT]"
2458 ) or (
2459 ("[PREV]" in command.strip()) and command.strip()[-6:] != "[PREV]"
2460 ):
2461 error_dialog = Gtk.MessageDialog(
2462 self.actions_dialog,
2463 Gtk.DialogFlags.MODAL,
2464 Gtk.MessageType.WARNING,
2465 Gtk.ButtonsType.CLOSE,
2466 _(
2467 "[PREV] and [NEXT] are only valid alone or at the end of the command"
2468 ),
2469 )
2470 error_dialog.set_title(_("Invalid Custom Action"))
2471 error_dialog.run()
2472 error_dialog.destroy()
2473 return
2474 shortcut = shortcut.get_label()
2475 batch = batchmode.get_active()
2476 dialog.destroy()
2477 if add_call:
2478 self.action_names.append(name)
2479 self.action_hashes.append(
2480 hashlib.sha1(name.encode("utf8")).hexdigest()
2481 )
2482 self.action_commands.append(command)
2483 self.action_shortcuts.append(shortcut)
2484 self.action_batch.append(batch)
2485 else:
2486 (model, iter) = self.actionwidget.get_selection().get_selected()
2487 (rownum,) = self.actionstore.get_path(iter)
2488 self.action_names[rownum] = name
2489 self.action_hashes[rownum] = hashlib.sha1(
2490 name.encode("utf8")
2491 ).hexdigest()
2492 self.action_commands[rownum] = command
2493 self.action_shortcuts[rownum] = shortcut
2494 self.action_batch[rownum] = batch
2495 self.populate_treeview()
2496 if add_call:
2497 rownum = len(self.action_names) - 1
2498 treeview.get_selection().select_path(rownum)
2499 while Gtk.events_pending():
2500 Gtk.main_iteration()
2501 # Keep item in visible rect:
2502 visible_rect = treeview.get_visible_rect()
2503 row_rect = treeview.get_background_area(rownum, self.tvcolumn1)
2504 if row_rect.y + row_rect.height > visible_rect.height:
2505 top_coord = (
2506 row_rect.y + row_rect.height - visible_rect.height
2507 ) + visible_rect.y
2508 treeview.scroll_to_point(-1, top_coord)
2509 elif row_rect.y < 0:
2510 treeview.scroll_to_cell(rownum)
2511 else:
2512 error_dialog = Gtk.MessageDialog(
2513 self.actions_dialog,
2514 Gtk.DialogFlags.MODAL,
2515 Gtk.MessageType.WARNING,
2516 Gtk.ButtonsType.CLOSE,
2517 _("Incomplete custom action specified."),
2518 )
2519 error_dialog.set_title(_("Invalid Custom Action"))
2520 error_dialog.run()
2521 error_dialog.destroy()
2522 else:
2523 dialog.destroy()
2524
2525 def custom_action_move_down(self, button, treeview):
2526 iter = None
2527 selection = treeview.get_selection()
2528 model, iter = selection.get_selected()
2529 if iter:
2530 rownum = int(model.get_string_from_iter(iter))
2531 if rownum < len(self.action_names) - 1:
2532 # Move item down:
2533 temp_name = self.action_names[rownum]
2534 temp_hash = self.action_hashes[rownum]
2535 temp_shortcut = self.action_shortcuts[rownum]
2536 temp_command = self.action_commands[rownum]
2537 temp_batch = self.action_batch[rownum]
2538 self.action_names[rownum] = self.action_names[rownum + 1]
2539 self.action_hashes[rownum] = self.action_hashes[rownum + 1]
2540 self.action_shortcuts[rownum] = self.action_shortcuts[rownum + 1]
2541 self.action_commands[rownum] = self.action_commands[rownum + 1]
2542 self.action_batch[rownum] = self.action_batch[rownum + 1]
2543 self.action_names[rownum + 1] = temp_name
2544 self.action_hashes[rownum + 1] = temp_hash
2545 self.action_shortcuts[rownum + 1] = temp_shortcut
2546 self.action_commands[rownum + 1] = temp_command
2547 self.action_batch[rownum + 1] = temp_batch
2548 # Repopulate treeview and keep item selected:
2549 self.populate_treeview()
2550 selection.select_path((rownum + 1,))
2551 while Gtk.events_pending():
2552 Gtk.main_iteration()
2553 # Keep item in visible rect:
2554 rownum = rownum + 1
2555 visible_rect = treeview.get_visible_rect()
2556 row_rect = treeview.get_background_area(rownum, self.tvcolumn1)
2557 if row_rect.y + row_rect.height > visible_rect.height:
2558 top_coord = (
2559 row_rect.y + row_rect.height - visible_rect.height
2560 ) + visible_rect.y
2561 treeview.scroll_to_point(-1, top_coord)
2562 elif row_rect.y < 0:
2563 treeview.scroll_to_cell(rownum)
2564
2565 def custom_action_move_up(self, button, treeview):
2566 iter = None
2567 selection = treeview.get_selection()
2568 model, iter = selection.get_selected()
2569 if iter:
2570 rownum = int(model.get_string_from_iter(iter))
2571 if rownum > 0:
2572 # Move item down:
2573 temp_name = self.action_names[rownum]
2574 temp_hash = self.action_hashes[rownum]
2575 temp_shortcut = self.action_shortcuts[rownum]
2576 temp_command = self.action_commands[rownum]
2577 temp_batch = self.action_batch[rownum]
2578 self.action_names[rownum] = self.action_names[rownum - 1]
2579 self.action_hashes[rownum] = self.action_hashes[rownum - 1]
2580 self.action_shortcuts[rownum] = self.action_shortcuts[rownum - 1]
2581 self.action_commands[rownum] = self.action_commands[rownum - 1]
2582 self.action_batch[rownum] = self.action_batch[rownum - 1]
2583 self.action_names[rownum - 1] = temp_name
2584 self.action_hashes[rownum - 1] = temp_hash
2585 self.action_shortcuts[rownum - 1] = temp_shortcut
2586 self.action_commands[rownum - 1] = temp_command
2587 self.action_batch[rownum - 1] = temp_batch
2588 # Repopulate treeview and keep item selected:
2589 self.populate_treeview()
2590 selection.select_path((rownum - 1,))
2591 while Gtk.events_pending():
2592 Gtk.main_iteration()
2593 # Keep item in visible rect:
2594 rownum = rownum - 1
2595 visible_rect = treeview.get_visible_rect()
2596 row_rect = treeview.get_background_area(rownum, self.tvcolumn1)
2597 if row_rect.y + row_rect.height > visible_rect.height:
2598 top_coord = (
2599 row_rect.y + row_rect.height - visible_rect.height
2600 ) + visible_rect.y
2601 treeview.scroll_to_point(-1, top_coord)
2602 elif row_rect.y < 0:
2603 treeview.scroll_to_cell(rownum)
2604
2605 def shortcut_clicked(self, widget):
2606 self.dialog_shortcut = Gtk.Dialog(
2607 _("Action Shortcut"),
2608 self.dialog_name,
2609 Gtk.DialogFlags.MODAL,
2610 (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT),
2611 )
2612 self.shortcut_label = Gtk.Label(
2613 label=_("Press the desired shortcut for the action.")
2614 )
2615 hbox = Gtk.HBox()
2616 hbox.pack_start(self.shortcut_label, False, False, 15)
2617 self.dialog_shortcut.vbox.pack_start(hbox, False, False, 5)
2618 self.dialog_shortcut.vbox.show_all()
2619 self.dialog_shortcut.connect("key-press-event", self.shortcut_keypress)
2620 self.dialog_shortcut.run()
2621 self.dialog_shortcut.destroy()
2622
2623 def shortcut_keypress(self, widget, event):
2624 shortcut = Gtk.accelerator_name(event.keyval, event.get_state())
2625 if "<Mod2>" in shortcut:
2626 shortcut = shortcut.replace("<Mod2>", "")
2627 if (
2628 shortcut[(len(shortcut) - 2) : len(shortcut)] != "_L"
2629 and shortcut[(len(shortcut) - 2) : len(shortcut)] != "_R"
2630 ):
2631 # Validate to make sure the shortcut hasn't already been used:
2632 for i in range(len(self.keys)):
2633 if shortcut == self.keys[i][1]:
2634 error_dialog = Gtk.MessageDialog(
2635 self.dialog_shortcut,
2636 Gtk.DialogFlags.MODAL,
2637 Gtk.MessageType.WARNING,
2638 Gtk.ButtonsType.CLOSE,
2639 _("The shortcut '%(shortcut)s' is already used for '%(key)s'.")
2640 % {"shortcut": shortcut, "key": self.keys[i][0]},
2641 )
2642 error_dialog.set_title(_("Invalid Shortcut"))
2643 error_dialog.run()
2644 error_dialog.destroy()
2645 return
2646 for i in range(len(self.action_shortcuts)):
2647 if shortcut == self.action_shortcuts[i]:
2648 error_dialog = Gtk.MessageDialog(
2649 self.dialog_shortcut,
2650 Gtk.DialogFlags.MODAL,
2651 Gtk.MessageType.WARNING,
2652 Gtk.ButtonsType.CLOSE,
2653 _("The shortcut '%(shortcut)s' is already used for '%(key)s'.")
2654 % {"shortcut": shortcut, "key": self.action_names[i]},
2655 )
2656 error_dialog.set_title(_("Invalid Shortcut"))
2657 error_dialog.run()
2658 error_dialog.destroy()
2659 return
2660 self.shortcut.set_label(shortcut)
2661 widget.destroy()
2662
2663 def remove_custom_action(self, button):
2664 (model, iter) = self.actionwidget.get_selection().get_selected()
2665 if iter != None:
2666 (row,) = self.actionstore.get_path(iter)
2667 self.action_names.pop(row)
2668 self.action_hashes.pop(row)
2669 self.action_shortcuts.pop(row)
2670 self.action_commands.pop(row)
2671 self.action_batch.pop(row)
2672 self.populate_treeview()
2673 self.actionwidget.grab_focus()
2674
2675 def populate_treeview(self):
2676 self.actionstore.clear()
2677 for i in range(len(self.action_names)):
2678 if self.action_batch[i]:
2679 pb = Gtk.STOCK_APPLY
2680 else:
2681 pb = None
2682 self.actionstore.append(
2683 [
2684 pb,
2685 "<big><b>"
2686 + self.action_names[i].replace("&", "&amp;")
2687 + "</b></big>\n<small>"
2688 + self.action_commands[i].replace("&", "&amp;")
2689 + "</small>",
2690 self.action_shortcuts[i],
2691 ]
2692 )
2693 self.tvcolumn0.clear()
2694 self.tvcolumn1.clear()
2695 self.tvcolumn2.clear()
2696 self.tvcolumn0.pack_start(self.cellbool, True, True, 0)
2697 self.tvcolumn1.pack_start(self.cell, True, True, 0)
2698 self.tvcolumn2.pack_start(self.cell, True, True, 0)
2699 self.tvcolumn0.add_attribute(self.cellbool, "stock-id", 0)
2700 self.tvcolumn1.set_attributes(self.cell, markup=1)
2701 self.tvcolumn2.set_attributes(self.cell, text=2)
2702 self.tvcolumn1.set_expand(True)
2703
2704 def screenshot(self, action, parameter, data):
2705 cancel = self.autosave_image()
2706 if cancel:
2707 return
2708 # Dialog:
2709 dialog = Gtk.Dialog(
2710 _("Screenshot"),
2711 self.window,
2712 Gtk.DialogFlags.MODAL,
2713 (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT),