This repository has been archived on 2023-02-02. You can view files and clone it, but cannot push or open issues or pull requests.
barkshark-web/barkshark_web/component/web_tab.py

651 lines
16 KiB
Python

import cairo
import threading
from bs4 import BeautifulSoup
from dns.resolver import NXDOMAIN, NoAnswer
from izzylib.exceptions import DNSResolverError
from izzylib.misc import class_name
from izzylib.url import Address
from .web_tab_settings import WebSettings
from .web_tab_webview_handler import WebviewHandler
from .. import var
from ..base import BuilderBase
from ..enums import EditAction, Javascript, FileChooserResponse
from ..objects import WebviewState
from ..widgets import FileChooser
from ..functions import (
connect,
resolve_address,
run_in_gui_thread,
set_image
)
class WebTab(BuilderBase, Gtk.Box):
def __init__(self, url=None):
BuilderBase.__init__(self, self.app.path.resources.join('tab.ui'))
Gtk.Box.__init__(self,
orientation = Gtk.Orientation.VERTICAL,
spacing = 5
)
for widget in ['navbar', 'search']:
self.add(self[widget])
self['navbar'].set_margin_top(5)
self.id = self.window.tab_new_id()
self.settings = WebSettings(self)
self.webview = None
self.search = None
self._data = DotDict(
history = {},
state = {},
post = None,
search = False,
favicon = False
)
self.set_button_icons()
self.setup_webview()
if url:
self.load_url(url)
self.setup_signals()
self.show_all()
@classmethod
def new_from_row(cls, row, load=False):
state = WebviewState.new_from_row(row)
tab = cls.new_from_state(state, row.active or load)
return tab
@classmethod
def new_from_state(cls, state, load=False):
tab = cls()
tab.webview.restore_session_state(state.session)
tab['label-favicon'].set_sensitive(True)
tab.id = state.tabid
tab.set_title(state.title or '')
tab.set_url(state.url or '')
tab._data.state = state
if load:
tab.state_load()
else:
tab['label-favicon'].set_sensitive(False)
return tab
## Booleans
@property
def is_loaded(self):
return not self._data.state
## Widgets
@property
def label(self):
return self['label']
@property
def menu(self):
return self['menu']
## Misc
@property
def context(self):
return self.app.context
@property
def favicon(self):
return self.webview.get_favicon()
@property
def state(self):
return self._data.state
@property
def title(self):
return self.webview.get_title()
@property
def url(self):
try:
return Url(self.webview.get_uri())
except:
return
@property
def zoom(self):
return self.webview.get_zoom_level()
@property
def window(self):
return self.app.window
def close(self):
self.window.tab_close(self.id)
def destroy(self):
self.webview.destroy()
Gtk.Box.destroy(self)
BuilderBase.destroy(self)
def edit_action(self, action):
if not isinstance(action, EditAction):
action_set = False
try:
action = EditAction[action.upper()]
except KeyError:
for value in EditAction:
if not new_action and value.name.lower() == action:
action = value
action_set = True
if not new_action:
raise KeyError(f'Invalid editing action: {action}')
self.webview.execute_editing_command(action.value)
def get_state(self, force_new=False):
if force_new or not self._data.state:
return WebviewState.new_from_tab(self)
return self._data.state
def inspector_action(self, action='toggle'):
inspector = self.webview.get_inspector()
if action == 'toggle':
if self.handler.inspector_open:
return self.inspector_action('close')
return self.inspector_action('open')
elif action == 'open':
return inspector.show()
elif action == 'close':
if self.handler.inspector_open:
return inspector.close()
def load_url(self, url):
threading.Thread(target=self.handle_load_url, args=[url]).start()
def page_action(self, action, **kwargs):
if action == 'refresh':
if not self.is_loaded:
self.state_load()
elif kwargs.get('ignore_cache'):
logging.verbose('Refreshing without cache')
self.webview.reload_bypass_cache()
else:
self.webview.reload()
elif action == 'back':
self.webview.go_back()
elif action == 'forward':
self.webview.go_forward()
elif action == 'stop':
self.webview.stop_loading()
self.set_button_state(False)
elif action == 'home':
with self.db.session as s:
self.load_url(s.get_config('homepage'))
elif action == 'print':
self.run_js(Javascript.PRINT)
elif action == 'source':
self.run_js('document.documentElement.outerHTML', self.handle_page_view_source)
elif action == 'inspector':
open = kwargs.get('open', True)
if open:
self.webview.get_inspector().show()
else:
self.webview.get_inspector().close()
elif action == 'save':
with self.db.session as s:
fc = FileChooser(self.window, s.get_config('download_dir'), f'{self.title}.mhtml')
fc.new_filter('MHTML Document', '*.mhtml')
fc.set_callback(FileChooserResponse.OK, self.handle_save_page)
fc.set_callback(FileChooserResponse.CANCEL, logging.verbose, 'Canceled web page download')
fc.run()
elif action == 'zoom':
zoom = kwargs.get('zoom')
if zoom == None:
with self.db.session as s:
zoom = s.get_config('zoom')
else:
zoom = round(float(zoom), 1)
if zoom < 0.1 or zoom > 4.0:
return
self.webview.set_zoom_level(zoom)
else:
logging.warning('Invalid page action:', action)
def run_js(self, js, callback=None, *args, **kwargs):
if isinstance(js, Javascript):
js = js.value
else:
try:
js = Javascript[js.upper()].value
except KeyError:
pass
if not callback:
self.webview.run_javascript(js, None)
else:
js_callback = lambda webview, task: self.handle_run_js(webview, task, callback, *args, **kwargs)
self.webview.run_javascript(js, None, js_callback)
def search_action(self, action, **kwargs):
search = self['search']
text = self['search-text']
no_search_msg = f'search action "{action}": no search started'
if action == 'open':
search.show()
self.run_js(Javascript.SELECTION, self.handle_search_get_selection)
text.grab_focus()
elif action == 'close':
self.search_action('clear')
search.hide()
elif action == 'clear':
if self._data.search:
self.search.search_finish()
self._data.search = None
text.set_text('')
elif action == 'toggle':
self.search_action('close' if search.get_visible() else 'open')
elif action == 'search':
search_text = kwargs.get('text', text.get_text())
if not kwargs.get('force', False) and search_text == self._data.search:
return self.search_action('next')
options = WebKit2.FindOptions.WRAP_AROUND
if kwargs.get('insensitive', True):
options = options | WebKit2.FindOptions.CASE_INSENSITIVE
self.search.search(search_text, options, int(kwargs.get('limit', 1000)))
self.search.search_previous()
self._data.search = search_text
elif action == 'next':
if not self._data.search:
return logging.verbose(no_search_msg)
self.search.search_next()
elif action == 'previous':
if not self._data.search:
return logging.verbose(no_search_msg)
self.search.search_previous()
else:
logging.warning('Invalid search action:', action)
def set_button_icons(self):
self.set_icon_from_resource('navbar-prev-icon', 'previous.svg', 24)
self.set_icon_from_resource('navbar-next-icon', 'next.svg', 24)
self.set_icon_from_resource('navbar-stop-icon', 'stop.svg', 24)
self.set_icon_from_resource('navbar-refresh-icon', 'refresh.svg', 24)
self.set_icon_from_resource('navbar-home-icon', 'home.svg', 24)
self.set_icon_from_resource('navbar-go-icon', 'go.svg', 24)
def set_button_state(self, loading=None):
#self['navbar-url'].set_text(self.url)
#self['navbar-stop'].set_sensitive(not self.idle)
#self['navbar-refresh'].set_sensitive(self.idle)
if loading == None:
loading = self.webview.is_loading()
self['navbar-stop'].set_sensitive(loading)
self['navbar-refresh'].set_sensitive(not loading)
#self.set_history_button_state()
if self.window.active_tab == self:
self.window.set_button_state(self.id)
def set_history_button_state(self):
for button, action in {'navbar-prev': self.webview.can_go_back(), 'navbar-next': self.webview.can_go_forward()}.items():
self[button].set_sensitive(action)
def set_favicon(self, icon=None):
icon = icon or self.favicon or 'image-x-generic'
for widget in ['label-favicon-icon', 'menu-favicon']:
set_image(self[widget], icon, 16)
self._data.favicon = icon != 'image-x-generic'
logging.debug(f'Set icon for page: {class_name(icon)}, {self.url.replace_properties(query=None, anchor=None)}')
def set_favicon_from_cache(self, url=None):
if not url:
url = self.url
if url.proto == var.local_proto:
return self.set_favicon(self.app.path.resources.join('icon.png'))
self.context.get_favicon(url, self.set_favicon)
def set_title(self, text=None):
if not text:
text = self.webview.get_title() or ''
for widget in ['label-title', 'menu-title']:
self[widget].set_text(text)
self[widget].set_tooltip_text(text)
def set_url(self, url=None):
self['navbar-url'].set_text(
url or self.webview.get_uri() or ''
)
def setup_signals(self):
## Navigation bar
self.connect('label-close', 'clicked', self.close)
self.connect('label-favicon', 'clicked', self.state_unload)
self.connect('navbar-prev', 'clicked', self.page_action, 'back')
self.connect('navbar-next', 'clicked', self.page_action, 'forward')
self.connect('navbar-stop', 'clicked', self.page_action, 'stop')
self.connect('navbar-refresh', 'clicked', self.page_action, 'refresh')
self.connect('navbar-home', 'clicked', self.page_action, 'home')
self.connect('navbar-go', 'clicked', self.page_action, 'go')
self.connect('navbar-url', 'key-press-event', self.handle_url_keys, original_args=True)
self.connect('navbar-url', 'populate-popup', self.handle_url_popup, original_args=True)
self.connect('navbar-zoom', 'clicked', self.page_action, 'zoom', zoom=None)
## Search bar
self.connect('search-previous', 'clicked', self.search_action, 'previous')
self.connect('search-next', 'clicked', self.search_action, 'next')
self.connect('search-find', 'clicked', self.search_action, 'search')
self.connect('search-close', 'clicked', self.search_action, 'close')
self.connect('search-text', 'activate', self.search_action, 'search')
self.connect('search-text', 'icon-press', self.search_action, 'clear')
self.connect('search-text', 'key-press-event', self.handle_search_keys, original_args=True)
# Misc
GObject.Object.connect(self, 'focus', lambda *args: self.handle_get_focus())
def setup_webview(self):
if self.webview:
self.webview.destroy()
self.webview = WebKit2.WebView(
settings = self.settings,
web_context = self.context,
#user_content_manager = self.userscripts
)
self.webview.set_property('expand', True)
self.webview.get_style_context().add_class('webview')
self.settings.apply()
self.search = self.webview.get_find_controller()
self.handler = WebviewHandler(self)
self.page_action('zoom')
self.webview.show()
self.add(self.webview)
connect(self.search, 'failed-to-find-text', self.handle_search_failed)
connect(self.search, 'found-text', self.handle_search_text_change)
def state_load(self):
if not self._data.state:
return logging.warning('No state to restore')
self.webview.restore_session_state(self._data.state.session)
self._data.state = None
self['label-favicon'].set_sensitive(True)
self.page_action('refresh')
def state_unload(self):
if not self._data.state:
self._data.state = self.get_state(True)
self.setup_webview()
self['label-favicon'].set_sensitive(False)
return self._data.state
def handle_button(self, name, **kwargs):
if name == 'go':
self.load_url(self['navbar-url'].get_text())
self.webview.grab_focus()
def handle_editing_action_check(self, webview, raw_result, data):
result = self.webview.can_execute_editing_command_finish(raw_result)
data['callback'](*data['args'], **data['kwargs'])
def handle_get_focus(self):
self.webview.grab_focus()
self['navbar-url'].select_region(0,0)
def handle_load_url(self, raw_url):
with self.db.session as s:
if Path(raw_url).exists():
return run_in_gui_thread(self.webview.load_uri, Url(f'local://{raw_url}'))
url = Url(raw_url)
# Can't register the ftp protocol, so use an alternative scheme name
if url.proto == 'ftp':
logging.verbose('ftp url. Redirecting to filetp instead')
url = url.replace_property('proto', 'filetp')
# Can't register the file protocol either
elif url.proto == 'file':
logging.verbose('file url. Redirecting to local instead')
url = url.replace_property('proto', 'local')
elif not url.proto:
## Check the first word is a search keyword
try:
keyword, data = raw_url.split(' ', 1)
except ValueError:
keyword, data = None, raw_url
if not keyword:
try:
Address(resolve_address(url.domain))
proto = 'https' if s.get_config('https_force') else 'http'
url = Url(f'{proto}://{url}')
logging.verbose('Url without protocol')
except (NXDOMAIN, NoAnswer, TypeError):
return run_in_gui_thread(
self.webview.load_uri,
url.replace_properties(proto='https')
)
if not (search := s.get_search(keyword, default=False)):
search = s.get_search()
data = raw_url
logging.verbose('Keyword search:', search.keyword)
url = search.compile(data)
## this is broken :/
# try:
# url.top
# except ValueError:
# url = url.replace_property('proto', 'http')
run_in_gui_thread(self.webview.load_uri, url)
def handle_page_view_source(self, text):
tab = self.window.tab_new(url=False, switch=True)
tab.webview.load_plain_text(BeautifulSoup(text, features='lxml').prettify())
tab.set_title(f'Source: {self.url}')
def handle_run_js(self, webview, task, callback, *args, **kwargs):
value = self.webview.run_javascript_finish(task).get_js_value().to_string()
callback(value, *args, **kwargs)
def handle_save_page(self, path):
self.webview.save_to_file(
Gio.File.new_for_path(path),
WebKit2.SaveMode.MHTML,
None,
self.handle_save_page_finish,
path
)
def handle_save_page_finish(self, webview, raw_result, path):
result = webview.save_to_file_finish(raw_result)
if result:
msg = 'Successfully saved web page'
level = 'INFO'
else:
msg = 'Failed to save webpage'
level = 'ERROR'
self.window.notification(msg, level, system=False)
def handle_search_get_selection(self, selection):
if not selection:
return
self.search_action('search', text=selection)
def handle_search_failed(self):
self.window.notification(f'Cannot find text on page: {self.search.get_search_text()}')
def handle_search_keys(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
self.search_action('close')
return self.webview.grab_focus()
def handle_search_text_change(self):
self['search-text'].set_text(self.search.get_search_text())
def handle_url_keys(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
widget.select_region(0,0)
widget.set_text(self.url or '')
return self.webview.grab_focus()
elif event.keyval in [Gdk.KEY_Return, Gdk.KEY_KP_Enter]:
return self.handle_button('go')
with self.app.db.session as s:
if not s.get_config('enable_autocomplete'):
return
if not self.autocomplete_timer:
self.autocomplete_timer = AutocompleteTimeout()
else:
self.autocomplete_timer.refresh()
def handle_url_popup(self, entry, menu):
if not self.app.clipboard.wait_is_text_available():
return
item = Gtk.MenuItem.new_with_label('Paste and go')
item.connect('activate', self.handle_url_popup_go)
item.show()
menu.insert(item, 3)
return menu
def handle_url_popup_go(self, item):
self['navbar-url'].set_text(self.app.clipboard.wait_for_text())
self.handle_button('go')