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