fix regression
[hband-tools.git] / xgui-tools / perspect
blob7d071e0bc4d9da5f50d4f43f0cfcbfc276f4b178
1 #!/usr/bin/env python2.7
2 # -*- coding: utf-8 -*-
4 import sys
5 import os
6 import gtk
7 import subprocess
8 import tempfile
9 import glib
10 import math
11 from shutil import copyfile
12 import re
13 import traceback
14 try: import better_exchook
15 except ImportError: pass
17 def add_key_binding(widget, keyname, callback):
18         accelgroup = gtk.AccelGroup()
19         key, modifier = gtk.accelerator_parse(keyname)
20         accelgroup.connect_group(key, modifier, gtk.ACCEL_VISIBLE, callback)
21         widget.add_accel_group(accelgroup)
23 class EventImage(gtk.EventBox):
24         def __init__(self):
25                 super(self.__class__, self).__init__()
26                 self.image = gtk.Image()
27                 self.image.set_alignment(0, 0)
28                 self.add(self.image)
29         def clear(self):
30                 return self.image.clear()
31         def set_from_pixbuf(self, *args):
32                 return self.image.set_from_pixbuf(*args)
33         def set_from_file(self, *args):
34                 return self.image.set_from_file(*args)
35         def set_from_file_at_size(self, path, w, h):
36                 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(path, w, h)
37                 self.image.set_from_pixbuf(pixbuf)
38         def set_size_request(self, *args):
39                 return self.image.set_size_request(*args)
40         @property
41         def size(self):
42                 pb = self.image.get_pixbuf()
43                 return pb.get_width(), pb.get_height()
44         @property
45         def width(self):
46                 return self.size[0]
47         @property
48         def height(self):
49                 return self.size[1]
50         @property
51         def pixbuf(self):
52                 return self.image.get_pixbuf()
53         @pixbuf.setter
54         def pixbuf(self, pb):
55                 self.image.set_from_pixbuf(pb)
56         def redraw(self):
57                 self.pixbuf = self.pixbuf
59 class StockButton(gtk.Button):
60         def __init__(self, label=None, stock=None, use_underline=True, icon_size=None):
61                 if stock is not None and stock in gtk.stock_list_ids():
62                         stock_tmp = stock
63                 else:
64                         stock_tmp = gtk.STOCK_ABOUT
65                 super(self.__class__, self).__init__(stock=stock_tmp, use_underline=use_underline)
66                 if label is not None:
67                         self.set_markup(label)
68                 if stock is None:
69                         self.set_icon('')
70                 elif stock not in gtk.stock_list_ids():
71                         self.set_icon(stock)
72                 if icon_size is not None:
73                         self.set_icon(stock, icon_size)
74         def __get_children(self):
75                 align = self.get_children()[0]
76                 hbox = align.get_children()[0]
77                 return hbox.get_children()
78         def set_label(self, label):
79                 x, lbl = self.__get_children()
80                 lbl.set_label(label)
81         def set_markup(self, label):
82                 x, lbl = self.__get_children()
83                 lbl.set_markup(label)
84         def set_icon(self, icon, size=gtk.ICON_SIZE_BUTTON):
85                 img, x = self.__get_children()
86                 if type(icon) == str:
87                         if icon == '':
88                                 img.props.visible = False
89                         else:
90                                 img.set_from_icon_name(icon, size)
91                                 img.props.visible = True
92                 else:
93                         img.set_from_pixbuf(icon)
94                         img.props.visible = True
96 class Coordinate(object):
97         def __init__(self, x, y=None):
98                 if type(x) == int:
99                         assert type(y) == int
100                         self.x = x
101                         self.y = y
102                 else:
103                         assert y is None
104                         x, y = x.split(',')
105                         self.x = int(x)
106                         self.y = int(y)
107         def __str__(self):
108                 return '%d,%d' % (self.x, self.y)
109         def __repr__(self):
110                 return '<%d,%d>' % (self.x, self.y)
112 def on_clicked(widget, event):
113         x = int(min(max(0, event.x), image.width))
114         y = int(min(max(0, event.y), image.height))
115         
116         if event.button == 1:
117                 if all([c.x!=x and c.y!=y for c in coordinates]):
118                         if len(coordinates) >= 4:
119                                 remove_closest_point(x, y)
120                         coordinates.append(Coordinate(x, y))
121         elif event.button == 3:
122                 remove_closest_point(x, y)
123         refresh_display()
124         update_statusline()
126 def remove_closest_point(x, y):
127         if len(coordinates) > 0:
128                 distances = map(lambda c: {'d': (abs(c.x - x)**2 + abs(c.y - y)**2)**0.5, 'p': c}, coordinates)
129                 closest = sorted(distances, key=lambda x: x['d'])[0]['p']
130                 coordinates.remove(closest)
132 def update_statusline():
133         topleft, bottomleft, topright, bottomright = get_quadrangle_points()
134         #text = '%d:%d; %d:%d; %d:%d; %d:%d' % (topleft.x, topleft.y, topright.x, topright.y, bottomright.x, bottomright.y, bottomleft.x, bottomleft.y)
135         text = '<span color="red">⸬</span> = '
136         text += ' '.join(map(lambda c: "(%d,%d)"%(c.x, c.y), filter(lambda c: c.x >= 0, [topleft, topright, bottomright, bottomleft])))
137         if len(coordinates) > 1:
138                 leftmost, topmost, rightmost, bottommost = get_cirumrectangle()
139                 text += '  <span color="green">▭</span> = %dx%d+%d+%d' % (rightmost-leftmost, bottommost-topmost, leftmost, topmost)
140                 
141                 vangle, hangle = get_linesegment_angle(coordinates[0], coordinates[1])
142                 text += '  ∡ = %0.2f° ⟂ %0.2f°' % (hangle, vangle)
143         
144         statusline.set_markup(text)
146 def refresh_display():
147         image.redraw()
148         glib.idle_add(draw_cages, priority=glib.PRIORITY_DEFAULT_IDLE)
150 def draw_cages():
151         if glob.show_quadrangle:
152                 draw_quadrangle_cage()
153         if glob.show_circumrectangle:
154                 draw_circumrectangle_cage()
156 def draw_quadrangle_cage():
157         topleft, bottomleft, topright, bottomright = get_quadrangle_points()
158         real_points = filter(lambda c: c.x >= 0, [topleft, topright, bottomright, bottomleft])
159         if len(real_points) > 0:
160                 drawable = image.window
161                 gc = gtk.gdk.GC(drawable)
162                 gc.set_rgb_fg_color(gtk.gdk.color_parse('red'))
163                 drawable.draw_lines(gc, [(c.x, c.y) for c in make_polygon(real_points)])
165 def draw_circumrectangle_cage():
166         if len(coordinates) > 1:
167                 leftmost, topmost, rightmost, bottommost = get_cirumrectangle()
168                 drawable = image.window
169                 gc = gtk.gdk.GC(drawable)
170                 gc.set_rgb_fg_color(gtk.gdk.color_parse('green'))
171                 drawable.draw_lines(gc, [(leftmost, topmost), (rightmost, topmost), (rightmost, bottommost), (leftmost, bottommost), (leftmost, topmost)])
173 def make_polygon(points):
174         for i in range(len(points)):
175                 yield points[i-1]
176                 yield points[i]
178 def get_quadrangle_points():
179         points_left_to_right = sorted(coordinates, cmp=lambda a, b: cmp(a.x, b.x))
180         while len(points_left_to_right) < 4: points_left_to_right.append(Coordinate(-1, -1))
181         src_left_points  = points_left_to_right[0:2]
182         src_right_points = points_left_to_right[-2:]
183         src_topleft     = sorted(src_left_points,  cmp=lambda a, b: cmp(a.y, b.y))[0]
184         src_bottomleft  = sorted(src_left_points,  cmp=lambda a, b: cmp(a.y, b.y))[1]
185         src_topright    = sorted(src_right_points, cmp=lambda a, b: cmp(a.y, b.y))[0]
186         src_bottomright = sorted(src_right_points, cmp=lambda a, b: cmp(a.y, b.y))[1]
187         return src_topleft, src_bottomleft, src_topright, src_bottomright
189 def get_cirumrectangle():
190         leftmost   = sorted(coordinates, cmp=lambda a, b: cmp(a.x, b.x))[0].x
191         topmost    = sorted(coordinates, cmp=lambda a, b: cmp(a.y, b.y))[0].y
192         rightmost  = sorted(coordinates, cmp=lambda a, b: cmp(a.x, b.x))[-1].x
193         bottommost = sorted(coordinates, cmp=lambda a, b: cmp(a.y, b.y))[-1].y
194         return leftmost, topmost, rightmost, bottommost
196 def do_magick(distortion=True, crop=True):
197         src_topleft, src_bottomleft, src_topright, src_bottomright = get_quadrangle_points()
198         leftmost, topmost, rightmost, bottommost = get_cirumrectangle()
199         trg_topleft     = Coordinate(leftmost, topmost)
200         trg_bottomleft  = Coordinate(leftmost, bottommost)
201         trg_topright    = Coordinate(rightmost, topmost)
202         trg_bottomright = Coordinate(rightmost, bottommost)
203         coordinatepairs = ' '.join(map(str, [
204                 src_topleft, trg_topleft, 
205                 src_topright, trg_topright,
206                 src_bottomright, trg_bottomright,
207                 src_bottomleft, trg_bottomleft,
208                 ]))
209         cmd = ["convert", "-verbose", glob.sourcefile, "-auto-orient"]
210         if distortion:
211                 cmd.extend(["-matte", "-virtual-pixel", "transparent", "-distort", "Perspective", coordinatepairs])
212         if crop:
213                 cmd.extend(["-crop", "%dx%d+%d+%d" % (rightmost-leftmost, bottommost-topmost, leftmost, topmost), "+repage"])
214         run_command_with_tempfile(cmd)
216 def run_command_with_tempfile(cmd):
217         _, suffix = os.path.splitext(glob.sourcefile)
218         _fd, outfile = tempfile.mkstemp(suffix=suffix)
219         cmd.extend([outfile])
220         run_command_background(cmd, callback=(cb_imagemagick, {'outfile': outfile, 'original_file': glob.sourcefile}))
222 def cb_imagemagick(err, user_params):
223         if err != 0:
224                 # don't display erro message on gtk gui because it's in a forked process.
225                 sys.stderr.write("imagemagick error: %d\n" % err)
226         else:
227                 os.environ['PERSPECT_ORIGIN_FILE'] = user_params['original_file']
228                 outfile = user_params['outfile']
229                 subprocess.Popen([sys.executable, sys.argv[0], outfile], stdout=sys.stdout, stderr=sys.stderr)
231 def run_command_background(cmd, callback=None):
232         # start convert in detached background process
233         pid1 = os.fork()
234         if pid1 == 0:
235                 os.closerange(3, 65535)
236                 pid2 = os.fork()
237                 if pid2 == 0:
238                         print cmd
239                         err = subprocess.call(cmd, stdout=sys.stdout, stderr=sys.stderr)
240                         if callback is not None:
241                                 callback[0](err, *callback[1:])
242                         os._exit(err)
243                 else:
244                         os._exit(0)
245         else:
246                 os.waitpid(pid1, 0)
248 def do_distortion_and_crop(*_):
249         do_magick(distortion=True, crop=True)
251 def do_distortion(*_):
252         do_magick(distortion=True, crop=False)
254 def do_crop(*_):
255         do_magick(distortion=False, crop=True)
257 def get_linesegment_angle(A, B):
258         tan = float(A.x - B.x) / float(A.y - B.y)
259         ang = math.degrees(math.atan(tan))
260         return ang, ang + 90 if ang <= 0 else ang - 90
262 def do_rotation_horizontal(*_):
263         vangle, hangle = get_linesegment_angle(coordinates[0], coordinates[1])
264         cmd = get_rotation_params(hangle)
265         run_command_with_tempfile(cmd)
267 def do_rotation_vertical(*_):
268         vangle, hangle = get_linesegment_angle(coordinates[0], coordinates[1])
269         cmd = get_rotation_params(vangle)
270         run_command_with_tempfile(cmd)
272 def do_rotation_horizontal_and_autocrop(*_):
273         vangle, hangle = get_linesegment_angle(coordinates[0], coordinates[1])
274         cmd = get_rotation_params(hangle)
275         cmd.extend(["-fuzz", "20%", "-trim", "+repage"])
276         run_command_with_tempfile(cmd)
277         
278 def do_rotation_vertical_and_autocrop(*_):
279         vangle, hangle = get_linesegment_angle(coordinates[0], coordinates[1])
280         cmd = get_rotation_params(vangle)
281         cmd.extend(["-fuzz", "20%", "-trim", "+repage"])
282         run_command_with_tempfile(cmd)
284 def get_rotation_params(angle):
285         cmd = ["convert", glob.sourcefile, "-auto-orient", "-rotate", str(angle)]
286         return cmd
288 import imagemetadata
290 def imagerotation_cb(transformation, user_data):
291         if transformation == imagemetadata.FLIP:
292                 user_data['pixbuf'] = user_data['pixbuf'].flip(horizontal=False)
293         elif transformation == imagemetadata.FLOP:
294                 user_data['pixbuf'] = user_data['pixbuf'].flip(horizontal=True)
295         elif transformation == imagemetadata.CLOCKWISE:
296                 user_data['pixbuf'] = user_data['pixbuf'].rotate_simple(gtk.gdk.PIXBUF_ROTATE_CLOCKWISE)
297         elif transformation == imagemetadata.UPSIDEDOWN:
298                 user_data['pixbuf'] = user_data['pixbuf'].rotate_simple(gtk.gdk.PIXBUF_ROTATE_UPSIDEDOWN)
299         elif transformation == imagemetadata.COUNTERCLOCKWISE:
300                 user_data['pixbuf'] = user_data['pixbuf'].rotate_simple(gtk.gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE)
301         else:
302                 #warnx("unknown image transformation code %d" % transformation)
303                 pass
305 def load_image(imageobj, sourcefile):
306         loader = gtk.gdk.PixbufLoader()
307         imagedata = open(sourcefile, 'r').read()
308         loader.write(imagedata)
309         loader.close()
310         pixbuf = loader.get_pixbuf()
311         
312         # transform the image on screen according to the file's EXIF metadata
313         rotation = {'pixbuf': pixbuf}
314         irot = imagemetadata.Image(imagedata = imagedata)
315         irot.rotate(imagerotation_cb, rotation)
316         
317         image.set_from_pixbuf(rotation['pixbuf'])
319 def do_open(*_):
320         run_command_background(["gpicview", glob.sourcefile])
322 def do_save_as(*_):
323         proposed_path = os.environ.get('PERSPECT_ORIGIN_FILE', glob.sourcefile)
324         save_path = file_choose_dialog_save(proposed_path)
325         if save_path is not None:
326                 if not os.path.exists(save_path) or question("Overwrite?\n<tt>%s</tt>"%glib.markup_escape_text(save_path), gtk.STOCK_SAVE, gtk.STOCK_CANCEL, window):
327                         try:
328                                 copyfile(glob.sourcefile, save_path)
329                         except Exception as e:
330                                 display_error(e)
333 def display_error(e):
334         text = None
335         if isinstance(e, OSError) or isinstance(e, IOError):
336                 text = '%s (#%d)' % (e.strerror, e.errno)
337                 if e.filename is not None:
338                         text += '\n%s' % (e.filename)
339         elif isinstance(e, Exception):
340                 text = e.message
341         elif type(e) == type([]):
342                 text = ''.join(e)
343         if text is None:
344                 text = str(e)
345         dlg = gtk.MessageDialog(window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text)
346         dlg.set_title("Error")
347         dlg.run()
348         dlg.destroy()
350 def question(msg, stock_yes=None, stock_no=None, parent=None):
351         dlg = gtk.MessageDialog(parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
352         dlg.set_markup(msg)
353         dlg.set_title("Question")
354         if stock_no is not None:
355                 dlg.get_widget_for_response(gtk.RESPONSE_NO).hide()
356                 if hasattr(stock_no, '__iter__'):
357                         btn_no = StockButton(label=stock_no[0], stock=stock_no[1])
358                 else:
359                         btn_no = StockButton(stock=stock_no)
360                 dlg.add_action_widget(btn_no, gtk.RESPONSE_NO)
361                 btn_no.show()
362         if stock_yes is not None:
363                 dlg.get_widget_for_response(gtk.RESPONSE_YES).hide()
364                 if hasattr(stock_yes, '__iter__'):
365                         btn_yes = StockButton(label=stock_yes[0], stock=stock_yes[1])
366                 else:
367                         btn_yes = StockButton(stock=stock_yes)
368                 dlg.add_action_widget(btn_yes, gtk.RESPONSE_YES)
369                 btn_yes.show()
370         resp = dlg.run()
371         dlg.destroy()
372         return (resp == gtk.RESPONSE_YES)
374 def file_choose_dialog_save(filepath):
375         global LastFolder
376         try: LastFolder
377         except NameError: LastFolder = None
378         selected = None
379         action = gtk.FILE_CHOOSER_ACTION_SAVE
380         
381         dlg = gtk.FileChooserDialog(parent=window, action=action, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT))
382         if LastFolder is not None: dlg.set_current_folder(LastFolder)
383         dlg.set_current_name(os.path.basename(filepath))
384         
385         last_resp_num = max(map(lambda a: int(getattr(gtk, a)), filter(lambda a: a.startswith('RESPONSE_'), dir(gtk))))
386         
387         resp_num_cwd = last_resp_num + 1
388         btn_cwd = StockButton(label="Working Dir", stock=gtk.STOCK_JUMP_TO)
389         dlg.add_action_widget(btn_cwd, resp_num_cwd)
390         btn_cwd.show()
391         
392         resp_num_fdir = last_resp_num + 2
393         btn_fdir = StockButton(label="Jump to File", stock=gtk.STOCK_JUMP_TO)
394         dlg.add_action_widget(btn_fdir, resp_num_fdir)
395         btn_fdir.show()
396         
397         while True:
398                 resp = dlg.run()
399                 if resp == gtk.RESPONSE_ACCEPT:
400                         selected = dlg.get_filename()
401                         break
402                 elif resp == resp_num_cwd:
403                         dlg.set_current_folder(os.getcwd())
404                 elif resp == resp_num_fdir:
405                         dlg.set_current_folder(os.path.dirname(filepath))
406                 else:
407                         break
408         LastFolder = dlg.get_current_folder()
409         dlg.destroy()
410         return selected
412 class SimpleStore(object):
413         pass
415 def toggle_quadrangle(*X):
416         glob.show_quadrangle = not glob.show_quadrangle
417         refresh_display()
419 def toggle_circumrectangle(*X):
420         glob.show_circumrectangle = not glob.show_circumrectangle
421         refresh_display()
423 def load_file(filepath):
424         load_image(image, filepath)
425         glob.sourcefile = filepath
426         window.set_title(window.get_title() + ": " + glob.sourcefile)
428 USER_LOG_FILE = '~/.perspect.coords.log'
430 def do_save_coordinates(*X):
431         curr_coords = get_quadrangle_points()
432         last_coords = read_last_saved_coordinates()
433         # append surrently pointed coordinates to the log file
434         # unless the exact same coords were saved most recently.
435         if any([cc.x != lc.x or cc.y != lc.y for cc, lc in zip(curr_coords, last_coords)]):
436                 with open(os.path.expanduser(USER_LOG_FILE), 'a') as fh:
437                         fh.write("%s %s %s %s\n" % curr_coords[0:4])
439 def read_last_saved_coordinates(*X):
440         last_coords = (Coordinate(-1, -1), Coordinate(-1, -1), Coordinate(-1, -1), Coordinate(-1, -1))
441         try:
442                 filecontext = open(os.path.expanduser(USER_LOG_FILE), 'r')
443         except (OSError, IOError) as e:
444                 traceback.print_exc()
445                 pass
446         else:
447                 with filecontext as fh:
448                         try: fh.seek(-32, 2)
449                         except IOError as e: pass
450                         while True:
451                                 pos = fh.tell()
452                                 buf = fh.read()
453                                 fh.seek(pos, 0)
454                                 if pos == 0: start_anchor_re = '^'
455                                 else: start_anchor_re = r'(?<=\n)'
456                                 matches = re.findall(start_anchor_re + r'(\S+) (\S+) (\S+) (\S+)(?=\n|$)', buf)
457                                 if matches:
458                                         print matches
459                                         last_coords = [Coordinate(x) for x in matches[-1]]
460                                         break
461                                 else:
462                                         if pos == 0: break
463                                         try: fh.seek(-32, 1)
464                                         except IOError as e: fh.seek(0, 0)
465         return last_coords
467 def do_load_coordinates(*X):
468         # replace currently pointed coordinates to the last saved ones
469         last_coords = read_last_saved_coordinates()
470         last_coords = [c for c in last_coords if not (c.x == -1 or c.y == -1)]
471         while coordinates:
472                 coordinates.pop()
473         coordinates.extend(last_coords)
474         refresh_display()
475         update_statusline()
477 def show_help(*X):
478         text = """Billentyűparancsok
480 <b>F1</b> : Perspektíva javítás
481 <b>Shift + F1</b> : Perspektíva javítás és méretre vágás
482 <b>F2</b> : Méretre vágás
483 <b>F3</b> : Forgatás úgy hogy a meghúzott vonal vízszintes legyen
484 <b>F4</b> : Forgatás úgy hogy a meghúzott vonal függőleges legyen
485 <b>Shift + F3/F4</b> : Forgatás ugyanígy és automatikus méretre vágás
487 <b>F5</b> : Kijelölt koordináták mentése <tt>"""+USER_LOG_FILE+"""</tt> fájlba
488 <b>F6</b> : Utoljára mentett koordináták visszaállítása
489 <b>Q</b> : Kijelölt négyszöget mutat/elrejt
490 <b>R</b> : Körülírt téglalapot mutat/elrejt
492 <b>Ctrl + S</b> : Aktuális fájl mentése más néven
493 <b>Ctrl + O</b> : Aktuális fájl megnyitása külső programmal
494 <b>Escape</b> : Kilépés
496 <b>Bal egérgomb</b> : Pontok megjelölése (max 4) egy szakasz vagy négyszög meghúzásához
497 <b>Jobb egérgomb</b> : A legközelebbi pont törlése
498 <b>Középső egérgomb</b> : Meghúzott szakasz vagy sokszög újrarajzoltatása
499 <b>Alt + Bal egérgomb</b> : Ablak mozgatása (ha az ablakkezelő támogatja)
501         dlg = gtk.MessageDialog(window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
502         dlg.set_markup(text)
503         dlg.set_title("Help")
504         dlg.run()
505         dlg.destroy()
509 window = gtk.Window()
510 window.set_title("Perspective correction")
511 window.connect('delete-event', lambda *x: gtk.main_quit())
512 add_key_binding(window, 'Escape', gtk.main_quit)
513 add_key_binding(window, 'question', show_help)
514 add_key_binding(window, 'r', toggle_circumrectangle)
515 add_key_binding(window, 'q', toggle_quadrangle)
516 add_key_binding(window, 'F1', do_distortion)
517 add_key_binding(window, '<Shift>F1', do_distortion_and_crop)
518 add_key_binding(window, 'F2', do_crop)
519 add_key_binding(window, 'F3', do_rotation_horizontal)
520 add_key_binding(window, '<Shift>F3', do_rotation_horizontal_and_autocrop)
521 add_key_binding(window, 'F4', do_rotation_vertical)
522 add_key_binding(window, '<Shift>F4', do_rotation_vertical_and_autocrop)
523 add_key_binding(window, 'F5', do_save_coordinates)
524 add_key_binding(window, 'F6', do_load_coordinates)
525 add_key_binding(window, '<Control>S', do_save_as)
526 add_key_binding(window, '<Control>O', do_open)
528 box1 = gtk.VBox()
529 statusline = gtk.Label()
530 statusline.set_selectable(True)
531 statusline.set_alignment(0, 0)
534 glob = SimpleStore()
535 glob.show_quadrangle = True
536 glob.show_circumrectangle = False
537 image = EventImage()
538 image.connect('button-release-event', on_clicked)
540 load_file(sys.argv[1])
544 coordinates = []
546 window.add(box1)
547 box1.pack_start(image, True, True)
548 box1.pack_start(statusline, False, True)
550 window.show_all()
551 window.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.CROSSHAIR))
552 gtk.main()