Source code for tagit.view.browser

"""Display a grid filled with images.

.. NOTE::
    Always test scroll / selection behaviour with several
    grid sizes, e.g.
        1xN     1x5, 5x1
        NxN     1x1, 3x3, 5x5, 2x2
        NxM     1x2, 3x4, 2x3

Part of the tagit module.
A copy of the license is provided with the project.
Author: Matthias Baumgartner, 2016

"""
# imports
from os.path import join, dirname, exists
from math import floor, ceil
from kivy.lang import Builder
from kivy.uix.image import AsyncImage
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.logger import Logger
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.image import Image
from kivy.graphics.instructions import InstructionGroup
from kivy.graphics import Rectangle, Color, Line, PushMatrix, PopMatrix, Rotate, Scale
from kivy.resources import resource_find

# inner-module imports
from dialogues import TextInputDialogue
from ..controller.browser import CBrowser

# exports
__all__ = ('VBrowser', )

# Load kv
Builder.load_file(join(dirname(__file__), 'browser.kv'))

[docs]class VBrowser(BoxLayout): """Image browser. Guaranteed childs (needed for Sideboxes): * cursor Current image (*VImage* instance) * selection Selected images * get_displayed Displayed images .. todo:: BUGFIX: When scrolling over a page or by row, the cursor sometimes multiplies (check cursor update messages) FEATURE: Configurable orientation; Currently we only have top-down (vertical) layout. """ __events__ = ('on_cursor_change', 'on_selection_change') # Prototypes def on_cursor_change(self, cursor): pass def on_selection_change(self, selection): pass
[docs] def on_parent(self, *args): """Set the controller factory. Reinitializes parts of the browser. """ self.controller = self.get_controller(CBrowser) if self.controller is not None: # Initialize some variables # FIXME: Currently to be able to set img_{cols|rows} to None self.set_grid_size(self.controller.cols, self.controller.rows) super(VBrowser, self).on_parent(*args)
[docs] def on_touch_down(self, touch): """Scroll in browser.""" if self.collide_point(*touch.pos): if touch.button == 'scrollup': return self.controller.request_scrollup() elif touch.button == 'scrolldown': return self.controller.request_scrolldown() return super(VBrowser, self).on_touch_down(touch)
[docs] def set_grid_size(self, num_cols, num_rows): """Set the grid size (cols and rows) according to proposed values.""" # Note: No need to set both values. Leaving one as None results # in valid and different behaviour. However, at least one should be set. self.cols = num_cols self.rows = num_rows
[docs] def redraw(self, images, offset, n_results, page_size): """Update the displayed images.""" self.navigation_info = "{0} - {1} of {2}".format( offset + 1, # first on page min(offset + page_size, n_results), # last on page n_results # total results ) widgets = self.image_canvas.draw(self.controller, images) for wx in widgets: # bindings self.bind(on_cursor_change=wx.on_cursor_change) self.bind(on_selection_change=wx.on_selection_change)
[docs] def redraw_selection(self, selected_images): """Redraw selection decoration.""" self.dispatch('on_selection_change', selected_images)
[docs] def redraw_cursor(self, cursor): """Redraw cursor decoration.""" self.dispatch('on_cursor_change', cursor)
[docs] def update_status(self, status): """Update the status line.""" self.status_info = status
[docs] def displayed_images(self): """Return the images currently displayed.""" images = [img.image_ref for img in self.image_canvas.children] images.reverse() return images
def first_displayed_image(self): try: return self.image_canvas.children[-1].image_ref except IndexError: return None def last_displayed_image(self): try: return self.image_canvas.children[0].image_ref except IndexError: return None def tag_dialogue(self, callback, text=''): keyctrl = self.controller.get_root().widget dlg = TextInputDialogue(text=text, keyboard_ctrl=keyctrl) dlg.bind(on_ok=lambda c: callback(c.text)) dlg.open()
[docs] def next_page(self): """Click on the next page button.""" self.controller.request_next_page()
[docs] def previous_page(self): """Click on the previous page button.""" self.controller.request_previous_page()
[docs] def scroll_up(self): """Click on the scroll up button.""" self.controller.request_scrolldown()
[docs] def scroll_down(self): """Click on the scroll down button.""" self.controller.request_scrollup()
[docs] def go_first(self): """Click on the go first button.""" self.controller.request_first()
[docs] def go_last(self): """Click on the go last button.""" self.controller.request_last()
class ImageSpace(GridLayout): """Representation of a grid of images. Both axes are flexible, i.e. can be set freely or not be defined (FIXME: May not be true). The grid (3x4) may look like this: +-+-+-+-+ |0|1|2| | +-+-+-+-+ | | | | | +-+-+-+-+ | | | | | +-+-+-+-O Kivy defines the origin at the bottom right corner (O). The coordinate grid is built up accordingly. However, the image order starts with 0 at the top left corner. """ def draw(self, controller, images): """Draw the image widgets into the allocated space.""" # redraw widgets # FIXME: Instead of creating new images, we can change the *source* property of the existing images self.clear_widgets() if len(images) > 0: n_cols, n_rows = self.cols, self.rows if n_cols is None and n_rows is None: n_cols = n_rows = 1 elif n_cols is None: n_cols = ceil(1.0 * len(images) / n_rows) elif n_rows is None: n_rows = ceil(1.0 * len(images) / n_cols) res_w = 1.0 * self.width / min(len(images), n_cols) res_h = 1.0 * self.height / min(len(images), n_rows) for img in images: src = controller.model.get_image(img, (res_w, res_h)) ori = controller.model.attributes.orientation(img) if not exists(src): src = resource_find('image-missing.png') wx = VImage( source_obj=img , source=src , orientation=ori , touch_clbk=lambda arg: self.browser.controller.select(arg.image_ref, self.browser.controller.SELECT_MOUSE) # FIXME: Hacky ) self.add_widget(wx) return self.children class VImage(FloatLayout): """Just an image. Parameter *touch_clbk* is executed whenever a touch inside the image is registered. Parameter *source_obj* holds a copy to the database reference object. """ def __init__(self, source_obj, orientation, touch_clbk, **kwargs): source = kwargs.pop('source') super(VImage, self).__init__(**kwargs) self.image_ref = source_obj self.image_orientation = orientation self._touch_clbk = touch_clbk self._selectBox = None self._cursorBox = None self.source = source # Somehow this assignment is broken in the default constructor for the very first VImage created. def on_touch_down(self, touch): """Click on image.""" if self.collide_point(*touch.pos): if touch.button == 'left': # Set cursor Logger.debug('Touchdown in VImage ' + self.source) self._touch_clbk(self) return True return super(VImage, self).on_touch_down(touch) def on_size(self, *args): """Redraw the canvas.""" self._redraw() def on_texture(self, *args): """Redraw the canvas.""" self._redraw() def on_cursor_change(self, instance, image): """Act on cursor change. Changes the image effect. """ if image is not None and image == self.image_ref: if self._cursorBox is None: #Logger.debug('Cursor changed to ' + self.image_ref) self._cursorBox = InstructionGroup() self._cursorBox.add(Color(1, 1, 1, 1.0)) self._cursorBox.add(Line(width=5, rectangle=( self.x, self.y, self.width, self.height ))) #Logger.debug('Draw box at ({0},{1}) x ({2},{3})'.format(self.x, self.y, self.width, self.height)) self.canvas.after.add(self._cursorBox) elif self._cursorBox is not None: #Logger.debug('Curser removed from ' + self.image_ref) self.canvas.after.remove(self._cursorBox) self._cursorBox = None def on_selection_change(self, instance, selection): """Act on selection change. Changes the image effect. """ if self.image_ref in selection: if self._selectBox is None: #Logger.debug('Selected ' + self.image_ref) self._selectBox = InstructionGroup() self._selectBox.add(Color(0, 0, 1, 0.5)) self._selectBox.add(Rectangle( pos=(self.x, self.center_y - int(self.height / 2.0)), size=(self.width, self.height), )) self.canvas.after.add(self._selectBox) else: self.on_texture() elif self._selectBox is not None: #Logger.debug('Unselected ' + self.image_ref) self.canvas.after.remove(self._selectBox) self._selectBox= None def _redraw(self): """Redraw the cursor and selection highlights.""" # Fix orientation width, height = self.size if self.image_orientation in (2, 4, 5, 7): # Mirror self.image.mirror = True if self.image_orientation in (3, 4): # Rotate 180deg self.image.angle = 180 if self.image_orientation in (5, 6): # Rotate clockwise, 90 deg self.image.angle = -90 width, height = height, width if self.image_orientation in (7, 8): # Rotate counter-clockwise, 90 deg self.image.angle = 90 width, height = height, width self.image.size = (width, height) self.image.center = self.center # Redraw the cursor if self._cursorBox is not None: self._cursorBox.children[-1].rectangle = ( self.x, self.center_y - int(self.height / 2.0), self.width, self.height ) # Redraw the select box if self._selectBox is not None: # redraw self._selectBox.children[-1].pos = (self.x, self.center_y - int(self.height / 2.0)) self._selectBox.children[-1].size = (self.width, self.height) """ orientation == 1: > Normal camera pose. (Top/Left) No op orientation == 2: > Mirrored image. (Top/Right) Mirror on vertical axis orientation == 3: > Camera was upside-down. (Bottom/Right) Rotate 180deg orientation == 4: > Horizontally mirrored image. (Bottom/Left) Rotate 180deg Mirror on vertical axis Shortcut: Mirror on horizontal axis orientation == 5: > Camera tilt to the right, Mirrored (Left/Top) Rotate clockwise, 90deg Mirror on vertical axis orientation == 6: > Camera tilt to the right (Right/Top) Rotate clockwise, 90deg orientation == 7: > Camera tilt to the left, Mirrored (Right/Bottom) Rotate counter-clockwise, 90deg Mirror on vertical axis orientation == 8: > Camera tilt to the left (Left/Bottom) Rotate counter-clockwise, 90deg """ class VBrowserStatus_Button(ButtonBehavior, Image): pass ## EOF ##