1#!/usr/bin/env python
2# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
3#   Catfish - a versatile file searching tool
4#   Copyright (C) 2007-2012 Christian Dywan <christian@twotoasts.de>
5#   Copyright (C) 2012-2020 Sean Davis <bluesabre@xfce.org>
6#
7#   This program is free software: you can redistribute it and/or modify it
8#   under the terms of the GNU General Public License version 2, as published
9#   by the Free Software Foundation.
10#
11#   This program is distributed in the hope that it will be useful, but
12#   WITHOUT ANY WARRANTY; without even the implied warranties of
13#   MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14#   PURPOSE.  See the GNU General Public License for more details.
15#
16#   You should have received a copy of the GNU General Public License along
17#   with this program.  If not, see <https://www.gnu.org/licenses/>.
18
19# pylint: disable=W0201
20# pylint: disable=C0103
21# pylint: disable=C0114
22# pylint: disable=C0116
23# pylint: disable=C0413
24
25
26import datetime
27import logging
28import mimetypes
29import os
30import subprocess
31import time
32import zipfile
33import tempfile
34from locale import gettext as _
35from shutil import copy2, rmtree
36from xml.sax.saxutils import escape
37
38# Thunar Integration
39import urllib
40import dbus
41
42import pexpect
43import gi
44gi.require_version('GLib', '2.0')  # noqa
45gi.require_version('GObject', '2.0')  # noqa
46gi.require_version('Pango', '1.0')  # noqa
47gi.require_version('Gdk', '3.0')  # noqa
48gi.require_version('GdkPixbuf', '2.0')  # noqa
49gi.require_version('Gtk', '3.0')  # noqa
50from gi.repository import GLib, GObject, Pango, Gdk, GdkPixbuf, Gtk, Gio
51
52from catfish.CatfishPrefsDialog import CatfishPrefsDialog
53from catfish.CatfishSearchEngine import CatfishSearchEngine, get_keyword_list
54from catfish_lib import catfishconfig, helpers, get_about
55from catfish_lib import CatfishSettings, SudoDialog, Window
56from catfish_lib import Thumbnailer
57
58LOGGER = logging.getLogger('catfish')
59
60
61# Initialize Gtk, GObject, and mimetypes
62if not helpers.check_gobject_version(3, 9, 1):
63    GObject.threads_init()
64    GLib.threads_init()
65mimetypes.init()
66
67
68def long(value):
69    return int(value)
70
71
72def get_application_path(application_name):
73    for path in os.getenv('PATH').split(':'):
74        if os.path.isdir(path):
75            if application_name in os.listdir(path):
76                return os.path.join(path, application_name)
77    return None
78
79
80def application_in_PATH(application_name):
81    """Return True if the application name is found in PATH."""
82    return get_application_path(application_name) is not None
83
84
85def is_file_hidden(folder, filename):
86    """Return TRUE if file is hidden or in a hidden directory."""
87    folder = os.path.abspath(folder)
88    filename = os.path.abspath(filename)
89    relpath = os.path.relpath(filename, folder)
90    for piece in relpath.split(os.sep):
91        if piece.startswith("."):
92            return True
93    return False
94
95
96def surrogate_escape(text, replace=False):
97    """Replace non-UTF8 characters with something displayable.
98    If replace is True, display (invalid encoding) after the text."""
99    try:
100        text.encode('utf-8')
101    except UnicodeEncodeError:
102        text = text.encode('utf-8', errors='surrogateescape').decode(
103            'utf-8', errors='replace')
104        if replace:
105            # Translators: this text is displayed next to
106            # a filename that is not utf-8 encoded.
107            text = _("%s (invalid encoding)") % text
108    except UnicodeDecodeError:
109        text = text.decode('utf-8', errors='replace')
110    return text
111
112
113# See catfish_lib.Window.py for more details about how this class works
114class CatfishWindow(Window):
115
116    """The application window."""
117    __gtype_name__ = "CatfishWindow"
118
119    filter_timerange = (0.0, 9999999999.0)
120    start_date = datetime.datetime.now()
121    end_date = datetime.datetime.now()
122
123    filter_formats = {'documents': False, 'folders': False, 'images': False,
124                      'music': False, 'videos': False, 'applications': False,
125                      'other': False, 'exact': False, 'hidden': False,
126                      'fulltext': False}
127
128    filter_custom_extensions = []
129    filter_custom_use_mimetype = False
130
131    mimetypes = dict()
132    search_in_progress = False
133
134    def get_about_dialog(self):
135        about = get_about()
136
137        dlg = GObject.new(Gtk.AboutDialog, use_header_bar=True)
138        dlg.set_program_name(about['program_name'])
139        dlg.set_version(about['version'])
140        dlg.set_logo_icon_name(about['icon_name'])
141        dlg.set_website(about['website'])
142        dlg.set_comments(about['comments'])
143        dlg.set_license_type(Gtk.License.GPL_2_0)
144        dlg.set_copyright(about['copyright'])
145        dlg.set_authors(about['authors'])
146        dlg.set_artists(about['artists'])
147        dlg.set_translator_credits(_("translator-credits"))
148        dlg.set_transient_for(self)
149
150        # Cleanup duplicate buttons
151        hbar = dlg.get_header_bar()
152        for child in hbar.get_children():
153            if type(child) in [Gtk.Button, Gtk.ToggleButton]:
154                child.destroy()
155
156        return dlg
157
158    def finish_initializing(self, builder):
159        """Set up the main window"""
160        super(CatfishWindow, self).finish_initializing(builder)
161
162        self.AboutDialog = self.get_about_dialog
163
164        self.settings = CatfishSettings.CatfishSettings()
165
166        # -- Folder Chooser Combobox -- #
167        self.folderchooser = builder.get_named_object("toolbar.folderchooser")
168
169        # -- Search Entry and Completion -- #
170        self.search_entry = builder.get_named_object("toolbar.search")
171        self.suggestions_engine = CatfishSearchEngine(['zeitgeist'])
172        completion = Gtk.EntryCompletion()
173        self.search_entry.set_completion(completion)
174        listmodel = Gtk.ListStore(str)
175        completion.set_model(listmodel)
176        completion.set_text_column(0)
177
178        # -- App Menu -- #
179        self.exact_match = builder.get_named_object("menus.application.exact")
180        self.hidden_files = builder.get_named_object(
181            "menus.application.hidden")
182        self.fulltext = builder.get_named_object("menus.application.fulltext")
183        self.sidebar_toggle_menu = builder.get_named_object(
184            "menus.application.advanced")
185
186        # -- Sidebar -- #
187        css = Gtk.CssProvider()
188        css.load_from_data(b".sidebar .view {background-color: transparent;}")
189        screen = Gdk.Screen.get_default()
190        style = Gtk.StyleContext()
191        style.add_provider_for_screen(
192              screen, css, Gtk.STYLE_PROVIDER_PRIORITY_SETTINGS)
193        self.button_time_custom = builder.get_named_object(
194            "sidebar.modified.options")
195        self.button_format_custom = builder.get_named_object(
196            "sidebar.filetype.options")
197
198        # -- Status Bar -- *
199        # Create a new GtkOverlay to hold the
200        # results list and Overlay Statusbar
201        overlay = Gtk.Overlay()
202
203        # Move the results list to the overlay and
204        # place the overlay in the window
205        scrolledwindow = builder.get_named_object("results.scrolled_window")
206        parent = scrolledwindow.get_parent()
207        parent.remove(scrolledwindow)
208        overlay.add(scrolledwindow)
209        parent.add(overlay)
210        overlay.show()
211
212        # Create the overlay statusbar
213        self.statusbar = Gtk.EventBox()
214        self.statusbar.set_margin_start(2)
215        self.statusbar.set_margin_end(3)
216        self.statusbar.set_margin_bottom(3)
217        self.statusbar.get_style_context().add_class("frame")
218        self.statusbar.get_style_context().add_class("background")
219        self.statusbar.get_style_context().add_class("floating-bar")
220        self.statusbar.connect("draw", self.on_floating_bar_draw)
221        self.statusbar.connect("enter-notify-event",
222                               self.on_floating_bar_enter_notify)
223        self.statusbar.set_halign(Gtk.Align.END)
224        self.statusbar.set_valign(Gtk.Align.END)
225
226        # Put the statusbar in the overlay
227        overlay.add_overlay(self.statusbar)
228
229        # Pack the spinner and label
230        self.spinner = Gtk.Spinner()
231        self.spinner.start()
232        self.statusbar_label = Gtk.Label()
233        self.statusbar_label.show()
234
235        box = Gtk.Box()
236        box.set_orientation(Gtk.Orientation.HORIZONTAL)
237        box.pack_start(self.spinner, False, False, 0)
238        box.pack_start(self.statusbar_label, False, False, 0)
239        box.set_margin_start(8)
240        box.set_margin_top(3)
241        box.set_margin_end(8)
242        box.set_margin_bottom(3)
243        self.spinner.set_margin_end(3)
244        box.show()
245
246        self.statusbar.add(box)
247        self.statusbar.set_halign(Gtk.Align.END)
248        self.statusbar.hide()
249
250        self.list_toggle = builder.get_named_object("toolbar.view.list")
251        self.thumbnail_toggle = builder.get_named_object("toolbar.view.thumbs")
252
253        # -- Treeview -- #
254        self.treeview = builder.get_named_object("results.treeview")
255        self.treeview.enable_model_drag_source(
256            Gdk.ModifierType.BUTTON1_MASK,
257            [('text/plain', Gtk.TargetFlags.OTHER_APP, 0),
258             ('text/uri-list', Gtk.TargetFlags.OTHER_APP, 0)],
259            Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
260        self.treeview.drag_source_add_text_targets()
261        self.file_menu = builder.get_named_object("menus.file.menu")
262        self.file_menu_save = builder.get_named_object("menus.file.save")
263        self.file_menu_delete = builder.get_named_object("menus.file.delete")
264        self.treeview_click_on = False
265
266        # -- Update Search Index Dialog -- #
267        menuitem = builder.get_named_object("menus.application.update")
268        if SudoDialog.check_dependencies(['locate', 'updatedb']):
269            self.update_index_dialog = \
270                builder.get_named_object("dialogs.update.dialog")
271            self.update_index_database = \
272                builder.get_named_object("dialogs.update.database_label")
273            self.update_index_modified = \
274                builder.get_named_object("dialogs.update.modified_label")
275            self.update_index_infobar = \
276                builder.get_named_object("dialogs.update.status_infobar")
277            self.update_index_icon = \
278                builder.get_named_object("dialogs.update.status_icon")
279            self.update_index_label = \
280                builder.get_named_object("dialogs.update.status_label")
281            self.update_index_close = \
282                builder.get_named_object("dialogs.update.close_button")
283            self.update_index_unlock = \
284                builder.get_named_object("dialogs.update.unlock_button")
285            self.update_index_active = False
286
287            self.last_modified = 0
288
289            now = datetime.datetime.now()
290            self.today = datetime.datetime(now.year, now.month, now.day)
291            locate_path, locate_date = self.check_locate()[1:3]
292
293            self.update_index_database.set_label("<tt>%s</tt>" % locate_path)
294            if not os.access(os.path.dirname(locate_path), os.R_OK):
295                modified = _("Unknown")
296            elif os.path.isfile(locate_path):
297                modified = locate_date.strftime("%x %X")
298            else:
299                modified = _("Never")
300            self.update_index_modified.set_label("<tt>%s</tt>" % modified)
301
302            if locate_date < self.today - datetime.timedelta(days=7):
303                infobar = builder.get_named_object("infobar.infobar")
304                infobar.show()
305        else:
306            menuitem.hide()
307
308        self.format_mimetype_box = \
309            builder.get_named_object("dialogs.filetype.mimetypes.box")
310        self.extensions_entry = \
311            builder.get_named_object("dialogs.filetype.extensions.entry")
312
313        self.search_engine = CatfishSearchEngine(
314            ['zeitgeist', 'locate', 'walk'],
315            self.settings.get_setting("exclude-paths"))
316
317        self.icon_theme = Gtk.IconTheme.get_default()
318        self.icon_theme.connect('changed', self.changed_icon)
319        self.changed_icon_theme = False
320        self.selected_filenames = []
321        self.rows = []
322
323        paned = builder.get_named_object("window.paned")
324        paned.set_property('height_request',
325                           self.settings.get_setting('window-height'))
326        paned.set_property('width_request',
327                           self.settings.get_setting('window-width'))
328
329        window_width = self.settings.get_setting('window-width')
330        window_height = self.settings.get_setting('window-height')
331        window_x = self.settings.get_setting('window-x')
332        window_y = self.settings.get_setting('window-y')
333        (screen_width, screen_height) = self.get_screen_size()
334        (display_width, display_height) = self.get_display_size()
335
336        if (screen_width, screen_height) == (-1, -1) or \
337                (display_width, display_height) == (-1, -1):
338            # Failed detection, likely using Wayland, don't resize
339            pass
340        else:
341            if (window_height > screen_height or window_width > screen_width):
342                window_width = min(display_width, 650)
343                window_height = min(display_height, 470)
344
345            paned.set_property('height_request', window_height)
346            paned.set_property('width_request', window_width)
347
348            if (window_x >= 0 and window_y >= 0):
349                if (window_x + window_width <= screen_width) and \
350                (window_y + window_height <= screen_height):
351                    self.move(window_x, window_y)
352
353        self.refresh_search_entry()
354
355        filetype_filters = builder.get_object("filetype_options")
356        filetype_filters.connect(
357            "row-activated", self.on_file_filters_changed, builder)
358
359        modified_filters = builder.get_object("modified_options")
360        modified_filters.connect(
361            "row-activated", self.on_modified_filters_changed, builder)
362
363        self.popovers = dict()
364
365        extension_filter = builder.get_object("filter_extensions")
366        extension_filter.connect(
367            "search-changed", self.on_filter_extensions_changed)
368
369        start_calendar = self.builder.get_named_object(
370            "dialogs.date.start_calendar")
371        end_calendar = self.builder.get_named_object(
372            "dialogs.date.end_calendar")
373        start_calendar.connect("day-selected", self.on_calendar_day_changed)
374        end_calendar.connect("day-selected", self.on_calendar_day_changed)
375
376        self.app_menu_event = False
377
378        self.thumbnailer = Thumbnailer.Thumbnailer()
379        self.configure_welcome_area(builder)
380        self.add_mimetypes()
381        self.tmpdir = tempfile.TemporaryDirectory(prefix='catfish-')
382        self.toolbar_hotkeys()
383
384    def add_mimetypes(self):
385        """Copies MIME info generated by update-mime-database (from
386           shared-mime-info) to Python's mimetypes module enabling it
387           to match local MIME db results (xdg-mime from xdg-utils)."""
388
389        glob2 = '/usr/share/mime/globs2'
390        if not os.path.exists(glob2):
391            return
392
393        with open("/usr/share/mime/globs2") as f:
394            lines = f.readlines()
395            for line in reversed(lines):
396                try:
397                    if ':*.' in line:
398                        s = line[3:].strip().split(':*')
399                    elif ':' in line:
400                        s = line[3:].strip('.*/\n').split(':')
401                    else:
402                        continue
403                    mimetypes.add_type(s[0], s[1], strict=True)
404                except IndexError:
405                    continue
406
407    def configure_welcome_area(self, builder):
408        welcome_area = builder.get_object("welcome_area")
409        content = _("Enter your query above to find your files\n"
410                    "or click the %s icon for more options.")
411        for line in content.split("\n"):
412            if "%s" in line:
413                row = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
414                parts = line.split("%s")
415                if len(parts[0].strip()) > 0:
416                    label = Gtk.Label.new(parts[0])
417                    row.pack_start(label, False, False, 0)
418                image = Gtk.Image.new_from_icon_name("open-menu-symbolic",
419                                                     Gtk.IconSize.BUTTON)
420                image.set_property("use-fallback", True)
421                row.pack_start(image, False, False, 0)
422                if len(parts[1].strip()) > 0:
423                    label = Gtk.Label.new(parts[1])
424                    row.pack_start(label, False, False, 0)
425            else:
426                row = Gtk.Label.new(line)
427            row.set_halign(Gtk.Align.CENTER)
428            welcome_area.pack_start(row, False, False, 0)
429        welcome_area.show_all()
430
431    def get_screen_size(self):
432        screen = Gdk.Screen.get_default()
433        if screen is None:
434            return (-1, -1)
435        return (screen.width(), screen.height())
436
437    def get_display_size(self):
438        display = Gdk.Display.get_default()
439        if display is None:
440            return (-1, -1)
441        window = Gdk.get_default_root_window()
442        mon = display.get_monitor_at_window(window)
443        monitor = mon.get_geometry()
444        return (monitor.width, monitor.height)
445
446    def on_calendar_day_changed(self, widget):  # pylint: disable=W0613
447        start_calendar = self.builder.get_named_object(
448            "dialogs.date.start_calendar")
449        end_calendar = self.builder.get_named_object(
450            "dialogs.date.end_calendar")
451
452        start_date = start_calendar.get_date()
453        self.start_date = datetime.datetime(start_date[0], start_date[1] + 1,
454                                            start_date[2])
455
456        end_date = end_calendar.get_date()
457        self.end_date = datetime.datetime(end_date[0], end_date[1] + 1,
458                                          end_date[2])
459        self.end_date = self.end_date + datetime.timedelta(days=1, seconds=-1)
460
461        self.filter_timerange = (time.mktime(self.start_date.timetuple()),
462                                 time.mktime(self.end_date.timetuple()))
463
464        self.refilter()
465
466    def on_application_menu_row_activated(self, listbox, row):
467        self.app_menu_event = not self.app_menu_event
468        if not self.app_menu_event:
469            return
470        if listbox.get_row_at_index(8) == row:
471            listbox.get_parent().hide()
472            self.on_menu_update_index_activate(row)
473        if listbox.get_row_at_index(10) == row:
474            listbox.get_parent().hide()
475            self.on_menu_preferences_activate(row)
476        if listbox.get_row_at_index(11) == row:
477            listbox.get_parent().hide()
478            self.on_mnu_about_activate(row)
479
480    def on_file_filters_changed(self, treeview, path, column, builder):
481        model = treeview.get_model()
482        treeiter = model.get_iter(path)
483        row = model[treeiter]
484        showPopup = row[2] == "other" and row[5] == 0
485        if treeview.get_column(2) == column:
486            if row[5]:
487                popover = self.get_popover(row[2], builder)
488                popover.show_all()
489                return
490            row[3], row[4] = row[4], row[3]
491        else:
492            row[3], row[4] = row[4], row[3]
493        if row[2] == 'other' or row[2] == 'custom':
494            row[5] = row[3]
495        if showPopup and row[5]:
496            popover = self.get_popover(row[2], builder)
497            popover.show_all()
498        self.filter_formats[row[2]] = row[3]
499        self.refilter()
500
501    def get_popover(self, name, builder):
502        if name == "other":
503            popover_id = "filetype"
504        elif name == "custom":
505            popover_id = "modified"
506        else:
507            return False
508        if popover_id not in self.popovers.keys():
509            builder.get_object(popover_id + "_popover")
510            popover = Gtk.Popover.new()
511            popover.connect("destroy", self.popover_content_destroy)
512            popover.add(builder.get_object(popover_id + "_popover"))
513            popover.set_relative_to(builder.get_object(name + "_helper"))
514            popover.set_position(Gtk.PositionType.BOTTOM)
515            self.popovers[popover_id] = popover
516        return self.popovers[popover_id]
517
518    def popover_content_destroy(self, widget):
519        widget.hide()
520        return False
521
522    def on_modified_filters_changed(self, treeview, path, column, builder):
523        model = treeview.get_model()
524        treeiter = model.get_iter(path)
525        selected = model[treeiter]
526        showPopup = selected[2] == "custom" and selected[5] == 0
527        treeiter = model.get_iter_first()
528        while treeiter:
529            row = model[treeiter]
530            row[3], row[4], row[5] = 0, 1, 0
531            treeiter = model.iter_next(treeiter)
532        selected[3], selected[4] = 1, 0
533        if selected[2] == "custom":
534            selected[5] = 1
535        if treeview.get_column(2) == column:
536            if selected[5]:
537                showPopup = True
538        if showPopup:
539            popover = self.get_popover(selected[2], builder)
540            popover.show_all()
541        self.set_modified_range(selected[2])
542        self.refilter()
543
544    def on_update_infobar_response(self, widget, response_id):
545        if response_id == Gtk.ResponseType.OK:
546            self.on_menu_update_index_activate(widget)
547        widget.hide()
548
549    def on_floating_bar_enter_notify(self, widget, event):  # pylint: disable=W0613
550        """Move the floating statusbar when hovered."""
551        if widget.get_halign() == Gtk.Align.END:
552            widget.hide()
553            widget.set_halign(Gtk.Align.START)
554            widget.show()
555
556        else:
557            widget.hide()
558            widget.set_halign(Gtk.Align.END)
559            widget.show()
560
561
562    def on_floating_bar_draw(self, widget, cairo_t):
563        """Draw the floating statusbar."""
564        context = widget.get_style_context()
565
566        context.save()
567        context.set_state(widget.get_state_flags())
568
569        Gtk.render_background(context, cairo_t, 0, 0,
570                              widget.get_allocated_width(),
571                              widget.get_allocated_height())
572
573        Gtk.render_frame(context, cairo_t, 0, 0,
574                         widget.get_allocated_width(),
575                         widget.get_allocated_height())
576
577        context.restore()
578
579        return False
580
581    def get_path(self, arg):
582        realpath = os.path.realpath(arg)
583        if os.path.isdir(realpath):
584            return realpath
585        realpath = os.path.realpath(os.path.expanduser(arg))
586        if os.path.isdir(realpath):
587            return realpath
588        return None
589
590    def parse_path_option(self, options, args):  # pylint: disable=W0613
591        # Set the selected folder path. Allow legacy --path option.
592        path = None
593
594        # New format, first argument
595        if self.options.path is None:
596            if len(args) > 0:
597                path = self.get_path(args[0])
598                if path:
599                    args.pop(0)
600
601        # Old format, --path
602        else:
603            path = self.get_path(self.options.path)
604
605        # Try the user home directory
606        if path is None:
607            path = self.get_path("~")
608
609        # Once all options are exhausted, return the root
610        if path is None:
611            path = "/"
612
613        return path
614
615    def parse_options(self, options, args):
616        """Parse commandline arguments into Catfish runtime settings."""
617        self.options = options
618        self.options.path = self.parse_path_option(options, args)
619
620        self.folderchooser.set_filename(self.options.path)
621
622        # Set non-flags as search keywords.
623        self.search_entry.set_text(' '.join(args))
624
625        # Set the time display format.
626        if self.options.time_iso:
627            self.time_format = '%Y-%m-%d %H:%M'
628        else:
629            self.time_format = None
630
631        # Set search defaults.
632        self.exact_match.set_active(
633            self.options.exact or
634            self.settings.get_setting('match-results-exactly'))
635        self.hidden_files.set_active(
636            self.options.hidden or
637            self.settings.get_setting('show-hidden-files'))
638        self.fulltext.set_active(
639            self.options.fulltext or
640            self.settings.get_setting('search-file-contents'))
641        self.sidebar_toggle_menu.set_active(
642            self.settings.get_setting('show-sidebar'))
643
644        self.show_thumbnail = self.options.thumbnails
645
646        # Set the interface to standard or preview mode.
647
648        if self.options.icons_large:
649            self.show_thumbnail = False
650            self.setup_large_view()
651            self.list_toggle.set_active(True)
652        elif self.settings.get_setting('show-thumbnails'):
653            self.show_thumbnail = True
654            self.setup_large_view()
655            self.thumbnail_toggle.set_active(True)
656        else:
657            self.show_thumbnail = False
658            self.setup_small_view()
659            self.list_toggle.set_active(True)
660
661        if self.options.start:
662            self.on_search_entry_activate(self.search_entry)
663
664    def preview_cell_data_func(self, col, renderer, model, treeiter, data):  # pylint: disable=W0613
665        """Cell Renderer Function for the preview."""
666        icon_name = model[treeiter][0]
667        fullpath = os.path.join(model[treeiter][3], model[treeiter][1])
668        emblem_icon = 'emblem-symbolic-link'
669        if os.path.isfile(icon_name):
670            # Load from thumbnail file.
671            if self.show_thumbnail:
672                pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_name)
673                renderer.set_property('pixbuf', pixbuf)
674                return
675            else:
676                mimetype = self.guess_mimetype(fullpath)
677                icon_name = self.get_file_icon(fullpath, mimetype)
678
679        if self.changed_icon_theme:
680            mimetype = self.guess_mimetype(fullpath)
681            icon_name = self.get_file_icon(fullpath, mimetype)
682
683        if os.path.islink(fullpath) and self.icon_theme.has_icon(emblem_icon):
684            pixbuf = self.create_symlink_icon(fullpath, icon_name, emblem_icon)
685        else:
686            pixbuf = self.get_icon_pixbuf(icon_name)
687
688        renderer.set_property('pixbuf', pixbuf)
689
690    def thumbnail_cell_data_func(self, col, renderer, model, treeiter, data):  # pylint: disable=W0613
691        """Cell Renderer Function to Thumbnails View."""
692        name, size, path, modified = model[treeiter][1:5]
693        name = escape(name)
694        size = self.format_size(size)
695        path = escape(path)
696        modified = self.get_date_string(modified)
697        displayed = '<b>%s</b> %s%s%s%s%s' % (name, size, os.linesep, path,
698                                              os.linesep, modified)
699        renderer.set_property('markup', displayed)
700
701    def load_symbolic_icon(self, icon_name, size, state=Gtk.StateFlags.ACTIVE):
702        """Return the symbolic version of icon_name, or the non-symbolic
703        fallback if unavailable."""
704        context = self.sidebar.get_style_context()
705        try:
706            icon_lookup_flags = Gtk.IconLookupFlags.FORCE_SVG | \
707                Gtk.IconLookupFlags.FORCE_SIZE
708            icon_info = self.icon_theme.choose_icon([icon_name + '-symbolic'],
709                                                    size,
710                                                    icon_lookup_flags)
711            color = context.get_color(state)
712            icon = icon_info.load_symbolic(color, color, color, color)[0]
713        except (AttributeError, GLib.GError):
714            icon_lookup_flags = Gtk.IconLookupFlags.FORCE_SVG | \
715                Gtk.IconLookupFlags.USE_BUILTIN | \
716                Gtk.IconLookupFlags.GENERIC_FALLBACK | \
717                Gtk.IconLookupFlags.FORCE_SIZE
718            icon = self.icon_theme.load_icon(
719                icon_name, size, icon_lookup_flags)
720        return icon
721
722    def check_locate(self):
723        """Evaluate which locate binary is in use, its path, and modification
724        date. Return these values in a tuple."""
725        path = get_application_path('locate')
726        if path is None:
727            return None
728        path = os.path.realpath(path)
729        locate = os.path.basename(path)
730        db = catfishconfig.get_locate_db_path()
731        if not os.access(os.path.dirname(db), os.R_OK):
732            modified = time.time()
733        elif os.path.isfile(db):
734            modified = os.path.getmtime(db)
735        else:
736            modified = 0
737
738        changed = self.last_modified != modified
739        self.last_modified = modified
740
741        item_date = datetime.datetime.fromtimestamp(modified)
742        return (locate, db, item_date, changed)
743
744    def on_filters_changed(self, box, row, user_data=None):  # pylint: disable=W0613
745        if row.is_selected():
746            box.unselect_row(row)
747        else:
748            box.select_row(row)
749        return True
750
751    # -- Update Search Index dialog -- #
752    def on_update_index_dialog_close(self, widget=None, event=None,  # pylint: disable=W0613
753                                     user_data=None):  # pylint: disable=W0613
754        """Close the Update Search Index dialog, resetting to default."""
755        if not self.update_index_active:
756            self.update_index_dialog.hide()
757
758            # Restore Unlock button
759            self.update_index_unlock.show()
760            self.update_index_unlock.set_can_default(True)
761            self.update_index_unlock.set_receives_default(True)
762            self.update_index_unlock.grab_focus()
763            self.update_index_unlock.grab_default()
764
765            # Restore Cancel button
766            self.update_index_close.set_label(_("Cancel"))
767            self.update_index_close.set_can_default(False)
768            self.update_index_close.set_receives_default(False)
769
770            self.update_index_infobar.hide()
771
772        return True
773
774    def show_update_status_infobar(self, status_code):
775        """Display the update status infobar based on the status code."""
776        # Error
777        if status_code in [1, 3, 127]:
778            icon = "dialog-error"
779            msg_type = Gtk.MessageType.WARNING
780            if status_code == 1:
781                status = _('An error occurred while updating the database.')
782            elif status_code in [3, 127]:
783                status = _("Authentication failed.")
784
785        # Warning
786        elif status_code in [2, 126]:
787            icon = "dialog-warning"
788            msg_type = Gtk.MessageType.WARNING
789            status = _("Authentication cancelled.")
790
791        # Info
792        else:
793            icon = "dialog-information"
794            msg_type = Gtk.MessageType.INFO
795            status = _('Search database updated successfully.')
796
797        self.update_index_infobar.set_message_type(msg_type)
798        self.update_index_icon.set_from_icon_name(icon, Gtk.IconSize.BUTTON)
799        self.update_index_label.set_label(status)
800
801        self.update_index_infobar.show()
802
803    def on_update_index_unlock_clicked(self, widget):  # pylint: disable=W0613
804        """Unlock admin rights and perform 'updatedb' query."""
805        self.update_index_active = True
806
807        # Get the password for sudo
808        if not SudoDialog.prefer_pkexec() and \
809                not SudoDialog.passwordless_sudo():
810            sudo_dialog = SudoDialog.SudoDialog(
811                parent=self.update_index_dialog,
812                icon='catfish',
813                name=get_about()['program_name'],
814                retries=3)
815            sudo_dialog.show_all()
816            response = sudo_dialog.run()
817            sudo_dialog.hide()
818            password = sudo_dialog.get_password()
819            sudo_dialog.destroy()
820
821            if response in [Gtk.ResponseType.NONE, Gtk.ResponseType.CANCEL]:
822                self.update_index_active = False
823                self.show_update_status_infobar(2)
824                return False
825
826            if response == Gtk.ResponseType.REJECT:
827                self.update_index_active = False
828                self.show_update_status_infobar(3)
829                return False
830
831            if not password:
832                self.update_index_active = False
833                self.show_update_status_infobar(2)
834                return False
835
836        # Subprocess to check if query has completed yet, runs at end of func.
837        def updatedb_subprocess():
838            """Subprocess run for the updatedb command."""
839            try:
840                self.updatedb_process.expect(pexpect.EOF)
841                done = True
842            except pexpect.TIMEOUT:
843                done = False
844            if done:
845                self.update_index_active = False
846                locate_date, changed = self.check_locate()[2:]
847                modified = locate_date.strftime("%x %X")
848                self.update_index_modified.set_label("<tt>%s</tt>" % modified)
849
850                # Hide the Unlock button
851                self.update_index_unlock.set_sensitive(True)
852                self.update_index_unlock.set_receives_default(False)
853                self.update_index_unlock.hide()
854
855                # Update the Cancel button to Close, make it default
856                self.update_index_close.set_label(_("Close"))
857                self.update_index_close.set_sensitive(True)
858                self.update_index_close.set_can_default(True)
859                self.update_index_close.set_receives_default(True)
860                self.update_index_close.grab_focus()
861                self.update_index_close.grab_default()
862
863                return_code = self.updatedb_process.exitstatus
864                if return_code not in [1, 2, 3, 126, 127] and not changed:
865                    return_code = 1
866                self.show_update_status_infobar(return_code)
867            return not done
868
869        # Set the dialog status to running.
870        self.update_index_modified.set_label("<tt>%s</tt>" % _("Updating..."))
871        self.update_index_close.set_sensitive(False)
872        self.update_index_unlock.set_sensitive(False)
873
874        if SudoDialog.prefer_pkexec():
875            if sys.platform.startswith('linux'):
876                self.updatedb_process = SudoDialog.env_spawn('pkexec updatedb', 1)
877            elif 'bsd' in sys.platform or sys.platform.startswith('dragonfly'):
878                self.updatedb_process = SudoDialog.env_spawn('pkexec /usr/libexec/locate.updatedb', 1)
879        else:
880            if sys.platform.startswith('linux'):
881                self.updatedb_process = SudoDialog.env_spawn('sudo updatedb', 1)
882            elif 'bsd' in sys.platform or sys.platform.startswith('dragonfly'):
883                self.updatedb_process = SudoDialog.env_spawn('sudo /usr/libexec/locate.updatedb', 1)
884            try:
885                # Check for password prompt or program exit.
886                self.updatedb_process.expect(".*ssword.*")
887                self.updatedb_process.sendline(password)
888                self.updatedb_process.expect(pexpect.EOF)
889            except pexpect.EOF:
890                # shell already has password, or its not needed
891                pass
892            except pexpect.TIMEOUT:
893                # Poll every 1 second for completion.
894                pass
895        GLib.timeout_add(1000, updatedb_subprocess)
896
897    # -- Search Entry -- #
898    def refresh_search_entry(self):
899        """Update the appearance of the search entry based on the application's
900        current state."""
901        # Default Appearance, used for blank entry
902        query = None
903        icon_name = "edit-find-symbolic"
904        sensitive = True
905        button_tooltip_text = None
906
907        # Search running
908        if self.search_in_progress:
909            icon_name = "process-stop"
910            button_tooltip_text = _('Stop Search')
911            entry_tooltip_text = _("Search is in progress...\nPress the "
912                                   "cancel button or the Escape key to stop.")
913
914        # Search not running
915        else:
916            entry_text = self.search_entry.get_text()
917            entry_tooltip_text = None
918            # Search not running, value in terms
919            if len(entry_text) > 0:
920                button_tooltip_text = _('Begin Search')
921                query = entry_text
922            else:
923                sensitive = False
924
925        self.search_entry.set_icon_from_icon_name(
926            Gtk.EntryIconPosition.SECONDARY, icon_name)
927        self.search_entry.set_icon_tooltip_text(
928            Gtk.EntryIconPosition.SECONDARY, button_tooltip_text)
929        self.search_entry.set_tooltip_text(entry_tooltip_text)
930        self.search_entry.set_icon_activatable(
931            Gtk.EntryIconPosition.SECONDARY, sensitive)
932        self.search_entry.set_icon_sensitive(
933            Gtk.EntryIconPosition.SECONDARY, sensitive)
934
935        return query
936
937    def on_search_entry_activate(self, widget):
938        """If the search entry is not empty, and there is no ongoing search, perform the query."""
939        if len(widget.get_text()) > 0:
940
941            # If a search is in progress, stop it
942            if self.search_in_progress:
943                self.stop_search = True
944                self.search_engine.stop()
945
946            self.statusbar.show()
947
948            # Store search start time for displaying friendly dates
949            now = datetime.datetime.now()
950            self.today = datetime.datetime(now.year, now.month, now.day)
951            self.yesterday = self.today - datetime.timedelta(days=1)
952            self.this_week = self.today - datetime.timedelta(days=6)
953
954            task = self.perform_query(widget.get_text())
955            GLib.idle_add(next, task)
956
957    def on_search_entry_icon_press(self, widget, event, user_data):  # pylint: disable=W0613
958        """If search in progress, stop the search, otherwise, start."""
959        if not self.search_in_progress:
960            self.on_search_entry_activate(self.search_entry)
961        else:
962            self.stop_search = True
963            self.search_engine.stop()
964
965    def on_search_entry_changed(self, widget):  # pylint: disable=W0613
966        """Update the search entry icon and run suggestions."""
967        text = self.refresh_search_entry()
968
969        if text is None:
970            return
971
972        task = self.get_suggestions(text)
973        GLib.idle_add(next, task)
974
975    def get_suggestions(self, keywords):
976        """Load suggestions from the suggestions engine into the search entry
977        completion."""
978        self.suggestions_engine.stop()
979
980        # Wait for an available thread.
981        while Gtk.events_pending():
982            Gtk.main_iteration()
983
984        folder = self.folderchooser.get_filename()
985        show_hidden = self.filter_formats['hidden']
986
987        # If the keywords start with a hidden character, show hidden files.
988        if len(keywords) != 0 and keywords[0] == '.':
989            show_hidden = True
990
991        completion = self.search_entry.get_completion()
992        if completion is not None:
993            model = completion.get_model()
994            model.clear()
995        results = []
996
997        for filename in self.suggestions_engine.run(keywords, folder, 10):
998            if isinstance(filename, str):
999                name = os.path.split(filename)[1]
1000                if name not in results:
1001                    try:
1002                        # Determine if file is hidden
1003                        hidden = is_file_hidden(folder, filename)
1004
1005                        if not hidden or show_hidden:
1006                            results.append(name)
1007                            model.append([name])
1008                    except OSError:
1009                        # file no longer exists
1010                        pass
1011            yield True
1012        yield False
1013
1014    # -- Application Menu -- #
1015    def on_menu_exact_match_toggled(self, widget):
1016        """Toggle the exact match settings, and restart the search
1017        if a fulltext search was previously run."""
1018        self.settings.set_setting('match-results-exactly', widget.get_active())
1019        self.filter_format_toggled("exact", widget.get_active())
1020        if self.filter_formats['fulltext']:
1021            self.on_search_entry_activate(self.search_entry)
1022
1023    def on_menu_hidden_files_toggled(self, widget):
1024        """Toggle the hidden files settings."""
1025        active = widget.get_active()
1026        self.filter_format_toggled("hidden", active)
1027        self.settings.set_setting('show-hidden-files', active)
1028
1029    def open_file_dialog_ok(self, widget):
1030        fc_dialog = self.builder.get_object("fc_open_dialog")
1031        fc_toolbutton = self.builder.get_object("toolbar_folderchooser")
1032        fc_warning = self.builder.get_object("no_folder_warning")
1033        fc_warning_label = self.builder.get_object("no_folder_warning_label")
1034        fc_warning_hide = GLib.timeout_add_seconds (3, self.hide_fc_warning)
1035        folder = fc_dialog.get_filename()
1036
1037        if folder is None:
1038            fc_warning_label.set_text(_("No folder selected."))
1039            fc_warning.show()
1040            fc_warning_hide
1041            return
1042        elif not os.path.isdir(folder):
1043            fc_warning_label.set_text(_("Folder not found."))
1044            fc_warning.show()
1045            fc_warning_hide
1046        elif os.path.isdir(folder):
1047            fc_toolbutton.set_filename(folder)
1048            fc_dialog.close()
1049
1050    def hide_fc_warning(self):
1051        self.builder.get_object("no_folder_warning").hide()
1052
1053    def open_folder_dialog(self, widget):
1054        self.builder.get_object("fc_open_dialog").show()
1055
1056    def toolbar_hotkeys(self):
1057        window = self.builder.get_object("Catfish")
1058        search_entry = self.builder.get_object("toolbar_search")
1059        fc_toolbutton = self.builder.get_object("toolbar_folderchooser")
1060
1061        fc_toolbutton.connect('grab_focus', self.open_folder_dialog)
1062
1063        accelerators = Gtk.AccelGroup()
1064        window.add_accel_group(accelerators)
1065
1066        signal = 'grab_focus'
1067        fc_hotkeys = ('<Control>l', '<Control>o')
1068
1069        key, mod = Gtk.accelerator_parse('<Control>f')
1070        search_entry.add_accelerator(signal, accelerators,
1071                                     key, mod, Gtk.AccelFlags.VISIBLE)
1072
1073        for key in fc_hotkeys:
1074            key, mod = Gtk.accelerator_parse(key)
1075            fc_toolbutton.add_accelerator(signal, accelerators,
1076                                           key, mod, Gtk.AccelFlags.VISIBLE)
1077
1078    def on_menu_fulltext_toggled(self, widget):
1079        """Toggle the fulltext settings, and restart the search."""
1080        self.settings.set_setting('search-file-contents', widget.get_active())
1081        self.filter_format_toggled("fulltext", widget.get_active())
1082        self.on_search_entry_activate(self.search_entry)
1083
1084    def on_menu_update_index_activate(self, widget):  # pylint: disable=W0613
1085        """Show the Update Search Index dialog."""
1086        self.update_index_dialog.show()
1087
1088    def on_menu_preferences_activate(self, widget):  # pylint: disable=W0613
1089        dialog = CatfishPrefsDialog()
1090        dialog.set_transient_for(self)
1091        dialog.connect_settings(self.settings)
1092        dialog.run()
1093        changed_properties = dialog.changed_properties
1094        dialog.destroy()
1095        self.refresh_from_settings(changed_properties)
1096
1097    def refresh_from_settings(self, changed_properties):
1098        for prop in changed_properties:
1099            setting = self.settings.get_setting(prop)
1100            if prop == "show-hidden-files":
1101                self.hidden_files.set_active(setting)
1102            if prop == "show-sidebar":
1103                self.set_sidebar_active(setting)
1104
1105    # -- Sidebar -- #
1106    def set_sidebar_active(self, active):
1107        if self.sidebar_toggle_menu.get_active() != active:
1108            self.sidebar_toggle_menu.set_active(active)
1109        if self.sidebar.get_visible() != active:
1110            self.sidebar.set_visible(active)
1111
1112    def on_sidebar_toggle_toggled(self, widget):
1113        """Toggle visibility of the sidebar."""
1114        if isinstance(widget, Gtk.CheckButton):
1115            active = widget.get_active()
1116        else:
1117            active = not self.settings.get_setting('show-sidebar')
1118        self.settings.set_setting('show-sidebar', active)
1119        self.set_sidebar_active(active)
1120
1121    def set_modified_range(self, value):
1122        if value == 'any':
1123            self.filter_timerange = (0.0, 9999999999.0)
1124            LOGGER.debug("Time Range: Beginning of time -> Eternity")
1125        elif value == 'today':
1126            now = datetime.datetime.now()
1127            today = time.mktime((
1128                datetime.datetime(now.year, now.month, now.day, 0, 0) -
1129                datetime.timedelta(1)).timetuple())
1130            self.filter_timerange = (today, 9999999999.0)
1131            LOGGER.debug(
1132                "Time Range: %s -> Eternity",
1133                time.strftime("%x %X", time.localtime(int(today))))
1134        elif value == 'week':
1135            now = datetime.datetime.now()
1136            week = time.mktime((
1137                datetime.datetime(now.year, now.month, now.day, 0, 0) -
1138                datetime.timedelta(7)).timetuple())
1139            self.filter_timerange = (week, 9999999999.0)
1140            LOGGER.debug(
1141                "Time Range: %s -> Eternity",
1142                time.strftime("%x %X", time.localtime(int(week))))
1143        elif value == 'month':
1144            now = datetime.datetime.now()
1145            month = time.mktime((
1146                datetime.datetime(now.year, now.month, now.day, 0, 0) -
1147                datetime.timedelta(31)).timetuple())
1148            self.filter_timerange = (month, 9999999999.0)
1149            LOGGER.debug(
1150                "Time Range: %s -> Eternity",
1151                time.strftime("%x %X", time.localtime(int(month))))
1152        elif value == 'custom':
1153            self.filter_timerange = (time.mktime(self.start_date.timetuple()),
1154                                     time.mktime(self.end_date.timetuple()))
1155            LOGGER.debug(
1156                "Time Range: %s -> %s",
1157                time.strftime("%x %X",
1158                              time.localtime(int(self.filter_timerange[0]))),
1159                time.strftime("%x %X",
1160                              time.localtime(int(self.filter_timerange[1]))))
1161        self.refilter()
1162
1163    def on_calendar_today_button_clicked(self, calendar_widget):
1164        """Change the calendar widget selected date to today."""
1165        today = datetime.datetime.now()
1166        calendar_widget.select_month(today.month - 1, today.year)
1167        calendar_widget.select_day(today.day)
1168
1169    # File Type toggles
1170    def filter_format_toggled(self, filter_format, enabled):
1171        """Update search filter when formats are modified."""
1172        self.filter_formats[filter_format] = enabled
1173        LOGGER.debug("File type filters updated: %s", str(self.filter_formats))
1174        self.refilter()
1175
1176    def on_filter_extensions_changed(self, widget):
1177        """Update the results when the extensions filter changed."""
1178        self.filter_custom_extensions = []
1179        extensions = widget.get_text().replace(',', ' ')
1180        for ext in extensions.split():
1181            ext = ext.strip()
1182            if len(ext) > 0:
1183                if ext[0] != '.':
1184                    ext = "." + ext
1185                self.filter_custom_extensions.append(ext)
1186
1187        # Reload the results filter.
1188        self.refilter()
1189
1190    def thunar_display_path(self, path):
1191        bus = dbus.SessionBus()
1192        obj = bus.get_object('org.xfce.Thunar', '/org/xfce/FileManager')
1193        iface = dbus.Interface(obj, 'org.xfce.FileManager')
1194
1195        method = iface.get_dbus_method('DisplayFolderAndSelect')
1196        dirname = os.path.dirname(path)
1197        filename = os.path.basename(path)
1198        return method(dirname, filename, '', '')
1199
1200    def get_exo_preferred_applications(self, filename):
1201        apps = {}
1202        if os.path.exists(filename):
1203            with open(filename, "r") as infile:
1204                for line in infile.readlines():
1205                    line = line.strip()
1206                    if "=" in line:
1207                        key, value = line.split("=", 2)
1208                        if len(value) > 0:
1209                            apps[key] = value
1210        return apps
1211
1212    def get_exo_preferred_file_manager(self):
1213        config = [GLib.get_user_config_dir()] + GLib.get_system_config_dirs()
1214        data_dir = GLib.get_user_data_dir()
1215        custFM = data_dir+"/xfce4/helpers/custom-FileManager.desktop"
1216        config_dirs = sorted(set(config), reverse=True)
1217
1218        for config_dir in config_dirs:
1219            cfg = "%s/xfce4/helpers.rc" % config_dir
1220            if os.path.exists(cfg):
1221                apps = self.get_exo_preferred_applications(cfg)
1222                if 'custom-FileManager' in apps['FileManager']:
1223                    with open(custFM) as f:
1224                        for line in f:
1225                            CFM = line.replace('X-XFCE-Commands=', '').strip()
1226                            if 'X-XFCE-Commands=' in line:
1227                                return CFM
1228
1229                if 'FileManager' in apps:
1230                    return apps['FileManager']
1231
1232        return "Thunar"
1233
1234    def using_thunar_fm(self):
1235        if os.environ.get("XDG_CURRENT_DESKTOP", "").lower() == 'xfce':
1236            fm = self.get_exo_preferred_file_manager()
1237            return "thunar" in fm.lower()
1238
1239        fm = subprocess.check_output(['xdg-mime', 'query', 'default',
1240                                      'inode/directory'])
1241        fm = fm.decode("utf-8", errors="replace")
1242        if "thunar" in fm.lower():
1243            return True
1244
1245        if "exo-file-manager" in fm.lower():
1246            fm = self.get_exo_preferred_file_manager()
1247            return "thunar" in fm.lower()
1248
1249        return False
1250
1251    def open_file(self, filename):
1252        """Open the specified filename in its default application."""
1253        LOGGER.debug("Opening %s" % filename)
1254
1255        if type(filename) is list:
1256            filename = filename[0]
1257        if filename.endswith('.AppImage') and os.access(filename, os.X_OK):
1258            command = [filename]
1259        elif os.path.isdir(filename) and \
1260                os.environ.get("XDG_CURRENT_DESKTOP", "").lower() == 'xfce':
1261            command = ['exo-open', '--launch', 'FileManager', filename]
1262        else:
1263            command = ['xdg-open', filename]
1264        try:
1265            subprocess.Popen(command, shell=False)
1266            if self.settings.get_setting('close-after-select'):
1267                self.destroy()
1268            return
1269        except Exception as msg:
1270            LOGGER.debug('Exception encountered while opening %s.' +
1271                         '\n  Exception: %s' +
1272                         filename, msg)
1273            self.get_error_dialog(_('\"%s\" could not be opened.') %
1274                                  os.path.basename(filename), str(msg))
1275
1276    # -- File Popup Menu -- #
1277    def on_menu_open_activate(self, widget):  # pylint: disable=W0613
1278        """Open the selected file in its default application."""
1279        compressed_files = []
1280        for filename in self.selected_filenames:
1281            if '//ARCHIVE//' in filename:
1282                compressed_files.append(filename)
1283            else:
1284                self.open_file(filename)
1285        self.open_compressed_files(compressed_files)
1286
1287    def open_compressed_files(self, compressed_files):
1288        for filename in compressed_files:
1289            archive = filename.split('//ARCHIVE//')[0]
1290            fname = filename.split('//ARCHIVE//')[1]
1291            if fname.endswith('/'):
1292                self.open_file(archive)
1293                continue
1294            with zipfile.ZipFile(archive) as z:
1295                fileinfo = z.getinfo(fname)
1296                fileinfo.filename = os.path.basename(fname)
1297                extract_dir = tempfile.mkdtemp(dir=self.tmpdir.name)
1298                tmpfile = z.extract(fname, path=extract_dir)
1299                self.open_file(tmpfile)
1300
1301    def on_menu_filemanager_activate(self, widget):  # pylint: disable=W0613
1302        """Open the selected file in the default file manager."""
1303
1304        file_manager = self.get_exo_preferred_file_manager().lower()
1305        files, dirs, nfiles = self.on_menu_filemanager_get_file_lists()
1306        num = len(files)
1307
1308        if 'nemo' or 'elementary' in file_manager:
1309            num = len(nfiles)
1310
1311        if 'thunar' in file_manager:
1312            for filename in files:
1313                self.thunar_display_path(filename)
1314        elif 'nautilus' in file_manager:
1315            for filename in files:
1316                subprocess.Popen([file_manager, '--select', filename])
1317        elif 'nemo' in file_manager:
1318            for nfilename in nfiles:
1319                subprocess.Popen([file_manager, nfilename])
1320        elif 'elementary' in file_manager:
1321            for nfilename in nfiles:
1322                subprocess.Popen([file_manager, '-n', nfilename])
1323        else:
1324            for dirname in dirs:
1325                subprocess.Popen([file_manager, dirname])
1326
1327        LOGGER.debug("Opening file manager for %i path(s)" % num)
1328        return
1329
1330    def on_menu_filemanager_get_file_lists(self):
1331        """Creates sets from selected files. Allows file managers to
1332        open and select file/folder when possible. If not it will open
1333        the parent folder of the file/folder. Sets prevent file manager
1334        from opening same location when multiple items are selected."""
1335
1336        files = set()
1337        dirs = set()
1338        nfiles = set()
1339
1340        for filename in self.selected_filenames:
1341            if '//ARCHIVE//' in filename:
1342                filename = filename.split('//ARCHIVE//')[0]
1343
1344            files.add(filename)
1345            dirs.add(os.path.dirname(filename))
1346
1347            if os.path.isfile(filename):
1348                nfiles.add(filename)
1349            elif os.path.isdir(filename):
1350                nfiles.add(os.path.dirname(filename))
1351        return files, dirs, nfiles
1352
1353    def on_menu_copy_location_activate(self, widget):  # pylint: disable=W0613
1354        """Copy the selected file name to the clipboard."""
1355        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
1356        locations = []
1357        for filename in self.selected_filenames:
1358            if "//ARCHIVE//" in filename:
1359                # If file is in archive, copies archive location
1360                archive = filename.split("//ARCHIVE//")[0]
1361                if archive in locations:
1362                    continue
1363                locations.append(surrogate_escape(archive))
1364            else:
1365                locations.append(surrogate_escape(filename))
1366        text = str(os.linesep).join(locations)
1367        clipboard.set_text(text, -1)
1368        clipboard.store()
1369        LOGGER.debug("Copying %i filename(s) to the clipboard" % len(locations))
1370
1371    def on_menu_save_activate(self, widget):  # pylint: disable=W0613
1372        """Show a save dialog and possibly write the results to a file."""
1373        selected_file = self.selected_filenames[0]
1374        if "//ARCHIVE//" in selected_file:
1375            archive = selected_file.split("//ARCHIVE//")[0]
1376            archive_filename = selected_file.split("//ARCHIVE//")[1]
1377            with zipfile.ZipFile(archive) as z:
1378                save_as = self.get_save_dialog(archive_filename)
1379                if save_as:
1380                    saved_dir = os.path.dirname(save_as)
1381                    saved_name = os.path.basename(save_as)
1382                    archive_fileinfo = z.getinfo(archive_filename)
1383                    archive_fileinfo.filename = saved_name
1384                    z.extract(archive_filename, path=saved_dir)
1385
1386        else:
1387            filename = self.get_save_dialog(
1388                       surrogate_escape(selected_file))
1389            original = selected_file
1390            if filename:
1391                try:
1392                    # Try to save the file.
1393                    copy2(original, filename)
1394
1395                except Exception as msg:
1396                    # If the file save fails, throw an error.
1397                    LOGGER.debug('Exception encountered while saving %s.' +
1398                                 '\n  Exception: %s', filename, msg)
1399                    self.get_error_dialog(_('\"%s\" could not be saved.') %
1400                                          os.path.basename(filename), str(msg))
1401
1402    def delete_file(self, filename):
1403        try:
1404            # Delete the file.
1405            if not os.path.exists(filename):
1406                return True
1407            if os.path.isdir(filename):
1408                rmtree(filename)
1409            else:
1410                os.remove(filename)
1411            return True
1412        except Exception as msg:
1413            # If the file cannot be deleted, throw an error.
1414            LOGGER.debug('Exception encountered while deleting %s.' +
1415                         '\n  Exception: %s', filename, msg)
1416            self.get_error_dialog(_("\"%s\" could not be deleted.") %
1417                                  os.path.basename(filename),
1418                                  str(msg))
1419        return False
1420
1421    def remove_filenames_from_treeview(self, filenames):
1422        removed = []
1423        model = self.treeview.get_model().get_model().get_model()
1424        treeiter = model.get_iter_first()
1425        while treeiter is not None:
1426            nextiter = model.iter_next(treeiter)
1427            row = model[treeiter]
1428            found = os.path.join(row[3], row[1])
1429            if found in filenames:
1430                model.remove(treeiter)
1431                removed.append(found)
1432            if len(removed) == len(filenames):
1433                return True
1434            treeiter = nextiter
1435        return False
1436
1437    def on_menu_delete_activate(self, widget):  # pylint: disable=W0613
1438        """Show a delete dialog and remove the file if accepted."""
1439        filenames = []
1440        if self.get_delete_dialog(self.selected_filenames):
1441            delete = sorted(self.selected_filenames)
1442            delete.reverse()
1443            for filename in delete:
1444                if self.delete_file(filename):
1445                    filenames.append(filename)
1446        self.remove_filenames_from_treeview(filenames)
1447        self.refilter()
1448
1449    def get_save_dialog(self, filename):
1450        """Show the Save As FileChooserDialog.
1451
1452        Return the filename, or None if cancelled."""
1453        basename = os.path.basename(filename)
1454
1455        dialog = Gtk.FileChooserDialog(title=_('Save "%s" as...') % basename,
1456                                       transient_for=self,
1457                                       action=Gtk.FileChooserAction.SAVE)
1458        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
1459                           Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT)
1460        dialog.set_default_response(Gtk.ResponseType.REJECT)
1461        dialog.set_current_name(basename.replace('�', '_'))
1462        dialog.set_do_overwrite_confirmation(True)
1463        response = dialog.run()
1464        save_as = dialog.get_filename()
1465        dialog.destroy()
1466        if response == Gtk.ResponseType.ACCEPT:
1467            return save_as
1468        return None
1469
1470    def get_error_dialog(self, primary, secondary):
1471        """Show an error dialog with the specified message."""
1472        dialog_text = "<big><b>%s</b></big>\n\n%s" % (escape(primary),
1473                                                      escape(secondary))
1474
1475        dialog = Gtk.MessageDialog(transient_for=self,
1476                                   modal=True,
1477                                   destroy_with_parent=True,
1478                                   message_type=Gtk.MessageType.ERROR,
1479                                   buttons=Gtk.ButtonsType.OK,
1480                                   text="")
1481
1482        dialog.set_markup(dialog_text)
1483        dialog.set_default_response(Gtk.ResponseType.OK)
1484        dialog.run()
1485        dialog.destroy()
1486
1487    def get_delete_dialog(self, filenames):
1488        """Show a delete confirmation dialog.  Return True if delete wanted."""
1489        if len(filenames) == 1:
1490            primary = _("Are you sure that you want to \n"
1491                        "permanently delete \"%s\"?") % \
1492                escape(os.path.basename(filenames[0]))
1493        else:
1494            primary = _("Are you sure that you want to \n"
1495                        "permanently delete the %i selected files?") % \
1496                len(filenames)
1497        secondary = _("If you delete a file, it is permanently lost.")
1498
1499        dialog_text = "<big><b>%s</b></big>\n\n%s" % (primary, secondary)
1500        dialog = Gtk.MessageDialog(transient_for=self,
1501                                   modal=True,
1502                                   destroy_with_parent=True,
1503                                   message_type=Gtk.MessageType.QUESTION,
1504                                   buttons=Gtk.ButtonsType.NONE,
1505                                   text="")
1506        dialog.set_markup(surrogate_escape(dialog_text))
1507
1508        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.NO,
1509                           Gtk.STOCK_DELETE, Gtk.ResponseType.YES)
1510
1511        dialog.set_default_response(Gtk.ResponseType.NO)
1512        response = dialog.run()
1513        dialog.destroy()
1514        return response == Gtk.ResponseType.YES
1515
1516    def setup_small_view(self):
1517        """Prepare the list view in the results pane."""
1518        for column in self.treeview.get_columns():
1519            self.treeview.remove_column(column)
1520        self.treeview.append_column(self.new_column(_('Filename'), 1,
1521                                                    'icon', 1))
1522        self.treeview.append_column(self.new_column(_('Size'), 2,
1523                                                    'filesize'))
1524        self.treeview.append_column(self.new_column(_('Location'), 3,
1525                                                    'ellipsize'))
1526        self.treeview.append_column(self.new_column(_('Modified'), 4,
1527                                                    'date', 1))
1528        self.icon_size = Gtk.IconSize.MENU
1529
1530    def setup_large_view(self):
1531        """Prepare the extended list view in the results pane."""
1532        for column in self.treeview.get_columns():
1533            self.treeview.remove_column(column)
1534        # Make the Preview Column
1535        cell = Gtk.CellRendererPixbuf()
1536        column = Gtk.TreeViewColumn(_('Preview'), cell)
1537        self.treeview.append_column(column)
1538        column.set_cell_data_func(cell, self.preview_cell_data_func, None)
1539
1540        # Make the Details Column
1541        cell = Gtk.CellRendererText()
1542        cell.set_property("ellipsize", Pango.EllipsizeMode.END)
1543        column = Gtk.TreeViewColumn(_("Details"), cell, markup=1)
1544
1545        column.set_sort_column_id(1)
1546        column.set_resizable(True)
1547        column.set_expand(True)
1548
1549        column.set_cell_data_func(cell, self.thumbnail_cell_data_func, None)
1550        self.treeview.append_column(column)
1551        self.icon_size = Gtk.IconSize.DIALOG
1552
1553    def on_treeview_list_toggled(self, widget):
1554        """Switch to the details view."""
1555        if widget.get_active():
1556            self.show_thumbnail = False
1557            self.settings.set_setting('list-toggle', True)
1558            self.settings.set_setting('show-thumbnails', False)
1559            if self.options.icons_large:
1560                self.setup_large_view()
1561            else:
1562                self.setup_small_view()
1563
1564    def on_treeview_thumbnails_toggled(self, widget):
1565        """Switch to the preview view."""
1566        if widget.get_active():
1567            self.show_thumbnail = True
1568            self.settings.set_setting('list-toggle', False)
1569            self.settings.set_setting('show-thumbnails', True)
1570            self.setup_large_view()
1571
1572    # -- Treeview -- #
1573    def on_treeview_row_activated(self, treeview, path, user_data):  # pylint: disable=W0613
1574        """Catch row activations by keyboard or mouse double-click."""
1575        # Get the filename from the row.
1576        model = treeview.get_model()
1577        file_path = self.treemodel_get_row_filename(model, path)
1578        self.selected_filenames = [file_path]
1579        # Open the selected file.
1580        if "//ARCHIVE//" in file_path:
1581            self.open_compressed_files([self.selected_filenames[0]])
1582        else:
1583            self.open_file(self.selected_filenames[0])
1584
1585    def on_treeview_drag_begin(self, treeview, context):  # pylint: disable=W0613
1586        """Treeview DND Begin."""
1587        if len(self.selected_filenames) > 1:
1588            treesel = treeview.get_selection()
1589            for row in self.rows:
1590                treesel.select_path(row)
1591        return True
1592
1593    def on_treeview_drag_data_get(self, treeview, context, selection, info,  # pylint: disable=W0613
1594                                  timestamp):  # pylint: disable=W0613
1595        """Treeview DND Get."""
1596        # Checks for archives in selection, disables DND if present
1597        if any("//ARCHIVE//" in arch for arch in self.selected_filenames):
1598            return False
1599        else:
1600            text = str(os.linesep).join(self.selected_filenames)
1601            selection.set_text(text, -1)
1602
1603            uris = ['file://' + path for path in self.selected_filenames]
1604            selection.set_uris(uris)
1605
1606            return True
1607
1608    def treemodel_get_row_filename(self, model, row):
1609        """Get the filename from a specified row."""
1610        if zipfile.is_zipfile(model[row][3]):
1611            filename = model[row][3] + "//ARCHIVE//" + model[row][1]
1612        else:
1613            filename = os.path.join(model[row][3], model[row][1])
1614        return filename
1615
1616    def treeview_get_selected_rows(self, treeview):
1617        """Get the currently selected rows from the specified treeview."""
1618        sel = treeview.get_selection()
1619        model, rows = sel.get_selected_rows()
1620        data = []
1621        for row in rows:
1622            data.append(self.treemodel_get_row_filename(model, row))
1623        return (model, rows, data)
1624
1625    def check_treeview_stats(self, treeview):
1626        if len(self.rows) == 0:
1627            return -1
1628        rows = self.treeview_get_selected_rows(treeview)[1]
1629        for row in rows:
1630            if row not in self.rows:
1631                return 2
1632        if self.rows != rows:
1633            return 1
1634        return 0
1635
1636    def update_treeview_stats(self, treeview, event=None):
1637        if event:
1638            self.treeview_set_cursor_if_unset(treeview,
1639                                              int(event.x),
1640                                              int(event.y))
1641        self.rows, self.selected_filenames = \
1642            self.treeview_get_selected_rows(treeview)[1:]
1643
1644    def maintain_treeview_stats(self, treeview, event=None):
1645        if len(self.selected_filenames) == 0:
1646            self.update_treeview_stats(treeview, event)
1647        elif self.check_treeview_stats(treeview) == 2:
1648            self.update_treeview_stats(treeview, event)
1649        else:
1650            treesel = treeview.get_selection()
1651            for row in self.rows:
1652                treesel.select_path(row)
1653
1654    def on_treeview_cursor_changed(self, treeview):
1655        if "Shift" in self.keys_pressed or "Control" in self.keys_pressed:
1656            self.update_treeview_stats(treeview)
1657        if "Up" in self.keys_pressed or "Down" in self.keys_pressed:
1658            self.update_treeview_stats(treeview)
1659        if len(self.selected_filenames) == 1:
1660            self.update_treeview_stats(treeview)
1661
1662    def treeview_set_cursor_if_unset(self, treeview, x=0, y=0):
1663        if treeview.get_selection().count_selected_rows() < 1:
1664            self.treeview_set_cursor_at_pos(treeview, x, y)
1665
1666    def treeview_set_cursor_at_pos(self, treeview, x, y):
1667        try:
1668            path = treeview.get_path_at_pos(int(x), int(y))[0]
1669            treeview.set_cursor(path)
1670        except TypeError:
1671            return False
1672        return True
1673
1674    def treeview_left_click(self, treeview, event=None):
1675        self.update_treeview_stats(treeview, event)
1676        return False
1677
1678    def treeview_middle_click(self, treeview, event=None):
1679        self.maintain_treeview_stats(treeview, event)
1680        self.treeview_set_cursor_if_unset(treeview, int(event.x), int(event.y))
1681        for filename in self.selected_filenames:
1682            self.open_file(filename)
1683        return True
1684
1685    def treeview_right_click(self, treeview, event=None):
1686        self.maintain_treeview_stats(treeview, event)
1687        # Checks if archive
1688        directory = os.path.isdir(self.selected_filenames[0]) or \
1689            self.selected_filenames[0].endswith("/")
1690        show_save_option = len(self.selected_filenames) == 1 and not \
1691            directory
1692        self.file_menu_save.set_visible(show_save_option)
1693        writeable = True
1694        for filename in self.selected_filenames:
1695            if type(filename) is list:
1696                filename = os.path.join(filename[0], filename[1])
1697            if not os.access(filename, os.W_OK):
1698                writeable = False
1699        self.file_menu_delete.set_sensitive(writeable)
1700        self.file_menu.popup_at_pointer()
1701        return True
1702
1703    def treeview_alt_clicked(self, treeview, event=None):
1704        self.update_treeview_stats(treeview, event)
1705        return False
1706
1707    def on_treeview_button_press_event(self, treeview, event):
1708        """Catch single mouse click events on the treeview and rows.
1709
1710            Left Click:     Ignore.
1711            Middle Click:   Open the selected file.
1712            Right Click:    Show the popup menu."""
1713        if "Shift" in self.keys_pressed or "Control" in self.keys_pressed:
1714            handled = self.treeview_alt_clicked(treeview, event)
1715        elif event.button == 1:
1716            handled = self.treeview_left_click(treeview, event)
1717        elif event.button == 2:
1718            handled = self.treeview_middle_click(treeview, event)
1719        elif event.button == 3:
1720            handled = self.treeview_right_click(treeview, event)
1721        else:
1722            handled = False
1723        return handled
1724
1725    def new_column(self, label, colid, special=None, markup=False):
1726        """New Column function for creating TreeView columns easily."""
1727        if special == 'icon':
1728            column = Gtk.TreeViewColumn(label)
1729            cell = Gtk.CellRendererPixbuf()
1730            column.pack_start(cell, False)
1731            column.set_cell_data_func(cell, self.preview_cell_data_func, None)
1732            cell = Gtk.CellRendererText()
1733            column.pack_start(cell, False)
1734            column.add_attribute(cell, 'text', colid)
1735        else:
1736            cell = Gtk.CellRendererText()
1737            if markup:
1738                column = Gtk.TreeViewColumn(label, cell, markup=colid)
1739            else:
1740                column = Gtk.TreeViewColumn(label, cell, text=colid)
1741            if special == 'ellipsize':
1742                column.set_min_width(120)
1743                cell.set_property('ellipsize', Pango.EllipsizeMode.START)
1744            elif special == 'filesize':
1745                cell.set_property('xalign', 1.0)
1746                column.set_cell_data_func(cell,
1747                                          self.cell_data_func_filesize, colid)
1748            elif special == 'date':
1749                column.set_cell_data_func(cell,
1750                                          self.cell_data_func_modified, colid)
1751        column.set_sort_column_id(colid)
1752        column.set_resizable(True)
1753        if colid == 3:
1754            column.set_expand(True)
1755        return column
1756
1757    def cell_data_func_filesize(self, column, cell_renderer,  # pylint: disable=W0613
1758                                tree_model, tree_iter, cellid):
1759        """File size cell display function."""
1760        size = long(tree_model.get_value(tree_iter, cellid))
1761
1762        filesize = self.format_size(size)
1763        cell_renderer.set_property('text', filesize)
1764
1765    def cell_data_func_modified(self, column, cell_renderer,  # pylint: disable=W0613
1766                                tree_model, tree_iter, cellid):
1767        """Modification date cell display function."""
1768        modification_int = int(tree_model.get_value(tree_iter, cellid))
1769        modified = self.get_date_string(modification_int)
1770
1771        cell_renderer.set_property('text', modified)
1772
1773    def get_date_string(self, modification_int):
1774        """Return the date string in the preferred format."""
1775        if self.time_format is not None:
1776            modified = time.strftime(self.time_format,
1777                                     time.localtime(modification_int))
1778        else:
1779            item_date = datetime.datetime.fromtimestamp(modification_int)
1780            if item_date >= self.today:
1781                modified = _("Today")
1782            elif item_date >= self.yesterday:
1783                modified = _("Yesterday")
1784            elif item_date >= self.this_week:
1785                modified = time.strftime("%A",
1786                                         time.localtime(modification_int))
1787            else:
1788                modified = time.strftime("%x",
1789                                         time.localtime(modification_int))
1790        return modified
1791
1792    def results_filter_func(self, model, treeiter, user_data):  # pylint: disable=W0613
1793        """Filter function for search results."""
1794        # hidden
1795        if model[treeiter][6]:
1796            if not self.filter_formats['hidden']:
1797                return False
1798
1799        # exact
1800        if not self.filter_formats['fulltext']:
1801            if not model[treeiter][7]:
1802                if self.filter_formats['exact']:
1803                    return False
1804
1805        # modified
1806        modified = model[treeiter][4]
1807        if modified < self.filter_timerange[0]:
1808            return False
1809        if modified > self.filter_timerange[1]:
1810            return False
1811
1812        # mimetype
1813        mimetype = model[treeiter][5]
1814        use_filters = False
1815        if self.filter_formats['folders']:
1816            use_filters = True
1817            if mimetype == 'inode/directory':
1818                return True
1819        if self.filter_formats['images']:
1820            use_filters = True
1821            if mimetype.startswith("image"):
1822                return True
1823        if self.filter_formats['music']:
1824            use_filters = True
1825            if mimetype.startswith("audio"):
1826                return True
1827        if self.filter_formats['videos']:
1828            use_filters = True
1829            if mimetype.startswith("video"):
1830                return True
1831        if self.filter_formats['documents']:
1832            use_filters = True
1833            if mimetype.startswith("text"):
1834                return True
1835        if self.filter_formats['applications']:
1836            use_filters = True
1837            if mimetype.startswith("application"):
1838                return True
1839        if self.filter_formats['other']:
1840            use_filters = True
1841            extension = os.path.splitext(model[treeiter][1])[1]
1842            if extension in self.filter_custom_extensions:
1843                return True
1844
1845        if use_filters:
1846            return False
1847
1848        return True
1849
1850    def refilter(self):
1851        """Reload the results filter, update the statusbar to reflect count."""
1852        try:
1853            self.results_filter.refilter()
1854            n_results = len(self.treeview.get_model())
1855            self.show_results(n_results)
1856        except AttributeError:
1857            pass
1858
1859    def show_results(self, count):
1860        if count == 0:
1861            self.builder.get_object("results_scrolledwindow").hide()
1862            self.builder.get_object("splash").show()
1863            self.builder.get_object(
1864                "splash_title").set_text(_("No files found."))
1865            self.builder.get_object("splash_status").set_text(
1866                _("Try making your search less specific\n"
1867                  "or try another directory."))
1868            self.builder.get_object("splash_status").show()
1869        else:
1870            self.builder.get_object("splash").hide()
1871            self.builder.get_object("results_scrolledwindow").show()
1872            if count == 1:
1873                self.statusbar_label.set_label(_("1 file found."))
1874            else:
1875                self.statusbar_label.set_label(_("%i files found.") % count)
1876
1877    def format_size(self, size, precision=1):
1878        """Make a file size human readable."""
1879        if isinstance(size, str):
1880            size = int(size)
1881        suffixes = [_('bytes'), 'kB', 'MB', 'GB', 'TB']
1882        suffixIndex = 0
1883        if size > 1024:
1884            while size > 1024:
1885                suffixIndex += 1
1886                size = size / 1024.0
1887            return "%.*f %s" % (precision, size, suffixes[suffixIndex])
1888        return "%i %s" % (size, suffixes[0])
1889
1890    def guess_mimetype(self, fullpath):
1891        """Guess the mimetype of the specified filename."""
1892        filename = os.path.basename(fullpath)
1893        mimetype = mimetypes.guess_type(filename)
1894        sub = mimetype[1]
1895        guess = mimetype[0]
1896        if os.path.isdir(fullpath) or fullpath.endswith('/'):
1897            return 'inode/directory'
1898        if filename in ['INSTALL', 'AUTHORS', 'COPYING', 'CHANGELOG',
1899                        'Makefile', 'Credits']:
1900            guess = 'text/x-%s' % filename.lower()
1901        if sub and 'x-tar' not in guess and sub in ['bzip2', 'gzip', 'xz']:
1902            guess = 'application/x-%s' % sub.strip('2')
1903        if guess is None:
1904            return 'text/plain'
1905        return guess
1906
1907    def changed_icon(self, widget):
1908        self.changed_icon_theme = True
1909        return
1910
1911    def create_symlink_icon(self, fullpath, icon_name, emblem_icon):
1912        """Creates a new transparent 22x21px image. Then centers and
1913        overlays/composites the mimetype icon. The emblem icon is then
1914        resized, offset and overlayed onto the 22x21px + mimetype image,
1915        creating the symbolic icon."""
1916
1917        load = self.icon_theme.load_icon
1918        icon_size = Gtk.icon_size_lookup(self.icon_size)[1]
1919
1920        icon = load(icon_name, icon_size, Gtk.IconLookupFlags.FORCE_SIZE)
1921        emblem = load(emblem_icon, icon_size, 0)
1922
1923        # Set GdkPixbuf composite properties (icon sizes, offsets)
1924        filtr = GdkPixbuf.InterpType.BILINEAR
1925        if self.show_thumbnail is False:
1926            new_sizex, new_sizey = (22, 21)
1927            emb_resize = 12
1928            icon_destx, icon_desty, icon_offx, icon_offy = (3, 2, 3.0, 2.0)
1929            emb_destx, emb_desty, emb_offx, emb_offy = (10, 9, 10.0, 9.0)
1930
1931        else:
1932            new_sizex, new_sizey = (50, 50)
1933            emb_resize = 24
1934            icon_destx, icon_desty, icon_offx, icon_offy = (2, 2, 2.0, 2.0)
1935            emb_destx, emb_desty, emb_offx, emb_offy = (26, 26, 26.0, 26.0)
1936
1937        # Create new icon, overlay icon, scale emblem, overlay emblem on top
1938        new_icon = GdkPixbuf.Pixbuf.new(
1939            GdkPixbuf.Colorspace(0), True, 8, new_sizex, new_sizey)
1940        new_icon.fill(0)
1941
1942        GdkPixbuf.Pixbuf.composite(
1943            icon, new_icon, icon_destx, icon_desty, icon_size,
1944            icon_size, icon_offx, icon_offy, 1.0, 1.0, filtr, 255)
1945
1946        emb_scaled = GdkPixbuf.Pixbuf.scale_simple(
1947            emblem, emb_resize, emb_resize, filtr)
1948
1949        GdkPixbuf.Pixbuf.composite(
1950            emb_scaled, new_icon, emb_destx, emb_desty, emb_resize,
1951            emb_resize, emb_offx, emb_offy, 1.0, 1.0, filtr, 255)
1952
1953        return new_icon
1954
1955    def get_icon_pixbuf(self, name):
1956        """Return a pixbuf for the icon name from the default icon theme."""
1957        icon_size = Gtk.icon_size_lookup(self.icon_size)[1]
1958        flags = Gtk.IconLookupFlags.FORCE_SIZE
1959        if self.icon_theme.has_icon(name):
1960            icon = self.icon_theme.load_icon(name, icon_size, flags)
1961        else:
1962            icon = self.icon_theme.load_icon('image-missing', icon_size,
1963                                             flags)
1964        return icon
1965
1966    def get_thumbnail(self, path, mime_type=None):
1967        """Try to fetch a thumbnail."""
1968        thumb = self.thumbnailer.get_thumbnail(path, mime_type,
1969                                               self.show_thumbnail)
1970        if thumb:
1971            return thumb
1972        return self.get_file_icon(path, mime_type)
1973
1974    def get_file_icon(self, fullpath, mime_type=None):  # pylint: disable=W0613
1975        """Retrieve the file icon."""
1976        path = os.path.basename(fullpath)
1977        if mime_type:
1978            if mime_type == 'inode/directory' or path.endswith('/'):
1979                return 'folder'
1980            mime_type = mime_type.split('/')
1981            if mime_type is not None:
1982                # Get icon from mimetype
1983                media, subtype = mime_type
1984
1985                variations = ['%s-%s' % (media, subtype),
1986                              '%s-x-%s' % (media, subtype), subtype]
1987
1988                variations.append('gnome-mime-%s-%s' % (media, subtype))
1989                if media == "application":
1990                    variations.append('application-x-executable')
1991                variations.append('%s-x-generic' % media)
1992
1993                for icon_name in variations:
1994                    if self.icon_theme.has_icon(icon_name):
1995                        return icon_name
1996        return "text-x-generic"
1997
1998    def size_sort_func(self, model, row1, row2, user_data):  # pylint: disable=W0613
1999        """Sort function used in Python 3."""
2000        sort_column = 2
2001        value1 = long(model.get_value(row1, sort_column))
2002        value2 = long(model.get_value(row2, sort_column))
2003        if value1 < value2:
2004            return -1
2005        if value1 == value2:
2006            return 0
2007        return 1
2008
2009    def perform_zip_query(self, filename, keywords, search_exact):
2010        for member, uncompressed_size, date_time in \
2011               self.search_engine.search_zip(filename, keywords, search_exact):
2012            dt = datetime.datetime(*date_time).timestamp()
2013            mimetype = self.guess_mimetype(member)
2014            icon = self.get_file_icon(member, mimetype)
2015            displayed = surrogate_escape(member.rstrip("/"), True)
2016            zip_path = surrogate_escape(filename)
2017            exact = keywords in member
2018            yield [icon, displayed, uncompressed_size,
2019                   zip_path, dt, mimetype, False, exact]
2020
2021    # -- Searching -- #
2022    def perform_query(self, keywords):  # noqa
2023        """Run the search query with the specified keywords."""
2024        self.stop_search = False
2025
2026        # Update the interface to Search Mode
2027        self.builder.get_object("results_scrolledwindow").hide()
2028        self.builder.get_object("splash").show()
2029        self.builder.get_object("splash_title").set_text(_("Searching..."))
2030        self.builder.get_object("splash_status").set_text(
2031            _("Results will be displayed as soon as they are found."))
2032        self.builder.get_object("splash_status").show()
2033        self.builder.get_object("welcome_area").hide()
2034        show_results = False
2035        self.get_window().set_cursor(Gdk.Cursor.new_from_name(
2036             Gdk.Display.get_default(), "progress"))
2037        self.set_title(_("Searching for \"%s\"") % keywords)
2038        self.spinner.show()
2039        self.statusbar_label.set_label(_("Searching..."))
2040
2041        self.search_in_progress = True
2042        self.refresh_search_entry()
2043
2044        # Be thread friendly.
2045        while Gtk.events_pending():
2046            Gtk.main_iteration()
2047
2048        # icon, name, size, path, modified, mimetype, hidden, exact
2049        model = Gtk.TreeStore(str, str, GObject.TYPE_INT64,
2050                              str, float, str, bool, bool)
2051
2052        # Initialize the results filter.
2053        self.results_filter = model.filter_new()
2054        self.results_filter.set_visible_func(self.results_filter_func)
2055        sort = Gtk.TreeModelSort(model=self.results_filter)
2056        sort.set_sort_func(2, self.size_sort_func, None)
2057        self.treeview.set_model(sort)
2058        sort.get_model().get_model().clear()
2059        self.treeview.columns_autosize()
2060
2061        # Enable multiple-selection
2062        sel = self.treeview.get_selection()
2063        if sel is not None:
2064            sel.set_mode(Gtk.SelectionMode.MULTIPLE)
2065
2066        folder = self.folderchooser.get_filename()
2067
2068        results = []
2069
2070        # Check if this is a fulltext query or standard query.
2071        if self.filter_formats['fulltext']:
2072            self.search_engine = \
2073                CatfishSearchEngine(['fulltext'],
2074                                    self.settings.get_setting("exclude-paths"))
2075            self.search_engine.set_exact(self.filter_formats['exact'])
2076        else:
2077            self.search_engine = CatfishSearchEngine(
2078                ['zeitgeist', 'locate', 'walk'],
2079                self.settings.get_setting("exclude-paths")
2080            )
2081
2082        search_zips = self.settings.get_setting('search-compressed-files')
2083        search_exact = self.settings.get_setting('match-results-exactly')
2084
2085        for filename in self.search_engine.run(keywords, folder, search_zips, regex=True):
2086            if self.stop_search:
2087                break
2088            if isinstance(filename, str) and filename not in results:
2089                try:
2090                    path, name = os.path.split(filename)
2091                    size = long(os.path.getsize(filename))
2092                    modified = os.path.getmtime(filename)
2093
2094                    mimetype = self.guess_mimetype(filename)
2095                    icon_name = self.get_thumbnail(filename, mimetype)
2096
2097                    hidden = is_file_hidden(folder, filename)
2098
2099                    exact = keywords in name
2100
2101                    results.append(filename)
2102
2103                    displayed = surrogate_escape(name, True)
2104                    path = surrogate_escape(path)
2105                    if zipfile.is_zipfile(filename):
2106                        parent = None
2107                        if not self.filter_formats['fulltext']:
2108                            if self.search_engine.search_filenames(filename, keywords, search_exact):
2109                                parent = model.append(None, [icon_name, displayed, size, path, modified, mimetype, hidden, search_exact])
2110                        if not search_zips:
2111                            continue
2112                        try:
2113                            for row in self.perform_zip_query(filename, keywords, search_exact):
2114                                if not parent:
2115                                    parent = model.append(None, [icon_name, displayed, size, path, modified, mimetype, hidden, search_exact])
2116                                model.append(parent, row)
2117                        except zipfile.BadZipFile as e:
2118                            LOGGER.debug(f'{e}: {path}')
2119                    else:
2120                        model.append(None, [icon_name, displayed, size, path, modified,
2121                                  mimetype, hidden, exact])
2122
2123                    if not show_results:
2124                        if len(self.treeview.get_model()) > 0:
2125                            show_results = True
2126                            self.builder.get_object("splash").hide()
2127                            self.builder.get_object(
2128                                "results_scrolledwindow").show()
2129
2130                except OSError:
2131                    # file no longer exists
2132                    pass
2133                except Exception as e:
2134                    LOGGER.error("Exception encountered: %s" % str(e))
2135
2136            yield True
2137            continue
2138
2139        # Return to Non-Search Mode.
2140        window = self.get_window()
2141        if window is not None:
2142            window.set_cursor(None)
2143        self.set_title(_('Search results for \"%s\"') % keywords)
2144        self.spinner.hide()
2145
2146        n_results = 0
2147        if self.treeview.get_model() is not None:
2148            n_results = len(self.treeview.get_model())
2149        self.show_results(n_results)
2150
2151        self.search_in_progress = False
2152        self.refresh_search_entry()
2153
2154        self.stop_search = False
2155        yield False
2156