metapho.main

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