import os
import re
import time
import math
from io import BytesIO
import PIL
from PIL import Image, ImageEnhance, ImageOps, ImageChops, ImageDraw, ImageFilter, ImageFile

from ffpyplayer.player import MediaPlayer
from ffpyplayer.pic import SWScale
from ffpyplayer import tools as fftools
from kivy.config import Config
Config.window_icon = "data/icon.png"
from kivy.app import App
from kivy.clock import Clock
from kivy.animation import Animation
from kivy.cache import Cache
from kivy.graphics.transformation import Matrix
from kivy.uix.bubble import Bubble
from kivy.uix.behaviors import ButtonBehavior, DragBehavior
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.togglebutton import ToggleButton
from kivy.properties import ObjectProperty, StringProperty, ListProperty, BooleanProperty, NumericProperty, DictProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scatterlayout import ScatterLayout
from kivy.uix.splitter import Splitter
from kivy.core.window import Window
from kivy.uix.popup import Popup
from kivy.uix.dropdown import DropDown
from kivy.uix.label import Label
from kivy.uix.treeview import TreeViewNode
from kivy.uix.image import Image as KivyImage
from kivy.core.image import Image as CoreImage
from kivy.core.image import ImageLoader
from kivy.uix.scrollview import ScrollView
from kivy.loader import Loader as ThumbLoader
from kivy.core.image.img_pil import ImageLoaderPIL
from kivy.uix.stencilview import StencilView
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.uix.slider import Slider

from generalconstants import *
from generalcommands import to_bool, isfile2, rotated_rect_with_max_area

from kivy.lang.builder import Builder
Builder.load_string("""
<SmallBufferY@Widget>:
    size_hint_y: None
    height: int(app.button_scale / 4)

<MediumBufferY@Widget>:
    size_hint_y: None
    height: int(app.button_scale / 2)

<LargeBufferY@Widget>:
    size_hint_y: None
    height: app.button_scale

<SmallBufferX@Widget>:
    size_hint_x: None
    width: int(app.button_scale / 4)

<MediumBufferX@Widget>:
    size_hint_x: None
    width: int(app.button_scale / 2)

<LargeBufferX@Widget>:
    size_hint_x: None
    width: app.button_scale

<HeaderBase@BoxLayout>:
    size_hint_y: None
    orientation: 'horizontal'

<Header@HeaderBase>:
    canvas.before:
        Color:
            rgba: app.theme.header_background
        Rectangle:
            size: self.size
            pos: self.pos
            source: 'data/headerbg.png'
    height: app.button_scale

<MainHeader@HeaderBase>:
    canvas.before:
        Color:
            rgba: app.theme.header_main_background
        Rectangle:
            size: self.size
            pos: self.pos
            source: 'data/headerbglight.png'
    height: int(app.button_scale * 1.25)
    padding: int(app.button_scale / 8)

<MainArea@BoxLayout>:
    canvas.before:
        Color:
            rgba: app.theme.main_background
        Rectangle:
            size: self.size
            pos: self.pos
            source: 'data/mainbg.png'

<NormalSlider>:
    #:set sizing 18
    canvas:
        Color:
            rgba: app.theme.slider_background
        BorderImage:
            border: (0, sizing, 0, sizing)
            pos: self.pos
            size: self.size
            source: 'data/sliderbg.png'
        Color:
            rgba: app.theme.slider_grabber
        Rectangle:
            pos: (self.value_pos[0] - app.button_scale/4, self.center_y - app.button_scale/2)
            size: app.button_scale/2, app.button_scale
            source: 'data/buttonflat.png'
    size_hint_y: None
    height: app.button_scale
    min: -1
    max: 1
    value: 0

<HalfSlider>:
    #:set sizing 18
    canvas:
        Color:
            rgba: app.theme.slider_background
        BorderImage:
            border: (0, sizing, 0, sizing)
            pos: self.pos
            size: self.size
            source: 'data/sliderbg.png'
        Color:
            rgba: app.theme.slider_grabber
        Rectangle:
            pos: (self.value_pos[0] - app.button_scale/4, self.center_y - app.button_scale/2)
            size: app.button_scale/2, app.button_scale
            source: 'data/buttonflat.png'
    size_hint_y: None
    height: app.button_scale
    min: 0
    max: 1
    value: 0

<HalfSliderLimited>:
    #:set sizing 18
    canvas:
        Color:
            rgba: app.theme.slider_background
        BorderImage:
            border: (0, sizing, 0, sizing)
            pos: self.pos
            size: self.size
            source: 'data/sliderbg.png'
        Color:
            rgba: 0, 0, 0, .5
        Rectangle:
            pos: 0, 0
            size: self.width * self.start, self.height
        Rectangle:
            pos: self.width * self.end, 0
            size: self.width * (1 - self.end), self.height
        Color:
            rgba: app.theme.slider_grabber
        Rectangle:
            pos: (self.value_pos[0] - app.button_scale/4, self.center_y - app.button_scale/2)
            size: app.button_scale/2, app.button_scale
            source: 'data/buttonflat.png'
    size_hint_y: None
    height: app.button_scale
    min: 0
    max: 1
    value: 0


<NormalLabel>:
    mipmap: True
    color: app.theme.text
    font_size: app.text_scale
    size_hint_y: None
    height: app.button_scale

<LeftNormalLabel@NormalLabel>:
    mipmap: True
    shorten: True
    shorten_from: 'right'
    font_size: app.text_scale
    size_hint_x: 1
    text_size: self.size
    halign: 'left'
    valign: 'middle'

<ShortLabel>:
    mipmap: True
    shorten: True
    shorten_from: 'right'
    font_size: app.text_scale
    size_hint_x: 1
    size_hint_max_x: self.texture_size[0] + 20
    #width: self.texture_size[0] + 20

<PhotoThumbLabel>:
    mipmap: True
    valign: 'middle'
    text_size: (self.width-10, self.height)
    size_hint_y: None
    size_hint_x: None
    height: (app.button_scale * 4)
    width: (app.button_scale * 4)
    text: ''

<InfoLabel>:
    canvas.before:
        Color:
            rgba: root.bgcolor
        Rectangle:
            pos: self.pos
            size: self.size
    mipmap: True
    text: app.infotext
    color: app.theme.info_text

<DatabaseLabel@ShortLabel>:
    mipmap: True
    text: app.database_update_text

<HeaderLabel@Label>:
    mipmap: True
    color: app.theme.header_text
    font_size: int(app.text_scale * 1.5)
    size_hint_y: None
    height: app.button_scale
    bold: True


<BubbleContent>:
    canvas:
        Clear:
    opacity: .7 if self.disabled else 1
    rows: 1

<InputMenu>:
    canvas.before:
        Color:
            rgba: app.theme.menu_background
        BorderImage:
            size: self.size
            pos: self.pos
            source: 'data/buttonflat.png'
    size_hint: None, None
    size: app.button_scale * 9, app.button_scale
    show_arrow: False
    MenuButton:
        text: 'Select All'
        on_release: root.select_all()
    MenuButton:
        text: 'Cut'
        on_release: root.cut()
    MenuButton:
        text: 'Copy'
        on_release: root.copy()
    MenuButton:
        text: 'Paste'
        on_release: root.paste()

<NormalInput>:
    mipmap: True
    cursor_color: app.theme.text
    write_tab: False
    background_color: app.theme.input_background
    hint_text_color: app.theme.disabled_text
    disabled_foreground_color: 1,1,1,.75
    foreground_color: app.theme.text
    size_hint_y: None
    height: app.button_scale
    font_size: app.text_scale

<FloatInput>:
    write_tab: False
    background_color: .2, .2, .3, .8
    disabled_foreground_color: 1,1,1,.75
    foreground_color: 1,1,1,1
    size_hint_y: None
    height: app.button_scale
    font_size: app.text_scale

<IntegerInput>:
    write_tab: False
    background_color: .2, .2, .3, .8
    disabled_foreground_color: 1,1,1,.75
    foreground_color: 1,1,1,1
    size_hint_y: None
    height: app.button_scale
    font_size: app.text_scale


<ButtonBase>:
    mipmap: True
    size_hint_y: None
    height: app.button_scale
    background_normal: 'data/button.png'
    background_down: 'data/button.png'
    background_disabled_down: 'data/button.png'
    background_disabled_normal: 'data/button.png'
    button_update: app.button_update

<NormalButton>:
    width: self.texture_size[0] + app.button_scale
    size_hint_x: None
    font_size: app.text_scale

<WideButton>:
    text_size: self.size
    halign: 'center'
    valign: 'middle'

<MenuButton>:
    menu: True
    size_hint_x: 1

<RemoveButton>:
    mipmap: True
    size_hint: None, None
    height: app.button_scale
    width: app.button_scale
    warn: True
    text: 'X'

<ExpandableButton>:
    cols: 1
    size_hint: 1, None
    height: self.minimum_height
    GridLayout:
        cols: 3
        size_hint: 1, None
        height: app.button_scale
        CheckBox:
            active: root.expanded
            size_hint: None, None
            height: app.button_scale
            width: app.button_scale
            background_checkbox_normal: 'data/tree_closed.png'
            background_checkbox_down: 'data/tree_opened.png'
            on_press: root.set_expanded(self.active)
        WideButton:
            on_press: root.dispatch('on_press')
            on_release: root.dispatch('on_release')
            text: root.text
        RemoveButton:
            on_release: root.dispatch('on_remove')
    GridLayout:
        canvas.before:
            Color:
                rgba: app.theme.menu_background
            BorderImage:
                pos: self.pos
                size: self.size
                source: 'data/buttonflat.png'
        padding: app.padding
        cols: 1
        size_hint: 1, None
        #height: self.minimum_height
        height: app.padding * 2
        opacity: 0
        id: contentContainer

<TreeViewButton>:
    color_selected: app.theme.selected
    odd_color: app.list_background_odd
    even_color: app.list_background_even
    orientation: 'vertical'
    size_hint_y: None
    height: app.button_scale
    NormalLabel:
        mipmap: True
        markup: True
        text_size: (self.width - 20, None)
        halign: 'left'
        text: root.folder_name + '   [b]' + root.total_photos + '[/b]'
    NormalLabel:
        mipmap: True
        id: subtext
        text_size: (self.width - 20, None)
        font_size: app.text_scale
        color: .66, .66, .66, 1
        halign: 'left'
        size_hint_y: None
        height: 0
        text: root.subtext

<MenuStarterButton@ButtonBase>:
    canvas.after:
        Color:
            rgba: self.color
        Rectangle:
            pos: (root.pos[0]+root.width-(root.height/1.5)), root.pos[1]
            size: root.height/2, root.height
            source: 'data/menuarrows.png'
    menu: True
    size_hint_y: None
    height: app.button_scale
    shorten: True
    shorten_from: 'right'
    font_size: app.text_scale
    size_hint_max_x: self.texture_size[0] + (app.button_scale * 1.2)

<MenuStarterButtonWide@ButtonBase>:
    canvas.after:
        Color:
            rgba: self.color
        Rectangle:
            pos: (root.pos[0]+root.width-(root.height/1.5)), root.pos[1]
            size: root.height/2, root.height
            source: 'data/menuarrows.png'
    menu: True
    size_hint_y: None
    height: app.button_scale
    text_size: self.size
    halign: 'center'
    valign: 'middle'
    shorten: True
    shorten_from: 'right'
    font_size: app.text_scale
    size_hint_x: 1

<NormalToggle@ToggleBase>:
    toggle: True
    size_hint_x: None
    width: self.texture_size[0] + 20

<ReverseToggle@ToggleBase>:
    canvas:
        Color:
            rgba: self.color
        Rectangle:
            pos: self.pos
            size: self.size
            source: 'data/arrowdown.png' if self.state == 'normal' else 'data/arrowup.png'
    menu: True
    size_hint: None, None
    height: app.button_scale
    width: app.button_scale

<SettingsButton@NormalButton>:
    text: '' if app.simple_interface else 'Settings'
    border: (0, 0, 0, 0) if app.simple_interface else (16, 16, 16, 16)
    background_normal: 'data/settings.png' if app.simple_interface else 'data/button.png'
    background_down: 'data/settings.png' if app.simple_interface else 'data/button.png'
    on_release: app.open_settings()

<VerticalButton>:
    size_hint_y: None
    width: app.button_scale
    size_hint_x: None
    font_size: app.text_scale
    height: textArea.texture_size[0] + 100
    background_down: 'data/buttonright.png'
    Label:
        id: textArea
        center: self.parent.center
        canvas.before:
            PushMatrix
            Rotate:
                angle: 90
                axis: 0,0,1
                origin: self.center
        canvas.after:
            PopMatrix
        color: self.parent.color
        text: self.parent.vertical_text

<PhotoRecycleViewButton>:
    canvas.after:
        Color:
            rgba: (1, 1, 1, 0) if self.found else(1, 0, 0, .33)
        Rectangle:
            pos: self.pos
            size: self.size
        Color:
            rgba: app.theme.favorite if self.favorite else [0, 0, 0, 0]
        Rectangle:
            source: 'data/star.png'
            pos: (self.pos[0]+(self.width-(self.height*.5)), self.pos[1]+(self.height*.5)-(self.height*.167))
            size: (self.height*.33, self.height*.33)
        Color:
            rgba: 1, 1, 1, .5 if self.video else 0
        Rectangle:
            source: 'data/play_overlay.png'
            pos: (self.pos[0]+(self.height*.25)), (self.pos[1]+(self.height*.25))
            size: (self.height*.5), (self.height*.5)
    size_hint_x: 1
    height: (app.button_scale * 2)
    AsyncThumbnail:
        id: thumbnail
        #photoinfo: root.photoinfo
        #source: root.source
        size_hint: None, None
        width: (app.button_scale * 2)
        height: (app.button_scale * 2)
    NormalLabel:
        mipmap: True
        size_hint_y: None
        height: (app.button_scale * 2)
        text_size: (self.width - 20, None)
        text: root.text
        halign: 'left'
        valign: 'center'


<NormalPopup>:
    canvas.before:
        Color:
            rgba: 0, 0, 0, .75 * self._anim_alpha
        Rectangle:
            size: self._window.size if self._window else (0, 0)
        Color:
            rgba: app.theme.sidebar_background
        Rectangle:
            size: self.size
            pos: self.pos
            source: 'data/panelbg.png'
    background_color: 1, 1, 1, 0
    background: 'data/transparent.png'
    separator_color: 1, 1, 1, .25
    title_size: app.text_scale * 1.25
    title_color: app.theme.header_text

<MessagePopup>:
    cols:1
    NormalLabel:
        text: root.text
    Label:
    GridLayout:
        cols:1
        size_hint_y: None
        height: app.button_scale
        WideButton:
            id: button
            text: root.button_text
            on_release: root.close()

<InputPopup>:
    cols:1
    NormalLabel:
        text: root.text
    NormalInput:
        id: input
        multiline: False
        hint_text: root.hint
        input_filter: app.test_album
        text: root.input_text
        focus: True
    Label:
    GridLayout:
        cols: 2
        size_hint_y: None
        height: app.button_scale
        WideButton:
            text: 'OK'
            on_release: root.dispatch('on_answer','yes')
        WideButton:
            text: 'Cancel'
            on_release: root.dispatch('on_answer', 'no')

<InputPopupTag>:
    cols:1
    NormalLabel:
        text: root.text
    NormalInput:
        id: input
        multiline: False
        hint_text: root.hint
        input_filter: app.test_tag
        text: root.input_text
        focus: True
    Label:
    GridLayout:
        cols: 2
        size_hint_y: None
        height: app.button_scale
        WideButton:
            text: 'OK'
            on_release: root.dispatch('on_answer','yes')
        WideButton:
            text: 'Cancel'
            on_release: root.dispatch('on_answer', 'no')

<ScanningPopup>:
    GridLayout:
        cols: 1
        NormalLabel:
            id: scanningText
            text: root.scanning_text
            text_size: self.size
        ProgressBar:
            id: scanningProgress
            value: root.scanning_percentage
            max: 100
        WideButton:
            id: scanningButton
            text: root.button_text

<ConfirmPopup>:
    cols:1
    NormalLabel:
        text: root.text
    Label:
    GridLayout:
        cols: 2
        size_hint_y: None
        height: app.button_scale
        WideButton:
            text: root.yes_text
            on_release: root.dispatch('on_answer','yes')
            warn: root.warn_yes
        WideButton:
            text: root.no_text
            on_release: root.dispatch('on_answer', 'no')
            warn: root.warn_no


<NormalDropDown>:
    canvas.before:
        Color:
            rgba: app.theme.menu_background
        Rectangle:
            size: root.width, root.height * root.show_percent
            pos: root.pos[0], root.pos[1] + (root.height * (1 - root.show_percent)) if root.invert else root.pos[1]
            source: 'data/buttonflat.png'

<AlbumSortDropDown>:
    MenuButton:
        text: 'Name'
        on_release: root.select(self.text)
    MenuButton:
        text: 'Path'
        on_release: root.select(self.text)
    MenuButton:
        text: 'Imported'
        on_release: root.select(self.text)
    MenuButton:
        text: 'Modified'
        on_release: root.select(self.text)

<AlbumExportDropDown>:
    MenuButton:
        text: 'Create Collage'
        disabled: not app.can_export
        on_release: root.dismiss()
        on_release: app.screen_manager.current_screen.collage_screen()
    MenuButton:
        text: 'Export'
        disabled: not app.can_export
        on_release: root.dismiss()
        on_release: app.screen_manager.current_screen.export_screen()


<RecycleItem>:
    canvas.before:
        Color:
            rgba: self.bgcolor
        Rectangle:
            pos: self.pos
            size: self.size
    size_hint_x: 1
    height: app.button_scale

<SimpleRecycleItem@RecycleItem>:
    NormalLabel:
        size_hint_y: None
        height: app.button_scale
        text_size: (self.width - 20, None)
        text: root.text
        halign: 'left'
        valign: 'center'

<PhotoRecycleThumb>:
    canvas.before:
        Color:
            rgba: self.underlay_color
            #rgba: app.theme.selected if self.selected else (0, 0, 0, 0)
        Rectangle:
            pos: (self.pos[0]-5, self.pos[1]-5)
            size: (self.size[0]+10, self.size[1]+10)
    canvas.after:
        Color:
            rgba: (1, 1, 1, 0) if self.found else(1, 0, 0, .33)
        Rectangle:
            pos: self.pos
            size: self.size
        Color:
            rgba: app.theme.favorite if root.favorite else [0, 0, 0, 0]
        Rectangle:
            source: 'data/star.png'
            pos: (self.pos[0]+(self.size[0]/2)-(self.size[0]*.05), self.pos[1]+(self.size[0]*.1))
            size: (self.size[0]*.1, self.size[0]*.1)
        Color:
            rgba: 1, 1, 1, .5 if root.video else 0
        Rectangle:
            source: 'data/play_overlay.png'
            pos: (self.pos[0]+self.width/8, self.pos[1]+self.width/8) if self.title else (self.pos[0]+self.width/4, self.pos[1]+self.width/4)
            size: (self.width/4, self.width/4) if self.title else (self.width/2, self.width/2)

    drag_rectangle: self.x, self.y, self.width, self.height
    drag_timeout: 10000000
    drag_distance: 0
    width: (app.button_scale * 4)
    height: (app.button_scale * 4)
    size_hint_y: None
    size_hint_x: None
    orientation: 'horizontal'
    AsyncThumbnail:
        id: thumbnail
        width: self.height
        size_hint_x: None

<PhotoRecycleThumbWide>:
    PhotoThumbLabel:
        text: root.title

<RecycleTreeViewButton>:
    orientation: 'vertical'
    size_hint_y: None
    #height: int((app.button_scale * 1.5 if self.subtext else app.button_scale) + (app.button_scale * .1 if self.end else 0))
    BoxLayout:
        orientation: 'horizontal'
        Widget:
            width: (app.button_scale * .25) + (app.button_scale * 0.5 * root.indent)
            size_hint_x: None
        Image:
            width: self.texture_size[0]
            size_hint_x: None
            source: 'data/tree_opened.png' if root.expanded else 'data/tree_closed.png'
            opacity: 1 if root.expandable else 0
        BoxLayout:
            orientation: 'vertical'
            NormalLabel:
                id: mainText
                markup: True
                text_size: (self.width - 20, None)
                halign: 'left'
                text: ''
            NormalLabel:
                id: subtext
                text_size: (self.width - 20, None)
                font_size: app.text_scale
                color: .66, .66, .66, 1
                halign: 'left'
                size_hint_y: None
                height: app.button_scale * .5 if root.subtext else 0
                text: root.subtext
    Widget:
        canvas.before:
            Color:
                rgba: 0, 0, 0, .2 if root.end else 0
            Rectangle:
                pos: self.pos
                size: self.size
        size_hint_y: None
        height: int(app.button_scale * .1) if root.end else 0

<TreenodeDrag>:
    canvas.before:
        Color:
            rgba: (.2, .2, .4, .4)
        Rectangle:
            pos: self.pos
            size: self.size
    orientation: 'vertical'
    size_hint_x: None
    width: 100
    size_hint_y: None
    height: app.button_scale
    NormalLabel:
        text_size: (self.width - 20, None)
        halign: 'left'
        text: root.text
    NormalLabel:
        id: subtext
        text_size: (self.width - 20, None)
        font_size: app.text_scale
        color: .66, .66, .66, 1
        halign: 'left'
        size_hint_y: None
        height: 0
        text: root.subtext

<SelectableRecycleBoxLayout>:
    default_size_hint: 1, None
    default_size: self.width, app.button_scale
    spacing: 2
    size_hint_x: 1
    orientation: 'vertical'
    size_hint_y: None
    height: self.minimum_height
    multiselect: False
    touch_multiselect: False

<SelectableRecycleGrid>:
    cols: max(1, int(self.width / ((app.button_scale * 4 * self.scale) + (app.button_scale / 2))))
    spacing: int(app.button_scale / 2)
    padding: int(app.button_scale / 2)
    focus: False
    touch_multiselect: True
    multiselect: True
    default_size: app.button_scale * 4 * self.scale, app.button_scale * 4 * self.scale
    default_size_hint: None, None
    height: self.minimum_height
    size_hint_y: None

<SelectableRecycleGridWide@SelectableRecycleGrid>:
    cols: max(1, int(self.width / ((app.button_scale * 8) + (app.button_scale / 2))))
    default_size: (app.button_scale * 8), (app.button_scale * 4)

<NormalRecycleView>:
    size_hint: 1, 1
    do_scroll_x: False
    do_scroll_y: True
    scroll_distance: 10
    scroll_timeout: 200
    bar_width: int(app.button_scale * .5)
    bar_color: app.theme.scroller_selected
    bar_inactive_color: app.theme.scroller
    scroll_type: ['bars', 'content']

<NormalTreeView@TreeView>:
    color_selected: app.theme.selected
    odd_color: app.list_background_odd
    even_color: app.list_background_even
    indent_level: int(app.button_scale * .5)
    size_hint: 1, None
    height: self.minimum_height
    hide_root: True


<SplitterResizer>:
    background_color: app.theme.sidebar_resizer
    background_normal: 'data/splitterbgup.png'
    background_down: 'data/splitterbgdown.png'
    border: 0, 0, 0, 0

<SplitterPanel>:
    canvas.before:
        Color:
            rgba: app.theme.sidebar_background
        Rectangle:
            size: self.size
            pos: self.pos
            source: 'data/panelbg.png'
    #keep_within_parent: True
    min_size: int(app.button_scale / 2)
    size_hint: None, 1
    strip_size: int(app.button_scale / 3)

<SplitterPanelLeft>:
    width: self.display_width
    disabled: self.hidden
    sizable_from: 'right'

<SplitterPanelRight>:
    width: self.display_width
    disabled: self.hidden
    sizable_from: 'left'


<CustomImage>:
    allow_stretch: True

<AsyncThumbnail>:
    canvas.before:
        PushMatrix
        Rotate:
            angle: self.angle
            axis: 0,0,1
            origin: self.center
    canvas.after:
        PopMatrix
    allow_stretch: True

<PhotoDrag>:
    height: (app.button_scale * 4)
    width: (app.button_scale * 4)
    size_hint_y: None
    size_hint_x: None
    Image:
        canvas.before:
            PushMatrix
            Rotate:
                angle: root.angle
                axis: 0,0,1
                origin: root.center
        canvas.after:
            PopMatrix
        id: image
        pos: root.pos
        size: root.size
        size_hint: None, None
        source: root.source
        fullpath: root.fullpath
    ShortLabel:
        pos: root.pos
        text: root.total_drags


<Scroller>:
    scroll_distance: 10
    scroll_timeout: 200
    bar_width: int(app.button_scale * .5)
    bar_color: app.theme.scroller_selected
    bar_inactive_color: app.theme.scroller
    scroll_type: ['bars', 'content']

<ColorPickerCustom_Label@Label>:
    mroot: None
    size_hint_x: None
    width: '30sp'
    text_size: self.size
    halign: "center"
    valign: "middle"

<ColorPickerCustom_Selector@BoxLayout>:
    foreground_color: None
    text: ''
    mroot: None
    mode: 'rgb'
    color: 0
    spacing: '2sp'
    ColorPickerCustom_Label:
        text: root.text
        mroot: root.mroot
        color: root.foreground_color or (1, 1, 1, 1)
    Slider:
        id: sldr
        size_hint: 1, .25
        pos_hint: {'center_y':.5}
        range: 0, 255
        value: root.color * 255
        on_value:
            root.mroot._trigger_update_clr(root.mode, root.clr_idx, args[1])

<ColorPickerCustom>:
    canvas.before:
        Color:
            rgba: self.color
        Rectangle:
            pos: self.pos
            size: self.size

    orientation: 'vertical'
    size_hint_y: None
    height: sp(33)*10 if self. orientation == 'vertical' else sp(33)*5
    foreground_color: (1, 1, 1, 1) if self.hsv[2] * wheel.a < .5 else (0, 0, 0, 1)
    wheel: wheel
    BoxLayout:
        orientation: root.orientation
        spacing: '5sp'
        ColorWheel:
            id: wheel
            color: root.color
            on_color: root.color[:3] = args[1][:3]
        GridLayout:
            cols: 1
            size_hint_y: None
            height: self.minimum_height
            canvas:
                Color:
                    rgba: root.color
                Rectangle:
                    size: self.size
                    pos: self.pos

            ColorPickerCustom_Selector:
                mroot: root
                text: 'R'
                clr_idx: 0
                color: wheel.r
                foreground_color: root.foreground_color
                size_hint_y: None
                height: 0
                disabled: True
                opacity: 0

            ColorPickerCustom_Selector:
                mroot: root
                mode: 'hsv'
                text: 'H'
                clr_idx: 0
                color: root.hsv[0]
                foreground_color: root.foreground_color
                size_hint_y: None
                height: app.button_scale

            ColorPickerCustom_Selector:
                mroot: root
                mode: 'hsv'
                text: 'S'
                clr_idx: 1
                color: root.hsv[1]
                foreground_color: root.foreground_color
                size_hint_y: None
                height: app.button_scale

            ColorPickerCustom_Selector:
                mroot: root
                mode: 'hsv'
                text: 'V'
                clr_idx: 2
                color: root.hsv[2]
                foreground_color: root.foreground_color
                size_hint_y: None
                height: app.button_scale

""")


#Misc ELements
class SpecialSlider(Slider):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos) and touch.is_double_tap:
            self.reset_value()
            return
        super(SpecialSlider, self).on_touch_down(touch)

    def reset_value(self, *_):
        pass


class HalfSlider(SpecialSlider):
    pass


class NormalSlider(SpecialSlider):
    pass


class InputMenu(Bubble):
    owner = ObjectProperty()

    def on_touch_down(self, touch):
        if not self.collide_point(*touch.pos):
            app = App.get_running_app()
            app.close_bubble()
        else:
            super(InputMenu, self).on_touch_down(touch)

    def select_all(self, *_):
        if self.owner:
            app = App.get_running_app()
            self.owner.select_all()
            app.close_bubble()

    def cut(self, *_):
        if self.owner:
            app = App.get_running_app()
            self.owner.cut()
            app.close_bubble()

    def copy(self, *_):
        if self.owner:
            app = App.get_running_app()
            self.owner.copy()
            app.close_bubble()

    def paste(self, *_):
        if self.owner:
            app = App.get_running_app()
            self.owner.paste()
            app.close_bubble()


class NormalInput(TextInput):
    messed_up_coords = BooleanProperty(False)
    long_press_time = NumericProperty(1)
    long_press_clock = None
    long_press_pos = None

    def on_touch_up(self, touch):
        if self.long_press_clock:
            self.long_press_clock.cancel()
            self.long_press_clock = None
        super(NormalInput, self).on_touch_up(touch)

    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            pos = self.to_window(*touch.pos)
            self.long_press_clock = Clock.schedule_once(self.do_long_press, self.long_press_time)
            self.long_press_pos = pos
            if touch.button == 'right':
                app = App.get_running_app()

                app.popup_bubble(self, pos)
                return
        super(NormalInput, self).on_touch_down(touch)

    def do_long_press(self, *_):
        app = App.get_running_app()
        app.popup_bubble(self, self.long_press_pos)


class HalfSliderLimited(SpecialSlider):
    start = NumericProperty(0.0)
    end = NumericProperty(1.0)


class StencilViewTouch(StencilView):
    """Custom StencilView that stencils touches as well as visual elements."""

    def on_touch_down(self, touch):
        """Modified to only register touch down events when inside stencil area."""
        if self.collide_point(*touch.pos):
            super(StencilViewTouch, self).on_touch_down(touch)


class LimitedScatterLayout(ScatterLayout):
    """Custom ScatterLayout that won't allow sub-widgets to be moved out of the visible area,
    and will not respond to touches outside of the visible area.
    """

    bypass = BooleanProperty(False)

    def on_bypass(self, instance, bypass):
        if bypass:
            self.transform = Matrix()

    def on_transform_with_touch(self, touch):
        """Modified to not allow widgets to be moved out of the visible area."""

        width = self.bbox[1][0]
        height = self.bbox[1][1]
        scale = self.scale

        local_bottom = self.bbox[0][1]
        local_left = self.bbox[0][0]
        local_top = local_bottom+height
        local_right = local_left+width

        local_xmax = width/scale
        local_xmin = 0
        local_ymax = height/scale
        local_ymin = 0

        if local_right < local_xmax:
            self.transform[12] = local_xmin - (width - local_xmax)
        if local_left > local_xmin:
            self.transform[12] = local_xmin
        if local_top < local_ymax:
            self.transform[13] = local_ymin - (height - local_ymax)
        if local_bottom > local_ymin:
            self.transform[13] = local_ymin

    def on_touch_down(self, touch):
        """Modified to only register touches in visible area."""

        if self.bypass:
            for child in self.children[:]:
                if child.dispatch('on_touch_down', touch):
                    return True
        else:
            if self.collide_point(*touch.pos):
                super(LimitedScatterLayout, self).on_touch_down(touch)


#Labels
class NormalLabel(Label):
    """Basic label widget"""
    pass


class ShortLabel(NormalLabel):
    """Label widget that will remain the minimum width"""
    pass


class PhotoThumbLabel(NormalLabel):
    pass


class InfoLabel(ShortLabel):
    bgcolor = ListProperty([1, 1, 0, 0])
    blinker = ObjectProperty()

    def on_text(self, instance, text):
        del instance
        app = App.get_running_app()
        if self.blinker:
            self.stop_blinking()
        if text:
            no_bg = [.5, .5, .5, 0]
            yes_bg = app.theme.info_background
            self.blinker = Animation(bgcolor=yes_bg, duration=0.33) + Animation(bgcolor=no_bg, duration=0.33) + Animation(bgcolor=yes_bg, duration=0.33) + Animation(bgcolor=no_bg, duration=0.33) + Animation(bgcolor=yes_bg, duration=0.33) + Animation(bgcolor=no_bg, duration=0.33) + Animation(bgcolor=yes_bg, duration=0.33) + Animation(bgcolor=no_bg, duration=0.33) + Animation(bgcolor=yes_bg, duration=0.33) + Animation(bgcolor=no_bg, duration=0.33) + Animation(bgcolor=yes_bg, duration=0.33) + Animation(bgcolor=no_bg, duration=0.33) + Animation(bgcolor=yes_bg, duration=0.33) + Animation(bgcolor=no_bg, duration=0.33)
            self.blinker.start(self)

    def stop_blinking(self, *_):
        if self.blinker:
            self.blinker.cancel(self)
        self.bgcolor = [1, 1, 0, 0]


#Text Inputs
class FloatInput(TextInput):
    pat = re.compile('[^0-9]')

    def insert_text(self, substring, from_undo=False):
        pat = self.pat
        if '.' in self.text:
            s = re.sub(pat, '', substring)
        else:
            s = '.'.join([re.sub(pat, '', s) for s in substring.split('.', 1)])
        return super(FloatInput, self).insert_text(s, from_undo=from_undo)


class IntegerInput(TextInput):
    pat = re.compile('[^0-9]')

    def insert_text(self, substring, from_undo=False):
        pat = self.pat
        s = re.sub(pat, '', substring)
        return super(IntegerInput, self).insert_text(s, from_undo=from_undo)


#RecycleView and Lists
class RecycleItem(RecycleDataViewBehavior, BoxLayout):
    bgcolor = ListProperty([0, 0, 0, 0])
    owner = ObjectProperty()
    text = StringProperty()
    selected = BooleanProperty(False)
    index = None
    data = {}

    def on_selected(self, *_):
        self.set_color()

    def set_color(self):
        app = App.get_running_app()

        if self.selected:
            self.bgcolor = app.theme.selected
        else:
            if self.index % 2 == 0:
                self.bgcolor = app.list_background_even
            else:
                self.bgcolor = app.list_background_odd

    def refresh_view_attrs(self, rv, index, data):
        self.index = index
        self.data = data
        self.set_color()
        return super(RecycleItem, self).refresh_view_attrs(rv, index, data)

    def apply_selection(self, rv, index, is_selected):
        self.selected = is_selected

    def on_touch_down(self, touch):
        if super(RecycleItem, self).on_touch_down(touch):
            return True
        if self.collide_point(*touch.pos):
            self.parent.selected = self.data
            try:
                self.owner.select(self)
            except:
                pass
            return True


class PhotoRecycleThumb(DragBehavior, BoxLayout, RecycleDataViewBehavior):
    """Wrapper widget for image thumbnails.  Used for displaying images in grid views."""

    underlay_color = ListProperty([0, 0, 0, 0])
    found = BooleanProperty(True)  # Used to add a red overlay to the thumbnail if the source file doesn't exist
    owner = ObjectProperty()
    target = StringProperty()
    type = StringProperty('None')
    filename = StringProperty()
    fullpath = StringProperty()
    folder = StringProperty()
    database_folder = StringProperty()
    selected = BooleanProperty(False)
    drag = False
    dragable = BooleanProperty(True)
    image = ObjectProperty()
    photo_orientation = NumericProperty(1)
    angle = NumericProperty(0)  # used to display the correct orientation of the image
    favorite = BooleanProperty(False)  # if True, a star overlay will be displayed on the image
    video = BooleanProperty(False)
    source = StringProperty()
    photoinfo = ListProperty()
    temporary = BooleanProperty(False)
    title = StringProperty('')
    view_album = BooleanProperty(True)
    mirror = BooleanProperty(False)
    index = NumericProperty(0)
    data = {}

    def on_selected(self, *_):
        app = App.get_running_app()
        if self.selected:
            new_color = app.theme.selected
        else:
            new_color = [0, 0, 0, 0]
        if app.animations:
            anim = Animation(underlay_color=new_color, duration=app.animation_length)
            anim.start(self)
        else:
            self.underlay_color = new_color

    def refresh_view_attrs(self, rv, index, data):
        """Called when widget is loaded into recycleview layout"""
        self.index = index
        self.data = data
        thumbnail = self.ids['thumbnail']
        thumbnail.temporary = self.data['temporary']
        thumbnail.photoinfo = self.data['photoinfo']
        thumbnail.source = self.data['source']
        self.image = thumbnail
        return super(PhotoRecycleThumb, self).refresh_view_attrs(rv, index, data)

    def on_source(self, *_):
        """Sets up the display image when first loaded."""

        found = isfile2(self.source)
        self.found = found
        if self.photo_orientation in [2, 4, 5, 7]:
            self.mirror = True
        else:
            self.mirror = False
        if self.photo_orientation == 3 or self.photo_orientation == 4:
            self.angle = 180
        elif self.photo_orientation == 5 or self.photo_orientation == 6:
            self.angle = 270
        elif self.photo_orientation == 7 or self.photo_orientation == 8:
            self.angle = 90
        else:
            self.angle = 0

    def apply_selection(self, rv, index, is_selected):
        self.selected = is_selected

    def on_touch_down(self, touch):
        super().on_touch_down(touch)
        if self.collide_point(*touch.pos):
            if touch.is_double_tap:
                if not self.temporary and self.view_album:
                    app = App.get_running_app()
                    if not app.shift_pressed:
                        app.show_album(self)
                        return
            app = App.get_running_app()
            if app.shift_pressed:
                self.parent.select_range(self.index, touch)
                return
            self.parent.select_with_touch(self.index, touch)
            self.owner.update_selected()
            if self.dragable:
                thumbnail = self.ids['thumbnail']
                self.drag = True
                app = App.get_running_app()
                temp_coords = self.to_parent(touch.opos[0], touch.opos[1])
                widget_coords = (temp_coords[0] - thumbnail.pos[0], temp_coords[1] - thumbnail.pos[1])
                window_coords = self.to_window(touch.pos[0], touch.pos[1])
                try:
                    num_photos = self.owner.get_selected_photos(fullpath=True)
                    num_photos.append(self.fullpath)
                    num_photos = len(set(num_photos))
                except:
                    num_photos = 1
                app.drag(self, 'start', window_coords, image=self.image, offset=widget_coords, fullpath=self.fullpath, photos=num_photos)

    def on_touch_move(self, touch):
        #super().on_touch_move(touch)
        if self.drag:
            if not self.selected:
                self.parent.select_node(self.index)
                self.owner.update_selected()
            app = App.get_running_app()
            window_coords = self.to_window(touch.pos[0], touch.pos[1])
            app.drag(self, 'move', window_coords)

    def on_touch_up(self, touch):
        super().on_touch_up(touch)
        if self.drag:
            app = App.get_running_app()
            window_coords = self.to_window(touch.pos[0], touch.pos[1])
            app.drag(self, 'end', window_coords)
            self.drag = False


class PhotoRecycleThumbWide(PhotoRecycleThumb):
    pass


class RecycleTreeViewButton(ButtonBehavior, RecycleItem):
    """Widget that displays a specific folder, album, or tag in the database treeview.
    Responds to clicks and double-clicks.
    """

    displayable = BooleanProperty(True)
    target = StringProperty()  #Folder, Album, or Tag
    fullpath = StringProperty()  #Folder name, used only on folder type targets
    folder = StringProperty()
    database_folder = StringProperty()
    type = StringProperty()  #The type the target is: folder, album, tag, extra
    total_photos = StringProperty()
    folder_name = StringProperty()
    subtext = StringProperty()
    total_photos_numeric = NumericProperty(0)
    drag = False
    dragable = BooleanProperty(False)
    droptype = StringProperty('folder')
    indent = NumericProperty(0)
    expanded = BooleanProperty(True)
    expandable = BooleanProperty(False)
    end = BooleanProperty(False)

    def refresh_view_attrs(self, rv, index, data):
        """Called when widget is loaded into recycleview layout"""

        app = App.get_running_app()
        self.total_photos_numeric = 0
        if data['displayable']:
            photo_type = data['type']
            if photo_type == 'Folder':
                fullpath = data['fullpath']
                if fullpath:
                    photos = app.database_get_folder(data['fullpath'])
                    self.total_photos_numeric = len(photos)
            elif photo_type == 'Album':
                for album in app.albums:
                    if album['name'] == data['target']:
                        self.total_photos_numeric = len(album['photos'])
                        break
            elif photo_type == 'Tag':
                photos = app.database_get_tag(data['target'])
                self.total_photos_numeric = len(photos)
        if self.total_photos_numeric > 0:
            self.total_photos = '(' + str(self.total_photos_numeric) + ')'
        else:
            self.total_photos = ''
        self.ids['mainText'].text = data['folder_name'] + '   [b]' + self.total_photos + '[/b]'
        return super(RecycleTreeViewButton, self).refresh_view_attrs(rv, index, data)

    def on_touch_down(self, touch):
        app = App.get_running_app()
        if self.collide_point(*touch.pos):
            if touch.is_double_tap and not app.shift_pressed:
                if self.displayable:
                    if self.total_photos_numeric > 0:
                        app.show_album(self)
            else:
                self.parent.selected = {}
                self.parent.selected = self.data
                self.on_press()
            if self.dragable:
                self.drag = True
                app = App.get_running_app()
                temp_coords = self.to_parent(touch.opos[0], touch.opos[1])
                widget_coords = (temp_coords[0]-self.pos[0], temp_coords[1]-self.pos[1])
                window_coords = self.to_window(touch.opos[0], touch.opos[1])
                app.drag_treeview(self, 'start', window_coords, offset=widget_coords)

    def on_press(self):
        self.owner.type = self.type
        self.owner.displayable = self.displayable
        #self.owner.set_selected(self.target)
        self.owner.selected = ''
        self.owner.selected = self.target

    def on_release(self):
        if self.expandable:
            if self.type == 'Album':
                self.owner.expanded_albums = not self.owner.expanded_albums
            elif self.type == 'Tag':
                self.owner.expanded_tags = not self.owner.expanded_tags
            elif self.type == 'Folder':
                self.owner.toggle_expanded_folder(self.target)
            self.owner.update_treeview()

    def on_touch_move(self, touch):
        if self.drag:
            delay = time.time() - touch.time_start
            if delay >= drag_delay:
                app = App.get_running_app()
                window_coords = self.to_window(touch.pos[0], touch.pos[1])
                app.drag_treeview(self, 'move', window_coords)

    def on_touch_up(self, touch):
        if self.collide_point(*touch.pos) and self.collide_point(*touch.opos):
            self.on_release()
        if self.drag:
            app = App.get_running_app()
            window_coords = self.to_window(touch.pos[0], touch.pos[1])
            app.drag_treeview(self, 'end', window_coords)
            self.drag = False


class TreenodeDrag(BoxLayout):
    """Widget that looks like a treenode thumbnail, used for showing the position of the drag-n-drop."""

    fullpath = StringProperty()
    text = StringProperty()
    subtext = StringProperty()


class SelectableRecycleBoxLayout(RecycleBoxLayout, LayoutSelectionBehavior):
    """Adds selection and focus behavior to the view."""
    selected = DictProperty()
    selects = ListProperty()
    multiselect = BooleanProperty(False)

    def select_range(self, *_):
        if self.multiselect:
            select_index = self.parent.data.index(self.selected)
            selected_nodes = []
            if self.selects:
                for select in self.selects:
                    selected_nodes.append(self.parent.data.index(select))
            else:
                selected_nodes = [0, len(self.parent.data)]
            closest_node = min(selected_nodes, key=lambda x: abs(x-select_index))

            for index in range(min(select_index, closest_node), max(select_index, closest_node)):
                selected = self.parent.data[index]
                if selected not in self.selects:
                    self.selects.append(selected)
            self.selects.append(self.selected)

    def toggle_select(self, *_):
        if self.multiselect:
            if self.selects:
                self.selects = []
            else:
                all_selects = self.parent.data
                for select in all_selects:
                    self.selects.append(select)
        else:
            if self.selected:
                self.selected = {}
        self.update_selected()

    def check_selected(self):
        temp_selects = []
        for select in self.selects:
            if select in self.parent.data:
                temp_selects.append(select)
        self.selects = temp_selects

    def on_selected(self, *_):
        app = App.get_running_app()
        if self.selected:
            if self.multiselect:
                self.check_selected()
                if self.selected in self.selects:
                    self.selects.remove(self.selected)
                else:
                    if app.shift_pressed:
                        self.select_range()
                    else:
                        self.selects.append(self.selected)
            self.update_selected()

    def on_children(self, *_):
        self.update_selected()

    def update_selected(self):
        for child in self.children:
            if self.multiselect:
                if child.data in self.selects:
                    child.selected = True
                else:
                    child.selected = False
            else:
                if child.data == self.selected:
                    child.selected = True
                else:
                    child.selected = False


class SelectableRecycleLayout(LayoutSelectionBehavior):
    """Custom selectable grid layout widget."""
    multiselect = BooleanProperty(True)

    def __init__(self, **kwargs):
        """ Use the initialize method to bind to the keyboard to enable
        keyboard interaction e.g. using shift and control for multi-select.
        """

        super(SelectableRecycleLayout, self).__init__(**kwargs)
        if str(platform) in ('linux', 'win', 'macosx'):
            keyboard = Window.request_keyboard(None, self)
            keyboard.bind(on_key_down=self.select_with_key_down, on_key_up=self.select_with_key_up)

    def toggle_select(self):
        if self.selected_nodes:
            selected = True
        else:
            selected = False
        self.clear_selection()
        if not selected:
            self.select_all()

    def select_all(self):
        for node in range(0, len(self.parent.data)):
            self.select_node(node)

    def select_with_touch(self, node, touch=None):
        if not self.multiselect:
            self.clear_selection()
        self._shift_down = False
        super(SelectableRecycleLayout, self).select_with_touch(node, touch)

    def _select_range(self, multiselect, keep_anchor, node, idx):
        pass

    def select_range(self, select_index, touch):
        #find the closest selected button

        if self.selected_nodes:
            selected_nodes = self.selected_nodes
        else:
            selected_nodes = [0, len(self.parent.data)]
        closest_node = min(selected_nodes, key=lambda x: abs(x-select_index))

        for index in range(min(select_index, closest_node), max(select_index, closest_node)+1):
            self.select_node(index)


class SelectableRecycleGrid(SelectableRecycleLayout, RecycleGridLayout):
    scale = NumericProperty(1)


class NormalRecycleView(RecycleView):
    def get_selected(self):
        selected = []
        for item in self.data:
            if item['selected']:
                selected.append(item)
        return selected


class PhotoListRecycleView(RecycleView):
    selected_index = NumericProperty(0)

    def scroll_to_selected(self):
        box = self.children[0]
        selected = box.selected
        for i, item in enumerate(self.data):
            if item == selected:
                self.selected_index = i
                break
        index = self.selected_index
        pos_index = (box.default_size[1] + box.spacing) * index
        scroll = self.convert_distance_to_scroll(0, pos_index - (self.height * 0.5))[1]
        if scroll > 1.0:
            scroll = 1.0
        elif scroll < 0.0:
            scroll = 0.0
        self.scroll_y = 1.0 - scroll

    def convert_distance_to_scroll(self, dx, dy):
        box = self.children[0]
        wheight = box.default_size[1] + box.spacing

        if not self._viewport:
            return 0, 0
        vp = self._viewport
        vp_height = len(self.data) * wheight
        if vp.width > self.width:
            sw = vp.width - self.width
            sx = dx / float(sw)
        else:
            sx = 0
        if vp_height > self.height:
            sh = vp_height - self.height
            sy = dy / float(sh)
        else:
            sy = 1
        return sx, sy


#Buttons
class ButtonBase(Button):
    """Basic button widget."""

    warn = BooleanProperty(False)
    target_background = ListProperty()
    target_text = ListProperty()
    background_animation = ObjectProperty()
    text_animation = ObjectProperty()
    last_disabled = False
    menu = BooleanProperty(False)
    toggle = BooleanProperty(False)

    button_update = BooleanProperty()

    def __init__(self, **kwargs):
        self.background_animation = Animation()
        self.text_animation = Animation()
        app = App.get_running_app()
        self.background_color = app.theme.button_up
        self.target_background = self.background_color
        self.color = app.theme.button_text
        self.target_text = self.color
        super(ButtonBase, self).__init__(**kwargs)

    def on_button_update(self, *_):
        Clock.schedule_once(self.set_color_instant)

    def set_color_instant(self, *_):
        self.set_color(instant=True)

    def set_color(self, instant=False):
        app = App.get_running_app()
        if self.disabled:
            self.set_text(app.theme.button_disabled_text, instant=instant)
            self.set_background(app.theme.button_disabled, instant=instant)
        else:
            self.set_text(app.theme.button_text, instant=instant)
            if self.menu:
                if self.state == 'down':
                    self.set_background(app.theme.button_menu_down, instant=True)
                else:
                    self.set_background(app.theme.button_menu_up, instant=instant)
            elif self.toggle:
                if self.state == 'down':
                    self.set_background(app.theme.button_toggle_true, instant=instant)
                else:
                    self.set_background(app.theme.button_toggle_false, instant=instant)

            elif self.warn:
                if self.state == 'down':
                    self.set_background(app.theme.button_warn_down, instant=True)
                else:
                    self.set_background(app.theme.button_warn_up, instant=instant)
            else:
                if self.state == 'down':
                    self.set_background(app.theme.button_down, instant=True)
                else:
                    self.set_background(app.theme.button_up, instant=instant)

    def on_disabled(self, *_):
        self.set_color()

    def on_menu(self, *_):
        self.set_color(instant=True)

    def on_toggle(self, *_):
        self.set_color(instant=True)

    def on_warn(self, *_):
        self.set_color(instant=True)

    def on_state(self, *_):
        self.set_color()

    def set_background(self, color, instant=False):
        if self.target_background == color:
            return
        app = App.get_running_app()
        self.background_animation.stop(self)
        if app.animations and not instant:
            self.background_animation = Animation(background_color=color, duration=app.animation_length)
            self.background_animation.start(self)
        else:
            self.background_color = color
        self.target_background = color

    def set_text(self, color, instant=False):
        if self.target_text == color:
            return
        app = App.get_running_app()
        self.text_animation.stop(self)
        if app.animations and not instant:
            self.text_animation = Animation(color=color, duration=app.animation_length)
            self.text_animation.start(self)
        else:
            self.color = color
        self.target_text = color


class ToggleBase(ToggleButton, ButtonBase):
    pass


class VerticalButton(ToggleBase):
    vertical_text = StringProperty('')


class NormalButton(ButtonBase):
    """Basic button widget."""
    pass


class WideButton(ButtonBase):
    """Full width button widget"""
    pass


class MenuButton(ButtonBase):
    """Basic class for a drop-down menu button item."""
    pass


class RemoveButton(ButtonBase):
    """Base class for a button to remove an item from a list."""

    remove = True
    to_remove = StringProperty()
    remove_from = StringProperty()
    owner = ObjectProperty()


class ExpandableButton(GridLayout):
    """Base class for a button with a checkbox to enable/disable an extra area.
    It also features an 'x' remove button that calls 'on_remove' when clicked."""

    text = StringProperty()  #Text shown in the main button area
    expanded = BooleanProperty(False)  #Determines if the expanded area is displayed
    content = ObjectProperty()  #Widget to be displayed when expanded is enabled
    index = NumericProperty()  #The button's index in the list - useful for the remove function
    animation = None

    def __init__(self, **kwargs):
        super(ExpandableButton, self).__init__(**kwargs)
        self.register_event_type('on_press')
        self.register_event_type('on_release')
        self.register_event_type('on_expanded')
        self.register_event_type('on_remove')

    def set_expanded(self, expanded):
        self.expanded = expanded

    def on_expanded(self, *_):
        if self.content:
            if self.expanded:
                content_container = self.ids['contentContainer']
                self.animate_expand()
            else:
                self.animate_close()

    def animate_close(self, instant=False, *_):
        app = App.get_running_app()
        content_container = self.ids['contentContainer']
        content_container.unbind(minimum_height=self.set_content_height)
        if app.animations and not instant:
            anim = Animation(height=app.padding * 2, opacity=0, duration=app.animation_length)
            anim.start(content_container)
        else:
            content_container.opacity = 0
            content_container.height = app.padding * 2
        content_container.clear_widgets()

    def animate_expand(self, instant=False, *_):
        content_container = self.ids['contentContainer']
        app = App.get_running_app()
        content_container.add_widget(self.content)
        if app.animations and not instant:
            if self.animation:
                self.animation.cancel(content_container)
            self.animation = Animation(height=(self.content.height + (app.padding * 2)), opacity=1, duration=app.animation_length)
            self.animation.start(content_container)
            self.animation.bind(on_complete=self.finish_expand)
        else:
            self.finish_expand()
            content_container.opacity = 1

    def finish_expand(self, *_):
        self.animation = None
        content_container = self.ids['contentContainer']
        content_container.bind(minimum_height=self.set_content_height)

    def set_content_height(self, *_):
        content_container = self.ids['contentContainer']
        content_container.height = content_container.minimum_height

    def on_press(self):
        pass

    def on_release(self):
        pass

    def on_remove(self):
        pass


class TreeViewButton(ButtonBehavior, BoxLayout, TreeViewNode):
    """Widget that displays a specific folder, album, or tag in the database treeview.
    Responds to clicks and double-clicks.
    """

    displayable = BooleanProperty(True)
    target = StringProperty()  #Folder, Album, or Tag
    fullpath = StringProperty()  #Folder name, used only on folder type targets
    folder = StringProperty()
    database_folder = StringProperty()
    type = StringProperty()  #The type the target is: folder, album, tag, extra
    total_photos = StringProperty()
    folder_name = StringProperty()
    subtext = StringProperty()
    total_photos_numeric = NumericProperty(0)
    view_album = BooleanProperty(True)
    drag = False
    dragable = BooleanProperty(False)
    owner = ObjectProperty()
    droptype = StringProperty('folder')

    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            if touch.is_double_tap:
                if self.view_album:
                    if self.total_photos_numeric > 0:
                        app = App.get_running_app()
                        if not app.shift_pressed:
                            app.show_album(self)
            else:
                self.on_press()
            if self.dragable:
                self.drag = True
                app = App.get_running_app()
                temp_coords = self.to_parent(touch.opos[0], touch.opos[1])
                widget_coords = (temp_coords[0]-self.pos[0], temp_coords[1]-self.pos[1])
                window_coords = self.to_window(touch.opos[0], touch.opos[1])
                app.drag_treeview(self, 'start', window_coords, offset=widget_coords)

    def on_press(self):
        self.owner.type = self.type
        self.owner.displayable = self.displayable
        self.owner.selected = ''
        self.owner.selected = self.target

    def on_release(self):
        if self.dragable:
            try:
                self.parent.toggle_node(self)
            except:
                pass

    def on_touch_move(self, touch):
        if self.drag:
            delay = time.time() - touch.time_start
            if delay >= drag_delay:
                app = App.get_running_app()
                window_coords = self.to_window(touch.pos[0], touch.pos[1])
                app.drag_treeview(self, 'move', window_coords)

    def on_touch_up(self, touch):
        if self.collide_point(*touch.pos):
            self.on_release()
        if self.drag:
            app = App.get_running_app()
            window_coords = self.to_window(touch.pos[0], touch.pos[1])
            app.drag_treeview(self, 'end', window_coords)
            self.drag = False


class PhotoRecycleViewButton(RecycleItem):
    video = BooleanProperty(False)
    favorite = BooleanProperty(False)
    fullpath = StringProperty()
    photoinfo = ListProperty()
    source = StringProperty()
    selectable = BooleanProperty(True)
    found = BooleanProperty(True)

    def on_source(self, *_):
        """Sets up the display image when first loaded."""

        found = isfile2(self.source)
        self.found = found

    def refresh_view_attrs(self, rv, index, data):
        super(PhotoRecycleViewButton, self).refresh_view_attrs(rv, index, data)
        thumbnail = self.ids['thumbnail']
        thumbnail.photoinfo = self.data['photoinfo']
        thumbnail.source = self.data['source']

    def on_touch_down(self, touch):
        super(PhotoRecycleViewButton, self).on_touch_down(touch)
        if self.collide_point(*touch.pos) and self.selectable:
            self.owner.fullpath = self.fullpath
            self.owner.photo = self.source
            self.parent.selected = self.data
            return True


#Popups
class NormalPopup(Popup):
    """Basic popup widget."""

    def open(self, *args, **kwargs):
        app = App.get_running_app()
        if app.animations:
            self.opacity = 0
            height = self.height
            self.height = 4 * self.height
            anim = Animation(opacity=1, height=height, duration=app.animation_length)
            anim.start(self)
        else:
            self.opacity = 1
        super(NormalPopup, self).open(*args, **kwargs)

    def dismiss(self, *args, **kwargs):
        app = App.get_running_app()
        if app.animations:
            anim = Animation(opacity=0, height=0, duration=app.animation_length)
            anim.start(self)
            anim.bind(on_complete=self.finish_dismiss)
        else:
            super(NormalPopup, self).dismiss()

    def finish_dismiss(self, *_):
        super(NormalPopup, self).dismiss()


class MessagePopup(GridLayout):
    """Basic popup message with a message and 'ok' button."""

    button_text = StringProperty('OK')
    text = StringProperty()

    def close(self, *_):
        app = App.get_running_app()
        app.popup.dismiss()


class InputPopup(GridLayout):
    """Basic text input popup message.  Calls 'on_answer' when either button is clicked."""

    input_text = StringProperty()
    text = StringProperty()  #Text that the user has input
    hint = StringProperty()  #Grayed-out hint text in the input field

    def __init__(self, **kwargs):
        self.register_event_type('on_answer')
        super(InputPopup, self).__init__(**kwargs)

    def on_answer(self, *args):
        pass


class InputPopupTag(GridLayout):
    """Basic text input popup message.  Calls 'on_answer' when either button is clicked."""

    input_text = StringProperty()
    text = StringProperty()  #Text that the user has input
    hint = StringProperty()  #Grayed-out hint text in the input field

    def __init__(self, **kwargs):
        self.register_event_type('on_answer')
        super(InputPopupTag, self).__init__(**kwargs)

    def on_answer(self, *args):
        pass


class MoveConfirmPopup(NormalPopup):
    """Popup that asks to confirm a file or folder move."""
    target = StringProperty()
    photos = ListProperty()
    origin = StringProperty()


class ScanningPopup(NormalPopup):
    """Popup for displaying database scanning progress."""

    button_text = StringProperty('Cancel')
    scanning_percentage = NumericProperty(0)
    scanning_text = StringProperty('Building File List...')


class ConfirmPopup(GridLayout):
    """Basic Yes/No popup message.  Calls 'on_answer' when either button is clicked."""

    text = StringProperty()
    yes_text = StringProperty('Yes')
    no_text = StringProperty('No')
    warn_yes = BooleanProperty(False)
    warn_no = BooleanProperty(False)

    def __init__(self, **kwargs):
        self.register_event_type('on_answer')
        super(ConfirmPopup, self).__init__(**kwargs)

    def on_answer(self, *args):
        pass


#Menus
class NormalDropDown(DropDown):
    """Base dropdown menu class."""

    show_percent = NumericProperty(1)
    invert = BooleanProperty(False)
    basic_animation = BooleanProperty(False)

    def open(self, *args, **kwargs):
        app = App.get_running_app()
        super(NormalDropDown, self).open(*args, **kwargs)

        if app.animations:
            if self.basic_animation:
                #Dont do fancy child opacity animation
                self.opacity = 0
                self.show_percent = 1
                anim = Animation(opacity=1, duration=app.animation_length)
                anim.start(self)
            else:
                #determine if we opened up or down
                if self.attach_to.y > self.y:
                    self.invert = True
                    children = reversed(self.container.children)
                else:
                    self.invert = False
                    children = self.container.children

                #Animate background
                self.opacity = 1
                self.show_percent = 0
                anim = Animation(show_percent=1, duration=app.animation_length)
                anim.start(self)

                if len(self.container.children) > 0:
                    item_delay = app.animation_length / len(self.container.children)
                else:
                    item_delay = 0

                for i, w in enumerate(children):
                    anim = (Animation(duration=i * item_delay) + Animation(opacity=1, duration=app.animation_length))
                    w.opacity = 0
                    anim.start(w)
        else:
            self.opacity = 1

    def dismiss(self, *args, **kwargs):
        app = App.get_running_app()
        if app.animations:
            anim = Animation(opacity=0, duration=app.animation_length)
            anim.start(self)
            anim.bind(on_complete=self.finish_dismiss)
        else:
            self.finish_dismiss()

    def finish_dismiss(self, *_):
        super(NormalDropDown, self).dismiss()


class AlbumSortDropDown(NormalDropDown):
    """Drop-down menu for sorting album elements"""
    pass


class AlbumExportDropDown(NormalDropDown):
    """Drop-down menu for album operations"""
    pass


#Splitter Panels
class SplitterResizer(Button):
    pass


class SplitterPanel(Splitter):
    """Base class for the left and right adjustable panels"""
    hidden = BooleanProperty(False)
    display_width = NumericProperty(0)
    animating = None
    strip_cls = SplitterResizer

    def done_animating(self, *_):
        self.animating = None
        if self.width == 0:
            self.opacity = 0
        else:
            self.opacity = 1

    def on_hidden(self, *_):
        app = App.get_running_app()
        if self.animating:
            self.animating.cancel(self)
        if self.hidden:
            if app.animations:
                self.animating = anim = Animation(width=0, opacity=0, duration=app.animation_length)
                anim.bind(on_complete=self.done_animating)
                anim.start(self)
            else:
                self.opacity = 0
                self.width = 0
        else:
            if app.animations:
                self.animating = anim = Animation(width=self.display_width, opacity=1, duration=app.animation_length)
                anim.bind(on_complete=self.done_animating)
                anim.start(self)
            else:
                self.opacity = 1
                self.width = self.display_width


class SplitterPanelLeft(SplitterPanel):
    """Left-side adjustable width panel."""

    def __init__(self, **kwargs):
        app = App.get_running_app()
        self.display_width = app.left_panel_width()
        super(SplitterPanelLeft, self).__init__(**kwargs)

    def on_hidden(self, *_):
        app = App.get_running_app()
        self.display_width = app.left_panel_width()
        super(SplitterPanelLeft, self).on_hidden()

    def on_width(self, instance, width):
        """When the width of the panel is changed, save to the app settings."""

        del instance
        if self.animating:
            return
        if width > 0:
            app = App.get_running_app()
            widthpercent = (width/Window.width)
            app.config.set('Settings', 'leftpanel', widthpercent)
        if self.hidden:
            self.width = 0


class SplitterPanelRight(SplitterPanel):
    """Right-side adjustable width panel."""

    def __init__(self, **kwargs):
        app = App.get_running_app()
        self.display_width = app.right_panel_width()
        super(SplitterPanelRight, self).__init__(**kwargs)

    def on_hidden(self, *_):
        app = App.get_running_app()
        self.display_width = app.right_panel_width()
        super(SplitterPanelRight, self).on_hidden()

    def on_width(self, instance, width):
        """When the width of the panel is changed, save to the app settings."""

        del instance
        if self.animating:
            return
        if width > 0:
            app = App.get_running_app()
            widthpercent = (width/Window.width)
            app.config.set('Settings', 'rightpanel', widthpercent)
        if self.hidden:
            self.width = 0


#Images
class CustomImage(KivyImage):
    """Custom image display widget.
    Enables editing operations, displaying them in real-time using a low resolution preview of the original image file.
    All editing variables are watched by the widget and it will automatically update the preview when they are changed.
    """

    exif = ''
    pixel_format = ''
    length = NumericProperty(0)
    framerate = ListProperty()
    video = BooleanProperty(False)
    player = ObjectProperty(None, allownone=True)
    position = NumericProperty(0.0)
    start_point = NumericProperty(0.0)
    end_point = NumericProperty(1.0)
    original_image = ObjectProperty()
    photoinfo = ListProperty()
    original_width = NumericProperty(0)
    original_height = NumericProperty(0)
    flip_horizontal = BooleanProperty(False)
    flip_vertical = BooleanProperty(False)
    mirror = BooleanProperty(False)
    angle = NumericProperty(0)
    rotate_angle = NumericProperty(0)
    fine_angle = NumericProperty(0)
    brightness = NumericProperty(0)
    shadow = NumericProperty(0)
    contrast = NumericProperty(0)
    gamma = NumericProperty(0)
    saturation = NumericProperty(0)
    temperature = NumericProperty(0)
    tint = ListProperty([1.0, 1.0, 1.0, 1.0])
    curve = ListProperty()
    crop_top = NumericProperty(0)
    crop_bottom = NumericProperty(0)
    crop_left = NumericProperty(0)
    crop_right = NumericProperty(0)
    filter = StringProperty('')
    filter_amount = NumericProperty(0)
    autocontrast = BooleanProperty(False)
    equalize = NumericProperty(0)
    histogram = ListProperty()
    edit_image = ObjectProperty()
    cropping = BooleanProperty(False)
    touch_point = ObjectProperty()
    active_cropping = BooleanProperty(False)
    crop_start = ListProperty()
    sharpen = NumericProperty(0)
    bilateral = NumericProperty(0.5)
    bilateral_amount = NumericProperty(0)
    median_blur = NumericProperty(0)
    vignette_amount = NumericProperty(0)
    vignette_size = NumericProperty(.5)
    edge_blur_amount = NumericProperty(0)
    edge_blur_size = NumericProperty(.5)
    edge_blur_intensity = NumericProperty(.5)
    cropper = ObjectProperty()  #Holder for the cropper overlay
    crop_controls = ObjectProperty()  #Holder for the cropper edit panel object
    adaptive_clip = NumericProperty(0)
    border_opacity = NumericProperty(1)
    border_image = ListProperty()
    border_tint = ListProperty([1.0, 1.0, 1.0, 1.0])
    border_x_scale = NumericProperty(.5)
    border_y_scale = NumericProperty(.5)
    crop_min = NumericProperty(100)
    size_multiple = NumericProperty(1)
    aspect = NumericProperty(1)
    lock_aspect = BooleanProperty(False)

    #Denoising variables
    denoise = BooleanProperty(False)
    luminance_denoise = NumericProperty(10)
    color_denoise = NumericProperty(10)
    search_window = NumericProperty(15)
    block_size = NumericProperty(5)

    frame_number = 0
    max_frames = 0
    start_seconds = 0
    first_frame = None

    def start_video_convert(self):
        self.close_video()
        self.player = MediaPlayer(self.source, ff_opts={'paused': True, 'ss': 0.0, 'an': True})
        #self.player.set_volume(0)  #crashes sometimes... hopefully not necessary
        self.frame_number = 0
        if self.start_point > 0 or self.end_point < 1:
            all_frames = self.length * (self.framerate[0] / self.framerate[1])
            self.max_frames = all_frames * (self.end_point - self.start_point)
        else:
            self.max_frames = 0

        #need to wait for load so the seek routine doesnt crash python
        self.first_frame = self.wait_frame()

        if self.start_point > 0:
            self.start_seconds = self.length * self.start_point
            self.first_frame = self.seek_player(self.start_seconds)

    def wait_frame(self):
        #Ensures that a frame is gotten
        frame = None
        while not frame:
            frame, value = self.player.get_frame(force_refresh=True)
        return frame

    def start_seek(self, seek):
        #tell the player to seek to a position
        self.player.set_pause(False)
        self.player.seek(pts=seek, relative=False, accurate=True)
        self.player.set_pause(True)

    def seek_player(self, seek):
        self.start_seek(seek)

        framerate = self.framerate[0] / self.framerate[1]
        target_seek_frame = seek * framerate

        loops = 0
        total_loops = 0
        while True:
            loops += 1
            total_loops += 1
            if loops > 5:
                #seek has been stuck for a while, try to seek again
                self.start_seek(seek)
                loops = 0
            #check if seek has gotten within a couple frames yet
            frame = self.wait_frame()
            current_seek = frame[1]
            current_seek_frame = current_seek * framerate
            frame_distance = abs(target_seek_frame - current_seek_frame)
            if frame_distance < 2 or total_loops >= 30:
                #seek has finished, or give up after a lot of tries to not freeze the program...
                break
        return frame

    def get_converted_frame(self):
        if self.first_frame:
            frame = self.first_frame
            self.first_frame = None
        else:
            self.player.set_pause(False)
            frame = None
            while not frame:
                frame, value = self.player.get_frame(force_refresh=False)
                if value == 'eof':
                    return None
            self.player.set_pause(True)
        self.frame_number = self.frame_number + 1
        if self.max_frames:
            if self.frame_number > self.max_frames:
                return None
        frame_image = frame[0]
        frame_size = frame_image.get_size()
        frame_converter = SWScale(frame_size[0], frame_size[1], frame_image.get_pixel_format(), ofmt='rgb24')
        new_frame = frame_converter.scale(frame_image)
        image_data = bytes(new_frame.to_bytearray()[0])
        image = Image.frombuffer(mode='RGB', size=(frame_size[0], frame_size[1]), data=image_data, decoder_name='raw')
        #for some reason, video frames are read upside-down? fix it here...
        image = image.transpose(PIL.Image.FLIP_TOP_BOTTOM)
        if image.mode != 'RGB':
            image = image.convert('RGB')
        image = self.adjust_image(image, preview=False)
        return [image, frame[1]]

    def close_video(self):
        if self.player:
            self.player.close_player()
            self.player = None

    def open_video(self):
        self.player = MediaPlayer(self.source, ff_opts={'paused': True, 'ss': 1.0, 'an': True})
        frame = None
        while not frame:
            frame, value = self.player.get_frame(force_refresh=True)
        data = self.player.get_metadata()
        self.length = data['duration']
        self.framerate = data['frame_rate']
        self.pixel_format = data['src_pix_fmt']

    def set_aspect(self, aspect_x=None, aspect_y=None, force=None):
        """Adjusts the cropping of the image to be a given aspect ratio.
        Attempts to keep the image as large as possible
        Arguments:
            aspect_x: Horizontal aspect ratio element, numerical value.
            aspect_y: Vertical aspect ratio element, numerical value.
            force: Forces the recrop function to horizontal or vertical.  Must be None, 'h' or 'v'
        """

        if aspect_x is not None and aspect_y is not None:
            self.aspect = aspect_x / aspect_y
        width = self.original_width - self.crop_left - self.crop_right
        height = self.original_height - self.crop_top - self.crop_bottom
        if aspect_x != width or aspect_y != height:
            current_ratio = width / height
            target_ratio = self.aspect
            if (force is None and target_ratio > current_ratio) or force == 'v':
                #crop top/bottom, width is the same
                new_height = width / target_ratio
                height_difference = height - new_height
                crop_right = 0
                crop_left = 0
                crop_top = height_difference / 2
                crop_bottom = crop_top
            else:
                #crop sides, height is the same
                new_width = height * target_ratio
                width_difference = width - new_width
                crop_top = 0
                crop_bottom = 0
                crop_left = width_difference / 2
                crop_right = crop_left
        else:
            crop_top = 0
            crop_right = 0
            crop_bottom = 0
            crop_left = 0
        self.crop_top = self.crop_top + crop_top
        self.crop_right = self.crop_right + crop_right
        self.crop_bottom = self.crop_bottom + crop_bottom
        self.crop_left = self.crop_left + crop_left
        self.reset_cropper()

    def crop_percent(self, side, percent):
        texture_width = self.original_width
        texture_height = self.original_height
        crop_min = self.crop_min

        if side == 'top':
            crop_amount = texture_height * percent
            if (texture_height - crop_amount - self.crop_bottom) < crop_min:
                crop_amount = texture_height - self.crop_bottom - crop_min
            self.crop_top = crop_amount
        elif side == 'right':
            crop_amount = texture_width * percent
            if (texture_width - crop_amount - self.crop_left) < crop_min:
                crop_amount = texture_width - self.crop_left - crop_min
            self.crop_right = crop_amount
        elif side == 'bottom':
            crop_amount = texture_height * percent
            if (texture_height - crop_amount - self.crop_top) < crop_min:
                crop_amount = texture_height - self.crop_top - crop_min
            self.crop_bottom = crop_amount
        else:
            crop_amount = texture_width * percent
            if (texture_width - crop_amount - self.crop_right) < crop_min:
                crop_amount = texture_width - self.crop_right - crop_min
            self.crop_left = crop_amount
        self.reset_cropper()
        self.update_crop_controls()

    def update_crop_controls(self):
        if self.crop_controls:
            self.crop_controls.update_crop()

    def get_crop_percent(self):
        width = self.original_width
        height = self.original_height
        top_percent = self.crop_top / height
        right_percent = self.crop_right / width
        bottom_percent = self.crop_bottom / height
        left_percent = self.crop_left / width
        return [top_percent, right_percent, bottom_percent, left_percent]

    def get_crop_size(self):
        new_width = self.original_width - self.crop_left - self.crop_right
        new_height = self.original_height - self.crop_top - self.crop_bottom
        new_aspect = new_width / new_height
        old_aspect = self.original_width / self.original_height
        return "Size: "+str(int(new_width))+"x"+str(int(new_height))+", Aspect: "+str(round(new_aspect, 2))+" (Original: "+str(round(old_aspect, 2))+")"

    def reset_crop(self):
        """Sets the crop values back to 0 for all sides"""

        self.crop_top = 0
        self.crop_bottom = 0
        self.crop_left = 0
        self.crop_right = 0
        self.reset_cropper(setup=True)

    def reset_cropper(self, setup=False):
        """Updates the position and size of the cropper overlay object."""

        if self.cropper:
            texture_size = self.get_texture_size()
            texture_top_edge = texture_size[0]
            texture_right_edge = texture_size[1]
            texture_bottom_edge = texture_size[2]
            texture_left_edge = texture_size[3]

            texture_width = (texture_right_edge - texture_left_edge)
            #texture_height = (texture_top_edge - texture_bottom_edge)

            divisor = self.original_width / texture_width
            top_edge = texture_top_edge - (self.crop_top / divisor)
            bottom_edge = texture_bottom_edge + (self.crop_bottom / divisor)
            left_edge = texture_left_edge + (self.crop_left / divisor)
            right_edge = texture_right_edge - (self.crop_right / divisor)
            width = right_edge - left_edge
            height = top_edge - bottom_edge

            self.cropper.pos = [left_edge, bottom_edge]
            self.cropper.size = [width, height]
            if setup:
                self.cropper.max_resizable_width = width
                self.cropper.max_resizable_height = height

    def get_texture_size(self):
        """Returns a list of the texture size coordinates.
        Returns:
            List of numbers: [Top edge, Right edge, Bottom edge, Left edge]
        """

        left_edge = (self.size[0] / 2) - (self.norm_image_size[0] / 2)
        right_edge = left_edge + self.norm_image_size[0]
        bottom_edge = (self.size[1] / 2) - (self.norm_image_size[1] / 2)
        top_edge = bottom_edge + self.norm_image_size[1]
        return [top_edge, right_edge, bottom_edge, left_edge]

    def point_over_texture(self, pos):
        """Checks if the given pos (x,y) value is over the image texture.
        Returns False if not over texture, returns point transformed to texture coordinates if over texture.
        """

        texture_size = self.get_texture_size()
        top_edge = texture_size[0]
        right_edge = texture_size[1]
        bottom_edge = texture_size[2]
        left_edge = texture_size[3]
        if pos[0] > left_edge and pos[0] < right_edge:
            if pos[1] > bottom_edge and pos[1] < top_edge:
                texture_x = pos[0] - left_edge
                texture_y = pos[1] - bottom_edge
                return [texture_x, texture_y]
        return False

    def detect_crop_edges(self, first, second):
        """Given two points, this will detect the proper crop area for the image.
        Arguments:
            first: First crop corner.
            second: Second crop corner.
        Returns a list of cropping values:
            [crop_top, crop_bottom, crop_left, crop_right]
        """

        if first[0] < second[0]:
            left = first[0]
            right = second[0]
        else:
            left = second[0]
            right = first[0]
        if first[1] < second[1]:
            top = second[1]
            bottom = first[1]
        else:
            top = first[1]
            bottom = second[1]
        scale = self.original_width / self.norm_image_size[0]
        crop_top = (self.norm_image_size[1] - top) * scale
        crop_bottom = bottom * scale
        crop_left = left * scale
        crop_right = (self.norm_image_size[0] - right) * scale
        return [crop_top, crop_bottom, crop_left, crop_right]

    def set_crop(self, posx, posy, width, height):
        """Sets the crop values based on the cropper widget."""

        texture_size = self.get_texture_size()
        texture_top_edge = texture_size[0]
        texture_right_edge = texture_size[1]
        texture_bottom_edge = texture_size[2]
        texture_left_edge = texture_size[3]

        left_crop = posx - texture_left_edge
        bottom_crop = posy - texture_bottom_edge
        right_crop = texture_right_edge - width - posx
        top_crop = texture_top_edge - height - posy

        texture_width = (texture_right_edge - texture_left_edge)
        divisor = self.original_width / texture_width
        if left_crop < 0:
            self.crop_left = 0
        else:
            self.crop_left = left_crop * divisor
        if right_crop < 0:
            self.crop_right = 0
        else:
            self.crop_right = right_crop * divisor
        if top_crop < 0:
            self.crop_top = 0
        else:
            self.crop_top = top_crop * divisor
        if bottom_crop < 0:
            self.crop_bottom = 0
        else:
            self.crop_bottom = bottom_crop * divisor
        #self.update_preview(recrop=False)
        self.update_crop_controls()

    def on_sharpen(self, *_):
        self.update_preview()

    def on_bilateral(self, *_):
        self.update_preview()

    def on_bilateral_amount(self, *_):
        self.update_preview()

    def on_median_blur(self, *_):
        self.update_preview()

    def on_border_opacity(self, *_):
        self.update_preview()

    def on_border_image(self, *_):
        self.update_preview()

    def on_border_x_scale(self, *_):
        self.update_preview()

    def on_border_y_scale(self, *_):
        self.update_preview()

    def on_vignette_amount(self, *_):
        self.update_preview()

    def on_vignette_size(self, *_):
        self.update_preview()

    def on_edge_blur_amount(self, *_):
        self.update_preview()

    def on_edge_blur_size(self, *_):
        self.update_preview()

    def on_edge_blur_intensity(self, *_):
        self.update_preview()

    def on_rotate_angle(self, *_):
        self.update_preview()

    def on_fine_angle(self, *_):
        self.update_preview()

    def on_flip_horizontal(self, *_):
        self.update_preview()

    def on_flip_vertical(self, *_):
        self.update_preview()

    def on_autocontrast(self, *_):
        self.update_preview()

    def on_adaptive_clip(self, *_):
        self.update_preview()

    def on_equalize(self, *_):
        self.update_preview()

    def on_brightness(self, *_):
        self.update_preview()

    def on_shadow(self, *_):
        self.update_preview()

    def on_gamma(self, *_):
        self.update_preview()

    def on_contrast(self, *_):
        self.update_preview()

    def on_saturation(self, *_):
        self.update_preview()

    def on_temperature(self, *_):
        self.update_preview()

    def on_curve(self, *_):
        self.update_preview()

    def on_tint(self, *_):
        self.update_preview()

    def on_border_tint(self, *_):
        self.update_preview()

    def on_size(self, *_):
        self.reset_cropper(setup=True)

    def on_source(self, *_):
        """The source file has been changed, reload image and regenerate preview."""

        self.video = os.path.splitext(self.source)[1].lower() in movietypes
        if self.video:
            self.open_video()
        self.reload_edit_image()
        self.update_texture(self.edit_image)
        self.update_aspect()
        #self.update_preview()

    def on_position(self, *_):
        self.reload_video_edit_image()

    def reload_video_edit_image(self):
        location = self.length * self.position
        frame = self.seek_player(location)
        Clock.schedule_once(self.reload_edit_image)

    def reload_edit_image(self, *_):
        """Regenerate the edit preview image."""
        if self.video:
            if not self.player:
                return
            location = self.length * self.position
            frame = self.seek_player(location)
            frame = frame[0]
            frame_size = frame.get_size()
            pixel_format = frame.get_pixel_format()
            frame_converter = SWScale(frame_size[0], frame_size[1], pixel_format, ofmt='rgb24')
            new_frame = frame_converter.scale(frame)
            image_data = bytes(new_frame.to_bytearray()[0])

            original_image = Image.frombuffer(mode='RGB', size=(frame_size[0], frame_size[1]), data=image_data, decoder_name='raw')
            #for some reason, video frames are read upside-down? fix it here...
            original_image = original_image.transpose(PIL.Image.FLIP_TOP_BOTTOM)
            self.original_width = original_image.size[0]
            self.original_height = original_image.size[1]
            self.original_image = original_image
            image = original_image.copy()

        else:
            original_image = Image.open(self.source)
            try:
                self.exif = original_image.info.get('exif', b'')
            except:
                self.exif = ''
            if self.angle != 0:
                if self.angle == 90:
                    original_image = original_image.transpose(PIL.Image.ROTATE_90)
                if self.angle == 180:
                    original_image = original_image.transpose(PIL.Image.ROTATE_180)
                if self.angle == 270:
                    original_image = original_image.transpose(PIL.Image.ROTATE_270)
            self.original_width = original_image.size[0]
            self.original_height = original_image.size[1]
            image = original_image.copy()
            self.original_image = original_image.copy()
            original_image.close()
        image_width = Window.width * .75
        width = int(image_width)
        height = int(image_width*(image.size[1]/image.size[0]))
        if width < 10:
            width = 10
        if height < 10:
            height = 10
        image = image.resize((width, height))
        if image.mode != 'RGB':
            image = image.convert('RGB')
        self.size_multiple = self.original_width / image.size[0]
        self.edit_image = image
        Clock.schedule_once(self.update_preview)
        #Clock.schedule_once(self.update_histogram)  #Need to delay this because kivy will mess up the drawing of it on first load.
        #self.histogram = image.histogram()

    def update_histogram(self, *_):
        self.histogram = self.edit_image.histogram()

    def update_aspect(self):
        self.aspect = self.edit_image.width / self.edit_image.height

    def on_texture(self, instance, value):
        if value is not None:
            self.texture_size = list(value.size)
        if self.mirror:
            self.texture.flip_horizontal()

    def denoise_preview(self, width, height, pos_x, pos_y):
        left = pos_x
        right = pos_x + width
        lower = pos_y + width
        upper = pos_y
        original_image = self.original_image
        preview = original_image.crop(box=(left, upper, right, lower))
        if preview.mode != 'RGB':
            preview = preview.convert('RGB')
        preview_cv = cv2.cvtColor(numpy.array(preview), cv2.COLOR_RGB2BGR)
        preview_cv = cv2.fastNlMeansDenoisingColored(preview_cv, None, self.luminance_denoise, self.color_denoise, self.search_window, self.block_size)
        preview_cv = cv2.cvtColor(preview_cv, cv2.COLOR_BGR2RGB)
        preview = Image.fromarray(preview_cv)
        preview_bytes = BytesIO()
        preview.save(preview_bytes, 'jpeg')
        preview_bytes.seek(0)
        return preview_bytes

    def update_preview(self, *_, denoise=False, recrop=True):
        """Update the preview image."""

        image = self.adjust_image(self.edit_image)
        if denoise and opencv:
            open_cv_image = cv2.cvtColor(numpy.array(image), cv2.COLOR_RGB2BGR)
            open_cv_image = cv2.fastNlMeansDenoisingColored(open_cv_image, None, self.luminance_denoise, self.color_denoise, self.search_window, self.block_size)
            open_cv_image = cv2.cvtColor(open_cv_image, cv2.COLOR_BGR2RGB)
            image = Image.fromarray(open_cv_image)

        self.update_texture(image)
        self.histogram = image.histogram()
        if recrop:
            self.reset_cropper(setup=True)

    def adjust_image(self, image, preview=True):
        """Applies all current editing opterations to an image.
        Arguments:
            image: A PIL image.
            preview: Generate edit image in preview mode (faster)
        Returns: A PIL image.
        """

        if not preview:
            orientation = self.photoinfo[13]
            if orientation == 3 or orientation == 4:
                image = image.transpose(PIL.Image.ROTATE_180)
            elif orientation == 5 or orientation == 6:
                image = image.transpose(PIL.Image.ROTATE_90)
            elif orientation == 7 or orientation == 8:
                image = image.transpose(PIL.Image.ROTATE_270)
            if orientation in [2, 4, 5, 7]:
                image = image.transpose(PIL.Image.FLIP_LEFT_RIGHT)
            size_multiple = self.size_multiple
        else:
            size_multiple = 1

        if self.sharpen != 0:
            enhancer = ImageEnhance.Sharpness(image)
            image = enhancer.enhance(self.sharpen+1)
        if self.median_blur != 0 and opencv:
            max_median = 10 * size_multiple
            median = int(self.median_blur * max_median)
            if median % 2 == 0:
                median = median + 1
            open_cv_image = cv2.cvtColor(numpy.array(image), cv2.COLOR_RGB2BGR)
            open_cv_image = cv2.medianBlur(open_cv_image, median)
            open_cv_image = cv2.cvtColor(open_cv_image, cv2.COLOR_BGR2RGB)
            image = Image.fromarray(open_cv_image)
        if self.bilateral != 0 and self.bilateral_amount != 0 and opencv:
            diameter = int(self.bilateral * 10 * size_multiple)
            if diameter < 1:
                diameter = 1
            sigma_color = self.bilateral_amount * 100 * size_multiple
            if sigma_color < 1:
                sigma_color = 1
            sigma_space = sigma_color
            open_cv_image = cv2.cvtColor(numpy.array(image), cv2.COLOR_RGB2BGR)
            open_cv_image = cv2.bilateralFilter(open_cv_image, diameter, sigma_color, sigma_space)
            open_cv_image = cv2.cvtColor(open_cv_image, cv2.COLOR_BGR2RGB)
            image = Image.fromarray(open_cv_image)
        if self.vignette_amount > 0 and self.vignette_size > 0:
            vignette = Image.new(mode='RGB', size=image.size, color=(0, 0, 0))
            filter_color = int((1-self.vignette_amount)*255)
            vignette_mixer = Image.new(mode='L', size=image.size, color=filter_color)
            draw = ImageDraw.Draw(vignette_mixer)
            shrink_x = int((self.vignette_size * (image.size[0]/2)) - (image.size[0]/4))
            shrink_y = int((self.vignette_size * (image.size[1]/2)) - (image.size[1]/4))
            draw.ellipse([0+shrink_x, 0+shrink_y, image.size[0]-shrink_x, image.size[1]-shrink_y], fill=255)
            vignette_mixer = vignette_mixer.filter(ImageFilter.GaussianBlur(radius=(self.vignette_amount*60)+60))
            image = Image.composite(image, vignette, vignette_mixer)
        if self.edge_blur_amount > 0 and self.edge_blur_intensity > 0 and self.edge_blur_size > 0:
            blur_image = image.filter(ImageFilter.GaussianBlur(radius=(self.edge_blur_amount*30)))
            filter_color = int((1-self.edge_blur_intensity)*255)
            blur_mixer = Image.new(mode='L', size=image.size, color=filter_color)
            draw = ImageDraw.Draw(blur_mixer)
            shrink_x = int((self.edge_blur_size * (image.size[0]/2)) - (image.size[0]/4))
            shrink_y = int((self.edge_blur_size * (image.size[1]/2)) - (image.size[1]/4))
            draw.ellipse([0+shrink_x, 0+shrink_y, image.size[0]-shrink_x, image.size[1]-shrink_y], fill=255)
            blur_mixer = blur_mixer.filter(ImageFilter.GaussianBlur(radius=(self.edge_blur_amount*30)))
            image = Image.composite(image, blur_image, blur_mixer)
        if self.crop_top != 0 or self.crop_bottom != 0 or self.crop_left != 0 or self.crop_right != 0:
            if preview:
                overlay = Image.new(mode='RGB', size=image.size, color=(0, 0, 0))
                divisor = self.original_width / image.size[0]
                draw = ImageDraw.Draw(overlay)
                draw.rectangle([0, 0, (self.crop_left / divisor), image.size[1]], fill=(255, 255, 255))
                draw.rectangle([0, 0, image.size[0], (self.crop_top / divisor)], fill=(255, 255, 255))
                draw.rectangle([(image.size[0] - (self.crop_right / divisor)), 0, (image.size[0]), image.size[1]],
                               fill=(255, 255, 255))
                draw.rectangle([0, (image.size[1] - (self.crop_bottom / divisor)), image.size[0], image.size[1]],
                               fill=(255, 255, 255))
                bright = ImageEnhance.Brightness(overlay)
                overlay = bright.enhance(.333)
                image = ImageChops.subtract(image, overlay)
            else:
                if self.crop_left >= image.size[0]:
                    crop_left = 0
                else:
                    crop_left = int(self.crop_left)
                if self.crop_top >= image.size[1]:
                    crop_top = 0
                else:
                    crop_top = int(self.crop_top)
                if self.crop_right >= image.size[0]:
                    crop_right = image.size[0]
                else:
                    crop_right = int(image.size[0] - self.crop_right)
                if self.crop_bottom >= image.size[1]:
                    crop_bottom = image.size[1]
                else:
                    crop_bottom = int(image.size[1] - self.crop_bottom)
                if self.video:
                    #ensure that image size is divisible by 2
                    new_width = crop_right - crop_left
                    new_height = crop_bottom - crop_top
                    if new_width % 2 == 1:
                        if crop_right < image.size[0]:
                            crop_right = crop_right + 1
                        else:
                            crop_right = crop_right - 1
                    if new_height % 2 == 1:
                        if crop_bottom < image.size[1]:
                            crop_bottom = crop_bottom + 1
                        else:
                            crop_bottom = crop_bottom - 1
                image = image.crop((crop_left, crop_top, crop_right, crop_bottom))
        if self.flip_horizontal:
            image = image.transpose(PIL.Image.FLIP_LEFT_RIGHT)
        if self.flip_vertical:
            image = image.transpose(PIL.Image.FLIP_TOP_BOTTOM)
        if self.rotate_angle != 0:
            if self.rotate_angle == 90:
                image = image.transpose(PIL.Image.ROTATE_270)
            if self.rotate_angle == 180:
                image = image.transpose(PIL.Image.ROTATE_180)
            if self.rotate_angle == 270:
                image = image.transpose(PIL.Image.ROTATE_90)
        if self.fine_angle != 0:
            total_angle = -self.fine_angle*10
            angle_radians = math.radians(abs(total_angle))
            width, height = rotated_rect_with_max_area(image.size[0], image.size[1], angle_radians)
            x = int((image.size[0] - width) / 2)
            y = int((image.size[1] - height) / 2)
            if preview:
                image = image.rotate(total_angle, expand=False)
            else:
                image = image.rotate(total_angle, resample=PIL.Image.BICUBIC, expand=False)
            image = image.crop((x, y, image.size[0] - x, image.size[1] - y))
        if self.autocontrast:
            image = ImageOps.autocontrast(image)
        if self.equalize != 0:
            equalize_image = ImageOps.equalize(image)
            image = Image.blend(image, equalize_image, self.equalize)
        temperature = int(round(abs(self.temperature)*100))
        if temperature != 0:
            temperature = temperature-1
            if self.temperature > 0:
                kelvin = negative_kelvin[99-temperature]
            else:
                kelvin = positive_kelvin[temperature]
            offset = 255 - min(kelvin)
            kelvin_r = (kelvin[0] + offset) / 255.0
            kelvin_g = (kelvin[1] + offset) / 255.0
            kelvin_b = (kelvin[2] + offset) / 255.0
            matrix = (kelvin_r, 0.0, 0.0, 0.0,
                      0.0, kelvin_g, 0.0, 0.0,
                      0.0, 0.0, kelvin_b, 0.0)
            image = image.convert('RGB', matrix)
        if self.brightness != 0:
            enhancer = ImageEnhance.Brightness(image)
            image = enhancer.enhance(1+self.brightness)
        if self.shadow != 0:
            if self.shadow < 0:
                floor = int(abs(self.shadow) * 128)
                table = [0] * floor
                remaining_length = 256 - floor
                for index in range(0, remaining_length):
                    value = int(round((index / remaining_length) * 256))
                    table.append(value)
                lut = table * 3
            else:
                floor = int(abs(self.shadow) * 128)
                table = []
                for index in range(0, 256):
                    percent = 1 - (index / 255)
                    value = int(round(index + (floor * percent)))
                    table.append(value)
                lut = table * 3
            image = image.point(lut)

        if self.gamma != 0:
            if self.gamma == -1:
                gamma = 99999999999999999
            elif self.gamma < 0:
                gamma = 1/(self.gamma+1)
            elif self.gamma > 0:
                gamma = 1/((self.gamma+1)*(self.gamma+1))
            else:
                gamma = 1
            lut = [pow(x/255, gamma) * 255 for x in range(256)]
            lut = lut*3
            image = image.point(lut)
        if self.contrast != 0:
            enhancer = ImageEnhance.Contrast(image)
            image = enhancer.enhance(1 + self.contrast)
        if self.saturation != 0:
            enhancer = ImageEnhance.Color(image)
            image = enhancer.enhance(1+self.saturation)
        if self.tint != [1.0, 1.0, 1.0, 1.0]:
            matrix = (self.tint[0], 0.0, 0.0, 0.0,
                      0.0, self.tint[1], 0.0, 0.0,
                      0.0, 0.0, self.tint[2], 0.0)
            image = image.convert('RGB', matrix)
        if self.curve:
            lut = self.curve*3
            image = image.point(lut)

        if self.denoise and not preview and opencv:
            open_cv_image = cv2.cvtColor(numpy.array(image), cv2.COLOR_RGB2BGR)
            open_cv_image = cv2.fastNlMeansDenoisingColored(open_cv_image, None, self.luminance_denoise, self.color_denoise, self.search_window, self.block_size)
            open_cv_image = cv2.cvtColor(open_cv_image, cv2.COLOR_BGR2RGB)
            image = Image.fromarray(open_cv_image)

        if self.adaptive_clip > 0 and opencv:
            open_cv_image = cv2.cvtColor(numpy.array(image), cv2.COLOR_RGB2Lab)
            channels = cv2.split(open_cv_image)
            clahe = cv2.createCLAHE(clipLimit=(self.adaptive_clip * 4), tileGridSize=(8, 8))
            clahe_image = clahe.apply(channels[0])
            channels[0] = clahe_image
            open_cv_image = cv2.merge(channels)
            open_cv_image = cv2.cvtColor(open_cv_image, cv2.COLOR_Lab2RGB)
            image = Image.fromarray(open_cv_image)

        if self.border_image:
            image_aspect = image.size[0]/image.size[1]
            closest_aspect = min(self.border_image[1], key=lambda x: abs(x-image_aspect))
            index = self.border_image[1].index(closest_aspect)
            image_file = os.path.join('borders', self.border_image[2][index])
            if preview:
                resample = PIL.Image.NEAREST
            else:
                resample = PIL.Image.BICUBIC
            border_image = Image.open(image_file)
            border_crop_x = int(border_image.size[0] * ((self.border_x_scale + 1) / 15))
            border_crop_y = int(border_image.size[1] * ((self.border_y_scale + 1) / 15))
            border_image = border_image.crop((border_crop_x, border_crop_y, border_image.size[0] - border_crop_x,
                                              border_image.size[1] - border_crop_y))
            border_image = border_image.resize(image.size, resample)

            if os.path.splitext(image_file)[1].lower() == '.jpg':
                alpha_file = os.path.splitext(image_file)[0]+'-mask.jpg'
                if not os.path.exists(alpha_file):
                    alpha_file = image_file
                alpha = Image.open(alpha_file)
                alpha = alpha.convert('L')
                alpha = alpha.crop((border_crop_x, border_crop_y, alpha.size[0] - border_crop_x,
                                    alpha.size[1] - border_crop_y))
                alpha = alpha.resize(image.size, resample)
            else:
                alpha = border_image.split()[-1]
                border_image = border_image.convert('RGB')
            if self.border_tint != [1.0, 1.0, 1.0, 1.0]:
                matrix = (self.border_tint[0], 0.0, 0.0, 1.0,
                          0.0, self.border_tint[1], 0.0, 1.0,
                          0.0, 0.0, self.border_tint[2], 1.0)
                border_image = border_image.convert('RGB', matrix)

            enhancer = ImageEnhance.Brightness(alpha)
            alpha = enhancer.enhance(self.border_opacity)
            image = Image.composite(border_image, image, alpha)

        return image

    def update_texture(self, image):
        """Saves a PIL image to the visible texture.
        Argument:
            image: A PIL image
        """

        image_bytes = BytesIO()
        image.save(image_bytes, 'jpeg')
        image_bytes.seek(0)
        self._coreimage = CoreImage(image_bytes, ext='jpg')
        self._on_tex_change()

    def get_full_quality(self):
        """Generate a full sized and full quality version of the source image.
        Returns: A PIL image.
        """

        image = self.original_image.copy()
        #if not self.video:
        #    if self.angle != 0:
        #        if self.angle == 90:
        #            image = image.transpose(PIL.Image.ROTATE_90)
        #        if self.angle == 180:
        #            image = image.transpose(PIL.Image.ROTATE_180)
        #        if self.angle == 270:
        #            image = image.transpose(PIL.Image.ROTATE_270)
        if image.mode != 'RGB':
            image = image.convert('RGB')
        image = self.adjust_image(image, preview=False)
        return image

    def close_image(self):
        self.original_image.close()


class AsyncThumbnail(KivyImage):
    """AsyncThumbnail is a modified version of the kivy AsyncImage class,
    used to automatically generate and save thumbnails of a specified image.
    """

    loadfullsize = BooleanProperty(False)  #Enable loading the full-sized image, thumbnail will be displayed temporarily
    temporary = BooleanProperty(False)  #This image is a temporary file, not in the database
    photoinfo = ListProperty()  #Photo data of the image
    mirror = BooleanProperty(False)
    loadanyway = BooleanProperty(False)  #Force generating thumbnail even if this widget isnt added to a parent widget
    thumbsize = None
    angle = NumericProperty(0)
    aspect = NumericProperty(1)
    lowmem = BooleanProperty(False)
    thumbnail = ObjectProperty()
    is_full_size = BooleanProperty(False)
    disable_rotate = BooleanProperty(False)

    def __init__(self, **kwargs):
        self._coreimage = None
        self._fullimage = None
        super(AsyncThumbnail, self).__init__(**kwargs)
        self.bind(source=self._load_source)
        if self.source:
            self._load_source()

    def load_thumbnail(self, filename):
        """Load from thumbnail database, or generate a new thumbnail of the given image filename.
        Argument:
            filename: Image filename.
        Returns: A Kivy image"""

        root_widget = True
        if root_widget or self.loadanyway:
            app = App.get_running_app()
            full_filename = filename
            photo = self.photoinfo

            file_found = isfile2(full_filename)
            if file_found:
                modified_date = int(os.path.getmtime(full_filename))
                if modified_date > photo[7]:
                    #if not self.temporary:
                    #    app.database_item_update(photo)
                    #    app.update_photoinfo(folders=[photo[1]])
                    app.database_thumbnail_update(photo[0], photo[2], modified_date, photo[13], temporary=self.temporary)
            thumbnail_image = app.database_thumbnail_get(photo[0], temporary=self.temporary)
            if thumbnail_image:
                imagedata = bytes(thumbnail_image[2])
                data = BytesIO()
                data.write(imagedata)
                data.seek(0)
                image = CoreImage(data, ext='jpg')
            else:
                if file_found:
                    updated = app.database_thumbnail_update(photo[0], photo[2], modified_date, photo[13], temporary=self.temporary)
                    if updated:
                        thumbnail_image = app.database_thumbnail_get(photo[0], temporary=self.temporary)
                        data = BytesIO(thumbnail_image[2])
                        image = CoreImage(data, ext='jpg')
                    else:
                        image = ImageLoader.load(full_filename)
                else:
                    image = ImageLoader.load('data/null.jpg')
            return image
        else:
            return ImageLoader.load('data/null.jpg')

    def set_angle(self):
        if not self.disable_rotate:
            orientation = self.photoinfo[13]
            if orientation in [2, 4, 5, 7]:
                self.mirror = True
            else:
                self.mirror = False
            if self.mirror:
                self.texture.flip_horizontal()

            if orientation == 3 or orientation == 4:
                self.angle = 180
            elif orientation == 5 or orientation == 6:
                self.angle = 270
            elif orientation == 7 or orientation == 8:
                self.angle = 90
            else:
                self.angle = 0

    def _load_source(self, *_):
        self.set_angle()
        source = self.source
        photo = self.photoinfo
        self.nocache = True
        if not source and not photo:
            if self._coreimage is not None:
                self._coreimage.unbind(on_texture=self._on_tex_change)
            self.texture = None
            self._coreimage = None
        elif not photo:
            Clock.schedule_once(lambda *dt: self._load_source(), .25)
        else:
            ThumbLoader.max_upload_per_frame = 50
            ThumbLoader.num_workers = 4
            ThumbLoader.loading_image = 'data/loadingthumbnail.png'
            self._coreimage = image = ThumbLoader.image(source, load_callback=self.load_thumbnail, nocache=self.nocache, mipmap=self.mipmap, anim_delay=self.anim_delay)
            image.bind(on_load=self._on_source_load)
            image.bind(on_texture=self._on_tex_change)
            self.texture = image.texture

    def on_loadfullsize(self, *_):
        if self.thumbnail and not self.is_full_size and self.loadfullsize:
            self._on_source_load()

    def _on_source_load(self, *_):
        try:
            image = self._coreimage.image
            if not image:
                return
        except:
            return
        self.thumbnail = image
        self.thumbsize = image.size
        self.texture = image.texture
        self.aspect = image.size[1] / image.size[0]

        if self.loadfullsize:
            Cache.remove('kv.image', self.source)
            try:
                self._coreimage.image.remove_from_cache()
                self._coreimage.remove_from_cache()
            except:
                pass
            Clock.schedule_once(lambda dt: self._load_fullsize())

    def _load_fullsize(self):
        app = App.get_running_app()
        if not self.lowmem:
            low_memory = to_bool(app.config.get("Settings", "lowmem"))
        else:
            low_memory = True
        if not low_memory:
            #load a screen-sized image instead of full-sized to save memory
            if os.path.splitext(self.source)[1].lower() == '.bmp':
                #default image loader messes up bmp files, use pil instead
                self._coreimage = ImageLoaderPIL(self.source)
            else:
                self._coreimage = KivyImage(source=self.source)
        else:
            #load and rescale image
            original_image = Image.open(self.source)
            image = original_image.copy()
            original_image.close()
            resize_width = Window.size[0]
            if image.size[0] > resize_width:
                width = int(resize_width)
                height = int(resize_width * (image.size[1] / image.size[0]))
                if width < 10:
                    width = 10
                if height < 10:
                    height = 10
                image = image.resize((width, height))
            if image.mode != 'RGB':
                image = image.convert('RGB')
            image_bytes = BytesIO()
            image.save(image_bytes, 'jpeg')
            image_bytes.seek(0)
            self._coreimage = CoreImage(image_bytes, ext='jpg')

        self.texture = self._coreimage.texture
        if self.mirror:
            self.texture.flip_horizontal()

    def _on_tex_change(self, *largs):
        if self._coreimage:
            self.texture = self._coreimage.texture

    def on_texture(self, *_):
        if self.loadfullsize:
            self.is_full_size = True

    def texture_update(self, *largs):
        pass


class PhotoDrag(FloatLayout):
    """Special image widget for displaying the drag-n-drop location."""

    angle = NumericProperty()
    offset = []
    opacity = .5
    fullpath = StringProperty()
    source = StringProperty()
    total_drags = StringProperty('')


#Scrollers
class Scroller(ScrollView):
    """Generic scroller container widget."""
    pass


class ScrollViewCentered(ScrollView):
    """Special ScrollView that begins centered"""

    def __init__(self, **kwargs):
        self.scroll_x = 0.5
        self.scroll_y = 0.5
        super(ScrollViewCentered, self).__init__(**kwargs)

    def window_to_parent(self, x, y, relative=False):
        return self.to_parent(*self.to_widget(x, y))


class ScrollerContainer(Scroller):
    def on_touch_down(self, touch):
        #Modified to allow one sub object to not be scrolled
        try:
            subscroller = self.children[0].children[0].ids['wrapper']
            coords = subscroller.window_to_parent(*touch.pos)
            collide = subscroller.collide_point(*coords)
            if collide:
                touch.apply_transform_2d(subscroller.window_to_parent)
                subscroller.on_touch_down(touch)
                return True
        except:
            pass
        super(ScrollerContainer, self).on_touch_down(touch)