class TreeViewItemTags(TreeViewItem):
    indent = 0
    can_new_folder = True

    def visit(self, visitor):
        super(TreeViewItemTags, self).visit()

        if self.expanded:
            visitor.data = self.deleteChild(visitor.data, self)
            self.expanded = False
        else:
            index = self.getItemIndex(visitor.data, self)
            for tag in self.item.all():
                index += 1
                tag_item = TreeViewItemTag(self.owner, tag, self.height, self)
                visitor.data.insert(index, tag_item)
            self.expanded = True

    def new_item(self, photoListRecyclerView):
        """Open new tag popup"""
        content = InputPopupTag(hint='Tag Name', text='Enter A Tag:')
        app = App.get_running_app()
        content.bind(on_answer=self.new_item_answer)
        self.photoListRecyclerView = photoListRecyclerView
        self.popup = NormalPopup(title='Create Tag',
                                 content=content,
                                 size_hint=(None, None),
                                 size=(app.popup_x, app.button_scale * 5),
                                 auto_dismiss=False)
        self.popup.open()

    def new_item_answer(self, instance=None, answer="yes"):
        """Adds the current input tag to the app tags."""

        if answer == "yes":
            if instance is not None:
                tag_name = instance.ids['input'].text.lower().strip(' ')
                if not tag_name:
                    self.dismiss_popup()
                    return
            else:
                tag_input = self.ids['newTag']
                tag_name = tag_input.text.lower().strip(' ')
                tag_input.text = ''

            tag = Tag(name=tag_name)
            app = App.get_running_app()
            app.session.add(tag)
            app.session.commit()
            self.popup.dismiss()

            # refresh tag treeview
            if self.expanded:
                self.visit(self.photoListRecyclerView)

            self.visit(self.photoListRecyclerView)
class ScreenImporting(Screen):
    """Screen layout for photo importing process.
    Displays photos from directories and lets you select which ones to import.
    """

    type = StringProperty('')
    selected = StringProperty('')
    import_to = StringProperty('')
    naming_method = StringProperty('')
    delete_originals = BooleanProperty(False)
    single_folder = BooleanProperty(False)
    import_from = ListProperty()
    popup = None
    import_photos = []
    duplicates = []
    photos = []
    folders = {}
    unsorted = []
    removed = []
    total_size = 0
    cancel_scanning = BooleanProperty(
        False
    )  # The importing process thread checks this and will stop if set to True.
    scanningpopup = None  # Popup dialog showing the importing process progress.
    scanningthread = None  # Importing files thread.
    popup_update_thread = None  # Updates the percentage and time index on scanning popup
    percent_completed = NumericProperty()
    start_time = NumericProperty()
    import_scanning = BooleanProperty(False)

    def get_selected_photos(self, fullpath=False):
        photos = self.ids['photos']
        selected_indexes = photos.selected_nodes
        photos_container = self.ids['photosContainer']
        selected_photos = []
        for selected in selected_indexes:
            if fullpath:
                selected_photos.append(
                    photos_container.data[selected]['fullpath'])
            else:
                selected_photos.append(
                    photos_container.data[selected]['photoinfo'])
        return selected_photos

    def dismiss_extra(self):
        """Cancels the import process if it is running"""

        if self.import_scanning:
            self.cancel_import()
            return True
        else:
            return False

    def date_to_folder(self, date):
        """Generates a string from a date in the format YYYYMMDD."""

        date_info = datetime.datetime.fromtimestamp(date)
        return str(date_info.year) + str(date_info.month).zfill(2) + str(
            date_info.day).zfill(2)

    def on_enter(self):
        """Called when the screen is entered.  Sets up variables, and scans the import folders."""

        app = App.get_running_app()
        self.ids['leftpanel'].width = app.left_panel_width()
        self.duplicates = []
        self.import_photos = []
        self.folders = {}
        self.unsorted = []
        self.removed = []
        self.total_size = 0
        self.import_scanning = False

        # Display message that folder scanning is in progress
        self.cancel_scanning = False
        self.scanningpopup = ScanningPopup(title='Scanning Import Folders...',
                                           auto_dismiss=False,
                                           size_hint=(None, None),
                                           size=(app.popup_x,
                                                 app.button_scale * 4))
        self.scanningpopup.open()
        scanning_button = self.scanningpopup.ids['scanningButton']
        scanning_button.bind(on_release=self.cancel_import)

        self.percent_completed = 0
        self.scanningthread = threading.Thread(target=self.scan_folders)
        self.import_scanning = True
        self.scanningthread.start()
        self.start_time = time.time()

    def scan_folders(self, *_):
        """Function that scans the import folders for valid files to import
            and now really import files without using the interface to select file.
            Will import all files in directories.
        """
        app = App.get_running_app()

        engine = create_engine(app.database_path,
                               echo=True,
                               connect_args={'check_same_thread': False})
        Session = sessionmaker(bind=engine)
        session = Session()

        # TODO: put this code in a secret file
        #gmaps = googlemaps.Client(key='xxx-LLtQzk0U')

        current_timestamp = time.time()

        # Scan the folders
        for folder in self.import_from:
            if os.path.isdir(folder):
                files = list_files(folder)
                print(folder)
                size = len(files)
                for file_info in files:
                    # update popup
                    if self.cancel_scanning:
                        self.scanning_canceled()
                        return
                    self.percent_completed = self.percent_completed + 1
                    if self.percent_completed > 100:
                        self.percent_completed = 0
                    self.scanningpopup.scanning_percentage = self.percent_completed

                    extension = os.path.splitext(file_info[0])[1].lower()
                    if extension in imagetypes or extension in movietypes:
                        photo = Photo()
                        photo.from_file_info(self.import_to, file_info)
                        self.import_photos.append(photo)

        nb_photo = len(self.import_photos)
        current_photo_index = 0
        for photo in self.import_photos:
            session.commit()
            self.scanningpopup.scanning_percentage = current_photo_index / nb_photo * 100
            current_photo_index += 1
            photo_in_db = session.query(Photo).filter_by(
                full_path=photo.full_path).first()
            if photo_in_db is None:
                folder = session.query(Folder).filter_by(
                    name=photo.folder_name()).first()
                if folder is None:
                    local_folder_path = os.path.join(self.import_to,
                                                     photo.folder_name())
                    if not os.path.isdir(local_folder_path):
                        os.makedirs(local_folder_path)

                    folder = Folder(name=photo.folder_name())
                    session.add(folder)
                    session.commit()

                photo.update_thumbnail()
                session.add(photo)
                session.commit()

                folder.photos.append(photo)
                session.commit()

                if self.delete_originals:
                    move(photo.old_full_filename(), photo.new_full_filename())
                else:
                    copy2(photo.old_full_filename(), photo.new_full_filename())

            # localize_photo(gmaps, session, photo)

            else:  # photo is already in database
                if self.delete_originals:
                    try:
                        os.remove(photo.old_full_filename())
                    except Exception as e:
                        print(e)

        for folder in self.import_from:
            while (removeEmptyFolders(folder, True)):
                pass

        self.scanningpopup.dismiss()
        self.scanningpopup = None
        self.scanningpopup = None
        self.import_scanning = False
        self.update_treeview()
        self.update_photolist()

    def scanning_canceled(self):
        app = App.get_running_app()
        app.message("Canceled import scanning.")
        self.scanningpopup.dismiss()
        self.scanningpopup = None
        self.import_scanning = False
        Clock.schedule_once(lambda *dt: app.show_database())

    def cancel_import(self, unknown=False):
        """Cancel the import process."""
        self.cancel_scanning = True

    def finalize_import(self):
        raise ValueError(
            "should not call this function anymore because the load is done on preset selection"
        )
        """Begin the final stage of the import - copying files."""
        app = App.get_running_app()

        # Create popup to show importing progress
        self.cancel_scanning = False
        self.scanningpopup = ScanningPopup(title='Importing Files',
                                           auto_dismiss=False,
                                           size_hint=(None, None),
                                           size=(app.popup_x,
                                                 app.button_scale * 4))
        self.scanningpopup.open()
        scanning_button = self.scanningpopup.ids['scanningButton']
        scanning_button.bind(on_release=self.cancel_import)

        # Start importing thread
        self.percent_completed = 0
        self.scanningthread = threading.Thread(target=self.importing_process)
        self.import_scanning = True
        self.scanningthread.start()
        self.start_time = time.time()

    def importing_process(self):
        """Function that actually imports the files."""

        raise ValueError(
            "should not call this function anymore because the load is done on preset selection"
        )

        app = App.get_running_app()
        folders = self.folders
        import_to = self.import_to
        total_size = self.total_size
        imported_size = 0
        self.scanningpopup.scanning_text = "Importing " + format_size(
            total_size) + '  0%'
        imported_folders = []
        imported_files = 0
        failed_files = 0

        if disk_usage:
            free_space = disk_usage(import_to)[2]
            if total_size > free_space:
                self.scanningpopup.dismiss()
                self.scanningpopup = None
                app.message("Not enough free drive space! Cancelled import.")
                Clock.schedule_once(lambda *dt: app.show_import())

        # Scan folders
        for folder_path in folders:
            if self.cancel_scanning:
                break
            folder = folders[folder_path]
            folder_name = folder['name']
            if folder['photos']:
                if folder['naming']:
                    folder_name = naming(self.naming_method,
                                         title=folder['title'],
                                         year=folder['year'],
                                         month=folder['month'],
                                         day=folder['day'])
                photos = folder['photos']
                parent = folder['parent']
                if parent:
                    path_string = []
                    while parent:
                        newfolder = folders[parent]
                        newfolder_name = newfolder['name']
                        if newfolder['naming']:
                            newfolder_name = naming(self.naming_method,
                                                    title=newfolder['title'],
                                                    year=newfolder['year'],
                                                    month=newfolder['month'],
                                                    day=newfolder['day'])
                        path_string.append(newfolder_name)
                        parent = newfolder['parent']
                    for path in path_string:
                        folder_name = os.path.join(path, folder_name)
                folderinfo = [
                    folder_name, folder['title'], folder['description']
                ]
                path = os.path.join(import_to, folder_name)
                if not os.path.isdir(path):
                    os.makedirs(path)
                if not app.Folder.exist(folderinfo[0]):
                    app.Folder.insert(folderinfo)
                else:
                    if folderinfo[1]:
                        app.Folder.update_title(folderinfo[0],
                                                folderinfo[1]).commit()
                    if folderinfo[2]:
                        app.Folder.update_description(path, description)(
                            folderinfo[0], folderinfo[2])

                # Scan and import photos in folder
                for photo in photos:
                    if self.cancel_scanning:
                        break
                    completed = (imported_size / total_size)
                    remaining = 1 - completed
                    self.percent_completed = 100 * completed
                    self.scanningpopup.scanning_percentage = self.percent_completed

                    seconds_elapsed = time.time() - self.start_time
                    time_elapsed = '  Time: ' + str(
                        datetime.timedelta(seconds=int(seconds_elapsed)))
                    if self.percent_completed > 0:
                        seconds_remain = (seconds_elapsed *
                                          remaining) / completed
                        time_remain = '  Remaining: ' + str(
                            datetime.timedelta(seconds=int(seconds_remain)))
                    else:
                        time_remain = ''
                    self.scanningpopup.scanning_text = "Importing " + format_size(
                        total_size) + '  ' + str(
                            int(self.percent_completed
                                )) + '%  ' + time_elapsed + time_remain
                    old_full_filename = os.path.join(photo[2], photo[0])
                    new_photo_fullpath = os.path.join(folder_name, photo[10])
                    new_full_filename = os.path.join(import_to,
                                                     new_photo_fullpath)
                    thumbnail_data = app.Photo.thumbnail(photo[2],
                                                         temporary=True)
                    if not app.Photo.exist(new_photo_fullpath):
                        photo[0] = new_photo_fullpath
                        photo[1] = folder_name
                        photo[2] = import_to
                        photo[6] = int(time.time())

                        try:
                            copy2(old_full_filename, new_full_filename)
                        except:
                            failed_files = failed_files + 1
                            imported_size = imported_size + photo[4]
                        else:
                            if self.delete_originals:
                                if os.path.isfile(new_full_filename):
                                    if os.path.getsize(new_full_filename
                                                       ) == os.path.getsize(
                                                           old_full_filename):
                                        os.remove(old_full_filename)
                            app.Photo.add(photo)
                            # app.database_imported_add(photo[0], photo[10], photo[3])
                            if thumbnail_data:
                                thumbnail = thumbnail_data[2]
                                app.Photo.thumbnail_write(
                                    photo[0], int(time.time()), thumbnail,
                                    photo[13])
                            imported_size = imported_size + photo[4]
                            imported_files = imported_files + 1
                    else:
                        failed_files = failed_files + 1
                        imported_size = imported_size + photo[4]
                """
                imported_folders.append(folder_name)
        """

        raise ValueError('muste complete import')

        app.update_photoinfo(folders=imported_folders)
        self.scanningpopup.dismiss()
        if failed_files:
            failed = ' Could not import ' + str(failed_files) + ' files.'
        else:
            failed = ''
        if not self.cancel_scanning:
            if imported_files:
                app.message("Finished importing " + str(imported_files) +
                            " files." + failed)
        else:
            if imported_files:
                app.message("Canceled importing, " + str(imported_files) +
                            " files were imported." + failed)
            else:
                app.message("Canceled importing, no files were imported.")
        self.scanningpopup = None
        self.import_scanning = False
        Clock.schedule_once(lambda *dt: app.show_database())

    def set_delete_originals(self, state):
        """Enable the 'Delete Originals' option."""

        if state == 'down':
            self.delete_originals = True
        else:
            self.delete_originals = False

    def previous_album(self):
        """Switch to the previous album in the list."""

        database = self.ids['folders']
        selected_album = database.selected_node
        if selected_album:
            nodes = list(database.iterate_all_nodes())
            index = nodes.index(selected_album)
            if index <= 1:
                index = len(nodes)
            new_selected_album = nodes[index - 1]
            database.select_node(new_selected_album)
            new_selected_album.on_press()
            database_container = self.ids['foldersContainer']
            database_container.scroll_to(new_selected_album)

    def next_album(self):
        """Switch to the next item on the list."""

        database = self.ids['folders']
        selected_album = database.selected_node
        if selected_album:
            nodes = list(database.iterate_all_nodes())
            index = nodes.index(selected_album)
            if index >= len(nodes) - 1:
                index = 0
            new_selected_album = nodes[index + 1]
            database.select_node(new_selected_album)
            new_selected_album.on_press()
            database_container = self.ids['foldersContainer']
            database_container.scroll_to(new_selected_album)

    def delete(self):
        """Remove selected files and place them in the unsorted folder."""

        if self.type == 'folder' or (self.type == 'extra'
                                     and self.selected == 'unsorted'):
            selected_files = self.get_selected_photos()
            for photo in selected_files:
                if self.selected != 'unsorted':
                    self.folders[self.selected]['photos'].remove(photo)
                    self.unsorted.append(photo)
            self.update_treeview()
            self.update_photolist()

    def add_folder(self):
        """Begin the add folder process, create an input popup."""

        content = InputPopup(hint='Folder Name', text='Enter A Folder Name:')
        app = App.get_running_app()
        content.bind(on_answer=self.add_folder_answer)
        self.popup = NormalPopup(title='Create Folder',
                                 content=content,
                                 size_hint=(None, None),
                                 size=(app.popup_x, app.button_scale * 4),
                                 auto_dismiss=False)
        self.popup.open()

    def add_folder_answer(self, instance, answer):
        """Confirm adding the folder.
        Arguments:
            instance: Dialog that called this function.
            answer: String, if set to 'yes', folder is created.
        """

        app = App.get_running_app()
        if answer == 'yes':
            text = instance.ids['input'].text.strip(' ')
            if text:
                if self.type == 'extra':
                    root = ''
                else:
                    root = self.selected
                path = os.path.join(root, text)
                if path not in self.folders:
                    self.folders[path] = {
                        'name': text,
                        'parent': root,
                        'naming': False,
                        'title': '',
                        'description': '',
                        'year': 0,
                        'month': 0,
                        'day': 0,
                        'photos': []
                    }
                else:
                    app.message("Folder already exists.")

        self.dismiss_popup()
        self.update_treeview()

    def delete_folder(self):
        """Delete the selected import folder and move photos to the unsorted folder."""

        if self.type == 'folder' and self.selected:
            folder_info = self.folders[self.selected]
            photos = folder_info['photos']
            for photo in photos:
                self.unsorted.append(photo)
            del self.folders[self.selected]
            self.selected = ''
            self.type = 'None'
            self.update_treeview()
            self.update_photolist()

    def toggle_select(self):
        """Toggles the selection of photos in the current album."""

        photos = self.ids['photos']
        photos.toggle_select()
        self.update_selected()

    def select_none(self):
        """Deselects all photos."""

        photos = self.ids['photos']
        photos.clear_selection()
        self.update_selected()

    def update_treeview(self):
        """Clears and repopulates the left-side folder list."""

        folder_list = self.ids['folders']

        # Clear the treeview list
        nodes = list(folder_list.iterate_all_nodes())
        for node in nodes:
            folder_list.remove_node(node)
        selected_node = None

        # folder_item = TreeViewButton(target='removed', type='extra', owner=self, view_album=False)
        # folder_item.folder_name = 'Removed (Never Scan Again)'
        # total_photos = len(self.removed)
        # folder_item.total_photos_numeric = total_photos
        # if total_photos > 0:
        #    folder_item.total_photos = '('+str(total_photos)+')'
        # folder_list.add_node(folder_item)
        # if self.selected == 'removed' and self.type == 'extra':
        #    selected_node = folder_item

        # Populate the 'Already Imported' folder
        folder_item = TreeViewButton(target='duplicates',
                                     type='extra',
                                     owner=self,
                                     view_album=False)
        folder_item.folder_name = 'Already Imported (Never Import Again)'
        total_photos = len(self.duplicates)
        folder_item.total_photos_numeric = total_photos
        if total_photos > 0:
            folder_item.total_photos = '(' + str(total_photos) + ')'
        folder_list.add_node(folder_item)
        if self.selected == 'duplicates' and self.type == 'extra':
            selected_node = folder_item

        # Populate the 'Unsorted' folder
        folder_item = TreeViewButton(target='unsorted',
                                     type='extra',
                                     owner=self,
                                     view_album=False)
        folder_item.folder_name = 'Unsorted (Not Imported This Time)'
        total_photos = len(self.unsorted)
        folder_item.total_photos_numeric = total_photos
        if total_photos > 0:
            folder_item.total_photos = '(' + str(total_photos) + ')'
        folder_list.add_node(folder_item)
        if self.selected == 'unsorted' and self.type == 'extra':
            selected_node = folder_item

        # Populate the importing folders
        sorted_folders = sorted(self.folders)
        self.total_size = 0
        to_parent = []
        added_nodes = {}
        for folder_date in sorted_folders:
            folder_info = self.folders[folder_date]
            target = folder_date
            folder_item = TreeViewButton(is_open=True,
                                         fullpath=target,
                                         dragable=True,
                                         target=target,
                                         type='folder',
                                         owner=self,
                                         view_album=False)
            if folder_info['naming']:
                folder_item.folder_name = naming(self.naming_method,
                                                 title=folder_info['title'],
                                                 year=folder_info['year'],
                                                 month=folder_info['month'],
                                                 day=folder_info['day'])
            else:
                folder_item.folder_name = folder_info['name']
            added_nodes[folder_date] = folder_item
            photos = folder_info['photos']
            for photo in photos:
                self.total_size = self.total_size + photo[4]
            total_photos = len(photos)
            folder_item.total_photos_numeric = total_photos
            if total_photos > 0:
                folder_item.total_photos = '(' + str(total_photos) + ')'
            if folder_info['parent']:
                to_parent.append([folder_item, folder_info['parent']])
            else:
                folder_list.add_node(folder_item)
            if self.selected == target and self.type == 'folder':
                selected_node = folder_item
        for item in to_parent:
            node = item[0]
            parent_name = item[1]
            if parent_name in added_nodes.keys():
                folder_list.add_node(node, parent=added_nodes[parent_name])
            else:
                folder_list.add_node(node)
        if selected_node:
            folder_list.select_node(selected_node)
        size_display = self.ids['totalSize']
        size_display.text = 'Total Size: ' + format_size(self.total_size)

    def on_selected(self, instance, value):
        """Called when a photo is selected.  Activate the delete button, and update photo view."""

        delete_button = self.ids['deleteButton']
        delete_button.disabled = True
        self.update_photolist()

    def new_description(self, description_editor):
        """Called when the description field of the currently selected folder is edited.
        Update internal variables to match.
        Argument:
            description_editor: The input box that has been edited.
        """

        description = description_editor.text
        if self.type == 'folder':
            self.folders[self.selected]['description'] = description

    def new_title(self, title_editor):
        """Called when the title field of the currently selected folder is edited.
        Update internal variables to match.
        Argument:
            title_editor: The input box that has been edited.
        """

        title = title_editor.text
        if self.type == 'folder':
            self.folders[self.selected]['title'] = title
            folder_info = self.folders[self.selected]
            self.update_treeview()
            folder_name = self.ids['folderName']
            folder_name.text = naming(self.naming_method,
                                      title=folder_info['title'],
                                      year=folder_info['year'],
                                      month=folder_info['month'],
                                      day=folder_info['day'])

    def update_photolist(self):
        """Redraw the photo list view for the currently selected folder."""

        folder_name = self.ids['folderName']
        photos = []
        name = ''
        title_editor = self.ids['folderTitle']
        description_editor = self.ids['folderDescription']
        dragable = True

        # Viewing an input folder.
        if self.type == 'folder':
            if self.selected in self.folders:
                folder_info = self.folders[self.selected]
                title_editor.text = folder_info['title']
                title_editor.disabled = False
                description_editor.text = folder_info['description']
                description_editor.disabled = False
                photos = folder_info['photos']
                if folder_info['naming']:
                    name = naming(self.naming_method,
                                  title=folder_info['title'],
                                  year=folder_info['year'],
                                  month=folder_info['month'],
                                  day=folder_info['day'])
                else:
                    name = self.selected

        # Viewing a special sorting folder.
        else:
            title_editor.text = ''
            title_editor.disabled = True
            description_editor.text = ''
            description_editor.disabled = True
            if self.selected == 'unsorted':
                photos = self.unsorted
                name = 'Unsorted (Not Imported This Time)'
            elif self.selected == 'removed':
                photos = self.removed
                name = 'Removed (Never Scanned Again)'
            elif self.selected == 'duplicates':
                dragable = False
                photos = self.duplicates
                name = 'Already Imported (Never Import Again)'

        folder_name.text = name

        # Populate photo view
        photos_container = self.ids['photosContainer']
        datas = []
        for photo in photos:
            full_filename = os.path.join(photo.database_folder, photo.fullpath)
            fullpath = photo.fullpath
            database_folder = photo.database_folder
            video = os.path.splitext(full_filename)[1].lower() in movietypes
            data = {
                'fullpath': fullpath,
                'temporary': True,
                'photo': photo,
                'folder': self.selected,
                'database_folder': database_folder,
                'filename': full_filename,
                'target': self.selected,
                'type': self.type,
                'owner': self,
                'video': video,
                'photo_orientation': photo.orientation,
                'source': full_filename,
                'title': photo.owner,
                'selected': False,
                'selectable': True,
                'dragable': dragable
            }
            datas.append(data)
        photos_container.data = datas
        self.select_none()

    def find_photo(self, photo_path, photo_list):
        """Searches through a list of photoinfo objects to find the specified photo.
        Arguments:
            photo_path: The screenDatabase-relative path to the photo to search for.
            photo_list: The list of photo info objects to look through.
        Returns:
            False if nothing found
            Photo info list if match found.
        """

        for photo in photo_list:
            if photo[0] == photo_path:
                return photo
        return False

    def drop_widget(self, fullpath, position, dropped_type='file', aspect=1):
        """Called when a widget is dropped after being dragged.
        Determines what to do with the widget based on where it is dropped.
        Arguments:
            fullpath: String, file location of the object being dragged.
            position: List of X,Y window coordinates that the widget is dropped on.
            dropped_type: String, describes the object being dropped.  May be: 'folder' or 'file'
        """

        app = App.get_running_app()
        folder_list = self.ids['folders']
        folder_container = self.ids['foldersContainer']
        if folder_container.collide_point(position[0], position[1]):
            offset_x, offset_y = folder_list.to_widget(position[0],
                                                       position[1])
            for widget in folder_list.children:
                if widget.collide_point(
                        position[0], offset_y
                ) and widget.type != 'None' and self.type != 'None' and not (
                        widget.target == 'duplicates'
                        and widget.type == 'extra'):

                    if dropped_type == 'folder':
                        # Dropped a folder
                        dropped_data = self.folders[fullpath]
                        new_path = os.path.join(widget.fullpath,
                                                dropped_data['name'])
                        if widget.fullpath != fullpath:
                            # this was actually a drag and not a long click
                            if new_path not in self.folders:
                                # this folder can be dropped here
                                old_parent = fullpath
                                dropped_data['parent'] = widget.fullpath
                                self.folders[new_path] = dropped_data
                                del self.folders[fullpath]

                                new_folders = {}
                                # rename child folders
                                for folder in self.folders:
                                    folder_info = self.folders[folder]
                                    parent = folder_info['parent']
                                    if old_parent and folder.startswith(
                                            old_parent):
                                        new_folder_path = new_path + folder[
                                            len(old_parent):]
                                        new_parent = new_path + parent[
                                            len(old_parent):]
                                        folder_info['parent'] = new_parent
                                        new_folders[
                                            new_folder_path] = folder_info
                                    else:
                                        new_folders[folder] = folder_info

                                self.folders = new_folders
                                self.update_treeview()
                                self.update_photolist()
                            else:
                                app.message("Invalid folder location.")
                    elif dropped_type == 'file':
                        # Dropped a file
                        photo_list = self.get_selected_photos(fullpath=True)
                        if fullpath not in photo_list:
                            photo_list.append(fullpath)
                        for photo_path in photo_list:
                            photo_info = False
                            if self.type == 'folder':
                                photo_info = self.find_photo(
                                    photo_path,
                                    self.folders[self.selected]['photos'])
                                if photo_info:
                                    self.folders[self.selected][
                                        'photos'].remove(photo_info)
                            else:
                                if self.selected == 'unsorted':
                                    photo_info = self.find_photo(
                                        photo_path, self.unsorted)
                                    if photo_info:
                                        self.unsorted.remove(photo_info)
                                elif self.selected == 'removed':
                                    photo_info = self.find_photo(
                                        photo_path, self.removed)
                                    if photo_info:
                                        self.removed.remove(photo_info)
                            if photo_info:
                                if widget.type == 'folder':
                                    self.folders[widget.target][
                                        'photos'].append(photo_info)
                                else:
                                    if widget.target == 'unsorted':
                                        self.unsorted.append(photo_info)
                                    elif widget.target == 'removed':
                                        self.removed.append(photo_info)

                        self.type = widget.type
                        self.selected = widget.target
                        self.update_treeview()
                        self.select_none()
                    break

    def update_selected(self):
        """Updates the delete button when files are selected or unselected.  Disables button if nothing is selected."""

        if self.type == 'folder' or (self.type == 'extra'
                                     and self.selected == 'unsorted'):
            photos = self.ids['photos']
            if photos.selected_nodes:
                selected = True
            else:
                selected = False
            delete_button = self.ids['deleteButton']
            if self.type != 'extra' and self.selected != 'unsorted':
                delete_button.disabled = not selected

    def has_popup(self):
        """Detects if the current screen has a popup active.
        Returns: True or False
        """

        if self.popup:
            if self.popup.open:
                return True
        return False

    def dismiss_popup(self, *_):
        """Close a currently open popup for this screen."""

        if self.popup:
            self.popup.dismiss()
            self.popup = None

    def text_input_active(self):
        """Detects if any text input fields are currently active (being typed in).
        Returns: True or False
        """

        input_active = False
        for widget in self.walk(restrict=True):
            if widget.__class__.__name__ == 'NormalInput' or widget.__class__.__name__ == 'FloatInput' or widget.__class__.__name__ == 'IntegerInput':
                if widget.focus:
                    input_active = True
                    break
        return input_active

    def key(self, key):
        """Handles keyboard shortcuts, performs the actions needed.
        Argument:
            key: The name of the key command to perform.
        """

        if self.text_input_active():
            pass
        else:
            if not self.popup or (not self.popup.open):
                if key == 'left' or key == 'up':
                    self.previous_album()
                if key == 'right' or key == 'down':
                    self.next_album()
                if key == 'delete':
                    self.delete()
                if key == 'a':
                    self.toggle_select()
            elif self.popup and self.popup.open:
                if key == 'enter':
                    self.popup.content.dispatch('on_answer', 'yes')
class TreeViewItemTag(TreeViewItem):
    indent = 1
    can_rename_folder = True
    can_delete_folder = True

    def visit(self, screenDatabase):
        super(TreeViewItemTag, self).visit()

        screenDatabase = self.owner
        datas = []
        for photo in self.item.photos:
            datas.append(photo.data_item(screenDatabase))

        screenDatabase.data = datas
        screenDatabase.update_can_browse()
        screenDatabase.update_selected()

    def visit_drop(self, elements):
        self.owner.add_to_tag(self.item, elements)
        self.target = 'allo'

    def rename_item(self, photoListRecyclerView):
        """Starts the folder renaming process, creates an input text popup."""

        content = InputPopup(hint=self.item.name, text='Rename To:')
        app = App.get_running_app()
        self.photoListRecyclerView = photoListRecyclerView
        content.bind(on_answer=self.rename_item_answer)
        self.photoListRecyclerView = photoListRecyclerView
        self.popup = NormalPopup(title='Rename Tag',
                                 content=content,
                                 size_hint=(None, None),
                                 size=(app.popup_x, app.button_scale * 5),
                                 auto_dismiss=False)
        self.popup.open()

    def rename_item_answer(self, instance, answer):
        """Tells the app to rename the folder if the dialog is confirmed.
        Arguments:
            instance: The dialog that called this function.
            answer: String, if 'yes', the folder will be renamed, all other answers will just close the dialog.
        """

        if answer == 'yes':
            text = instance.ids['input'].text.strip(' ')
            app = App.get_running_app()
            self.item.name = text
            app.session.commit()

        self.popup.dismiss()
        self.__refresh_tree_view()

    def delete_item(self, photoListRecyclerView):
        """Starts the delete folder process, creates the confirmation popup."""

        text = "Delete tag " + self.item.name + "\nThe Contained Files Will Not Be Deleted."
        content = ConfirmPopup(text=text,
                               yes_text='Delete',
                               no_text="Don't Delete",
                               warn_yes=True)
        app = App.get_running_app()
        self.photoListRecyclerView = photoListRecyclerView
        content.bind(on_answer=self.delete_item_answer)
        self.popup = NormalPopup(title='Confirm Delete',
                                 content=content,
                                 size_hint=(None, None),
                                 size=(app.popup_x, app.button_scale * 4),
                                 auto_dismiss=False)
        self.popup.open()

    def delete_item_answer(self, instance, answer):
        """Tells the app to delete the folder if the dialog is confirmed.
        Arguments:
            instance: The dialog that called this function.
            answer: String, if 'yes', the folder will be deleted, all other answers will just close the dialog.
        """
        del instance
        if answer == 'yes':
            self.item.delete()
            #self.previous_album()
        self.popup.dismiss()
        self.__refresh_tree_view()

    def __refresh_tree_view(self):
        if self.treeViewItemParent.expanded:
            self.treeViewItemParent.visit(self.photoListRecyclerView)

        self.treeViewItemParent.visit(self.photoListRecyclerView)
Example #4
0
class ScreenCollage(Screen):
    sort_reverse_button = StringProperty('normal')
    images = []
    collage_background = ListProperty([0, 0, 0, 1])
    resolution = StringProperty('Medium')
    aspect = NumericProperty(1.3333)
    aspect_text = StringProperty('4:3')
    filename = StringProperty('')
    export_scale = 1

    #Widget holder variables
    sort_dropdown = ObjectProperty()  #Holder for the sort method dropdown menu
    popup = None  #Holder for the screen's popup dialog
    resolution_select = ObjectProperty()
    color_select = ObjectProperty()
    aspect_select = ObjectProperty()
    add_remove = ObjectProperty()

    #Variables relating to the photo list view on the left
    selected = StringProperty(
        '')  #The current folder/album/tag being displayed
    type = StringProperty('None')  #'Folder', 'Album', 'Tag'
    target = StringProperty(
    )  #The identifier of the album/folder/tag that is being viewed
    photos = []  #Photoinfo of all photos in the album
    sort_method = StringProperty('Name')  #Current album sort method
    sort_reverse = BooleanProperty(False)

    def deselect_images(self):
        collage = self.ids['collageHolder']
        for image in collage.children:
            image.selected = False

    def delete_selected(self):
        collage = self.ids['collageHolder']
        for image in collage.children:
            if image.selected:
                collage.remove_widget(image)

    def clear_collage(self):
        collage = self.ids['collageHolder']
        collage.clear_widgets()
        self.images = []
        self.collage_background = [0, 0, 0, 1]

    def add_all(self):
        #adds all photos to the collage using a fimonacci spiral pattern
        collage = self.ids['collageHolder']
        size = (1 / (len(self.photos)**0.5))  #average scale of photo
        photos = list(self.photos)
        random.shuffle(photos)

        tau = (1 + 5**0.5) / 2
        inc = (2 - tau) * 2 * math.pi
        theta = 0

        max_x = 0
        max_y = 0
        coords = []
        offset_scale = .5
        app = App.get_running_app()
        app.message("Added " + str(len(photos)) + " images.")
        #Generate basis coordinates and determine min/max
        for index in range(0, len(photos)):
            offset = (random.random() * offset_scale) - (
                .5 * offset_scale)  #random angle variation
            r = index**0.5
            theta = theta + inc + offset
            pos_x = 0.5 + r * math.cos(theta)
            if abs(pos_x) > max_x:
                max_x = abs(pos_x)
            pos_y = 0.5 + r * math.sin(theta)
            if abs(pos_y) > max_y:
                max_y = abs(pos_y)
            coords.append((pos_x, pos_y))

        #add photos to collage
        for index, photo in enumerate(photos):
            rand_angle = random.randint(-33, 33)
            pos_x, pos_y = coords[index]
            #scale points down by max size
            pos_x = pos_x / max_x
            pos_y = pos_y / max_y
            #scale points down to prevent photos overlapping edges
            pos_x = pos_x * (1 - (size / 2))
            pos_y = pos_y * (1 - (size / 2))
            #convert to kivy's coordinate system
            pos_x = (pos_x + 1) / 2
            pos_y = (pos_y + 1) / 2
            #offset points to correct for photo size
            pos_x = pos_x - (size / 2)
            pos_y = pos_y - (size / 2)
            position = (collage.width * pos_x, collage.height * pos_y)

            #forces lowmem mode if more than a certain number of photos
            if len(photos) > 8:
                lowmem = True
            else:
                lowmem = False
            self.add_collage_image(collage,
                                   photo[0],
                                   position,
                                   size=size,
                                   angle=rand_angle,
                                   lowmem=lowmem)

    def export(self):
        self.deselect_images()
        self.filechooser_popup()

    def filechooser_popup(self):
        content = FileBrowser(ok_text='Export',
                              directory_select=False,
                              file_editable=True,
                              export_mode=True,
                              file='collage.jpg')
        content.bind(on_cancel=self.dismiss_popup)
        content.bind(on_ok=self.export_check)
        self.popup = NormalPopup(title="Select File To Export To",
                                 content=content,
                                 size_hint=(0.9, 0.9))
        self.popup.open()

    def export_check(self, *_):
        path = self.popup.content.path
        file = self.popup.content.file
        self.dismiss_popup()
        if not file.lower().endswith('.jpg'):
            file = file + '.jpg'
        self.filename = os.path.join(path, file)
        if os.path.isfile(self.filename):
            app = App.get_running_app()
            content = ConfirmPopup(text='Overwrite the file "' +
                                   self.filename + '"?',
                                   yes_text='Overwrite',
                                   no_text="Cancel",
                                   warn_yes=True)
            content.bind(on_answer=self.export_overwrite_answer)
            self.popup = NormalPopup(title='Confirm Overwrite',
                                     content=content,
                                     size_hint=(None, None),
                                     size=(app.popup_x, app.button_scale * 4),
                                     auto_dismiss=False)
            self.popup.open()
        else:
            self.export_finish()

    def export_overwrite_answer(self, instance, answer):
        del instance
        if answer == 'yes':
            self.dismiss_popup()
            self.export_finish()

    def export_finish(self):
        app = App.get_running_app()
        content = MessagePopup(text='Exporting Collage')
        if len(self.images) > 8:
            message = 'Exporting Collage, This May Take Several Minutes, Please Wait...'
        else:
            message = 'Exporting Collage, Please Wait...'
        self.popup = NormalPopup(title=message,
                                 content=content,
                                 size_hint=(None, None),
                                 size=(app.popup_x, app.button_scale * 4))
        self.popup.open()

        #Wait a cycle so the popup can display
        Clock.schedule_once(self.export_process)

    def export_process(self, *_):
        if self.resolution == 'High':
            self.export_scale = 4
        elif self.resolution == 'Low':
            self.export_scale = 1
        else:
            self.export_scale = 2

        #wait for full sized images to load
        check_images = []
        if self.export_scale > 1:
            for image in self.images:
                async_image = image.children[0].children[0]
                if not async_image.is_full_size:
                    async_image.loadfullsize = True
                    async_image._load_fullsize()
                    check_images.append(async_image)
        while check_images:
            for image in check_images:
                if image.is_full_size:
                    check_images.remove(image)

        #wait a cycle so kivy can finish displaying the textures
        Clock.schedule_once(self.export_collage_as_image)

    def export_collage_as_image(self, *_):
        collage = self.ids['collageHolder']
        exported = self.export_scaled_jpg(collage,
                                          self.filename,
                                          image_scale=self.export_scale)
        app = App.get_running_app()
        self.dismiss_popup()
        if exported is True:
            app.message("Exported " + self.filename)
        else:
            app.message('Export error: ' + exported)

    def export_scaled_jpg(self, widget, filename, image_scale=1):
        from kivy.graphics import (Translate, Fbo, ClearColor, ClearBuffers,
                                   Scale)
        re_size = (widget.width * image_scale, widget.height * image_scale)

        if widget.parent is not None:
            canvas_parent_index = widget.parent.canvas.indexof(widget.canvas)
            if canvas_parent_index > -1:
                widget.parent.canvas.remove(widget.canvas)

        try:
            fbo = Fbo(size=re_size, with_stencilbuffer=True)
            with fbo:
                ClearColor(0, 0, 0, 0)
                ClearBuffers()
                Scale(image_scale, -image_scale, image_scale)
                Translate(-widget.x, -widget.y - widget.height, 0)

            fbo.add(widget.canvas)
            fbo.draw()
            from io import BytesIO
            image_bytes = BytesIO()
            fbo.texture.save(image_bytes, flipped=False, fmt='png')
            image_bytes.seek(0)
            from PIL import Image
            image = Image.open(image_bytes)
            image = image.convert('RGB')
            image.save(filename)
            exported = True
        except Exception as ex:
            exported = str(ex)
        try:
            fbo.remove(widget.canvas)
        except:
            pass

        if widget.parent is not None and canvas_parent_index > -1:
            widget.parent.canvas.insert(canvas_parent_index, widget.canvas)
        return exported

    def change_transform(self, transform_mode):
        for container in self.images:
            if transform_mode == 'rotscale':
                container.do_rotation = True
                container.do_scale = True
                container.do_translation = False
            elif transform_mode == 'rotate':
                container.do_rotation = True
                container.do_scale = False
                container.do_translation = False
            elif transform_mode == 'scale':
                container.do_rotation = False
                container.do_scale = True
                container.do_translation = False
            else:
                container.do_rotation = True
                container.do_scale = True
                container.do_translation = True

    def on_sort_reverse(self, *_):
        """Updates the sort reverse button's state variable, since kivy doesnt just use True/False for button states."""

        app = App.get_running_app()
        self.sort_reverse_button = 'down' if to_bool(
            app.config.get('Sorting', 'album_sort_reverse')) else 'normal'

    def drop_widget(self, fullpath, position, dropped_type='file', aspect=1):
        """Called when a widget is dropped.  Determine photo dragged, and where it needs to go."""

        collage = self.ids['collageHolder']
        position = collage.to_widget(*position)
        self.add_collage_image(collage, fullpath, position, aspect=aspect)

    def add_collage_image(self,
                          collage,
                          fullpath,
                          position,
                          size=0.5,
                          angle=0,
                          lowmem=False,
                          aspect=1):
        if not lowmem:
            if len(self.images) > 8:
                lowmem = True
        if collage.collide_point(*position):
            self.deselect_images()
            app = App.get_running_app()
            photoinfo = app.Photo.exist(fullpath)
            file = os.path.join(photoinfo[2], photoinfo[0])
            orientation = photoinfo[13]
            if orientation == 3 or orientation == 4:
                angle_offset = 180
            elif orientation == 5 or orientation == 6:
                angle_offset = 270
            elif orientation == 7 or orientation == 8:
                angle_offset = 90
            else:
                angle_offset = 0
            if orientation in [2, 4, 5, 7]:
                mirror = True
            else:
                mirror = False
            width = collage.width
            image_holder = ScatterImage(owner=self,
                                        source=file,
                                        rotation=angle + angle_offset,
                                        mirror=mirror,
                                        image_angle=0,
                                        photoinfo=photoinfo,
                                        lowmem=lowmem,
                                        aspect=aspect)
            image_holder.scale = size
            image_holder.selected = True
            if aspect < 1:
                image_holder.width = width * aspect
                image_holder.height = width
            else:
                image_holder.width = width
                image_holder.height = width / aspect
            self.images.append(image_holder)
            image_holder.pos = (position[0] - (width * size / 2),
                                position[1] - (width * size / 2))
            collage.add_widget(image_holder)

    def show_selected(self):
        """Scrolls the treeview to the currently selected folder"""

        database = self.ids['albumContainer']
        database_interior = self.ids['album']
        selected = self.selected
        data = database.data
        current_folder = None
        for i, node in enumerate(data):
            if node['target'] == selected and node['type'] == self.type:
                current_folder = node
                break
        if current_folder is not None:
            database_interior.selected = current_folder
            database.scroll_to_selected()

    def text_input_active(self):
        """Detects if any text input fields are currently active (being typed in).
        Returns: True or False
        """

        input_active = False
        for widget in self.walk(restrict=True):
            if widget.__class__.__name__ == 'NormalInput' or widget.__class__.__name__ == 'FloatInput' or widget.__class__.__name__ == 'IntegerInput':
                if widget.focus:
                    input_active = True
                    break
        return input_active

    def has_popup(self):
        """Detects if the current screen has a popup active.
        Returns: True or False
        """

        if self.popup:
            if self.popup.open:
                return True
        return False

    def dismiss_popup(self, *_):
        """Close a currently open popup for this screen."""

        if self.popup:
            self.popup.dismiss()
            self.popup = None

    def dismiss_extra(self):
        """Deactivates running processes if applicable.
        Returns: True if something was deactivated, False if not.
        """

        return False

    def key(self, key):
        """Handles keyboard shortcuts, performs the actions needed.
        Argument:
            key: The name of the key command to perform.
        """

        if self.text_input_active():
            pass
        else:
            if not self.popup or (not self.popup.open):
                #normal keypresses
                pass
            elif self.popup and self.popup.open:
                if key == 'enter':
                    self.popup.content.dispatch('on_answer', 'yes')

    def scroll_photolist(self):
        """Scroll the right-side photo list to the current active photo."""

        photolist = self.ids['albumContainer']
        self.show_selected()
        photolist.scroll_to_selected()

    def refresh_all(self):
        self.refresh_photolist()
        self.refresh_photoview()

    def update_selected(self):
        pass

    def refresh_photolist(self):
        """Reloads and sorts the photo list"""

        app = App.get_running_app()

        #Get photo list
        self.photos = []
        if self.type == 'Album':
            for albuminfo in app.albums:
                if albuminfo['name'] == self.target:
                    photo_paths = albuminfo['photos']
                    for fullpath in photo_paths:
                        photoinfo = app.Photo.exist(fullpath)
                        if photoinfo:
                            self.photos.append(photoinfo)
        elif self.type == 'Tag':
            self.photos = app.Tag.photos(self.target)
        else:
            self.photos = app.Photo.by_folder(self.target)

        #Remove video files
        temp_photos = []
        for photo in self.photos:
            source = os.path.join(photo[2], photo[0])
            isvideo = os.path.splitext(source)[1].lower() in movietypes
            if not isvideo:
                temp_photos.append(photo)
        self.photos = temp_photos

        #Sort photos
        if self.sort_method == 'Imported':
            sorted_photos = sorted(self.photos,
                                   key=lambda x: x[6],
                                   reverse=self.sort_reverse)
        elif self.sort_method == 'Modified':
            sorted_photos = sorted(self.photos,
                                   key=lambda x: x[7],
                                   reverse=self.sort_reverse)
        elif self.sort_method == 'Owner':
            sorted_photos = sorted(self.photos,
                                   key=lambda x: x[11],
                                   reverse=self.sort_reverse)
        elif self.sort_method == 'Name':
            sorted_photos = sorted(self.photos,
                                   key=lambda x: os.path.basename(x[0]),
                                   reverse=self.sort_reverse)
        else:
            sorted_photos = sorted(self.photos,
                                   key=lambda x: x[0],
                                   reverse=self.sort_reverse)
        self.photos = sorted_photos

    def refresh_photoview(self):
        #refresh recycleview
        photolist = self.ids['albumContainer']
        photodatas = []
        for photo in self.photos:
            source = os.path.join(photo[2], photo[0])
            filename = os.path.basename(photo[0])
            photodata = {
                'text': filename,
                'fullpath': photo[0],
                'temporary': True,
                'photoinfo': photo,
                'folder': self.selected,
                'database_folder': photo[2],
                'filename': filename,
                'target': self.selected,
                'type': self.type,
                'owner': self,
                'video': False,
                'photo_orientation': photo[13],
                'source': source,
                'title': photo[10],
                'selected': False,
                'selectable': True,
                'dragable': True
            }
            photodatas.append(photodata)
        photolist.data = photodatas

    def clear_photolist(self):
        photolist = self.ids['albumContainer']
        photolist.data = []

    def resort_method(self, method):
        """Sets the album sort method.
        Argument:
            method: String, the sort method to use
        """

        self.sort_method = method
        app = App.get_running_app()
        app.config.set('Sorting', 'album_sort', method)
        self.refresh_all()
        Clock.schedule_once(lambda *dt: self.scroll_photolist())

    def resort_reverse(self, reverse):
        """Sets the album sort reverse.
        Argument:
            reverse: String, if 'down', reverse will be enabled, disabled on any other string.
        """

        app = App.get_running_app()
        sort_reverse = True if reverse == 'down' else False
        app.config.set('Sorting', 'album_sort_reverse', sort_reverse)
        self.sort_reverse = sort_reverse
        self.refresh_all()
        Clock.schedule_once(lambda *dt: self.scroll_photolist())

    def on_leave(self):
        """Called when the screen is left.  Clean up some things."""

        self.clear_collage()
        self.clear_photolist()

    def on_enter(self):
        """Called when the screen is entered.  Set up variables and widgets, and prepare to view images."""

        app = App.get_running_app()
        self.ids['leftpanel'].width = app.left_panel_width()
        self.ids['moveButton'].state = 'down'
        self.ids['rotateButton'].state = 'normal'
        self.color_select = ColorDropDown(owner=self)
        self.resolution_select = ResolutionDropDown(owner=self)
        self.aspect_select = ExportAspectRatioDropDown(owner=self)
        self.add_remove = AddRemoveDropDown(owner=self)

        #import variables
        self.target = app.target
        self.type = app.type

        #set up sort buttons
        self.sort_dropdown = AlbumSortDropDown()
        self.sort_dropdown.bind(
            on_select=lambda instance, x: self.resort_method(x))
        self.sort_method = app.config.get('Sorting', 'album_sort')
        self.sort_reverse = to_bool(
            app.config.get('Sorting', 'album_sort_reverse'))

        #refresh views
        self.refresh_photolist()
        self.refresh_photoview()