#!/usr/bin/env python2
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#   Catfish - a versatile file searching tool
#   Copyright (C) 2007-2012 Christian Dywan <christian@twotoasts.de>
#   Copyright (C) 2012-2014 Sean Davis <smd.seandavis@gmail.com>
#
#   This program is free software: you can redistribute it and/or modify it
#   under the terms of the GNU General Public License version 2, as published
#   by the Free Software Foundation.
#
#   This program is distributed in the hope that it will be useful, but
#   WITHOUT ANY WARRANTY; without even the implied warranties of
#   MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
#   PURPOSE.  See the GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License along
#   with this program.  If not, see <http://www.gnu.org/licenses/>.

from locale import gettext as _

from gi.repository import Gtk, Gdk, GdkPixbuf, GObject, GLib, Pango

from xml.sax.saxutils import escape
from shutil import copy2, rmtree

from calendar import timegm
import datetime
import time

import mimetypes
import hashlib
import os

import logging
logger = logging.getLogger('catfish')

from catfish_lib import Window, CatfishSettings, SudoDialog
from catfish.AboutCatfishDialog import AboutCatfishDialog
from catfish.CatfishSearchEngine import *

import pexpect

import sys
pyversion = float(str(sys.version_info[0]) + '.' + str(sys.version_info[1]))

# Initialize Gtk, GObject, and mimetypes
if GObject.pygobject_version < (3, 9, 1):
    GObject.threads_init()
    GLib.threads_init()
mimetypes.init()


def application_in_PATH(application_name):
    """Return True if the application name is found in PATH."""
    for path in os.getenv('PATH').split(':'):
        if os.path.isdir(path):
            if application_name in os.listdir(path):
                return True
    return False


def is_file_hidden(folder, filename):
    """Return TRUE if file is hidden or in a hidden directory."""
    relative = filename.lstrip(folder)
    splitpath = os.path.split(relative)
    while splitpath[1] != '':
        if splitpath[1][0] == '.':
            return True
        splitpath = os.path.split(splitpath[0])
    return False


def surrogate_escape(text, replace=False):
    """Replace non-UTF8 characters with something displayable.
    If replace is True, display (invalid encoding) after the text."""
    try:
        text.encode('utf-8')
    except UnicodeEncodeError:
        text = text.encode('utf-8', errors='surrogateescape').decode(
            'utf-8', errors='replace')
        if replace:
            # Translators: this text is displayed next to
            # a filename that is not utf-8 encoded.
            text = _("%s (invalid encoding)") % text
    except UnicodeDecodeError:
        text = text.decode('utf-8', errors='replace')
    return text


# See catfish_lib.Window.py for more details about how this class works
class CatfishWindow(Window):
    """The application window."""
    __gtype_name__ = "CatfishWindow"

    folder_thumbnails = os.path.expanduser('~/.thumbnails/')

    filter_timerange = (0.0, 9999999999.0)
    start_date = datetime.datetime.now()
    end_date = datetime.datetime.now()

    filter_formats = {'documents': False, 'folders': False, 'images': False,
                      'music': False, 'videos': False, 'applications': False,
                      'other': False, 'exact': False, 'hidden': False,
                      'fulltext': False}

    filter_custom_mimetype = {'category_text': "", 'category_id': -1,
                              'type_text': "", 'type_id': -1}
    filter_custom_extensions = []
    filter_custom_use_mimetype = True

    mimetypes = dict()
    search_in_progress = False

    def finish_initializing(self, builder):  # pylint: disable=E1002
        """Set up the main window"""
        super(CatfishWindow, self).finish_initializing(builder)
        self.set_wmclass("Catfish", "Catfish")

        self.AboutDialog = AboutCatfishDialog

        # -- Folder Chooser Combobox -- #
        self.folderchooser = builder.get_object("folderchooser")

        # -- Search Entry and Completion -- #
        self.search_entry = builder.get_object("search_entry")
        self.suggestions_engine = CatfishSearchEngine(['zeitgeist'])
        completion = Gtk.EntryCompletion()
        self.search_entry.set_completion(completion)
        listmodel = Gtk.ListStore(str)
        completion.set_model(listmodel)
        completion.set_text_column(0)

        # -- App Menu -- #
        self.exact_match = builder.get_object("menu_exact_match")
        self.hidden_files = builder.get_object("menu_hidden_files")
        self.fulltext = builder.get_object("menu_fulltext")

        # -- Sidebar -- #
        self.sidebar_toggle_menu = builder.get_object("menu_show_advanced")
        self.button_time_custom = builder.get_object("button_time_custom")
        self.button_format_custom = builder.get_object("button_format_custom")

        # -- Status Bar -- *
        # Create a new GtkOverlay to hold the
        # results list and Overlay Statusbar
        overlay = Gtk.Overlay()

        # Move the results list to the overlay and
        # place the overlay in the window
        scrolledwindow = builder.get_object("scrolledwindow2")
        parent = scrolledwindow.get_parent()
        scrolledwindow.reparent(overlay)
        parent.add(overlay)
        overlay.show()

        # Create the overlay statusbar
        self.statusbar = Gtk.EventBox()
        self.statusbar.get_style_context().add_class("background")
        self.statusbar.get_style_context().add_class("floating-bar")
        self.statusbar.connect("draw", self.on_floating_bar_draw)
        self.statusbar.connect("enter-notify-event",
                               self.on_floating_bar_enter_notify)
        self.statusbar.set_halign(Gtk.Align.START)
        self.statusbar.set_valign(Gtk.Align.END)

        # Put the statusbar in the overlay
        overlay.add_overlay(self.statusbar)

        # Pack the spinner and label
        self.spinner = Gtk.Spinner()
        self.spinner.start()
        self.statusbar_label = Gtk.Label()
        self.statusbar_label.show()

        box = Gtk.Box()
        box.set_orientation(Gtk.Orientation.HORIZONTAL)
        box.pack_start(self.spinner, False, False, 0)
        box.pack_start(self.statusbar_label, False, False, 0)
        box.set_margin_left(6)
        box.set_margin_top(3)
        box.set_margin_right(6)
        box.set_margin_bottom(3)
        self.spinner.set_margin_right(3)
        box.show()

        self.statusbar.add(box)
        self.statusbar.set_halign(Gtk.Align.END)
        self.statusbar.hide()

        self.icon_cache_size = 0

        self.list_toggle = builder.get_object('view_list')
        self.thumbnail_toggle = builder.get_object('view_thumbnails')

        # -- Treeview -- #
        self.row_activated = False
        self.treeview = builder.get_object("treeview")
        self.treeview.enable_model_drag_source(
            Gdk.ModifierType.BUTTON1_MASK,
            [('text/plain', Gtk.TargetFlags.OTHER_APP, 0)],
            Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
        self.treeview.drag_source_add_text_targets()
        self.file_menu = builder.get_object("file_menu")
        self.file_menu_save = builder.get_object("menu_save")
        self.file_menu_delete = builder.get_object("menu_delete")

        # -- Update Search Index dialog -- #
        self.update_index_dialog = builder.get_object("update_index_dialog")
        self.update_index_spinner = builder.get_object("update_index_spinner")
        self.update_index_updating = builder.get_object(
            "update_index_updating")
        self.update_index_done = builder.get_object("update_index_done")
        self.update_index_close = builder.get_object("update_index_close")
        self.update_index_unlock = builder.get_object("update_index_unlock")
        self.update_index_active = False

        self.format_mimetype_box = builder.get_object("format_mimetype_box")
        self.extensions_entry = builder.get_object("extensions")

        self.search_engine = CatfishSearchEngine()

        self.icon_cache = {}
        self.icon_theme = Gtk.IconTheme.get_default()

        self.selected_filenames = []

        # Load the symbolic (or fallback) icons.
        self.reload_symbolic_icons(self.icon_theme, builder)

        # Update the symbolic icons when GTK or icon themes change.
        self.icon_theme.connect("changed", self.reload_symbolic_icons, builder)
        self.sidebar.get_style_context().connect("changed",
                                                 self.reload_symbolic_icons,
                                                 builder)

        self.settings = CatfishSettings.CatfishSettings()

    def on_floating_bar_enter_notify(self, widget, event):
        """Move the floating statusbar when hovered."""
        if widget.get_halign() == Gtk.Align.START:
            widget.set_halign(Gtk.Align.END)
        else:
            widget.set_halign(Gtk.Align.START)

    def on_floating_bar_draw(self, widget, cairo_t):
        """Draw the floating statusbar."""
        context = widget.get_style_context()

        context.save()
        context.set_state(widget.get_state_flags())

        Gtk.render_background(context, cairo_t, 0, 0,
                              widget.get_allocated_width(),
                              widget.get_allocated_height())

        Gtk.render_frame(context, cairo_t, 0, 0,
                         widget.get_allocated_width(),
                         widget.get_allocated_height())

        context.restore()

        return False

    def reload_symbolic_icons(self, widget, builder):
        """Reload the symbolic icons on GTK or icon theme change."""
        # Modification Time icons
        modified_icon = self.load_symbolic_icon('document-open-recent', 16)
        builder.get_object("image9").set_from_pixbuf(modified_icon)
        builder.get_object("image10").set_from_pixbuf(modified_icon)
        builder.get_object("image11").set_from_pixbuf(modified_icon)

        # Configuration icons
        settings_icon = self.load_symbolic_icon('document-properties', 16,
                                                Gtk.StateFlags.INSENSITIVE)
        self.modified_settings_image = builder.get_object("image8")
        self.document_settings_image = builder.get_object("image12")
        self.modified_settings_image.set_from_pixbuf(settings_icon)
        self.document_settings_image.set_from_pixbuf(settings_icon)

        # File format icons
        documents_icon = self.load_symbolic_icon('folder-documents', 16)
        photos_icon = self.load_symbolic_icon('folder-pictures', 16)
        music_icon = self.load_symbolic_icon('folder-music', 16)
        videos_icon = self.load_symbolic_icon('folder-videos', 16)
        apps_icon = self.load_symbolic_icon('applications-utilities', 16)
        folder_icon = self.load_symbolic_icon('folder', 16)
        custom_format_icon = self.load_symbolic_icon('list-add', 16)
        builder.get_object("image1").set_from_pixbuf(documents_icon)
        builder.get_object("image3").set_from_pixbuf(photos_icon)
        builder.get_object("image4").set_from_pixbuf(music_icon)
        builder.get_object("image5").set_from_pixbuf(videos_icon)
        builder.get_object("image6").set_from_pixbuf(apps_icon)
        builder.get_object("image7").set_from_pixbuf(custom_format_icon)
        builder.get_object("image19").set_from_pixbuf(folder_icon)

    def parse_options(self, options, args):
        """Parse commandline arguments into Catfish runtime settings."""
        self.options = options

        if not application_in_PATH(self.options.fileman):
            self.options.fileman = None
        if not application_in_PATH(self.options.open_wrapper):
            self.options.open_wrapper = None

        # Set the selected folder path.
        self.folderchooser.set_filename(self.options.path)

        # Set non-flags as search keywords.
        self.search_entry.set_text(' '.join(args))

        # Set the time display format.
        if self.options.time_iso:
            self.time_format = '%Y-%m-%d %H:%M'
        else:
            self.time_format = None

        # Set search defaults.
        self.exact_match.set_active(self.options.exact)
        self.hidden_files.set_active(
            self.options.hidden or
            self.settings.get_setting('show-hidden-files'))
        self.fulltext.set_active(self.options.fulltext)
        self.sidebar_toggle_menu.set_active(
            self.settings.get_setting('show-sidebar'))

        self.show_thumbnail = self.options.thumbnails

        # Set the interface to standard or preview mode.

        if self.options.icons_large:
            self.show_thumbnail = False
            self.setup_large_view()
            self.list_toggle.set_active(True)
        elif self.options.thumbnails:
            self.show_thumbnail = True
            self.setup_large_view()
            self.thumbnail_toggle.set_active(True)
        else:
            self.show_thumbnail = False
            self.setup_small_view()
            self.list_toggle.set_active(True)

    def preview_cell_data_func(self, col, renderer, model, treeiter, data):
        """Cell Renderer Function for the preview."""
        icon_name = model[treeiter][0]
        filename = model[treeiter][1]

        if os.path.isfile(icon_name):
            # Load from thumbnail file.
            if self.show_thumbnail:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_name)
                icon_name = None
            else:
                # Get the mimetype image..
                mimetype, override = self.guess_mimetype(filename)
                icon_name = self.get_file_icon(filename, mimetype)

        if icon_name is not None:
            pixbuf = self.get_icon_pixbuf(icon_name)

        renderer.set_property('pixbuf', pixbuf)
        return

    def thumbnail_cell_data_func(self, col, renderer, model, treeiter, data):
        """Cell Renderer Function to Thumbnails View."""
        icon, name, size, path, modified, mime, hidden, exact, counter = \
                                                        model[treeiter][:]
        name = escape(name)
        size = self.format_size(size)
        path = escape(path)
        modified = self.get_date_string(modified)
        displayed = '<b>%s</b> %s%s%s%s%s' % (name, size, os.linesep, path,
                                            os.linesep, modified)
        renderer.set_property('markup', displayed)
        return

    def load_symbolic_icon(self, icon_name, size, state=Gtk.StateFlags.ACTIVE):
        """Return the symbolic version of icon_name, or the non-symbolic
        fallback if unavailable."""
        context = self.sidebar.get_style_context()
        try:
            icon_lookup_flags = Gtk.IconLookupFlags.FORCE_SVG
            icon_info = self.icon_theme.choose_icon([icon_name + '-symbolic'],
                                                    size,
                                                    icon_lookup_flags)
            color = context.get_color(state)
            icon = icon_info.load_symbolic(color, color, color, color)[0]
        except AttributeError:
            icon_lookup_flags = Gtk.IconLookupFlags.FORCE_SVG | \
                Gtk.IconLookupFlags.USE_BUILTIN | \
                Gtk.IconLookupFlags.GENERIC_FALLBACK
            icon = self.icon_theme.load_icon(
                icon_name, size, icon_lookup_flags)
        return icon

    # -- Update Search Index dialog -- #
    def on_update_index_dialog_close(self, widget=None, event=None,
                                     user_data=None):
        """Close the Update Search Index dialog, resetting to default."""
        if not self.update_index_active:
            self.update_index_dialog.hide()
            self.update_index_close.set_label(Gtk.STOCK_CANCEL)
            self.update_index_unlock.show()
            self.update_index_updating.set_sensitive(True)
            self.update_index_updating.hide()
            self.update_index_done.hide()
        return True

    def on_update_index_unlock_clicked(self, widget):
        """Unlock admin rights and perform 'updatedb' query."""
        self.update_index_active = True

        # Get the password for sudo
        sudo_dialog = SudoDialog.SudoDialog(
            icon='catfish', name=_("Catfish File Search"), retries=3)
        sudo_dialog.run()
        sudo_dialog.hide()
        password = sudo_dialog.get_password()

        if not password:
            self.update_index_active = False
            self.update_index_done.set_label(_("Authentication failed."))
            self.update_index_done.show()
            return False

        self.update_index_done.hide()

        # Subprocess to check if query has completed yet, runs at end of func.
        def updatedb_subprocess():
            """Subprocess run for the updatedb command."""
            try:
                self.updatedb_process.expect(pexpect.EOF)
                done = True
            except pexpect.TIMEOUT:
                done = False
            if done:
                self.update_index_active = False
                self.update_index_close.set_label(Gtk.STOCK_CLOSE)
                self.update_index_updating.set_sensitive(False)
                self.update_index_spinner.hide()
                self.update_index_close.set_sensitive(True)
                self.update_index_unlock.hide()
                self.update_index_unlock.set_sensitive(True)
                return_code = self.updatedb_process.exitstatus
                if return_code == 0:
                    status = _('Locate database updated successfully.')
                elif return_code == 1:
                    status = _('An error occurred while updating locatedb.')
                elif return_code == 2:
                    status = _("User aborted authentication.")
                elif return_code == 3:
                    status = _("Authentication failed.")
                else:
                    status = _('Locate database updated successfully.')
                self.update_index_done.set_label(status)
                self.update_index_done.show()
            return not done

        # Set the dialog status to running.
        self.update_index_spinner.show()
        self.update_index_updating.show()
        self.update_index_close.set_sensitive(False)
        self.update_index_unlock.set_sensitive(False)

        self.updatedb_process = pexpect.spawn('sudo updatedb')
        self.updatedb_process.timeout = 1
        try:
            # Check for password prompt or program exit.
            self.updatedb_process.expect(".*ssword.*")
            self.updatedb_process.sendline(password)
            self.updatedb_process.expect(pexpect.EOF)
        except pexpect.EOF:
            # shell already has password, or its not needed
            pass
        except pexpect.TIMEOUT:
            # Poll every 1 second for completion.
            pass
        GLib.timeout_add(1000, updatedb_subprocess)

    # -- Search Entry -- #
    def on_search_entry_activate(self, widget):
        """If the search entry is not empty, perform the query."""
        if len(widget.get_text()) > 0:
            self.statusbar.show()

            # Store search start time for displaying friendly dates
            now = datetime.datetime.now()
            self.today = datetime.datetime(now.year, now.month, now.day)
            self.yesterday = self.today - datetime.timedelta(days=1)
            self.this_week = self.today - datetime.timedelta(days=6)

            task = self.perform_query(widget.get_text())
            GLib.idle_add(next, task)

    def on_search_entry_icon_press(self, widget, event, user_data):
        """If search in progress, stop the search, otherwise, clear the search
        entry field."""
        if not self.search_in_progress:
            widget.set_text("")
        else:
            self.stop_search = True
            self.search_engine.stop()

    def on_search_entry_changed(self, widget):
        """Update the search entry icon and run suggestions."""
        text = widget.get_text()

        if not self.search_in_progress:
            if len(text) == 0:
                icon = Gtk.STOCK_FIND
                msg = _('Enter search terms and press ENTER')
            else:
                icon = Gtk.STOCK_CLEAR
                msg = _('Clear search terms')
            widget.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, icon)
            widget.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, msg)

        task = self.get_suggestions(text)
        GLib.idle_add(next, task)

    def get_suggestions(self, keywords):
        """Load suggestions from the suggestions engine into the search entry
        completion."""
        self.suggestions_engine.stop()

        # Wait for an available thread.
        while Gtk.events_pending():
            Gtk.main_iteration()

        folder = self.folderchooser.get_filename()
        show_hidden = self.filter_formats['hidden']

        # If the keywords start with a hidden character, show hidden files.
        if len(keywords) != 0 and keywords[0] == '.':
            show_hidden = True

        model = self.search_entry.get_completion().get_model()
        model.clear()
        results = []

        for filename in self.suggestions_engine.run(keywords, folder, 10):
            if isinstance(filename, str):
                path, name = os.path.split(filename)
                if name not in results:
                    try:
                        # Determine if file is hidden
                        hidden = is_file_hidden(folder, filename)

                        if not hidden or show_hidden:
                            results.append(name)
                            model.append([name])
                    except OSError:
                        # file no longer exists
                        pass
            yield True
        yield False

    # -- Application Menu -- #
    def on_menu_exact_match_toggled(self, widget):
        """Toggle the exact match settings, and restart the search
        if a fulltext search was previously run."""
        self.filter_format_toggled("exact", widget.get_active())
        if self.filter_formats['fulltext']:
            self.on_search_entry_activate(self.search_entry)

    def on_menu_hidden_files_toggled(self, widget):
        """Toggle the hidden files settings."""
        active = widget.get_active()
        self.filter_format_toggled("hidden", active)
        self.settings.set_setting('show-hidden-files', active)

    def on_menu_fulltext_toggled(self, widget):
        """Toggle the fulltext settings, and restart the search."""
        self.filter_format_toggled("fulltext", widget.get_active())
        self.on_search_entry_activate(self.search_entry)

    def on_menu_update_index_activate(self, widget):
        """Show the Update Search Index dialog."""
        self.update_index_dialog.show()

    # -- Sidebar -- #
    def on_sidebar_toggle_toggled(self, widget):
        """Toggle visibility of the sidebar."""
        active = widget.get_active()
        self.settings.set_setting('show-sidebar', active)
        if active != self.sidebar.get_visible():
            self.sidebar.set_visible(active)

    def on_radio_time_any_toggled(self, widget):
        """Set the time range filter to allow for any modification time."""
        if widget.get_active():
            self.filter_timerange = (0.0, 9999999999.0)
            logger.debug("Time Range: Beginning of time -> Eternity")
            self.refilter()

    def on_radio_time_week_toggled(self, widget):
        """Set the time range filter to allow for files modified since one week
        ago until eternity (to account for newer files)."""
        # Use one week ago for lower bounds, use eternity for upper.
        if widget.get_active():
            now = datetime.datetime.now()
            week = timegm((
                datetime.datetime(now.year, now.month, now.day, 0, 0) -
                datetime.timedelta(7)).timetuple())
            self.filter_timerange = (week, 9999999999.0)
            logger.debug(
                "Time Range: %s -> Eternity",
                time.strftime("%x %X", time.gmtime(int(week))))
            self.refilter()

    def on_radio_time_custom_toggled(self, widget):
        """Set the time range filter to the custom settings chosen in the date
        chooser dialog."""
        active = widget.get_active()
        if active:
            pixbuf = self.load_symbolic_icon('document-properties', 16)

            self.filter_timerange = (timegm(self.start_date.timetuple()),
                                     timegm(self.end_date.timetuple()))
            logger.debug(
                "Time Range: %s -> %s",
                time.strftime("%x %X",
                              time.gmtime(int(self.filter_timerange[0]))),
                time.strftime("%x %X",
                              time.gmtime(int(self.filter_timerange[1]))))

            self.refilter()
        else:
            pixbuf = self.load_symbolic_icon('document-properties', 16,
                                             Gtk.StateFlags.INSENSITIVE)

        self.button_time_custom.set_sensitive(active)
        self.modified_settings_image.set_from_pixbuf(pixbuf)

    def on_button_time_custom_clicked(self, widget):
        """Show the custom time range dialog."""
        dialog = self.builder.get_object("custom_date_dialog")
        start_calendar = self.builder.get_object("start_calendar")
        end_calendar = self.builder.get_object("end_calendar")

        dialog.show_all()
        if dialog.run() == Gtk.ResponseType.APPLY:
            # Update the time range filter values.
            start_date = start_calendar.get_date()
            self.start_date = datetime.datetime(start_date[0],
                                                start_date[1] + 1,
                                                start_date[2])

            end_date = end_calendar.get_date()
            self.end_date = datetime.datetime(end_date[0], end_date[1] + 1,
                                              end_date[2])

            self.filter_timerange = (timegm(self.start_date.timetuple()),
                                     timegm(self.end_date.timetuple()))

            logger.debug(
                "Time Range: %s -> %s",
                time.strftime("%x %X",
                              time.gmtime(int(self.filter_timerange[0]))),
                time.strftime("%x %X",
                              time.gmtime(int(self.filter_timerange[1]))))

            # Reload the results filter.
            self.refilter()
        else:
            # Reset the calendar widgets to their previous values.
            start_calendar.select_month(self.start_date.month - 1,
                                        self.start_date.year)
            start_calendar.select_day(self.start_date.day)

            end_calendar.select_month(self.end_date.month - 1,
                                      self.end_date.year)
            end_calendar.select_day(self.end_date.day)

        dialog.hide()

    def on_calendar_today_button_clicked(self, calendar_widget):
        """Change the calendar widget selected date to today."""
        today = datetime.datetime.now()
        calendar_widget.select_month(today.month - 1, today.year)
        calendar_widget.select_day(today.day)

    # File Type toggles
    def filter_format_toggled(self, filter_format, enabled):
        """Update search filter when formats are modified."""
        self.filter_formats[filter_format] = enabled
        logger.debug("File type filters updated: %s", str(self.filter_formats))
        self.refilter()

    def on_documents_toggled(self, widget):
        """Update search filter when documents is toggled."""
        self.filter_format_toggled("documents", widget.get_active())

    def on_folders_toggled(self, widget):
        """Update search filter when folders is toggled."""
        self.filter_format_toggled("folders", widget.get_active())

    def on_images_toggled(self, widget):
        """Update search filter when images is toggled."""
        self.filter_format_toggled("images", widget.get_active())

    def on_music_toggled(self, widget):
        """Update search filter when music is toggled."""
        self.filter_format_toggled("music", widget.get_active())

    def on_videos_toggled(self, widget):
        """Update search filter when videos is toggled."""
        self.filter_format_toggled("videos", widget.get_active())

    def on_applications_toggled(self, widget):
        """Update search filter when applications is toggled."""
        self.filter_format_toggled("applications", widget.get_active())

    def on_other_format_toggled(self, widget):
        """Update search filter when other format is toggled."""
        active = widget.get_active()
        self.filter_format_toggled("other", active)
        self.button_format_custom.set_sensitive(active)
        if active:
            pixbuf = self.load_symbolic_icon('document-properties', 16)

        else:
            pixbuf = self.load_symbolic_icon('document-properties', 16,
                                             Gtk.StateFlags.INSENSITIVE)
        self.document_settings_image.set_from_pixbuf(pixbuf)

    def on_button_format_custom_clicked(self, widget):
        """Show the custom formats dialog."""
        dialog = self.builder.get_object("custom_format_dialog")

        radio_mimetypes = self.builder.get_object("radio_custom_mimetype")
        categories = self.builder.get_object("mimetype_categories")
        types = self.builder.get_object("mimetype_types")

        radio_extensions = self.builder.get_object("radio_custom_extensions")

        # Lazily load the mimetypes.
        if len(self.mimetypes) == 0:
            mimes = list(mimetypes.types_map.values())
            if not isinstance(mimes, list):
                mimes = list(mimes)
            mimes.sort()
            for mime in mimes:
                category, mimetype = str(mime).split("/")
                if category not in list(self.mimetypes.keys()):
                    self.mimetypes[category] = []
                if mimetype not in self.mimetypes[category]:
                    self.mimetypes[category].append(mimetype)

            keys = list(self.mimetypes.keys())
            if not isinstance(keys, list):
                keys = list(keys)
            keys.sort()
            for category in keys:
                categories.append_text(category)

            types.remove_all()

        # Load instance defaults.
        if self.filter_custom_mimetype['category_id'] == -1:
            categories.set_active(0)
        else:
            categories.set_active(self.filter_custom_mimetype['category_id'])
        if self.filter_custom_mimetype['type_id'] == -1:
            types.set_active(0)
        else:
            types.set_active(self.filter_custom_mimetype['type_id'])
        extensions_str = ', '.join(self.filter_custom_extensions)
        self.extensions_entry.set_text(extensions_str)

        if self.filter_custom_use_mimetype:
            radio_mimetypes.set_active(True)
        else:
            radio_extensions.set_active(True)

        dialog.show_all()

        if dialog.run() == Gtk.ResponseType.APPLY:
            # Update filter settings and instance defaults.
            self.filter_custom_mimetype = {
                'category_text': categories.get_active_text(),
                'category_id': categories.get_active(),
                'type_text': types.get_active_text(),
                'type_id': types.get_active()}

            self.filter_custom_extensions = []
            extensions = self.extensions_entry.get_text().replace(',', ' ')
            for ext in extensions.split():
                ext = ext.rstrip().lstrip()
                if len(ext) > 0:
                    if ext[0] != '.':
                        ext = "." + ext
                    self.filter_custom_extensions.append(ext)

            self.filter_custom_use_mimetype = radio_mimetypes.get_active()

            logger.debug(
                "Updated file type settings:" +
                "\n  Mimetype:     " + str(self.filter_custom_mimetype) +
                "\n  Extensions:   " + str(self.filter_custom_extensions) +
                "\n  Use mimetype: " + str(self.filter_custom_use_mimetype))

            # Reload the results filter.
            self.refilter()

        dialog.hide()

    def on_radio_custom_mimetype_toggled(self, widget):
        """Make widgets sensitive when radio buttons are toggled."""
        self.format_mimetype_box.set_sensitive(widget.get_active())

    def on_radio_custom_extensions_toggled(self, widget):
        """Make widgets sensitive when radio buttons are toggled."""
        self.extensions_entry.set_sensitive(widget.get_active())

    def on_mimetype_categories_changed(self, combobox):
        """Update the mime subtypes when a different category is selected."""
        types = self.builder.get_object("mimetype_types")

        # Remove all existing rows.
        types.remove_all()

        # Add each mimetype.
        if combobox.get_active() != -1:
            for mime in self.mimetypes[combobox.get_active_text()]:
                types.append_text(mime)

        # Set the combobox to the first item.
        types.set_active(0)

    def open_file(self, filename):
        """Open the specified filename in its default application."""
        logger.debug("Opening %s" % filename)
        command = None
        if os.path.isdir(filename):
            if self.options.fileman:
                command = [self.options.fileman, filename]
            else:
                # Translators: This message will only appear when trying to
                # open the default file manager but it cannot be found.
                msg = _("Catfish could not find the default file manager.")
        else:
            if self.options.open_wrapper:
                command = [self.options.open_wrapper, filename]
            else:
                # Translators: This message will only appear when the default
                # application for opening a file cannot be found.
                msg = _("Catfish could not find the default open wrapper.")
        if command:
            try:
                subprocess.Popen(command, shell=False)
                return
            except Exception as msg:
                logger.debug('Exception encountered while opening %s.' +
                             '\n  Exception: %s' +
                             '\n  The wrapper was %s.' +
                             '\n  The filemanager was %s.',
                             filename, msg,
                             str(self.options.open_wrapper),
                             str(self.options.fileman))

        self.get_error_dialog(_('\"%s\" could not be opened.') %
                              os.path.basename(filename), str(msg))

    # -- File Popup Menu -- #
    def on_menu_open_activate(self, widget):
        """Open the selected file in its default application."""
        for filename in self.selected_filenames:
            self.open_file(filename)

    def on_menu_filemanager_activate(self, widget):
        """Open the selected file in the default file manager."""
        logger.debug("Opening file manager for %i path(s)" %
                     len(self.selected_filenames))
        dirs = []
        for filename in self.selected_filenames:
            path = os.path.split(filename)[0]
            if path not in dirs:
                dirs.append(path)
        for path in dirs:
            self.open_file(path)

    def on_menu_copy_location_activate(self, widget):
        """Copy the selected file name to the clipboard."""
        logger.debug("Copying %i filename(s) to the clipboard" %
                     len(self.selected_filenames))
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        locations = []
        for filename in self.selected_filenames:
            locations.append(surrogate_escape(filename))
        text = str(os.linesep).join(locations)
        clipboard.set_text(text, -1)
        clipboard.store()

    def on_menu_save_activate(self, widget):
        """Show a save dialog and possibly write the results to a file."""
        filename = self.get_save_dialog(
            surrogate_escape(self.selected_filenames[0]))
        if filename:
            try:
                # Try to save the file.
                copy2(self.selected_filenames[0], filename)

            except Exception as msg:
                # If the file save fails, throw an error.
                logger.debug('Exception encountered while saving %s.' +
                             '\n  Exception: %s', filename, msg)
                self.get_error_dialog(_('\"%s\" could not be saved.') %
                                      os.path.basename(filename), str(msg))

    def on_menu_delete_activate(self, widget):
        """Show a delete dialog and remove the file if accepted."""
        if self.get_delete_dialog(self.selected_filenames):
            for filename in self.selected_filenames:
                try:
                    # Delete the file.
                    if os.path.isdir(filename):
                        rmtree(filename)
                    else:
                        os.remove(filename)

                    # Remove the selected from from the treeview.
                    model = self.treeview.get_model().get_model().get_model()
                    path = self.treeview.get_cursor()[0]
                    treeiter = model.get_iter(path)
                    model.remove(treeiter)
                    self.refilter()

                except Exception as msg:
                    # If the file cannot be deleted, throw an error.
                    logger.debug('Exception encountered while deleting %s.' +
                                 '\n  Exception: %s', filename, msg)
                    self.get_error_dialog(_("\"%s\" could not be deleted.") %
                                          os.path.basename(filename),
                                          str(msg))

    def get_save_dialog(self, filename):
        """Show the Save As FileChooserDialog.

        Return the filename, or None if cancelled."""
        basename = os.path.basename(filename)

        buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
                   Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT)
        dialog = Gtk.FileChooserDialog(_('Save "%s" as…') % basename, self,
                                       Gtk.FileChooserAction.SAVE, buttons)
        dialog.set_default_response(Gtk.ResponseType.REJECT)
        dialog.set_current_name(basename.replace('�', '_'))
        dialog.set_do_overwrite_confirmation(True)
        response = dialog.run()
        save_as = dialog.get_filename()
        dialog.destroy()
        if response == Gtk.ResponseType.ACCEPT:
            return save_as
        return None

    def get_error_dialog(self, primary, secondary):
        """Show an error dialog with the specified message."""
        dialog_text = "<big><b>%s</b></big>\n\n%s" % (escape(primary),
                                                      escape(secondary))

        dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR,
                                   Gtk.ButtonsType.OK, "")
        dialog.set_markup(dialog_text)
        dialog.set_default_response(Gtk.ResponseType.OK)
        dialog.run()
        dialog.destroy()

    def get_delete_dialog(self, filenames):
        """Show a delete confirmation dialog.  Return True if delete wanted."""
        if len(filenames) == 1:
            primary = _("Are you sure that you want to \n"
                        "permanently delete \"%s\"?") % \
                escape(os.path.basename(filenames[0]))
        else:
            primary = _("Are you sure that you want to \n"
                        "permanently delete the %i selected files?") % \
                len(filenames)
        secondary = _("If you delete a file, it is permanently lost.")

        dialog_text = "<big><b>%s</b></big>\n\n%s" % (primary, secondary)
        dialog = Gtk.MessageDialog(self, 0,
                                   Gtk.MessageType.QUESTION,
                                   Gtk.ButtonsType.NONE, "")
        dialog.set_markup(surrogate_escape(dialog_text))

        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.NO,
                           Gtk.STOCK_DELETE, Gtk.ResponseType.YES)

        dialog.set_default_response(Gtk.ResponseType.NO)
        response = dialog.run()
        dialog.destroy()
        return response == Gtk.ResponseType.YES

    def setup_small_view(self):
        """Prepare the list view in the results pane."""
        for column in self.treeview.get_columns():
            self.treeview.remove_column(column)
        self.treeview.append_column(self.new_column(_('Filename'), 1,
                                                    'icon', 1))
        self.treeview.append_column(self.new_column(_('Size'), 2,
                                                    'filesize'))
        self.treeview.append_column(self.new_column(_('Location'), 3,
                                                    'ellipsize'))
        self.treeview.append_column(self.new_column(_('Modified'), 4,
                                                    'date', 1))
        self.icon_size = Gtk.IconSize.MENU

    def setup_large_view(self):
        """Prepare the extended list view in the results pane."""
        for column in self.treeview.get_columns():
            self.treeview.remove_column(column)
        # Make the Preview Column
        cell = Gtk.CellRendererPixbuf()
        column = Gtk.TreeViewColumn(_('Preview'), cell)
        self.treeview.append_column(column)

        column.set_cell_data_func(cell, self.preview_cell_data_func, None)

        # Make the Details Column
        cell = Gtk.CellRendererText()
        cell.set_property("ellipsize", Pango.EllipsizeMode.END)
        column = Gtk.TreeViewColumn(_("Details"), cell, markup=1)

        column.set_sort_column_id(1)
        column.set_resizable(True)
        column.set_expand(True)

        column.set_cell_data_func(cell, self.thumbnail_cell_data_func, None)
        self.treeview.append_column(column)
        self.icon_size = Gtk.IconSize.DIALOG

    def on_treeview_list_toggled(self, widget):
        """Switch to the details view."""
        if widget.get_active():
            self.show_thumbnail = False
            if self.options.icons_large:
                self.setup_large_view()
            else:
                self.setup_small_view()

    def on_treeview_thumbnails_toggled(self, widget):
        """Switch to the preview view."""
        if widget.get_active():
            self.show_thumbnail = True
            self.setup_large_view()

    # -- Treeview -- #
    def on_treeview_row_activated(self, treeview, path, user_data):
        """Catch row activations by keyboard or mouse double-click."""
        if self.row_activated:
            self.row_activated = False
            return
        else:
            self.row_activated = True

        # Get the filename from the row.
        model = treeview.get_model()
        file_path = self.treemodel_get_row_filename(model, path)
        self.selected_filenames = [file_path]

        # Open the selected file.
        self.open_file(self.selected_filenames[0])

    def on_treeview_drag_begin(self, treeview, context):
        """Treeview DND Begin."""
        if len(self.selected_filenames) > 1:
            treesel = treeview.get_selection()
            for row in self.rows:
                treesel.select_path(row)
        return True

    def on_treeview_drag_data_get(self, treeview, context, selection, info,
                                  time):
        """Treeview DND Get."""
        text = str(os.linesep).join(self.selected_filenames)
        selection.set_text(text, -1)
        return True

    def treemodel_get_row_filename(self, model, row):
        """Get the filename from a specified row."""
        index = model[row][8]
        filename = self.file_paths[index]
        return filename

    def treeview_get_selected_rows(self, treeview):
        """Get the currently selected rows from the specified treeview."""
        sel = treeview.get_selection()
        model, rows = sel.get_selected_rows()
        data = []
        for row in rows:
            data.append(self.treemodel_get_row_filename(model, row))
        return (model, rows, data)

    def on_treeview_button_press_event(self, treeview, event):
        """Catch single mouse click events on the treeview and rows.

            Left Click:     Ignore.
            Middle Click:   Open the selected file.
            Right Click:    Show the popup menu."""

        model, self.rows, self.selected_filenames = \
            self.treeview_get_selected_rows(treeview)

        # If left click, ignore.
        if event.button == 1:
            return False

        # Get the selected row path, raises TypeError if dead space.
        if treeview.get_selection().count_selected_rows() <= 1:
            try:
                path = treeview.get_path_at_pos(int(event.x), int(event.y))[0]
                treeview.set_cursor(path)
            except TypeError:
                return False

        # If middle click, open the selected file.
        if event.button == 2:
            for filename in selected_filenames:
                self.open_file(filename)

        # If right click, show the popup menu.
        if event.button == 3:
            show_save_option = len(self.selected_filenames) == 1 and not \
                os.path.isdir(self.selected_filenames[0])
            self.file_menu_save.set_visible(show_save_option)
            writeable = True
            for filename in self.selected_filenames:
                if not os.access(filename, os.W_OK):
                    writeable = False
            self.file_menu_delete.set_sensitive(writeable)
            self.file_menu.popup(None, None, None, None,
                                 event.button, event.time)
        return True

    def new_column(self, label, id, special=None, markup=False):
        """New Column function for creating TreeView columns easily."""
        if special == 'icon':
            column = Gtk.TreeViewColumn(label)
            cell = Gtk.CellRendererPixbuf()
            column.pack_start(cell, False)
            column.set_cell_data_func(cell, self.preview_cell_data_func, None)
            cell = Gtk.CellRendererText()
            column.pack_start(cell, False)
            column.add_attribute(cell, 'text', id)
        else:
            cell = Gtk.CellRendererText()
            if markup:
                column = Gtk.TreeViewColumn(label, cell, markup=id)
            else:
                column = Gtk.TreeViewColumn(label, cell, text=id)
            if special == 'ellipsize':
                column.set_min_width(120)
                cell.set_property('ellipsize', Pango.EllipsizeMode.START)
            elif special == 'filesize':
                cell.set_property('xalign', 1.0)
                column.set_cell_data_func(cell,
                                          self.cell_data_func_filesize, id)
            elif special == 'date':
                column.set_cell_data_func(cell,
                                          self.cell_data_func_modified, id)
        column.set_sort_column_id(id)
        column.set_resizable(True)
        if id == 3:
            column.set_expand(True)
        return column

    def cell_data_func_filesize(self, column, cell_renderer,
                                tree_model, tree_iter, id):
        """File size cell display function."""
        if pyversion >= 3.0:
            size = int(tree_model.get_value(tree_iter, id))
        else:
            size = long(tree_model.get_value(tree_iter, id))

        filesize = self.format_size(size)
        cell_renderer.set_property('text', filesize)
        return

    def cell_data_func_modified(self, column, cell_renderer,
                                tree_model, tree_iter, id):
        """Modification date cell display function."""
        modification_int = int(tree_model.get_value(tree_iter, id))
        modified = self.get_date_string(modification_int)

        cell_renderer.set_property('text', modified)
        return

    def get_date_string(self, modification_int):
        """Return the date string in the preferred format."""
        if self.time_format is not None:
            modified = time.strftime(self.time_format,
                                     time.gmtime(modification_int))
        else:
            item_date = datetime.datetime.fromtimestamp(modification_int)
            if item_date >= self.today:
                modified = _("Today")
            elif item_date >= self.yesterday:
                modified = _("Yesterday")
            elif item_date >= self.this_week:
                modified = time.strftime("%A", time.gmtime(modification_int))
            else:
                modified = time.strftime("%x", time.gmtime(modification_int))
        return modified

    def results_filter_func(self, model, iter, user_data):
        """Filter function for search results."""
        # hidden
        if model[iter][6]:
            if not self.filter_formats['hidden']:
                return False

        # exact
        if not self.filter_formats['fulltext']:
            if not model[iter][7]:
                if self.filter_formats['exact']:
                    return False

        # modified
        modified = model[iter][4]
        if modified < self.filter_timerange[0] or \
                modified > self.filter_timerange[1]:
            return False

        # mimetype
        mimetype = model[iter][5]
        use_filters = False
        if self.filter_formats['folders']:
            use_filters = True
            if mimetype == 'inode/directory':
                return True
        if self.filter_formats['images']:
            use_filters = True
            if mimetype.startswith("image"):
                return True
        if self.filter_formats['music']:
            use_filters = True
            if mimetype.startswith("audio"):
                return True
        if self.filter_formats['videos']:
            use_filters = True
            if mimetype.startswith("video"):
                return True
        if self.filter_formats['documents']:
            use_filters = True
            if mimetype.startswith("text"):
                return True
        if self.filter_formats['applications']:
            use_filters = True
            if mimetype.startswith("application"):
                return True
        if self.filter_formats['other']:
            use_filters = True
            if self.filter_custom_use_mimetype:
                if mimetype == self.filter_custom_mimetype['category_text'] + \
                        "/" + self.filter_custom_mimetype['type_text']:
                    return True
            else:
                extension = os.path.splitext(model[iter][1])[1]
                if extension in self.filter_custom_extensions:
                    return True

        if use_filters:
            return False

        return True

    def refilter(self):
        """Reload the results filter, update the statusbar to reflect count."""
        try:
            self.results_filter.refilter()
            n_results = len(self.treeview.get_model())
            if n_results == 0:
                self.statusbar_label.set_label(_("No files found."))
            elif n_results == 1:
                self.statusbar_label.set_label(_("1 file found."))
            else:
                self.statusbar_label.set_label(
                    _("%i files found.") % n_results)
        except AttributeError:
            pass

    def format_size(self, size, precision=1):
        """Make a file size human readable."""
        if isinstance(size, str):
            size = int(size)
        suffixes = [_('bytes'), 'kB', 'MB', 'GB', 'TB']
        suffixIndex = 0
        if size > 1024:
            while size > 1024:
                suffixIndex += 1
                size = size / 1024.0
            return "%.*f %s" % (precision, size, suffixes[suffixIndex])
        else:
            return "%i %s" % (size, suffixes[0])

    def guess_mimetype(self, filename):
        """Guess the mimetype of the specified filename.

        Return a tuple containing (guess, override)."""
        override = None
        if os.path.isdir(filename):
            return ('inode/directory', override)

        extension = os.path.splitext(filename)[1]
        if extension in [
                '.abw', '.ai', '.chrt', '.doc', '.docm', '.docx', '.dot',
                '.dotm', '.dotx', '.eps', '.gnumeric', '.kil', '.kpr', '.kpt',
                '.ksp', '.kwd', '.kwt', '.latex', '.mdb', '.mm', '.nb', '.nbp',
                '.odb', '.odc', '.odf', '.odm', '.odp', '.ods', '.odt', '.otg',
                '.oth', '.odp', '.ots', '.ott', '.pdf', '.php', '.pht',
                '.phtml', '.potm', '.potx', '.ppa', '.ppam', '.pps', '.ppsm',
                '.ppsx', '.ppt', '.pptm', '.pptx', '.ps', '.pwz', '.rtf',
                '.sda', '.sdc', '.sdd', '.sds', '.sdw', '.stc', '.std', '.sti',
                '.stw', '.sxc', '.sxd', '.sxg', '.sxi', '.sxm', '.sxw', '.wiz',
                '.wp5', '.wpd', '.xlam', '.xlb', '.xls', '.xlsb', '.xlsm',
                '.xlsx', '.xlt', '.xltm', '.xlsx', '.xml']:
            override = 'text/plain'
        elif extension in ['.cdy']:
            override = 'video/x-generic'
        elif extension in ['.odg', '.odi']:
            override = 'audio/x-generic'

        guess = mimetypes.guess_type(filename)
        if guess[0] is None:
            return ('text/plain', override)
        return (guess[0], override)

    def get_icon_pixbuf(self, name):
        """Return a pixbuf for the icon name from the default icon theme."""
        try:
            # Clear the icon cache if the current size is not the cached size.
            if self.icon_cache_size != self.icon_size:
                self.icon_cache.clear()
                self.icon_cache_size = self.icon_size
            return self.icon_cache[name]
        except KeyError:
            icon_size = Gtk.icon_size_lookup(self.icon_size)[1]
            if self.icon_theme.has_icon(name):
                icon = self.icon_theme.load_icon(name, icon_size, 0)
            else:
                icon = self.icon_theme.load_icon('image-missing', icon_size, 0)
            self.icon_cache[name] = icon
            return icon

    def get_thumbnail(self, path, mime_type=None):
        """Try to fetch a thumbnail."""
        thumbnails_directory = os.path.expanduser('~/.thumbnails/normal')
        uri = 'file://' + path
        if pyversion >= 3.0:
            uri = uri.encode('utf-8')
        md5_hash = hashlib.md5(uri).hexdigest()
        thumbnail_path = os.path.join(
            thumbnails_directory, '%s.png' % md5_hash)
        if os.path.isfile(thumbnail_path):
            return thumbnail_path
        if mime_type.startswith('image'):
            new_thumb = self.create_thumbnail(path, thumbnail_path)
            if new_thumb:
                return thumbnail_path
        return self.get_file_icon(path, mime_type)

    def create_thumbnail(self, filename, path):
        """Create a thumbnail image and save it to the thumbnails directory.
        Return True if successful."""
        try:
            pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
            pixbuf_w = pixbuf.get_width()
            pixbuf_h = pixbuf.get_height()
            if pixbuf_w < 128 and pixbuf_h < 128:
                pixbuf.savev(path, "png", [], [])
                return pixbuf
            if pixbuf_w > pixbuf_h:
                thumb_w = 128
                thumb_h = int(pixbuf_h / (pixbuf_w / 128.0))
            else:
                thumb_h = 128
                thumb_w = int(pixbuf_w / (pixbuf_h / 128.0))
            thumb_pixbuf = pixbuf.scale_simple(
                thumb_w, thumb_h, GdkPixbuf.InterpType.BILINEAR)
            thumb_pixbuf.savev(path, "png", [], [])
            return True
        except Exception as e:
            print (e)
            return False

    def get_file_icon(self, path, mime_type=None):
        """Retrieve the file icon."""
        if mime_type:
            if mime_type == 'inode/directory':
                return Gtk.STOCK_DIRECTORY
            else:
                mime_type = mime_type.split('/')
                if mime_type is not None:
                    # Get icon from mimetype
                    media, subtype = mime_type
                    for icon_name in ['gnome-mime-%s-%s' % (media, subtype),
                                      'gnome-mime-%s' % media]:
                        if self.icon_theme.has_icon(icon_name):
                            return icon_name
        return Gtk.STOCK_FILE

    def python_three_size_sort_func(self, model, row1, row2, user_data):
        """Sort function used in Python 3."""
        sort_column = 2
        value1 = int(model.get_value(row1, sort_column))
        value2 = int(model.get_value(row2, sort_column))
        if value1 < value2:
            return -1
        elif value1 == value2:
            return 0
        else:
            return 1

    # -- Searching -- #
    def perform_query(self, keywords):
        """Run the search query with the specified keywords."""
        self.stop_search = False

        # Update the interface to Search Mode
        self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH))
        self.set_title(_("Searching for \"%s\"") % keywords)
        self.spinner.show()
        self.statusbar_label.set_label(_("Searching…"))
        self.search_entry.set_icon_from_stock(
            Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_STOP)
        self.search_entry.set_icon_tooltip_text(
            Gtk.EntryIconPosition.SECONDARY, _('Stop Search'))

        self.search_in_progress = True

        # Be thread friendly.
        while Gtk.events_pending():
            Gtk.main_iteration()

        # icon, name, size, path, modified, mimetype, hidden, exact
        model = Gtk.ListStore(str, str, GObject.TYPE_LONG,
                              str, float, str, bool, bool, int)

        # Initialize the results filter.
        self.results_filter = model.filter_new()
        self.results_filter.set_visible_func(self.results_filter_func)
        sort = Gtk.TreeModelSort(model=self.results_filter)
        if pyversion >= 3.0:
            sort.set_sort_func(2, self.python_three_size_sort_func, None)
        self.treeview.set_model(sort)
        self.treeview.columns_autosize()

        folder = self.folderchooser.get_filename()

        results = []
        counter = 0
        self.file_paths = []

        # Check if this is a fulltext query or standard query.
        if self.filter_formats['fulltext']:
            self.search_engine = CatfishSearchEngine(['fulltext'])
            self.search_engine.set_exact(self.filter_formats['exact'])
        else:
            self.search_engine = CatfishSearchEngine()

        for filename in self.search_engine.run(keywords, folder, regex=True):
            if isinstance(filename, str) and not self.stop_search and \
                    filename not in results:
                try:
                    path, name = os.path.split(filename)

                    if pyversion >= 3.0:
                        size = int(os.path.getsize(filename))
                    else:
                        size = long(os.path.getsize(filename))

                    modified = os.path.getmtime(filename)

                    mimetype, override = self.guess_mimetype(filename)
                    icon = self.get_thumbnail(filename, mimetype)
                    if override:
                        mimetype = override

                    hidden = is_file_hidden(folder, filename)

                    exact = keywords in name

                    results.append(filename)
                    self.file_paths.append(filename)

                    displayed = surrogate_escape(name, True)
                    path = surrogate_escape(path)
                    model.append([icon, displayed, size, path, modified,
                                  mimetype, hidden, exact, counter])
                except OSError:
                    # file no longer exists
                    pass
                except Exception as e:
                    logger.error("Exception encountered: ", str(e))
                counter += 1

            yield True
            continue

        # Return to Non-Search Mode.
        self.get_window().set_cursor(None)
        self.set_title(_('Search results for \"%s\"') % keywords)
        self.spinner.hide()

        n_results = len(self.treeview.get_model())
        if n_results == 0:
            self.statusbar_label.set_label(_("No files found."))
        elif n_results == 1:
            self.statusbar_label.set_label(_("1 file found."))
        else:
            self.statusbar_label.set_label(_("%i files found.") % n_results)

        self.search_in_progress = False
        if len(self.search_entry.get_text()) == 0:
            self.search_entry.set_icon_from_stock(
                Gtk.EntryIconPosition.SECONDARY,
                Gtk.STOCK_FIND)
            self.search_entry.set_icon_tooltip_text(
                Gtk.EntryIconPosition.SECONDARY,
                _('Enter search terms and press ENTER'))
        else:
            self.search_entry.set_icon_from_stock(
                Gtk.EntryIconPosition.SECONDARY,
                Gtk.STOCK_CLEAR)
            self.search_entry.set_icon_tooltip_text(
                Gtk.EntryIconPosition.SECONDARY,
                _('Clear search terms'))

        self.stop_search = False
        yield False
