1# Copyright (c) 2014-2020 Cedric Bellegarde <cedric.bellegarde@adishatz.org>
2# This program is free software: you can redistribute it and/or modify
3# it under the terms of the GNU General Public License as published by
4# the Free Software Foundation, either version 3 of the License, or
5# (at your option) any later version.
6# This program is distributed in the hope that it will be useful,
7# but WITHOUT ANY WARRANTY; without even the implied warranty of
8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9# GNU General Public License for more details.
10# You should have received a copy of the GNU General Public License
11# along with this program. If not, see <http://www.gnu.org/licenses/>.
12
13from gi.repository import Gtk, GLib, Gdk
14
15from gettext import gettext as _
16
17from eolie.utils import get_current_monitor_model
18from eolie.helper_passwords import PasswordsHelper
19from eolie.logger import Logger
20from eolie.define import App
21
22
23class SettingsDialog:
24    """
25        Dialog showing Eolie settings
26    """
27
28    __BOOLEAN = ["night-mode", "use-system-fonts", "remember-session",
29                 "open-downloads", "enable-plugins", "developer-extras",
30                 "do-not-track", "autoplay-videos", "remember-passwords",
31                 "enable-firefox-sync", "enable-suggestions"]
32
33    __RANGE = ["min-font-size", "max-popular-items"]
34    __COMBO = ["start-page", "cookie-storage", "history-storage"]
35    __ENTRY = ["start-page-custom"]
36    __LOCKED_ON = {"use-system-fonts": ["sans-serif_row",
37                                        "serif_row",
38                                        "monospace_row"]}
39    __LOCKED_OFF = {"enable-firefox-sync": ["status_row",
40                                            "sync_row",
41                                            "configure_row"]}
42
43    def __init__(self, window):
44        """
45            Init dialog
46            @param window as Window
47        """
48        self.__window = window
49        self.__locked = []
50        self.__passwords_helper = PasswordsHelper()
51        builder = Gtk.Builder()
52        builder.add_from_resource("/org/gnome/Eolie/SettingsDialog.ui")
53        self.__settings_dialog = builder.get_object("settings_dialog")
54        self.__firefox_sync_button = builder.get_object(
55            "enable-firefox-sync_boolean")
56        self.__start_page_custom_entry = builder.get_object(
57            "start-page-custom_entry")
58        # Firefox sync
59        self.__status_row = builder.get_object("status_row")
60        self.__sync_button = builder.get_object("sync_button")
61        self.__username_entry = builder.get_object("username_entry")
62        self.__password_entry = builder.get_object("password_entry")
63        self.__code_entry = builder.get_object("code_entry")
64        for dic in [self.__LOCKED_ON, self.__LOCKED_OFF]:
65            for key in dic.keys():
66                for locked in dic[key]:
67                    widget = builder.get_object(locked)
68                    widget.set_name(key)
69                    self.__locked.append(widget)
70        self.__set_default_zoom_level(builder.get_object("default_zoom_level"))
71        download_chooser = builder.get_object("download_chooser")
72        dir_uri = App().settings.get_value("download-uri").get_string()
73        if not dir_uri:
74            directory = GLib.get_user_special_dir(
75                GLib.UserDirectory.DIRECTORY_DOWNLOAD)
76            if directory is not None:
77                dir_uri = GLib.filename_to_uri(directory, None)
78        if dir_uri:
79            download_chooser.set_uri(dir_uri)
80        else:
81            download_chooser.set_uri("file://" + GLib.getenv("HOME"))
82        script_uri = App().settings.get_value("user-script-uri").get_string()
83        builder.get_object("user_script_chooser").set_uri(script_uri)
84        builder.connect_signals(self)
85        for setting in self.__BOOLEAN:
86            button = builder.get_object("%s_boolean" % setting)
87            value = App().settings.get_value(setting)
88            # Only setting switch on fires a signal
89            if value:
90                button.set_state(value)
91            else:
92                self._on_boolean_state_set(button, False)
93        for setting in self.__ENTRY:
94            entry = builder.get_object("%s_entry" % setting)
95            value = App().settings.get_value(setting).get_string()
96            entry.set_text(value)
97        for setting in self.__RANGE:
98            widget = builder.get_object("%s_range" % setting)
99            value = App().settings.get_value(setting).get_int32()
100            widget.set_value(value)
101        for setting in self.__COMBO:
102            widget = builder.get_object("%s_combo" % setting)
103            value = App().settings.get_enum(setting)
104            widget.set_active(value)
105        self.__multi_press = Gtk.EventControllerKey.new(self.__settings_dialog)
106        self.__multi_press.connect("key-released", self.__on_key_released)
107        self.__passwords_helper.get_sync(self.__on_get_sync)
108        self.__check_sync_status()
109        if App().sync_worker is not None:
110            App().sync_worker.connect("syncing", self.__on_syncing)
111
112    def show(self):
113        """
114            Show dialog
115        """
116        self.__settings_dialog.show()
117
118    @property
119    def stack(self):
120        """
121            Get main stack
122            @return Gtk.Stack
123        """
124        return self.__settings_dialog.get_child()
125
126#######################
127# PROTECTED           #
128#######################
129    def _on_dialog_destroy(self, widget):
130        """
131            Clean up
132            @param widget as Gtk.Widget
133        """
134        if App().sync_worker is not None:
135            App().sync_worker.disconnect_by_func(self.__on_syncing)
136
137    def _on_boolean_state_set(self, widget, state):
138        """
139            Save setting
140            @param widget as Gtk.Switch
141            @param state as bool
142        """
143        setting = widget.get_name()
144        App().settings.set_value(setting,
145                                 GLib.Variant("b", state))
146        if setting in self.__LOCKED_ON.keys():
147            self.__lock_for_setting(setting, not state)
148        elif setting in self.__LOCKED_OFF.keys():
149            self.__lock_for_setting(setting, state)
150        if setting == "enable-firefox-sync" and\
151                App().sync_worker is not None:
152            if state:
153                App().sync_worker.set_credentials()
154                App().sync_worker.pull_loop()
155                self.__status_row.set_subtitle(_("Connecting…"))
156                GLib.timeout_add(5000, self.__check_sync_status)
157            else:
158                App().sync_worker.stop(True)
159                self.__status_row.set_subtitle(_("Not running"))
160
161    def _on_range_changed(self, widget):
162        """
163            Save value
164            @param widget as Gtk.Range
165        """
166        setting = widget.get_name()
167        value = widget.get_value()
168        App().settings.set_value(setting, GLib.Variant("i", value))
169
170    def _on_entry_changed(self, widget):
171        """
172            Save value
173            @param widget as Gtk.Entry
174        """
175        setting = widget.get_name()
176        value = widget.get_text()
177        App().settings.set_value(setting, GLib.Variant("s", value))
178
179    def _on_combo_changed(self, widget):
180        """
181            Save value
182            @param widget as Gtk.ComboBoxText
183        """
184        setting = widget.get_name()
185        value = widget.get_active()
186        App().settings.set_enum(setting, value)
187        if setting == "start-page":
188            if value == 4:
189                self.__start_page_custom_entry.show()
190            else:
191                self.__start_page_custom_entry.hide()
192
193    def _on_download_selection_changed(self, chooser):
194        """
195            Save uri
196            @chooser as Gtk.FileChooserButton
197        """
198        uri = chooser.get_uri()
199        if uri is None:
200            uri = ""
201        App().settings.set_value("download-uri", GLib.Variant("s", uri))
202
203    def _on_user_script_selection_changed(self, chooser):
204        """
205            Save uri
206            @chooser as Gtk.FileChooserButton
207        """
208        uri = chooser.get_uri()
209        if uri is None:
210            uri = ""
211        App().settings.set_value("user-script-uri", GLib.Variant("s", uri))
212
213    def _on_font_sans_serif_set(self, fontbutton):
214        """
215            Save font setting
216            @param fontchooser as Gtk.FontButton
217        """
218        value = GLib.Variant("s", fontbutton.get_font_name())
219        App().settings.set_value("font-sans-serif", value)
220        App().set_setting("sans-serif-font-family", fontbutton.get_font_name())
221
222    def _on_font_serif_set(self, fontbutton):
223        """
224            Save font setting
225            @param fontchooser as Gtk.FontButton
226        """
227        value = GLib.Variant("s", fontbutton.get_font_name())
228        App().settings.set_value("font-serif", value)
229        App().set_setting("serif-font-family", fontbutton.get_font_name())
230
231    def _on_font_monospace_set(self, fontbutton):
232        """
233            Save font setting
234            @param fontchooser as Gtk.FontButton
235        """
236        value = GLib.Variant("s", fontbutton.get_font_name())
237        App().settings.set_value("font-monospace", value)
238        App().set_setting("monospace-font-family", fontbutton.get_font_name())
239
240    def _on_configure_engines_clicked(self, button):
241        """
242            Show Web engines configurator
243            @param button as Gtk.Button
244        """
245        from eolie.dialog_search_engine import SearchEngineDialog
246        dialog = SearchEngineDialog()
247        dialog.show()
248        dialog.connect("destroy-me", self.__on_sub_dialog_destroyed)
249        self.stack.add(dialog)
250        self.stack.set_visible_child(dialog)
251
252    def _on_configure_notifications_clicked(self, button):
253        """
254            Show Web engines configurator
255            @param button as Gtk.Button
256        """
257        from eolie.dialog_notifications import NotificationsDialog
258        dialog = NotificationsDialog()
259        dialog.show()
260        dialog.connect("destroy-me", self.__on_sub_dialog_destroyed)
261        self.stack.add(dialog)
262        self.stack.set_visible_child(dialog)
263
264    def _on_clear_personnal_data_clicked(self, button):
265        """
266            Show clear personnal data dialog
267            @param button as Gtk.button
268        """
269        from eolie.dialog_clear_data import ClearDataDialog
270        dialog = ClearDataDialog()
271        dialog.show()
272        dialog.connect("destroy-me", self.__on_sub_dialog_destroyed)
273        self.stack.add(dialog)
274        self.stack.set_visible_child(dialog)
275
276    def _on_manage_passwords_clicked(self, button):
277        """
278            Launch searhorse
279            @param button as Gtk.Button
280        """
281        from eolie.dialog_credentials import CredentialsDialog
282        dialog = CredentialsDialog()
283        dialog.show()
284        dialog.connect("destroy-me", self.__on_sub_dialog_destroyed)
285        self.stack.add(dialog)
286        self.stack.set_visible_child(dialog)
287
288    def _on_sync_now_clicked(self, button):
289        """
290            Sync now with Firefox Sync
291            @param button as Gtk.Button
292        """
293        App().sync_worker.pull(True)
294        App().sync_worker.push()
295
296    def _on_sync_button_clicked(self, button):
297        """
298            Connect to Firefox Sync to get tokens
299            @param button as Gtk.Button
300        """
301        if button.get_style_context().has_class("suggested-action"):
302            button.get_style_context().remove_class("destructive-action")
303            self.__status_row.set_subtitle(_("Connecting…"))
304            App().task_helper.run(self.__connect_firefox_sync,
305                                  self.__username_entry.get_text(),
306                                  self.__password_entry.get_text(),
307                                  self.__code_entry.get_text(),
308                                  callback=(self.__on_sync_result,))
309        else:
310            App().sync_worker.stop(True)
311            App().sync_worker.delete_secret()
312            button.get_style_context().remove_class("suggested-action")
313            button.get_style_context().add_class("destructive-action")
314            self.__status_row.set_subtitle(_("Stopped…"))
315
316    def _on_credentials_changed(self, entry):
317        """
318            Update widgets state
319            @param entry as Gtk.Entry
320        """
321        credentials_ok = self.__username_entry.get_text() != "" and (
322            self.__password_entry.get_text() != "" or
323            self.__code_entry.get_text() != "")
324        self.__sync_button.set_sensitive(credentials_ok)
325        if self.__password_entry.get_text() != "":
326            self.__password_entry.set_sensitive(True)
327            self.__code_entry.set_sensitive(False)
328        elif self.__code_entry.get_text() != "":
329            self.__password_entry.set_sensitive(False)
330            self.__code_entry.set_sensitive(True)
331        else:
332            self.__password_entry.set_sensitive(True)
333            self.__code_entry.set_sensitive(True)
334
335#######################
336# PRIVATE             #
337#######################
338    def __get_sync_status(self):
339        """
340            Get sync status
341            return int
342        """
343        return App().sync_worker.status
344
345    def __check_sync_status(self):
346        """
347            Check worker status
348        """
349        if App().sync_worker is not None:
350            App().task_helper.run(self.__get_sync_status,
351                                  callback=(self.__on_get_sync_status,))
352        else:
353            App().settings.set_value("enable-firefox-sync",
354                                     GLib.Variant("b", False))
355            self.__firefox_sync_button.set_sensitive(False)
356            from eolie.firefox_sync import SyncWorker
357            if not SyncWorker.check_modules():
358                cmd =\
359                 "$ pip3 install requests-hawk PyFxA pycrypto cryptography"
360                self.__status_row.set_subtitle(cmd)
361            else:
362                self.__status_row.set_subtitle(_("Not running"))
363
364    def __lock_for_setting(self, setting, sensitive):
365        """
366            Lock widgets for setting
367            @param setting as str
368            @param sensitive as bool
369        """
370        for locked in self.__locked:
371            if locked.get_name() == setting:
372                locked.set_sensitive(sensitive)
373
374    def __connect_firefox_sync(self, username, password, code):
375        """
376            Connect to firefox sync
377            @param username as str
378            @param password as str
379            @param code as str
380            @return bool
381        """
382        try:
383            App().sync_worker.new_session()
384            App().sync_worker.login({"login": username}, password, code)
385            return True
386        except Exception as e:
387            Logger.error("SettingsDialog::__connect_firefox_sync(): %s", e)
388            if str(e) == "Unverified account":
389                return -1
390        return False
391
392    def __set_default_zoom_level(self, widget):
393        """
394            Set default zoom level
395            @param widget as Gtk.SpinButton
396        """
397        monitor_model = get_current_monitor_model(self.__window)
398        zoom_levels = App().settings.get_value("default-zoom-level")
399        wanted_zoom_level = 1.0
400        try:
401            for zoom_level in zoom_levels:
402                zoom_splited = zoom_level.split('@')
403                if zoom_splited[0] == monitor_model:
404                    wanted_zoom_level = float(zoom_splited[1])
405        except:
406            pass
407        percent_zoom = int(wanted_zoom_level * 100)
408        widget.set_value(percent_zoom)
409        widget.set_text("{} %".format(percent_zoom))
410        widget.connect("value-changed", self.__on_default_zoom_changed)
411
412    def __on_default_zoom_changed(self, button):
413        """
414            Save size
415            @param button as Gtk.SpinButton
416        """
417        button.set_text("{} %".format(int(button.get_value())))
418        monitor_model = get_current_monitor_model(self.__window)
419        try:
420            # Add current value less monitor model
421            zoom_levels = []
422            for zoom_level in App().settings.get_value("default-zoom-level"):
423                zoom_splited = zoom_level.split('@')
424                if zoom_splited[0] == monitor_model:
425                    continue
426                else:
427                    zoom_levels.append("%s@%s" % (zoom_splited[0],
428                                                  zoom_splited[1]))
429            # Add new zoom value for monitor model
430            zoom_levels.append("%s@%s" % (monitor_model,
431                                          button.get_value() / 100))
432            App().settings.set_value("default-zoom-level",
433                                     GLib.Variant("as", zoom_levels))
434            for window in App().windows:
435                window.update_zoom_level(True)
436        except Exception as e:
437            Logger.error("SettingsDialog::__on_default_zoom_changed(): %s", e)
438
439    def __on_key_released(self, event_controller, keyval, keycode, state):
440        """
441            Quit on escape
442            @param event_controller as Gtk.EventController
443            @param keyval as int
444            @param keycode as int
445            @param state as Gdk.ModifierType
446        """
447        if keyval == Gdk.KEY_Escape:
448            self.__settings_dialog.destroy()
449
450    def __on_sync_result(self, result):
451        """
452            Show result to user
453            @param result as bool
454        """
455        if result is True:
456            self.__status_row.set_subtitle(_("Connected"))
457        elif result == -1:
458            self.__status_row.set_subtitle(
459                _("Check your email and connect again"))
460        else:
461            self.__status_row.set_subtitle(_("Failed"))
462
463    def __on_get_sync(self, attributes, password, uri, index, count):
464        """
465            Set username and password
466            @param attributes as {}
467            @param password as str
468            @param uri as None
469            @param index as int
470            @param count as int
471        """
472        if attributes is None:
473            return
474        try:
475            self.__username_entry.set_text(attributes["login"])
476        except Exception as e:
477            Logger.error("SettingsDialog::__on_get_sync(): %s", e)
478
479    def __on_get_sync_status(self, status):
480        """
481            Show a message about missing fxa module
482            @param status as bool
483        """
484        if status:
485            self.__status_row.set_subtitle(_("Connected"))
486            App().sync_worker.pull_loop()
487
488    def __on_syncing(self, worker, message):
489        """
490            Show message as status
491            @param worker as SyncWorker
492            @param message as str
493        """
494        self.__status_row.set_subtitle(_("Syncing %s") % message)
495
496    def __on_sub_dialog_destroyed(self, widget):
497        """
498            Restore previous dialog
499            @param widget as Gtk.Widget
500        """
501        for child in self.stack.get_children():
502            if child != widget:
503                self.stack.set_visible_child(child)
504                break
505        GLib.timeout_add(1000, widget.destroy)
506