1# Copyright (c) 2017-2021 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, Gdk, GLib, Pango, Gio 14 15from gettext import gettext as _ 16from urllib.parse import urlparse 17from time import time 18 19from eolie.define import App 20from eolie.utils import emit_signal 21from eolie.helper_task import TaskHelper 22from eolie.popover_uri import UriPopover 23from eolie.widget_smooth_progressbar import SmoothProgressBar 24from eolie.widget_uri_entry_icons import UriEntryIcons 25from eolie.helper_gestures import GesturesHelper 26from eolie.helper_size_allocation import SizeAllocationHelper 27 28 29class UriEntry(Gtk.Overlay, SizeAllocationHelper): 30 """ 31 URI Entry for toolbar 32 """ 33 34 def __init__(self, window): 35 """ 36 Init toolbar 37 @param window as Window 38 """ 39 Gtk.Overlay.__init__(self) 40 self.get_style_context().add_class("input") 41 self.__cancellable = Gio.Cancellable.new() 42 self.__window = window 43 self.__task_helper = TaskHelper() 44 self.__input_warning_shown = False 45 self.__entry_changed_id = None 46 self.__secure_content = True 47 self.__size_allocation_timeout = None 48 self.__current_value = "" 49 self.__entry = Gtk.Entry.new() 50 self.__entry.show() 51 self.__entry.get_style_context().add_class("uribar") 52 self.__signal_id = self.__entry.connect("changed", 53 self.__on_entry_changed) 54 self.__entry.connect("populate-popup", self.__on_entry_populate_popup) 55 self.__entry.connect("icon-release", self.__on_entry_icon_release) 56 self.__entry_gesture = GesturesHelper( 57 self.__entry, 58 primary_press_callback=self.__on_entry_press) 59 self.__entry_controller = Gtk.EventControllerKey.new(self.__entry) 60 self.__entry_controller.connect("focus-in", self.__on_entry_focus_in) 61 self.__entry_controller.connect("focus-out", self.__on_entry_focus_out) 62 self.__entry_controller.connect("key-pressed", 63 self.__on_entry_key_pressed) 64 65 self.__icons = UriEntryIcons(self.__entry, window) 66 self.__icons.show() 67 68 self.__placeholder = Gtk.Label.new() 69 self.__placeholder.show() 70 self.__placeholder.set_property("halign", Gtk.Align.START) 71 self.__placeholder.set_hexpand(True) 72 self.__placeholder.set_ellipsize(Pango.EllipsizeMode.END) 73 self.__placeholder.get_style_context().add_class("placeholder") 74 self.__placeholder.set_opacity(0.8) 75 76 grid = Gtk.Grid() 77 grid.show() 78 grid.set_valign(Gtk.Align.CENTER) 79 grid.add(self.__placeholder) 80 grid.add(self.__icons) 81 82 self.__progress = SmoothProgressBar() 83 84 self.add(self.__entry) 85 self.add_overlay(grid) 86 self.add_overlay(self.__progress) 87 self.set_overlay_pass_through(grid, True) 88 89 # Inline completion 90 self.__completion_model = Gtk.ListStore(str) 91 self.__completion = Gtk.EntryCompletion.new() 92 self.__completion.set_model(self.__completion_model) 93 self.__completion.set_text_column(0) 94 self.__completion.set_inline_completion(True) 95 self.__completion.set_popup_completion(False) 96 self.__entry.set_completion(self.__completion) 97 98 self.__popover = UriPopover(window) 99 self.__popover.set_relative_to(self.__entry) 100 self.__popover.connect("closed", self.__on_popover_closed) 101 102 # Some on the fly css styling 103 context = self.__entry.get_style_context() 104 self.__css_allocation = Gtk.CssProvider() 105 self.__css_color = Gtk.CssProvider() 106 context.add_provider_for_screen(Gdk.Screen.get_default(), 107 self.__css_allocation, 108 Gtk.STYLE_PROVIDER_PRIORITY_USER) 109 context.add_provider_for_screen(Gdk.Screen.get_default(), 110 self.__css_color, 111 Gtk.STYLE_PROVIDER_PRIORITY_USER) 112 SizeAllocationHelper.__init__(self, self.__icons) 113 114 def set_text_entry(self, text): 115 """ 116 Set uri in Gtk.Entry 117 @param text as str 118 """ 119 if self.__entry.get_text() == text: 120 return 121 if self.__signal_id is not None: 122 self.__entry.disconnect(self.__signal_id) 123 # Do not show this in titlebar 124 parsed = urlparse(text) 125 if parsed.scheme in ["populars", "about"]: 126 text = "" 127 # Should not be needed but set_text("") do not clear text 128 self.__entry.delete_text(0, -1) 129 self.__entry.set_text(text) 130 self.__entry.set_position(-1) 131 if text: 132 self.__placeholder.set_opacity(0) 133 else: 134 self.__placeholder.set_opacity(0.8) 135 self.__signal_id = self.__entry.connect("changed", 136 self.__on_entry_changed) 137 138 def set_uri(self, uri): 139 """ 140 Set toolbar URI 141 @param uri as str 142 """ 143 self.set_tooltip_text(uri) 144 self.__input_warning_shown = False 145 self.__secure_content = True 146 self.__update_secure_content_indicator() 147 bookmark_id = App().bookmarks.get_id(uri) 148 self.__icons.set_bookmarked(bookmark_id is not None) 149 150 def set_title(self, title): 151 """ 152 Set toolbar title 153 @param title as str 154 """ 155 if title is None: 156 self.set_default_placeholder() 157 else: 158 self.__window.set_title(title) 159 self.__placeholder.set_text(title) 160 self.set_text_entry("") 161 162 def set_insecure_content(self): 163 """ 164 Mark uri as insecure 165 """ 166 self.__secure_content = False 167 self.__entry.set_icon_tooltip_text( 168 Gtk.EntryIconPosition.PRIMARY, 169 _("This page contains insecure content")) 170 self.__entry.set_icon_from_icon_name( 171 Gtk.EntryIconPosition.PRIMARY, 172 "channel-insecure-symbolic") 173 174 def focus(self, child="bookmarks"): 175 """ 176 Focus wanted child entry 177 @param child as str 178 """ 179 self.get_toplevel().set_focus(self.__entry) 180 uri = self.__window.container.webview.uri 181 parsed = urlparse(uri) 182 if parsed.scheme in ["http", "https", "file"]: 183 self.set_text_entry(uri) 184 self.__entry.select_region(0, -1) 185 self.__popover.popup(child) 186 187 def set_default_placeholder(self): 188 """ 189 Show search placeholder 190 """ 191 if self.__placeholder.get_text() == _("Search or enter address"): 192 return 193 self.set_text_entry("") 194 self.__placeholder.set_text(_("Search or enter address")) 195 self.__entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, 196 "system-search-symbolic") 197 self.__entry.set_icon_tooltip_text(Gtk.EntryIconPosition.PRIMARY, 198 "") 199 200 def update_style(self): 201 """ 202 Update color based on parent values 203 Used for dark headerbar (Ubuntu like) 204 """ 205 # Getting color for headerbar as not possible in pure CSS 206 style = self.get_parent().get_style_context() 207 color = style.get_color(Gtk.StateFlags.ACTIVE).to_string() 208 css = ".uribar { color: %s; caret-color:%s}" % (color, color) 209 self.__css_color.load_from_data(css.encode("utf-8")) 210 211 @property 212 def widget(self): 213 """ 214 Get main entry widget 215 @return Gtk.entry 216 """ 217 return self.__entry 218 219 @property 220 def placeholder(self): 221 """ 222 Get placeholder 223 @return Gtk.Label 224 """ 225 return self.__placeholder 226 227 @property 228 def popover(self): 229 """ 230 Get main URI popover 231 @return UriPopover 232 """ 233 return self.__popover 234 235 @property 236 def icons(self): 237 """ 238 Get entry icons 239 @return UriEntryIcons 240 """ 241 return self.__icons 242 243 @property 244 def progress(self): 245 """ 246 Get progress bar 247 @return Gtk.ProgressBar 248 """ 249 return self.__progress 250 251####################### 252# PRIVATE # 253####################### 254 def __focus_out(self): 255 """ 256 Focus out widget 257 """ 258 webview = self.__window.container.webview 259 self.set_text_entry("") 260 uri = webview.uri 261 if uri is not None: 262 bookmark_id = App().bookmarks.get_id(uri) 263 self.__icons.set_bookmarked(bookmark_id is not None) 264 265 def __update_secure_content_indicator(self): 266 """ 267 Update PRIMARY icon, Gtk.Entry should be set 268 """ 269 webview = self.__window.container.webview 270 if time() - webview.ctime < 10: 271 self.__entry.set_icon_from_icon_name( 272 Gtk.EntryIconPosition.PRIMARY, "dialog-password-symbolic") 273 self.__entry.set_icon_tooltip_text( 274 Gtk.EntryIconPosition.PRIMARY, 275 _("Save password for: %s") % webview.credentials_uri) 276 else: 277 uri = webview.uri 278 parsed = urlparse(uri) 279 self.__entry.set_icon_tooltip_text(Gtk.EntryIconPosition.PRIMARY, 280 "") 281 if (parsed.scheme == "https" and self.__secure_content) or\ 282 parsed.scheme == "file": 283 self.__entry.set_icon_from_icon_name( 284 Gtk.EntryIconPosition.PRIMARY, 285 "channel-secure-symbolic") 286 elif parsed.scheme in ["http", "https"]: 287 self.set_insecure_content() 288 else: 289 self.__entry.set_icon_from_icon_name( 290 Gtk.EntryIconPosition.PRIMARY, 291 "system-search-symbolic") 292 293 def __populate_completion(self, value): 294 """ 295 @param value as str 296 @thread safe 297 """ 298 def populate(match, value): 299 if match is not None and self.__current_value == value: 300 iterator = get_iterator() 301 self.__completion_model.set_value(iterator, 302 0, 303 match) 304 self.__completion.insert_prefix() 305 else: 306 # Only happen if nothing matched 307 self.__completion_model.clear() 308 309 def look_for_match(value): 310 parsed = urlparse(value) 311 # Look for a match in bookmarks 312 match = App().bookmarks.get_match(value) 313 if match is None: 314 # Look for a match in history 315 match = App().history.get_match(value) 316 if match is not None: 317 match = match.split("://")[-1].split("www.")[-1] 318 # We want result to match value slashes 319 slash_count = parsed.path.count("/") 320 split = match.split("/")[:-slash_count - 1] 321 if split: 322 match = "/".join(split) 323 GLib.idle_add(populate, match, value) 324 325 def get_iterator(): 326 iterator = self.__completion_model.get_iter_first() 327 if iterator is None: 328 iterator = self.__completion_model.insert(0) 329 return iterator 330 331 self.__task_helper.run(look_for_match, value) 332 333 def __on_popover_closed(self, popover): 334 """ 335 Clean titlebar 336 @param popover as Gtk.popover 337 """ 338 webview = self.__window.container.webview 339 webview.grab_focus() 340 self.__focus_out() 341 value = self.__entry.get_text().lstrip().rstrip() 342 if value: 343 webview.add_text_entry(value) 344 self.__entry.delete_selection() 345 346 def __on_entry_changed(self, entry): 347 """ 348 Delayed entry changed 349 @param entry as Gtk.Entry 350 """ 351 value = entry.get_text() 352 # Block completion 353 for completion in self.__completion_model: 354 if completion[0] == value: 355 return 356 do_completion = len(value) > len(self.__current_value) 357 self.__current_value = value 358 self.__cancellable.cancel() 359 self.__cancellable = Gio.Cancellable.new() 360 if value: 361 self.__placeholder.set_opacity(0) 362 if not self.__popover.is_visible(): 363 self.__popover.popup("bookmarks") 364 else: 365 webview = self.__window.container.webview 366 webview.clear_text_entry() 367 if self.__entry_changed_id is not None: 368 GLib.source_remove(self.__entry_changed_id) 369 self.__entry_changed_id = GLib.timeout_add( 370 100, 371 self.__on_entry_changed_timeout, 372 entry, 373 do_completion) 374 375 def __on_entry_changed_timeout(self, entry, do_completion): 376 """ 377 Update popover search if needed 378 @param entry as Gtk.Entry 379 @param do_completion as bool 380 """ 381 self.__entry_changed_id = None 382 self.__window.container.webview.add_text_entry(self.__current_value) 383 if do_completion: 384 self.__populate_completion(self.__current_value) 385 parsed = urlparse(self.__current_value) 386 is_uri = parsed.scheme in ["about, http", "file", "https", "populars"] 387 if is_uri: 388 self.__popover.set_search_text(parsed.netloc + parsed.path) 389 else: 390 self.__popover.set_search_text(self.__current_value) 391 self.__entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, 392 "system-search-symbolic") 393 self.__entry.set_icon_tooltip_text(Gtk.EntryIconPosition.PRIMARY, "") 394 395 def __on_entry_focus_in(self, entry, event): 396 """ 397 Block entry on uri 398 @param entry as Gtk.Entry 399 @param event as Gdk.Event 400 """ 401 if self.__popover.is_visible(): 402 return 403 webview = self.__window.container.webview 404 self.__icons.show_clear_button() 405 uri = self.__window.container.webview.uri 406 parsed = urlparse(uri) 407 value = webview.get_current_text_entry() 408 if value: 409 self.set_text_entry(value) 410 elif parsed.scheme in ["http", "https", "file"]: 411 self.set_text_entry(uri) 412 self.__placeholder.set_opacity(0) 413 else: 414 self.set_default_placeholder() 415 416 def __on_entry_focus_out(self, entry, event): 417 """ 418 Show title 419 @param entry as Gtk.Entry 420 @param event as Gdk.Event (do not use) 421 """ 422 if self.__popover.is_visible(): 423 return 424 self.__focus_out() 425 426 def __on_entry_populate_popup(self, entry, menu): 427 """ 428 @param entry as Gtk.Entry 429 @param menu as Gtk.Menu 430 """ 431 def on_item_activate(item, clipboard): 432 self.__window.container.webview.load_uri(clipboard) 433 clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD).wait_for_text() 434 if clipboard is not None: 435 item = Gtk.MenuItem.new_with_label(_("Paste and load")) 436 item.connect("activate", on_item_activate, clipboard) 437 item.show() 438 menu.attach(item, 0, 1, 3, 4) 439 440 def __on_entry_icon_release(self, entry, position, event): 441 """ 442 Show popover related to icon 443 @param entry as Gtk.Entry 444 @param position as Gtk.EntryIconPosition 445 @param event as Gdk.EventButton 446 """ 447 if self.__entry.get_icon_name(Gtk.EntryIconPosition.PRIMARY) ==\ 448 "dialog-password-symbolic": 449 self.__update_secure_content_indicator() 450 from eolie.popover_credentials import CredentialsPopover 451 credentials_popover = CredentialsPopover(self.__window) 452 credentials_popover.set_relative_to(self.__entry) 453 credentials_popover.set_pointing_to( 454 self.__entry.get_icon_area(Gtk.EntryIconPosition.PRIMARY)) 455 credentials_popover.popup() 456 else: 457 from eolie.popover_tls import TLSPopover 458 tls_popover = TLSPopover(self.__window) 459 tls_popover.set_relative_to(self.__entry) 460 tls_popover.set_pointing_to( 461 self.__entry.get_icon_area(Gtk.EntryIconPosition.PRIMARY)) 462 tls_popover.popup() 463 464 def __on_entry_press(self, x, y, event): 465 """ 466 Show popover if hidden 467 @param x as int 468 @param y as int 469 @param event as Gdk.EventButton 470 """ 471 # 30 for primary icon 472 if x > 30 and not self.__popover.get_visible(): 473 self.__on_entry_focus_in(self.__entry, event) 474 self.__popover.popup("bookmarks") 475 476 def __on_entry_key_pressed(self, controller, keyval, keycode, state): 477 """ 478 Forward to popover history listbox if needed 479 @param controller as Gtk.EventControllerKey. 480 @param keyval as int 481 @param keycode as int 482 @param state as Gdk.ModifierType 483 """ 484 webview = self.__window.container.webview 485 uri = self.__entry.get_text().lstrip().rstrip() 486 487 # Walk history if Ctrl + [zZ] 488 if state & Gdk.ModifierType.CONTROL_MASK: 489 value = None 490 if keyval == Gdk.KEY_z: 491 value = webview.get_prev_text_entry(uri) 492 elif keyval == Gdk.KEY_Z: 493 value = webview.get_next_text_entry() 494 elif keyval == Gdk.KEY_Return: 495 bounds = self.__entry.get_selection_bounds() 496 if bounds: 497 current = self.__entry.get_text()[:bounds[0]] 498 else: 499 current = self.__entry.get_text() 500 value = current + ".com" 501 if value is not None: 502 self.set_text_entry(value) 503 emit_signal(self.__entry, "changed") 504 return 505 506 # Forward event to popover, if not used, handle input 507 forwarded = self.__popover.forward_event(keyval, state) 508 if forwarded: 509 return True 510 else: 511 # Close popover and save current entry 512 if keyval == Gdk.KEY_Escape: 513 self.set_text_entry("") 514 webview.clear_text_entry() 515 GLib.idle_add(self.__window.close_popovers) 516 return True 517 # Close popover, save current entry and load text content 518 elif keyval in [Gdk.KEY_Return, Gdk.KEY_KP_Enter]: 519 webview.clear_text_entry() 520 GLib.idle_add(self.__window.close_popovers) 521 parsed = urlparse(uri) 522 # Search a missing scheme 523 if uri.find(".") != -1 and\ 524 uri.find(" ") == -1 and\ 525 parsed.scheme not in ["http", "https"]: 526 # Add missing www. 527 if not uri.startswith("www."): 528 db_uri = App().history.get_match("www." + uri) 529 if db_uri is not None: 530 uri = "www." + uri 531 # Add missing scheme 532 db_uri = App().history.get_match(uri, True) 533 if db_uri is None: 534 uri = "http://" + uri 535 else: 536 uri = "https://" + uri 537 self.__window.container.load_uri(uri) 538 self.__window.container.set_expose(False) 539 if self.__entry_changed_id is not None: 540 GLib.source_remove(self.__entry_changed_id) 541 self.__entry_changed_id = None 542 webview.grab_focus() 543 return True 544 545 def _handle_width_allocate(self, allocation): 546 """ 547 @param allocation as Gtk.Allocation 548 @return True if allocation is valid 549 """ 550 if SizeAllocationHelper._handle_width_allocate(self, allocation): 551 style = self.__entry.get_style_context() 552 if Gtk.Widget.get_default_direction() == Gtk.TextDirection.RTL: 553 state = Gtk.StateFlags.NORMAL | Gtk.StateFlags.DIR_RTL 554 else: 555 state = Gtk.StateFlags.NORMAL | Gtk.StateFlags.DIR_LTR 556 border = style.get_border(state).bottom 557 padding_start = style.get_padding(state).left 558 padding_end = style.get_padding(state).right 559 margin_start = style.get_margin(state).left 560 margin_end = style.get_margin(state).right 561 margin_bottom = style.get_margin(state).bottom 562 css = ".progressbar { margin-bottom: %spx;\ 563 margin-left: %spx;\ 564 margin-right: %spx; }" % (margin_bottom, 565 margin_start + border, 566 margin_end + border) 567 # 5 is grid margin (see ui file) 568 uribar_padding = allocation.width + 5 569 css += ".uribar { padding-right: %spx; }" % (uribar_padding) 570 css += ".uribar:dir(rtl)"\ 571 "{ padding-left: %spx;padding-right: %spx}" %\ 572 (uribar_padding, padding_end) 573 # 22 is Gtk.EntryIconPosition.PRIMARY 574 if Gtk.Widget.get_default_direction() == Gtk.TextDirection.RTL: 575 placeholder_margin = padding_end + 22 + border 576 else: 577 placeholder_margin = padding_start + 22 + border 578 css += ".placeholder {margin-left: %spx;}" % placeholder_margin 579 css += ".placeholder:dir(rtl)"\ 580 "{margin-right: %spx;\ 581 margin-left: 0px;}" % placeholder_margin 582 self.__css_allocation.load_from_data(css.encode("utf-8")) 583 self.update_style() 584