Package metapho :: Module main
[hide private]

Source Code for Module metapho.main

  1  #!/usr/bin/env python 
  2   
  3  ''' 
  4  metapho: an image tagger and viewer. 
  5   
  6  This is the runnable script, which calls up a GTK-based user interface 
  7  for viewing images and tagging them. 
  8  ''' 
  9   
 10  # Copyright 2013,2016 by Akkana Peck: share and enjoy under the GPL v2 or later. 
 11   
 12  from . import metapho 
 13  from . import gtkpho 
 14   
 15  import gtk 
 16   
 17  import sys, os 
 18   
 19  import traceback 
 20   
21 -class MetaPhoWindow(object):
22 '''The main controller window for metapho. 23 This holds any child widgets, like the image viewer and tags window, 24 and manages key events and other user commands. 25 ''' 26
27 - def __init__(self, file_list):
28 for filename in file_list: 29 metapho.Image.g_image_list.append(metapho.Image(filename)) 30 self.imgno = 0 31 32 # The size of the image viewing area: 33 self.imgwidth = 640 34 self.imgheight = 600 35 36 self.isearch = False 37 38 self.win = gtk.Window(gtk.WINDOW_TOPLEVEL) 39 self.win.set_border_width(10) 40 41 self.win.connect("delete_event", self.quit) 42 self.win.connect("destroy", self.quit) 43 44 main_hbox = gtk.HBox(spacing=8) 45 46 self.viewer = gtkpho.ImageViewer() 47 self.viewer.set_size_request(self.imgwidth, self.imgheight) 48 main_hbox.pack_start(self.viewer) 49 50 self.tagger = gtkpho.TagViewer(self.win) 51 main_hbox.pack_start(self.tagger, expand=True) 52 53 self.win.add(main_hbox) 54 55 self.win.connect("key-press-event", self.key_press_event) 56 self.win.show_all(); 57 58 self.read_all_tags()
59
60 - def quit(self, widget=None, data=None):
61 # If focus is currently in a text entry, there may be changed 62 # text that hasn't been updated yet in the tags list. 63 # Call the tagger to warn about that. 64 if type(self.win.get_focus()) is gtk.Entry: 65 self.tagger.check_entry_tag(self.win.get_focus()) 66 67 print "===========" 68 print self.tagger 69 self.tagger.write_tag_file() 70 71 # Can't call main_quit here: RuntimeError: called outside of a mainloop 72 # Apparently this is what you're supposed to do instead: 73 self.win.connect('event-after', gtk.main_quit)
74
75 - def read_all_tags(self):
76 '''Read tags in all directories used by images in argv. 77 ''' 78 dirlist = [] 79 for img in metapho.Image.g_image_list: 80 dirname = os.path.dirname(img.filename) 81 if dirname not in dirlist: 82 dirlist.append(dirname) 83 self.tagger.read_tags(dirname) 84 self.tagger.display_tags()
85
86 - def first_image(self):
87 self.imgno = -1 88 self.next_image()
89
90 - def lastImage(self):
91 self.imgno = len(metapho.Image.g_image_list) 92 self.prev_image()
93
94 - def next_image(self):
95 '''Advance to the next image, if possible. 96 Tell the viewer to load and show the image. 97 ''' 98 loaded = False 99 100 # Save the tags of the current image, so we can copy them 101 # into the next image if it doesn't have any yet. 102 oldtags = None 103 try: 104 if self.imgno >= 0 and metapho.Image.g_image_list[self.imgno].tags: 105 oldtags = metapho.Image.g_image_list[self.imgno].tags 106 except: 107 print "Couldn't load image #", self.imgno 108 print "Tags:", metapho.Image.g_image_list[self.imgno].tags 109 pass 110 111 while self.imgno < len(metapho.Image.g_image_list)-1 and not loaded: 112 self.imgno += 1 113 img = metapho.Image.g_image_list[self.imgno] 114 if img.displayed: 115 loaded = self.viewer.load_image(img) 116 if not loaded: 117 print "next_image: couldn't show", img.filename 118 img.displayed = False 119 # Should arguably delete it from the list 120 # so we don't continue to save tags for a 121 # file we can't load. But what if it's just 122 # temporarily unreadable and the user can fix it? 123 #del(metapho.Image.g_image_list[self.imgno]) 124 # The loop is about to increment imgno, but we actually want 125 # it to stay the same since deleting the nonexistent image 126 # slid the next image into the current position; 127 # so decrement imgno now. 128 #self.imgno -= 1 129 130 if loaded: 131 # If we have an image, and it has no tags set yet, 132 # clone the tags from the previous image: 133 if oldtags and not metapho.Image.g_image_list[self.imgno].tags: 134 metapho.Image.g_image_list[self.imgno].tags = oldtags[:] 135 136 self.tagger.set_image(metapho.Image.g_image_list[self.imgno]) 137 138 else : # couldn't load anything in the list 139 print "No more images" 140 dialog = gtk.MessageDialog(self.win, 141 gtk.DIALOG_DESTROY_WITH_PARENT, 142 gtk.MESSAGE_QUESTION, 143 gtk.BUTTONS_OK_CANCEL, 144 "No more images: quit?") 145 dialog.set_default_response(gtk.RESPONSE_OK) 146 response = dialog.run() 147 dialog.destroy() 148 if response == gtk.RESPONSE_OK: 149 self.quit()
150
151 - def prev_image(self):
152 loaded = False 153 while self.imgno >= 1 and not loaded: 154 self.imgno -= 1 155 img = metapho.Image.g_image_list[self.imgno] 156 if img.displayed: 157 loaded = self.viewer.load_image(img) 158 if not loaded: 159 print "prev_image: couldn't show", img.filename 160 img.displayed = False 161 # See comment in next_image 162 #del(metapho.Image.g_image_list[self.imgno]) 163 164 if loaded: 165 self.tagger.set_image(metapho.Image.g_image_list[self.imgno]) 166 else : # couldn't load anything in the list 167 print "Can't go before first image"
168
169 - def delete_confirm(self):
170 '''Ask the user whether to really delete an image. 171 Return True for yes, False for no. 172 Accept some keystrokes beyond the usual ones, 173 e.g. d or ctrl-d confirms the delete. 174 ''' 175 dialog = gtk.MessageDialog(self.win, 176 gtk.DIALOG_DESTROY_WITH_PARENT, 177 gtk.MESSAGE_QUESTION, 178 #gtk.BUTTONS_YES_NO, 179 gtk.BUTTONS_CANCEL, 180 "Delete %s ?" % \ 181 metapho.Image.g_image_list[self.imgno]) 182 delete_btn = dialog.add_button("Delete", gtk.RESPONSE_YES) 183 184 # Handle key events on the dialog, 185 # to make it easier for the user to respond. 186 # d (with or without ctrl) confirms the delete. 187 # n or q cancels (in addition to the usual ESC). 188 def delete_dialog_key_press(widget, event, dialog): 189 if event.string in ('q', 'n'): 190 dialog.emit("response", gtk.RESPONSE_NO) 191 return True 192 elif event.keyval == gtk.keysyms.d : # d with or without ctrl 193 dialog.emit("response", gtk.RESPONSE_YES) 194 return True 195 return False
196 dialog.connect("key-press-event", delete_dialog_key_press, dialog) 197 198 response = dialog.run() 199 dialog.destroy() 200 if response == gtk.RESPONSE_YES: 201 return True 202 return False
203
204 - def key_press_event(self, widget, event):
205 '''Handle a key press event anywhere in the window''' 206 if self.isearch: 207 return self.isearch_key_press(widget, event) 208 209 entry_focused = (type(self.win.get_focus()) is gtk.Entry) 210 211 # ctrl-space goes to the next image, even if we're typing 212 # in an entry. Nothing should be focused afterward. 213 # or out of the entries if we're already typing in one. 214 # Ctrl-space also goes to the next image. 215 if (event.keyval == gtk.keysyms.space and \ 216 event.state & gtk.gdk.CONTROL_MASK): 217 if entry_focused: 218 self.tagger.focus_none() 219 self.next_image() 220 return True 221 222 # ESC shifts focus out of the current entry (if any) 223 # and makes sure nothing is focused. 224 if event.keyval == gtk.keysyms.Escape: 225 self.tagger.focus_none() 226 return True 227 228 # Return shifts focus to the next tag entry (never out of the entries). 229 if event.keyval == gtk.keysyms.Return: 230 self.tagger.focus_next_entry() 231 return True 232 233 if event.keyval == gtk.keysyms.Return and entry_focused: 234 # Return when in an entry goes to the next entry 235 self.tagger.focus_next_entry() 236 return True 237 238 # For any other keys, if focus is in a text entry, just let 239 # the user type, and don't try to navigate. 240 if entry_focused: 241 #print "Focus is in an entry" 242 return False 243 244 # Ctrl-d means delete the current image (after confirmation) 245 if event.keyval == gtk.keysyms.d and \ 246 event.state & gtk.gdk.CONTROL_MASK: 247 if self.delete_confirm(): 248 metapho.Image.g_image_list[self.imgno].delete() 249 self.imgno -= 1 250 self.next_image() 251 return True 252 253 # Ctrl-U: clear tags, then leave focus in the first empty tag field. 254 if event.keyval == gtk.keysyms.u and \ 255 event.state & gtk.gdk.CONTROL_MASK: 256 self.tagger.clear_tags(metapho.Image.g_image_list[self.imgno]) 257 # Turns out auto-focusing the next entry is annoying, 258 # so don't do it: 259 # self.tagger.focus_next_entry() 260 return True 261 262 # Ctrl-Z is for when you accidentally hit a key that opens a 263 # new tag, but the current tag is blank and you don't want 264 # focus in that text field. 265 # XXX 266 267 # Ctrl-q quits. 268 if event.keyval == gtk.keysyms.q and \ 269 event.state & gtk.gdk.CONTROL_MASK: 270 self.quit() 271 return True 272 273 if event.string == " ": 274 self.next_image() 275 return True 276 if event.keyval == gtk.keysyms.BackSpace: 277 self.prev_image() 278 return True 279 if event.keyval == gtk.keysyms.Home: 280 self.first_image() 281 return True 282 if event.keyval == gtk.keysyms.End: 283 self.lastImage() 284 return True 285 if event.keyval == gtk.keysyms.Right: 286 self.viewer.rotate(270) 287 return True 288 if event.keyval == gtk.keysyms.Left: 289 self.viewer.rotate(90) 290 return True 291 if event.keyval in [ gtk.keysyms.Up, gtk.keysyms.Down ]: 292 self.viewer.rotate(180) 293 return True 294 295 # Alpha: it's a tag 296 if event.string.isalpha(): 297 self.tagger.toggle_tag_by_letter(event.string, 298 metapho.Image.g_image_list[self.imgno]) 299 return True 300 301 # Digits: go to a specific tag category 302 # (ignore digits too large to have a category). 303 if event.string.isdigit(): 304 try: 305 self.tagger.show_category_by_number(int(event.string)) 306 except IndexError: 307 pass 308 return True 309 310 # + or -: go to next or previous tag 311 if event.string == '+': 312 self.tagger.next_category(1) 313 return True 314 if event.string == '-': 315 self.tagger.next_category(-1) 316 return True 317 318 if event.string == '/': 319 self.search_string = '' 320 self.tagger.title.set_text("search: ") 321 self.isearch = True 322 return True 323 324 # A key we didn't understand 325 #print "Read key:", event.string, "keyval", event.keyval 326 return False
327
328 - def isearch_key_press(self, widget, event):
329 '''Handle key presses when we're in isearch mode, 330 typing in a search pattern. 331 ''' 332 333 # Return shifts out of isearch mode 334 # but also accepts (shifts focus to) the first match. 335 if event.keyval == gtk.keysyms.Return: 336 self.tagger.focus_first_match(self.search_string) 337 self.isearch = False 338 self.tagger.title.set_text(os.path.basename(\ 339 metapho.Image.g_image_list[self.imgno].filename)) 340 return True 341 342 # ESC shifts out of isearch mode. 343 if event.keyval == gtk.keysyms.Escape: 344 self.isearch = False 345 self.tagger.show_matches('') 346 return True 347 348 if event.string: 349 self.search_string += event.string 350 self.tagger.show_matches(self.search_string) 351 return True 352 353 return False
354
355 - def main(self):
356 gtk.main()
357
358 -def main():
359 def Usage(): 360 print "Usage: %s file [file file ...]" \ 361 % os.path.basename(sys.argv[0])
362 363 print "Hello, and welcome to metapho." 364 365 if len(sys.argv) <= 1: 366 Usage() 367 sys.exit(1) 368 if sys.argv[1] == "-h" or sys.argv[1] == "--help": 369 Usage() 370 sys.exit(0) 371 if sys.argv[1] == "-v" or sys.argv[1] == "--version": 372 print metapho.__version__ 373 sys.exit(0) 374 375 metapho = MetaPhoWindow(sys.argv[1:]) 376 metapho.first_image() 377 try: 378 metapho.main() 379 except KeyboardInterrupt: 380 # Deliberately don't call self.quit() -- we may be using Ctrl-C 381 # as a way to quit without updating anything. 382 print '\n' 383 # This doesn't do anything useful: 384 # traceback.print_stack() 385 sys.exit(1) 386 387 if __name__ == '__main__': 388 main() 389