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=C0114
20# pylint: disable=C0116
21
22import logging
23from locale import gettext as _
24
25from gi.repository import Gtk, Gdk  # pylint: disable=E0611
26
27from catfish_lib import CatfishSettings
28from . helpers import get_builder
29
30logger = logging.getLogger('catfish_lib')
31
32# GtkBuilder Mappings
33__builder__ = {
34    # Builder name
35    "ui_file": "CatfishWindow",
36
37    "window": {
38        "main": "Catfish",
39        "sidebar": "catfish_window_sidebar",
40        "paned": "catfish_window_paned"
41    },
42
43    # Toolbar
44    "toolbar": {
45        "folderchooser": "toolbar_folderchooser",
46        "search": "toolbar_search",
47        "view": {
48            "list": "toolbar_view_list",
49            "thumbs": "toolbar_view_thumbnails"
50        },
51    },
52
53    # Menus
54    "menus": {
55        # Application (AppMenu)
56        "application": {
57            "menu": "application_menu",
58            "placeholder": "toolbar_custom_appmenu",
59            "exact": "application_menu_exact",
60            "hidden": "application_menu_hidden",
61            "fulltext": "application_menu_fulltext",
62            "advanced": "application_menu_advanced",
63            "update": "application_menu_update",
64            "preferences": "application_menu_prefs",
65        },
66        # File Context Menu
67        "file": {
68            "menu": "file_menu",
69            "save": "file_menu_save",
70            "delete": "file_menu_delete"
71        }
72    },
73
74    # Locate Infobar
75    "infobar": {
76        "infobar": "catfish_window_infobar"
77    },
78
79    # Sidebar
80    "sidebar": {
81        "modified": {
82            "options": "sidebar_filter_custom_date_options",
83            "icons": {
84                "any": "sidebar_filter_modified_any_icon",
85                "today": "sidebar_filter_modified_day_icon",
86                "week": "sidebar_filter_modified_week_icon",
87                "month": "sidebar_filter_modified_month_icon",
88                "custom": "sidebar_filter_custom_date_icon",
89                "options": "sidebar_filter_custom_date_options_icon"
90            }
91        },
92        "filetype": {
93            "options": "sidebar_filter_custom_options",
94            "icons": {
95                "documents": "sidebar_filter_documents_icon",
96                "folders": "sidebar_filter_folders_icon",
97                "photos": "sidebar_filter_images_icon",
98                "music": "sidebar_filter_music_icon",
99                "videos": "sidebar_filter_videos_icon",
100                "applications": "sidebar_filter_applications_icon",
101                "custom": "sidebar_filter_custom_filetype_icon",
102                "options": "sidebar_filter_custom_options_icon"
103            }
104        }
105    },
106
107    # Results Window
108    "results": {
109        "scrolled_window": "results_scrolledwindow",
110        "treeview": "results_treeview"
111    },
112
113    "dialogs": {
114        # Custom Filetypes
115        "filetype": {
116            "dialog": "filetype_dialog",
117            "mimetypes": {
118                "radio": "filetype_mimetype_radio",
119                "box": "filetype_mimetype_box",
120                "categories": "filetype_mimetype_categories",
121                "types": "filetype_mimetype_types"
122            },
123            "extensions": {
124                "radio": "filetype_extension_radio",
125                "entry": "filetype_extension_entry"
126            }
127        },
128
129        # Custom Date Range
130        "date": {
131            "dialog": "date_dialog",
132            "start_calendar": "date_start_calendar",
133            "end_calendar": "date_end_calendar",
134        },
135
136        # Update Search Index
137        "update": {
138            "dialog": "update_dialog",
139            "database_label": "update_dialog_database_details_label",
140            "modified_label": "update_dialog_modified_details_label",
141            "status_infobar": "update_dialog_infobar",
142            "status_icon": "update_dialog_infobar_icon",
143            "status_label": "update_dialog_infobar_label",
144            "close_button": "update_close",
145            "unlock_button": "update_unlock"
146        }
147    }
148}
149
150
151class Window(Gtk.Window):
152
153    """This class is meant to be subclassed by CatfishWindow. It provides
154    common functions and some boilerplate."""
155    __gtype_name__ = "Window"
156
157    # To construct a new instance of this method, the following notable
158    # methods are called in this order:
159    # __new__(cls)
160    # __init__(self)
161    # finish_initializing(self, builder)
162    # __init__(self)
163    #
164    # For this reason, it's recommended you leave __init__ empty and put
165    # your initialization code in finish_initializing
166
167    def __new__(cls):
168        """Special static method that's automatically called by Python when
169        constructing a new instance of this class.
170
171        Returns a fully instantiated BaseCatfishWindow object.
172        """
173        builder = get_builder(__builder__['ui_file'])
174        builder.add_name_mapping(__builder__)
175        new_object = builder.get_named_object("window.main")
176        new_object.finish_initializing(builder)
177        return new_object
178
179    def finish_initializing(self, builder):
180        """Called while initializing this instance in __new__
181
182        finish_initializing should be called after parsing the UI definition
183        and creating a CatfishWindow object with it in order to finish
184        initializing the start of the new CatfishWindow instance.
185        """
186        # Get a reference to the builder and set up the signals.
187        self.builder = builder
188        self.ui = builder.get_ui(self, True)
189        self.AboutDialog = None  # class
190
191        self.sidebar = builder.get_named_object("window.sidebar")
192
193        # Widgets
194        # Folder Chooser
195        chooser = self.builder.get_named_object("toolbar.folderchooser")
196        # Search
197        search = self.builder.get_named_object("toolbar.search")
198
199        # AppMenu
200        button = Gtk.MenuButton()
201        button.set_size_request(32, 32)
202        image = Gtk.Image.new_from_icon_name("open-menu-symbolic",
203                                             Gtk.IconSize.MENU)
204        button.set_image(image)
205        popover = Gtk.Popover.new(button)
206        appmenu = self.builder.get_named_object("menus.application.menu")
207        popover.add(appmenu)
208        button.set_popover(popover)
209
210        settings = CatfishSettings.CatfishSettings()
211        if settings.get_setting('use-headerbar'):
212            self.setup_headerbar(chooser, search, button)
213        else:
214            self.setup_toolbar(chooser, search, button)
215
216        search.grab_focus()
217        self.keys_pressed = []
218
219        self.search_engine = None
220        self.settings = None
221        self.hidden_files = None
222
223    def on_sidebar_toggle_toggled(self, widget):
224        pass
225
226    def setup_headerbar(self, chooser, search, button):
227        headerbar = Gtk.HeaderBar.new()
228        headerbar.set_show_close_button(True)
229
230        headerbar.pack_start(chooser)
231        headerbar.set_title(_("Catfish"))
232        headerbar.set_custom_title(search)
233        headerbar.pack_end(button)
234
235        self.set_titlebar(headerbar)
236        headerbar.show_all()
237
238    def setup_toolbar(self, chooser, search, button):
239        toolbar = Gtk.Toolbar.new()
240
241        toolitem = Gtk.ToolItem.new()
242        toolitem.add(chooser)
243        toolitem.set_margin_end(6)
244        toolbar.insert(toolitem, 0)
245
246        toolitem = Gtk.ToolItem.new()
247        toolitem.add(search)
248        search.set_hexpand(True)
249        toolitem.set_expand(True)
250        toolitem.set_margin_end(6)
251        toolbar.insert(toolitem, 1)
252
253        toolitem = Gtk.ToolItem.new()
254        toolitem.add(button)
255        toolbar.insert(toolitem, 2)
256
257        self.get_children()[0].pack_start(toolbar, False, False, 0)
258        self.get_children()[0].reorder_child(toolbar, 0)
259        toolbar.show_all()
260
261    def on_mnu_about_activate(self, widget, data=None):  # pylint: disable=W0613
262        """Display the about box for catfish."""
263        if self.AboutDialog is not None:
264            about = self.AboutDialog()  # pylint: disable=E1102
265            about.set_transient_for(self)
266            about.run()
267            about.destroy()
268
269    def on_destroy(self, widget, data=None):  # pylint: disable=W0613
270        """Called when the CatfishWindow is closed."""
271        self.search_engine.stop()
272        self.settings.write()
273        Gtk.main_quit()
274
275    def on_catfish_window_window_state_event(self, widget, event):  # pylint: disable=W0613
276        """Properly handle window-manager fullscreen events."""
277        self.window_is_fullscreen = bool(event.new_window_state &
278                                         Gdk.WindowState.FULLSCREEN)
279
280    def get_keys_from_event(self, event):
281        keys = []
282        keys.append(Gdk.keyval_name(event.keyval))
283        if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
284            keys.append("Control")
285        if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
286            keys.append("Shift")
287        if event.get_state() & Gdk.ModifierType.SUPER_MASK:
288            keys.append("Super")
289        if event.get_state() & Gdk.ModifierType.MOD1_MASK:
290            keys.append("Alt")
291        return keys
292
293    def map_key(self, key):
294        if key.endswith("_L"):
295            return key.replace("_L", "")
296        if key.endswith("_R"):
297            return key.replace("_R", "")
298        return key
299
300    def add_keys(self, keys):
301        for key in keys:
302            self.add_key(key)
303
304    def add_key(self, key):
305        if key is None:
306            return
307        key = self.map_key(key)
308        if key in ["Escape"]:
309            return
310        if key not in self.keys_pressed:
311            self.keys_pressed.append(key)
312
313    def remove_keys(self, keys):
314        for key in keys:
315            if key in self.keys_pressed:
316                self.remove_key(key)
317                self.remove_key(key.upper())
318
319    def remove_key(self, key):
320        if key is None:
321            return
322        key = self.map_key(key)
323        try:
324            self.keys_pressed.remove(key)
325        except ValueError:
326            pass
327
328    def on_catfish_window_key_press_event(self, widget, event):
329        """Handle keypresses for the Catfish window."""
330        keys = self.get_keys_from_event(event)
331        self.add_keys(keys)
332
333        if "Escape" in keys:
334            self.search_engine.stop()
335            return True
336        if "Control" in keys and ("q" in keys or "Q" in keys):
337            self.destroy()
338            return True
339        if "Control" in keys and ("h" in keys or "H" in keys):
340            self.hidden_files.activate()
341            return True
342        if 'F9' in keys:
343            self.on_sidebar_toggle_toggled(widget)
344            return True
345        if 'F11' in keys:
346            if self.window_is_fullscreen:
347                self.unfullscreen()
348            else:
349                self.fullscreen()
350            return True
351        return False
352
353    def on_catfish_window_key_release_event(self, widget, event):  # pylint: disable=W0613
354        """Handle key releases for the Catfish window."""
355        keys = self.get_keys_from_event(event)
356        self.remove_keys(keys)
357        return False
358
359    def on_catfish_window_size_allocate(self, widget, allocation):  # pylint: disable=W0613
360        paned = self.builder.get_named_object("window.paned")
361        allocation = paned.get_allocation()
362        self.settings.set_setting('window-height', allocation.height)
363        self.settings.set_setting('window-width', allocation.width)
364        paned.set_property('height_request', -1)
365        paned.set_property('width_request', -1)
366
367    def on_catfish_window_configure_event(self, widget, event):  # pylint: disable=W0613
368        pos = self.get_position()
369        self.settings.set_setting('window-x', pos.root_x)
370        self.settings.set_setting('window-y', pos.root_y)
371