564 lines
13 KiB
Python
564 lines
13 KiB
Python
import cairo
|
|
import threading
|
|
|
|
from bs4 import BeautifulSoup
|
|
from izzylib.exceptions import DNSResolverError
|
|
|
|
from .web_tab_settings import WebSettings
|
|
from .web_tab_webview_handler import WebviewHandler
|
|
|
|
from ..enums import EditAction, Javascript
|
|
from ..objects import WebviewState
|
|
from ..widgets import Box, FileChooser
|
|
from ..functions import (
|
|
BuilderBase,
|
|
connect,
|
|
run_in_gui_thread,
|
|
set_image,
|
|
surface_to_pixbuf
|
|
)
|
|
|
|
|
|
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()
|
|
|
|
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 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 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()
|
|
|
|
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)
|
|
|
|
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 = False
|
|
|
|
text.set_text('')
|
|
|
|
elif action == 'toggle':
|
|
self.search_action('close' if search.get_visible() else 'open')
|
|
|
|
elif action == 'search':
|
|
if self._data.search:
|
|
return
|
|
|
|
if kwargs.get('text'):
|
|
search_text = kwargs.get('text')
|
|
|
|
else:
|
|
search_text = text.get_text()
|
|
|
|
options = WebKit2.FindOptions.WRAP_AROUND
|
|
|
|
if kwargs.get('insensitive'):
|
|
options += WebKit2.FindOptions.CASE_INSENSITIVE
|
|
|
|
self.search.search(search_text, options, int(kwargs.get('limit', 1000)))
|
|
self._data.search = True
|
|
|
|
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):
|
|
#self['navbar-url'].set_text(self.url)
|
|
#self['navbar-stop'].set_sensitive(not self.idle)
|
|
#self['navbar-refresh'].set_sensitive(self.idle)
|
|
|
|
for button, action in {'navbar-prev': self.webview.can_go_back(), 'navbar-next': self.webview.can_go_forward()}.items():
|
|
self[button].set_sensitive(action)
|
|
|
|
if self.window.active_tab == self:
|
|
self.window.set_button_state(self.id)
|
|
|
|
|
|
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 = True
|
|
|
|
|
|
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)
|
|
|
|
## 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.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())
|
|
|
|
|
|
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, full_url):
|
|
keyword = None
|
|
address = None
|
|
|
|
if Path(full_url).exists():
|
|
full_url = f'local://{full_url}'
|
|
|
|
full_url = Url(full_url)
|
|
|
|
if not full_url.domain and full_url.path and not full_url.proto:
|
|
try:
|
|
url = Url('https://' + full_url)
|
|
except ValueError:
|
|
self.window.notification(f'Not a valid url: {full_url}')
|
|
return
|
|
|
|
else:
|
|
url = Url(full_url)
|
|
|
|
# Can't register the ftp protocol, so use an alternative scheme name
|
|
if full_url.proto == 'ftp':
|
|
logging.verbose('ftp url. Redirecting to filetp instead')
|
|
url = full_url.replace_property('proto', 'filetp')
|
|
|
|
# Can't register the file protocol either
|
|
elif full_url.proto == 'file':
|
|
logging.verbose('file url. Redirecting to local instead')
|
|
url = full_url.replace_property('proto', 'local')
|
|
|
|
elif not full_url.proto:
|
|
with self.db.session as s:
|
|
## Check the first word is a search keyword
|
|
try:
|
|
keyword, data = full_url.split(' ', 1)
|
|
|
|
except ValueError:
|
|
keyword, data = None, full_url
|
|
|
|
## Is the url.domain an actual domain?
|
|
if not keyword:
|
|
try:
|
|
address = url.resolve_hostname(1)
|
|
|
|
logging.verbose('Url without protocol')
|
|
|
|
if address.is_type('private'):
|
|
url = url.replace_properties(
|
|
proto = 'http',
|
|
port = 80 if url.port == 443 else url.port
|
|
)
|
|
|
|
except DNSResolverError:
|
|
pass
|
|
|
|
if not address:
|
|
search = s.get_search(keyword, default=True)
|
|
logging.verbose('Keyword search:', search.keyword)
|
|
url = search.compile(full_url)
|
|
|
|
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_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')
|