607 lines
16 KiB
Python
607 lines
16 KiB
Python
|
|
#!/usr/bin/env python3
|
|
import json, os, signal, sys, time
|
|
|
|
from threading import Thread
|
|
from os.path import join, isfile, isdir, basename, splitext
|
|
from collections import namedtuple, OrderedDict
|
|
from time import sleep
|
|
|
|
from IzzyLib import logging
|
|
from IzzyLib.misc import DotDict
|
|
from urllib.parse import urlparse, unquote
|
|
from uuid import uuid4
|
|
from bs4 import BeautifulSoup
|
|
from gi.repository import Gtk, Gdk, Notify, WebKit2, GdkPixbuf, Gio, GLib, GObject
|
|
|
|
from .. import handlers, database, __version__ as version, args as arguments
|
|
from ..config import var, dirs, files, webkitconfig
|
|
from ..functions import SetProcName, Notif
|
|
from ..database import db
|
|
from ..dbus import Provider, Client
|
|
|
|
from . import dialog, components, tabs
|
|
|
|
|
|
app = None
|
|
gui = None
|
|
homepage = f'pyweb://' #need to make an actual page for new tabs
|
|
|
|
#print(GObject.signal_list_names(WebKit2.WebView()))
|
|
|
|
class Application(Gtk.Application):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, application_id="xyz.barkshark.pyweb", **kwargs)
|
|
self.window = None
|
|
self.clipboard = Gtk.Clipboard.get_default(Gdk.Display.get_default())
|
|
|
|
|
|
def do_activate(self):
|
|
global gui
|
|
|
|
if not self.window:
|
|
self.window = BrowserWindow(application=self, title="Barkshark Web")
|
|
|
|
logging.verbose('Exporting D-Bus objects')
|
|
self.dbus_provider = Provider(self.window)
|
|
|
|
gui = self.window
|
|
|
|
self._handle_startup()
|
|
self.window.show_all()
|
|
self.window.present()
|
|
|
|
|
|
|
|
def do_startup(self):
|
|
Gtk.Application.do_startup(self)
|
|
|
|
accel = Gio.SimpleAction.new('accel', GLib.VariantType.new('s'))
|
|
accel.connect('activate', self._handle_accel)
|
|
self.add_action(accel)
|
|
|
|
keybinds = [
|
|
#('paste', ['<Ctrl>V', '<Shift>Insert']),
|
|
('toggle-fullscreen', ['F11']),
|
|
('new-web-tab', ['<Ctrl>T']),
|
|
('close-tab', ['<Ctrl>W']),
|
|
('new-bookmark', ['<Ctrl>B']),
|
|
('reload', ['F5', '<Ctrl>R']),
|
|
('force-reload', ['<Shift>F5', '<Ctrl><Shift>R']),
|
|
('focus-urlbar', ['<Ctrl>L'])
|
|
]
|
|
|
|
for action, keybind in keybinds:
|
|
self.set_accels_for_action(f'app.accel::{action}', keybind)
|
|
|
|
|
|
def quit(self, *args):
|
|
if self.window:
|
|
self.window._window_close()
|
|
|
|
super().quit()
|
|
|
|
|
|
def _handle_accel(self, signal, action):
|
|
window = self.get_active_window()
|
|
action = action.get_string()
|
|
tab = self.window.tabs.get_nth_page(self.window.tabs.get_current_page())
|
|
|
|
if action == 'toggle-fullscreen':
|
|
handlers.button.ToggleFullscreen(window)
|
|
|
|
elif action == 'paste':
|
|
widget = window.get_focus()
|
|
|
|
try:
|
|
widget.paste_clipboard()
|
|
except:
|
|
pass
|
|
|
|
#if type(widget) == WebKit2.WebView:
|
|
#return True
|
|
#pasted_text = self.clipboard.wait_for_rich_text()
|
|
|
|
#else:
|
|
#try:
|
|
#widget.paste_clipboard()
|
|
#except:
|
|
#pass
|
|
|
|
elif action == 'new-web-tab':
|
|
self.window.NewWebTab()
|
|
|
|
elif action == 'close-tab':
|
|
self.window.CloseTab()
|
|
|
|
elif action == 'new-bookmark':
|
|
'double heck'
|
|
|
|
elif action == 'reload':
|
|
tab.webview.reload()
|
|
|
|
elif action == 'force-reload':
|
|
tab.webview.reload_bypass_cache()
|
|
|
|
elif action == 'focus-urlbar':
|
|
tab.objects.urlbar.grab_focus()
|
|
|
|
def _handle_startup(self):
|
|
for row in db.fetch('tabs', single=False, orderby='order'):
|
|
self.window.NewWebTab(row=row)
|
|
|
|
if len(arguments.urls) > 0:
|
|
for url in arguments.urls:
|
|
self.window.NewWebTab(url, False)
|
|
|
|
if len(self.window.tabdata.keys()) < 1:
|
|
self.window.NewWebTab(homepage, False)
|
|
|
|
return False
|
|
|
|
|
|
class BrowserWindow(Gtk.ApplicationWindow):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.overlay = components.Overlay()
|
|
self.add(self.overlay)
|
|
self.set_resizable(True)
|
|
|
|
with db.session(False) as s:
|
|
size = s.get.config('size')
|
|
|
|
if s.get.config('maximized'):
|
|
self.maximize()
|
|
|
|
self.set_default_size(size.width, size.height)
|
|
self.is_fullscreen = s.get.config('fullscreen')
|
|
self.tooltips = s.get.config('tooltips')
|
|
|
|
self.set_position(Gtk.WindowPosition.CENTER)
|
|
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
|
self.iconsize = Gtk.IconSize.BUTTON
|
|
|
|
self.app = Gio.Application.get_default
|
|
|
|
self.tabs = Gtk.Notebook(show_tabs=True)
|
|
self.tabs.set_tab_pos(Gtk.PositionType(2))
|
|
self.tabs.set_scrollable(True)
|
|
|
|
## This listens to double-clicks anywhere in the notebook. Not sure how to fix that yet
|
|
#self.tabs.connect('button-press-event', self._notebook_click)
|
|
|
|
newtab = Gtk.Button.new_from_icon_name('document-new', self.iconsize)
|
|
newtab.set_tooltip_text('New Tab')
|
|
newtab.connect('clicked', self._handle_new_tab)
|
|
newtab.show()
|
|
|
|
self.tabs.set_action_widget(newtab, Gtk.PackType.END)
|
|
|
|
self.tabdata = {}
|
|
self.lasttab = 0
|
|
|
|
self.overlay.add(self.tabs)
|
|
|
|
## Setup storage
|
|
self.storage = WebKit2.WebsiteDataManager(
|
|
base_data_directory = dirs.storage.str(),
|
|
base_cache_directory = dirs.cache.str()
|
|
)
|
|
|
|
## Setup browser settings
|
|
self.context = WebKit2.WebContext.new_with_website_data_manager(self.storage)
|
|
self.context.set_spell_checking_enabled(True)
|
|
self.context.set_favicon_database_directory(dirs.favicon.str())
|
|
#self.context.set_web_extensions_directory(dirs.extensions.str())
|
|
#self.context.set_web_extensions_initialization_user_data(GLib.Variant.new_string('heck'))
|
|
|
|
## Connect context signals
|
|
self.signals = {
|
|
'download-started': lambda *args: Thread(None, handlers.signal.DownloadFile, args=[self]+list(args)).start(),
|
|
'initialize-web-extensions': self._web_ext_init
|
|
}
|
|
|
|
for signal, handler in self.signals.items():
|
|
self.context.connect(signal, handler)
|
|
|
|
self.connect('window-state-event', self._new_window_state)
|
|
self.connect('delete-event', self._window_close)
|
|
#self.connect('configure-event', self._window_size_change)
|
|
|
|
## Setup cache
|
|
self.context.set_cache_model(WebKit2.CacheModel(1))
|
|
|
|
## Setup Cookie storage
|
|
self.cookiemanager = self.context.get_cookie_manager()
|
|
self.cookiemanager.set_persistent_storage(files.cookies.str(), WebKit2.CookiePersistentStorage(1))
|
|
self.cookiemanager.set_accept_policy(WebKit2.CookieAcceptPolicy(2))
|
|
|
|
## Register local uri schemes
|
|
self.security_manager = self.context.get_security_manager()
|
|
self.security_manager.register_uri_scheme_as_local(var.local)
|
|
self.security_manager.register_uri_scheme_as_local('local://')
|
|
|
|
## Setup custom protocols
|
|
self.context.register_uri_scheme(var.local[:-3], handlers.protocol.Local)
|
|
self.context.register_uri_scheme('sftp', handlers.protocol.Sftp)
|
|
self.context.register_uri_scheme('source', handlers.protocol.Source)
|
|
|
|
## Webkit won't let me set these normally
|
|
self.context.register_uri_scheme('filetp', handlers.protocol.Ftp)
|
|
self.context.register_uri_scheme('local', handlers.protocol.File)
|
|
|
|
|
|
def _handle_buttons(self, *arguments, user_data=None, **kwargs):
|
|
if not signal:
|
|
logging.error('Failed to specify a signal')
|
|
return
|
|
|
|
handler = self.signals.get(user_data)
|
|
|
|
if not handler:
|
|
logging.error(f'Invalid handler: {user_data}')
|
|
return
|
|
|
|
|
|
def _handle_new_tab(self, button):
|
|
self.NewWebTab()
|
|
|
|
|
|
def _web_ext_init(self, context):
|
|
context.set_web_extensions_directory(dirs.extensions.str())
|
|
|
|
|
|
def _notebook_click(self, notebook, event):
|
|
if event.type == Gdk.EventType._2BUTTON_PRESS:
|
|
self.NewWebTab()
|
|
|
|
|
|
def _new_window_state(self, window, state):
|
|
self.is_fullscreen = bool(Gdk.WindowState.FULLSCREEN & state.new_window_state)
|
|
|
|
|
|
def _window_close(self, *args):
|
|
## save tab urls and titles on close to reopen them on startup
|
|
logging.debug('Saving data')
|
|
|
|
with db.session() as s:
|
|
active_tab = self.tabs.get_nth_page(self.tabs.get_current_page())
|
|
|
|
for tabid, tab in self.tabdata.items():
|
|
index = self.tabs.page_num(tab.widget)
|
|
webview = tab.contents.webview
|
|
|
|
if not webview:
|
|
continue
|
|
|
|
state = webview.get_session_state().serialize()
|
|
active = active_tab == tab.widget
|
|
tabrow = s.fetch('tabs', tabid=tabid)
|
|
data = {
|
|
'state': state.get_data(),
|
|
'active': active,
|
|
'order': index
|
|
}
|
|
|
|
if not tabrow:
|
|
s.insert('tabs', tabid=tabid, **data)
|
|
|
|
else:
|
|
s.update(row=tabrow, **data)
|
|
|
|
for row in s.fetch('tabs', single=False):
|
|
if row.tabid not in self.tabdata:
|
|
s.remove('tabs', rowid=row.id)
|
|
|
|
s.put.config('maximized', self.is_maximized())
|
|
|
|
if not self.is_maximized():
|
|
size = self.get_size()
|
|
s.put.config('size', {'width': size.width, 'height': size.height})
|
|
|
|
s.commit()
|
|
|
|
logging.debug('Finished saving data')
|
|
|
|
|
|
# probably won't need this, but keeping it here anyway just in case
|
|
def _window_size_change(self, window, event):
|
|
screen = window.get_screen()
|
|
scwidth, scheight = (screen.width(), screen.height())
|
|
width, height = window.get_size()
|
|
|
|
print(width, height)
|
|
|
|
if width > scwidth:
|
|
print('Setting width to reasonable size')
|
|
window.set_size_request(scwidth, height)
|
|
|
|
if height > scheight:
|
|
print('Setting height to reasonable size')
|
|
window.set_size_request(width, scheight)
|
|
|
|
|
|
def NewWebTab(self, url=homepage, row=None, switch=True):
|
|
if row:
|
|
currtab = row.tabid
|
|
|
|
else:
|
|
# keep this until I decided to break compat with older python versions
|
|
if sys.version_info < (3, 8, 0):
|
|
currtab = None
|
|
|
|
while not currtab or currtab == self.tabdata.get(currtab):
|
|
logging.verbose('Generating new uuid for tab')
|
|
currtab = uuid4().hex
|
|
|
|
else:
|
|
while (currtab := uuid4().hex) == self.tabdata.get(currtab):
|
|
'making sure other tab data is not overwritten'
|
|
print('pass', url)
|
|
|
|
self.tabdata[currtab] = DotDict({
|
|
'contents': BrowserTab(self, tabid=currtab, starturl=None if row else url),
|
|
})
|
|
|
|
page_container = self.tabdata[currtab].contents
|
|
tab_num = self.tabs.append_page(page_container, None)
|
|
|
|
self.tabdata[currtab].widget = self.tabs.get_nth_page(tab_num)
|
|
self.tabs.set_tab_reorderable(self.tabdata[currtab].widget, True)
|
|
|
|
if row:
|
|
self.tabdata[currtab].contents.LoadState(row)
|
|
switch = row.active
|
|
|
|
## Tab label
|
|
label = Gtk.Label('New Tab')
|
|
label.set_width_chars(15)
|
|
label.set_justify(Gtk.Justification.CENTER)
|
|
label.set_ellipsize(3)
|
|
|
|
## Close button
|
|
close = Gtk.Button.new_from_icon_name('window-close', Gtk.IconSize(2))
|
|
close.set_relief(Gtk.ReliefStyle(2))
|
|
close.connect('clicked', self.CloseTab, currtab)
|
|
|
|
if self.tooltips:
|
|
close.set_tooltip_text('Close')
|
|
|
|
## Put it all together
|
|
container = Gtk.Grid()
|
|
container.attach(label, 0, 0, 1, 1)
|
|
container.attach(close, 1, 0, 1, 1)
|
|
container.show_all()
|
|
|
|
self.tabs.set_tab_label(self.tabdata[currtab].widget, container)
|
|
|
|
self.tabs.show_all()
|
|
logging.verbose('New tab:', currtab, url)
|
|
|
|
if switch:
|
|
self.tabs.set_current_page(tab_num)
|
|
|
|
return page_container
|
|
|
|
|
|
def NewWidgetTab(self, widget, title):
|
|
if self.tabdata.get(title):
|
|
tab_num = self.tabs.page_num(self.tabdata[title].contents)
|
|
self.tabs.set_current_page(tab_num)
|
|
return
|
|
|
|
self.tabdata[title] = DotDict({
|
|
'contents': widget(self),
|
|
})
|
|
|
|
page_container = self.tabdata[title].contents
|
|
tab_num = self.tabs.append_page(page_container, None)
|
|
|
|
self.tabdata[title].widget = self.tabs.get_nth_page(tab_num)
|
|
self.tabs.set_tab_reorderable(self.tabdata[title].widget, True)
|
|
|
|
## Tab label
|
|
label = Gtk.Label(self.tabdata[title].contents.title)
|
|
label.set_width_chars(15)
|
|
label.set_justify(Gtk.Justification.CENTER)
|
|
label.set_ellipsize(3)
|
|
|
|
## Close button
|
|
close = Gtk.Button.new_from_icon_name('window-close', Gtk.IconSize(2))
|
|
close.set_relief(Gtk.ReliefStyle(2))
|
|
close.connect('clicked', self.CloseTab, title)
|
|
|
|
if self.tooltips:
|
|
close.set_tooltip_text('Close')
|
|
|
|
## Put it all together
|
|
container = Gtk.Grid()
|
|
container.attach(label, 0, 0, 1, 1)
|
|
container.attach(close, 1, 0, 1, 1)
|
|
container.show_all()
|
|
|
|
self.tabs.set_tab_label(self.tabdata[title].widget, container)
|
|
|
|
self.tabs.show_all()
|
|
self.tabs.set_current_page(tab_num)
|
|
|
|
|
|
def CloseTab(self, widget=None, tabid=None):
|
|
tabid = tabid if tabid else self.tabs.get_current_page()
|
|
tab = self.tabdata[tabid]
|
|
tabnum = tabid if type(tabid) == int else self.tabs.page_num(self.tabdata[tabid].widget)
|
|
|
|
if tabnum == -1:
|
|
logging.error(f'Invalid page id: {tabnum}')
|
|
return
|
|
|
|
logging.verbose(f'Closing tab with id: {tabnum}, {tabid}')
|
|
|
|
self.tabs.remove_page(tabnum)
|
|
|
|
try:
|
|
tab.contents.webview.destroy()
|
|
|
|
except Exception as e:
|
|
'heck'
|
|
|
|
del self.tabdata[tabid]
|
|
|
|
if self.tabs.get_current_page() == -1:
|
|
for tab in self.tabdata.keys():
|
|
del self.tabdata[tab]
|
|
|
|
self.NewWebTab()
|
|
|
|
|
|
class BrowserTab(Gtk.Box):
|
|
def __init__(self, window, starturl='https://ddg.gg', tabid=None):
|
|
super().__init__(self,
|
|
orientation='vertical',
|
|
homogeneous=False
|
|
)
|
|
|
|
self.starturl = starturl
|
|
self.homepage = homepage
|
|
self.tooltips = True
|
|
self.iconsize = window.iconsize
|
|
self.tabid = tabid
|
|
|
|
self.ui = DotDict()
|
|
self.window = window
|
|
self.context = window.context
|
|
|
|
## Create WebView
|
|
self.webview = WebKit2.WebView.new_with_context(self.context)
|
|
self.webview.set_property('expand', True)
|
|
|
|
self.ui.navbar = components.NavBar(app, self)
|
|
self.ui.status = components.StatusBar(app, self)
|
|
|
|
#self.ui.scrolled = Gtk.ScrolledWindow()
|
|
#self.ui.scrolled.set_property('expand', True)
|
|
#self.ui.scrolled.set_overlay_scrolling(True)
|
|
#self.ui.scrolled.add(self.webview)
|
|
|
|
## Put it all together
|
|
self.add(self.ui.navbar)
|
|
self.add(self.webview)
|
|
self.add(self.ui.status)
|
|
|
|
self.settings = self.webview.get_settings()
|
|
|
|
for k, v in webkitconfig['Browser'].items():
|
|
if k == 'hardware-acceleration-policy':
|
|
v = WebKit2.HardwareAccelerationPolicy(v)
|
|
|
|
self.settings.set_property(k, v)
|
|
|
|
self.settings.set_user_agent_with_application_details('pyWeb', version)
|
|
|
|
self.signals = {
|
|
'close': handlers.signal.CloseTab,
|
|
'context-menu': handlers.signal.ContextMenu,
|
|
'create': handlers.signal.NewWindow,
|
|
'enter-fullscreen': handlers.signal.Fullscreen,
|
|
'insecure-content-detected': handlers.signal.InsecureContent,
|
|
'leave-fullscreen': handlers.signal.Fullscreen,
|
|
'load-changed': handlers.signal.LoadChanged,
|
|
'load-failed-with-tls-errors': handlers.signal.TlsError,
|
|
'mouse-target-changed': handlers.signal.MouseHover,
|
|
'resource-load-started': handlers.signal.ResourceFilter,
|
|
'script-dialog': handlers.signal.ScriptDialog,
|
|
'show-notification': handlers.signal.Notification,
|
|
#'submit-form': handlers.signal.SubmitForm,
|
|
'permission-request': handlers.signal.PermissionRequest,
|
|
'web-process-terminated': handlers.signal.TabCrash,
|
|
}
|
|
|
|
## Connect signals
|
|
for signame, sigfunc in self.signals.items():
|
|
self.SetupSignal(signame, sigfunc)
|
|
|
|
self.webview.set_settings(self.settings)
|
|
|
|
## Keeping here for compatibility reasons
|
|
self.maingui = window
|
|
|
|
if starturl:
|
|
self.LoadUrl(self.starturl)
|
|
|
|
|
|
def LoadState(self, row):
|
|
tabid = row.tabid
|
|
state = WebKit2.WebViewSessionState.new(GLib.Bytes.new(row.state))
|
|
|
|
if tabid != self.tabid:
|
|
self.window.tabdata[tabid] = self
|
|
del self.window.tabdata[self.tabid]
|
|
self.tabid = tabid
|
|
|
|
self.webview.restore_session_state(state)
|
|
self.webview.reload()
|
|
|
|
|
|
def LoadUrl(self, url):
|
|
## try to load urls without a protocol as https
|
|
try:
|
|
proto, url = url.split('://')
|
|
|
|
except ValueError:
|
|
proto = 'https'
|
|
url = url
|
|
|
|
## Can't register the ftp protocol, so why not use a custom one
|
|
if proto == 'ftp':
|
|
proto = 'filetp'
|
|
|
|
elif proto == 'file':
|
|
proto = 'local'
|
|
|
|
self.webview.load_uri(proto + '://' + url)
|
|
self.webview.grab_focus()
|
|
|
|
|
|
def SetupSignal(self, name, func):
|
|
if name == 'enter-fullscreen':
|
|
self.webview.connect(name, lambda *args: func(self, 'enter', *args))
|
|
|
|
elif name == 'exit-fullscreen':
|
|
self.webview.connect(name, lambda *args: func(self, 'exit', *args))
|
|
|
|
else:
|
|
self.webview.connect(name, lambda *args: func(self, *args))
|
|
|
|
|
|
def SetTitle(self, title):
|
|
try:
|
|
label = self.window.tabs.get_tab_label(self.window.tabdata[self.tabid].widget)
|
|
label.get_children()[1].set_label(title)
|
|
label.set_tooltip_text(title)
|
|
|
|
except (AttributeError) as e:
|
|
print(e)
|
|
|
|
|
|
def handle_close(*args):
|
|
if gui:
|
|
gui._window_close()
|
|
sys.exit()
|
|
|
|
|
|
def run(urls=None):
|
|
global app
|
|
|
|
#SetProcName()
|
|
Notify.init('PyWeb')
|
|
|
|
app = Application()
|
|
|
|
signal.signal(signal.SIGHUP, app.quit)
|
|
signal.signal(signal.SIGINT, app.quit)
|
|
signal.signal(signal.SIGQUIT, app.quit)
|
|
signal.signal(signal.SIGTERM, app.quit)
|
|
|
|
app.run()
|