Linux-Fan

Ma_Sys.ma Plan View: Simple Python Image Viewer

May 21st, 2012
59
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/usr/bin/python
  2. #
  3. # Thanks to http://www.dzone.com/snippets/python-example-simple
  4. #
  5.  
  6. import gtk
  7. import gtk.gdk
  8. import os
  9. import sys
  10.  
  11. class MaSysMaPlanView(gtk.Window):
  12.  
  13.     def handle_scrollbar_updated_event(self, adjustment):
  14.         """Invoked when the scrollbar is changed. This method depends on the value of the self.react_on_update field.
  15.         If react_on_update is False, this method does nothing. If it is 1, the scrollbar is scrolled down. If it is 2,
  16.         the display status is updated. After the invocation this method sets react_on_update back to False in order to
  17.         prevent further invocations until react_on_update is again changed."""
  18.         if self.react_on_update == False:
  19.             return False
  20.         if self.react_on_update == 1:
  21.             self.scrollbar.get_vscrollbar().set_value(adjustment.upper - adjustment.page_size)
  22.         elif self.react_on_update == 2:
  23.             self.apply_display_change()
  24.         self.react_on_update = False
  25.         return False
  26.  
  27.     def handle_scroll_event(self, widget, event):
  28.         """Invoked when the scrollbar is scrolled. While CTRL is held down, this is used for zooming and switching to
  29.         the next file after having reached the top/end of a "page"."""
  30.         if event.state.first_value_name == "GDK_CONTROL_MASK":
  31.             delta_f = 0.1
  32.             if event.direction == gtk.gdk.SCROLL_UP:
  33.                 return self.zoom(1 + delta_f)
  34.             elif event.direction == gtk.gdk.SCROLL_DOWN:
  35.                 return self.zoom(1 - delta_f)
  36.             else:
  37.                 return False
  38.         elif event.state.first_value_name != "GDK_MOD1_MASK":
  39.             adjustment = self.scrollbar.get_vscrollbar().get_adjustment()
  40.             if event.direction == gtk.gdk.SCROLL_UP:
  41.                 return self.scroll_image(adjustment, True)
  42.             elif event.direction == gtk.gdk.SCROLL_DOWN:
  43.                 return self.scroll_image(adjustment, False)
  44.             else:
  45.                 return False
  46.         return False
  47.  
  48.     def handle_window_resize_event(self, widget, event):
  49.         """Should resize the image on window resize if needed. This does not always work as it does not do this itself
  50.         but wants handle_scrollbar_updated_event() to handle this."""
  51.         # TODO does not always work
  52.         self.react_on_update = 2
  53.         return False
  54.  
  55.     def handle_key_event(self, widget, event):
  56.         """Main keyboard interaction method."""
  57.         key = gtk.gdk.keyval_name(event.keyval)
  58.         if key == "c": # center (only affects width)
  59.             # TODO does not really work seamless (second invocation corrects... it is realted to the auto-vanishing scrollbar, etc.)
  60.             self.apply_display_change(2)
  61.         elif key == "dollar": # end (zoom away as needed...)
  62.             self.apply_display_change(3)
  63.         elif key == "0": # start (zom in until not scaled anymore)
  64.             self.apply_display_change(1)
  65.         elif key == "q" or (key == "w" and event.state.first_value_name == "GDK_CONTROL_MASK") or key == "F10" \
  66.                 or (key == "F4" and event.state.first_value_name == "GDK_MOD1_MASK") or key == "Escape": # quit
  67.             self.exit()
  68.         elif key == "space" or key == "l" or key == "Right":
  69.             self.next_file()
  70.         elif key == "h" or key == "BackSpace" or key == "Left":
  71.             self.next_file(increment = -1)
  72.         elif (key == "r" and event.state.first_value_name == "GDK_CONTROL_MASK") or key == "F5": # reload
  73.             self.reload_image()
  74.         elif key == "r": # reload dir by doing a cd to current directory
  75.             self.chdir(self.cd_str)
  76.         # TODO allow "j" and "Down" for regular scrolling, too (also with up)
  77.         elif key == "j" or key == "Down" or key == "Page_Down":
  78.             return self.scroll_image(self.scrollbar.get_vscrollbar().get_adjustment(), False)
  79.         elif key == "k" or key == "Up" or key == "Page_Up":
  80.             return self.scroll_image(self.scrollbar.get_vscrollbar().get_adjustment(), True)
  81.         elif key == "o" or key == "g" or key == "d":
  82.             self.chdir_gui()
  83.         elif key == "F11":
  84.             self.is_fullscreen = not self.is_fullscreen
  85.             if self.is_fullscreen:
  86.                 self.fullscreen()
  87.             else:
  88.                 self.unfullscreen()
  89.         elif key == "F2":
  90.             dialog = gtk.AboutDialog()
  91.             dialog.set_program_name("Ma_Sys.ma Plan View")
  92.             dialog.set_comments("a simple keyboard only image viewer")
  93.             dialog.set_version("0.9.0.0")
  94.             dialog.set_copyright(
  95.                 "Copyright (c) 2012 Ma_Sys.ma, For further info send an e-mail to Ma_Sys.ma@web.de.")
  96.             license_file = "/usr/share/common-licenses/GPL-3"
  97.             if os.path.exists(license_file):
  98.                 link=open(license_file, 'r')
  99.                 dialog.set_license(link.read())
  100.                 link.close()
  101.             else:
  102.                 dialog.set_license(self.get_license_small())
  103.             dialog.run()
  104.             dialog.destroy()
  105.         elif key == "F1":
  106.             dialog = gtk.MessageDialog(self, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_INFO,
  107.                 gtk.BUTTONS_OK, None)
  108.             dialog.set_title("Help")
  109.             # TODO use tupel for assoc and then a loop to build markup strings. also add half line separator after one command/descr assoc.
  110.             dialog.set_markup("<b>List of all keyboard commands</b>\n\n" +
  111.                 "<tt>F1</tt>\n\tDisplay this help\n" +
  112.                 "<tt>F2</tt>\n\tVersion Information\n" +
  113.                 "<tt>F5, CTRL-R</tt>\n\tReload current file\n" +
  114.                 "<tt>r</tt>\n\tReload directory\n" +
  115.                 "<tt>F10, ESC, q, CTRL-W, ALT-F4</tt>\n\tExit\n" +
  116.                 "<tt>F11</tt>\n\tToggle fullscreen mode\n" +
  117.                 "<tt>Page Down</tt>\n\tPage down (next file if end of page reached)\n" +
  118.                 "<tt>Page Up</tt>\n\tPage up (previous file if begin of page reached)\n" +
  119.                 "<tt>Home</tt>\n\tGo to first file\n" +
  120.                 "<tt>End</tt>\n\tGo to last file\n" +
  121.                 "<tt>c</tt>\n\tCenter, adjust image to fit page width\n" +
  122.                 "<tt>$</tt>\n\tZoom out until the whole image is visible\n" +
  123.                 "<tt>0</tt>\n\tRestore original image size\n" +
  124.                 "<tt>o, g, d</tt>\n\topen, goto, (change)dir: change directory\n" +
  125.                 "<tt>CTRL-[SCROLL]</tt>\n\tAllows you to zoom in and out.\n" +
  126.                 "<tt>Space, l, Right</tt>\n\tNext file.\n" +
  127.                 "<tt>Backspace, h, Left</tt>\n\tPrevious file.")
  128.             dialog.run()
  129.             dialog.destroy()
  130.         elif key == "Home":
  131.             self.cd_sub = 0
  132.             self.next_file_open()
  133.         elif key == "End":
  134.             self.cd_sub = len(self.cd) - 1
  135.             self.next_file_open(increment = -1)
  136.  
  137.         return False
  138.  
  139.     def scroll_image(self, adjustment, up = True):
  140.         """Scrolls the image completely up or down (up if up == True, down if up == False)."""
  141.         if adjustment.value == adjustment.lower and up:
  142.             self.next_file(increment = -1)
  143.             # Scroll down after change was registered
  144.             self.react_on_update = 1
  145.         # Thanks to http://www.pygtk.org/pygtk2tutorial/sec-RangeWidgetEample.html
  146.         elif adjustment.value == (adjustment.upper - adjustment.page_size) and not up:
  147.             self.next_file()
  148.             # As this always remains 0.0 it is not problematic that it is not yet updated...
  149.             self.scrollbar.get_vscrollbar().set_value(adjustment.lower)
  150.         else:
  151.             return False
  152.         return True
  153.  
  154.     def zoom(self, factor):
  155.         """Zooms into the image. Warning: Scaling is done on the CPU meaning it is not *that* efficient and especially
  156.         not fast with very big scaled images."""
  157.         if self.display_mode != 4:
  158.             self.apply_display_change(4)
  159.         if (self.zoom_height < 32 or self.zoom_width < 32) and factor < 1:
  160.             return True
  161.         self.zoom_height = int(self.zoom_height * factor)
  162.         self.zoom_width = int(self.zoom_width * factor)
  163.         self.apply_display_change(4)
  164.         return True
  165.    
  166.     def apply_display_change(self, change_display_mode = False):
  167.         """Applies a display change by recalculating the displayed image."""
  168.         if change_display_mode != False:
  169.             self.display_mode = change_display_mode;
  170.         self.current_pixbuf = self.current_source_pixbuf
  171.         new_width = -1;
  172.         new_height = -1;
  173.         if self.display_mode == 4:
  174.             if self.zoom_width == -1 or self.zoom_height == -1:
  175.                 self.zoom_width = self.current_pixbuf.get_width()
  176.                 self.zoom_height = self.current_pixbuf.get_height()
  177.             new_width = self.zoom_width
  178.             new_height = self.zoom_height
  179.         else:
  180.             self.zoom_width = -1
  181.             self.zoom_height = -1
  182.             if self.display_mode != 1:
  183.                 wnd_rectangle = self.viewport.get_allocation()
  184.                 wnd_width = wnd_rectangle.width
  185.                 wnd_height = wnd_rectangle.height
  186.                 img_width = self.current_source_pixbuf.get_width()
  187.                 img_height = self.current_source_pixbuf.get_height()
  188.                 if self.display_mode == 2 and img_width > wnd_width:
  189.                     new_width = wnd_width
  190.                     new_height = (wnd_width * img_height) / img_width
  191.                 elif self.display_mode == 3:
  192.                     # If you remve this statement you will force the image to fill the window even if it's
  193.                     # smaller than the window
  194.                     if img_width > wnd_width or img_height > wnd_height:
  195.                         if img_width - wnd_width > img_height - wnd_height:
  196.                             new_width = wnd_width;
  197.                             new_height = (new_width * img_height) / img_width
  198.                         else:
  199.                             new_height = wnd_height;
  200.                             new_width = (new_height * img_width) / img_height
  201.         if new_width != -1 and new_height != -1:
  202.             # TODO not very efficient as not done on the gpu, ram comsumption extremly high with upscaled images
  203.             self.current_pixbuf = self.current_source_pixbuf.scale_simple(
  204.                 new_width, new_height, gtk.gdk.INTERP_HYPER)
  205.         self.current_image.set_from_pixbuf(self.current_pixbuf)
  206.  
  207.     def next_file(self, zskip = False, increment = 1):
  208.         """Tries to open the next file (you can select the "previous" file via increment = -1)."""
  209.         self.cd_sub += increment
  210.         return self.next_file_open(zskip, increment)
  211.  
  212.     def next_file_open(self, zskip = False, increment = 1):
  213.         """Method called to open the "next" file without incrementing self.cd_sub and therefore useful for reloading
  214.         operations, etc. Non-Image files are automatically skipped. After having reached the end of the list, the
  215.         program returns to the begin of the list."""
  216.         if self.cd_sub == len(self.cd):
  217.             if len(self.cd) == 0:
  218.                 dialog = gtk.MessageDialog(
  219.                     self, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR,
  220.                     gtk.BUTTONS_OK, "The directory does not contain a single file.")
  221.                 dialog.set_title("Error")
  222.                 dialog.run()
  223.                 dialog.destroy()
  224.                 return False
  225.             else:
  226.                 self.cd_sub = 0
  227.         elif self.cd_sub < 0:
  228.             self.cd_sub = len(self.cd) - 1
  229.         elif not self.cd[self.cd_sub].rpartition('.')[2].lower() in self.filetypes:
  230.             if zskip == True:
  231.                 dialog = gtk.MessageDialog(
  232.                     self, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR,
  233.                     gtk.BUTTONS_OK, "The directory does not contain a single useful image file.")
  234.                 dialog.set_title("Error")
  235.                 dialog.run()
  236.                 dialog.destroy()
  237.                 return False
  238.             else:
  239.                 return self.next_file(self.cd_sub == 0, increment)
  240.         self.reload_image()
  241.         return True
  242.  
  243.     def reload_image(self):
  244.         """Tries to reload current image from file."""
  245.         try:
  246.             self.current_source_pixbuf = gtk.gdk.pixbuf_new_from_file(self.cd_str + os.sep + self.cd[self.cd_sub])
  247.         except:
  248.             dialog = gtk.MessageDialog(self, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR,
  249.                 gtk.BUTTONS_OK, "Could not load image file \"" + self.cd[self.cd_sub] + "\".")
  250.             dialog.run()
  251.             dialog.destroy()
  252.             return
  253.         self.apply_display_change()
  254.    
  255.     def chdir_gui(self):
  256.         """Displays a gtk.FileChooserDialog to select another directory. Available via the "d" command."""
  257.         chooser = gtk.FileChooserDialog("Select new Directory", self, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
  258.             ("Cancel", 1, "Select", 0))
  259.         chooser.set_filename(self.cd_str)
  260.         has_selected = chooser.run()
  261.         selection = None
  262.         if has_selected == 0:
  263.             selection = chooser.get_filename()
  264.         chooser.destroy()
  265.         if selection == None:
  266.             return False
  267.         else:
  268.             return self.chdir(selection)
  269.  
  270.     def chdir(self, path, first_run = False):
  271.         """Tries to cd into the given path. If first_run == True it does not store the current values of important
  272.         fields like self.cd_str and self.cd_sub that are used to restore on failure if first_run == False. If there is
  273.         a failure and first_run == True, this method exits the program with return code 2."""
  274.         prev = None
  275.         prev_pos = None
  276.         if not first_run:
  277.             prev = self.cd_str
  278.             prev_pos = self.cd_sub
  279.         if first_run or not self.cd_str == path:
  280.             self.cd_sub = -1
  281.         else:
  282.             # As we call next_file() we need to go back one file first.
  283.             self.cd_sub -= 1
  284.         self.cd_str = path
  285.         self.cd = os.listdir(self.cd_str)
  286.         self.cd.sort()
  287.         # We cannot give a "zero move step" here as this can result in endless loops.
  288.         if not self.next_file():
  289.             if first_run:
  290.                 self.exit(exit_status = 2)
  291.             elif not self.chdir_gui():
  292.                 # Restore settings
  293.                 self.cd_str = prev
  294.                 self.cd_sub = prev_pos
  295.                 self.cd = os.listdir(prev)
  296.                 self.cd.sort()
  297.    
  298.     def exit(self, widget = None, exit_status = 0):
  299.         """Terminates the application with the given exit_status regardless of the widget parameter or the gtk main
  300.         loop being run or not."""
  301.         if gtk.main_level() == 0:
  302.             exit(exit_status)
  303.         else:
  304.             gtk.main_quit()
  305.             exit(exit_status)
  306.  
  307.     def __init__(self):
  308.         """Initializes GUI and fields. Tries to load the directory given via parameter 1 or to use the current directory
  309.         if no parameter is given."""
  310.         # Create window
  311.         gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
  312.         self.set_title("Ma_Sys.ma Plan View 0.1")
  313.         #self.modify_bg(gtk.STATE_SELECTED, gtk.gdk.Color(20, 20, 20))
  314.  
  315.         # Display modes:
  316.         # 1. non-scaled
  317.         # 2. scaled to fit width
  318.         # 3. scaled to fit height and width
  319.         # 4. specially scaled (see zoom variables)
  320.         self.display_mode = 1
  321.         self.zoom_width = -1
  322.         self.zoom_height = -1
  323.        
  324.         self.react_on_update = False
  325.         self.is_fullscreen = False
  326.  
  327.         # Create image container
  328.         self.current_image = gtk.Image()
  329.  
  330.         # Create scrollbar
  331.         # From http://stackoverflow.com/questions/6300816/
  332.         # prevent-scrollbars-from-showing-up-when-placing-a-drawing-area-inside-a-scrolled
  333.         self.scrollbar = gtk.ScrolledWindow()
  334.         self.scrollbar.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  335.         self.scrollbar.set_shadow_type(gtk.SHADOW_NONE)
  336.         #self.scrollbar.get_vscrollbar().connect("value_changed", self.handle_scroll_event)
  337.         self.scrollbar.connect("scroll_event", self.handle_scroll_event)
  338.         self.scrollbar.get_vscrollbar().get_adjustment().connect("changed", self.handle_scrollbar_updated_event)
  339.         # Create image drawing facility (viewport)
  340.         self.viewport = gtk.Viewport()
  341.         self.viewport.set_shadow_type(gtk.SHADOW_NONE)
  342.         self.viewport.add(self.current_image)
  343.         # Merge viewport and scrollbar
  344.         self.scrollbar.add(self.viewport)
  345.         self.add(self.scrollbar)
  346.  
  347.         # Connect events
  348.         self.connect("destroy", self.exit)
  349.         self.connect("key_press_event", self.handle_key_event)
  350.         self.set_events(gtk.gdk.KEY_PRESS_MASK)
  351.         self.connect("configure_event", self.handle_window_resize_event)
  352.  
  353.         # Set supported filetypes (tga and png are the most important)
  354.         self.filetypes = ["tga", "gif", "jpg", "jpeg", "png", "bmp", "tiff", "ico"]
  355.         # TODO allow runtime interpolation type change... ('z')
  356.  
  357.         # Determine startup directory and load image
  358.         if len(sys.argv) == 2:
  359.             self.chdir(sys.argv[1], True)
  360.         else:
  361.             self.chdir(".", True)
  362.  
  363.         # Display window
  364.         self.resize(1050, 900)
  365.         self.set_gravity(gtk.gdk.GRAVITY_CENTER)
  366.         self.show_all()
  367.         gtk.main()
  368.  
  369.     def get_license_small(self):
  370.         """Returns a short notice that is used if the GPL Text file is not available. This is required on e.g. Windows
  371.         Systems that certainly do not have an /usr directory."""
  372.         return """    Ma_Sys.ma Plan View: a simple keyboard only image viewer
  373.    Copyright (C) 2012  Ma_Sys.ma
  374.  
  375.    This program is free software: you can redistribute it and/or modify
  376.    it under the terms of the GNU General Public License as published by
  377.    the Free Software Foundation, either version 3 of the License, or
  378.    (at your option) any later version.
  379.  
  380.    This program is distributed in the hope that it will be useful,
  381.    but WITHOUT ANY WARRANTY; without even the implied warranty of
  382.    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  383.    GNU General Public License for more details.
  384.  
  385.    You should have received a copy of the GNU General Public License
  386.    along with this program.  If not, see <http://www.gnu.org/licenses/>."""
  387.  
  388. if __name__ == "__main__":
  389.     MaSysMaPlanView()
RAW Paste Data