rework tabs

This commit is contained in:
Izalia Mae 2022-08-05 20:04:12 -04:00
parent 5baadc9258
commit 3dd8f17c71
24 changed files with 2350 additions and 1445 deletions

View file

@ -1,7 +1,7 @@
__software__ = 'Barkshark Web'
__shortname__ = 'bsweb'
__author__ = 'Zoey Mae'
__version__ = '0.3.8'
__version__ = '0.4.0'
import os, sys, gi, izzylib

View file

@ -6,50 +6,45 @@ from izzylib_http_async import Template
from izzylib_sql import Database
from urllib.parse import quote
from .web_context import WebContext
from .window import Window
from .. import dbus, var
from .. import __version__ as version, __software__ as swname
from ..cookies import get_cookie_db
from ..database import get_database
from ..enums import LibraryPage
from ..exceptions import AccountNotFoundError, NoAccountsError
from ..functions import connect
class Application(Gtk.Application):
library_pages = (
'bookmarks', 'downloads', 'history', 'passwords',
'search', 'fediverse', 'extensions', 'preferences',
'help'
)
def __init__(self, profile, *urls):
super().__init__()
self.profile = profile
self.path = None
self.cfg = None
self.template = None
self.db = None
self.cookies = None
self.setup_paths(profile)
self.setup_database(profile)
self.setup_misc(profile)
self.window = None
self.http_client = HttpClient()
self.clipboard = Gtk.Clipboard.get_default(Gdk.Display.get_default())
self.startup_urls = urls
self.accounts = []
self.connect('activate', self.handle_activate)
self.connect('startup', self.handle_startup)
self.path = None
self.cfg = None
self.template = None
self.db = None
self.cookies = None
self.context = None
self.window = None
self.setup(profile)
connect(self, 'activate', self.handle_activate, urls)
connect(self, 'startup', self.handle_startup)
signal_handler(self.quit)
def setup_paths(self, profile):
def setup(self, profile):
script = Path(__file__).parent.parent
data = Path.app_data_dir('config', 'barkshark', 'web')
profilepath = data.join(profile or 'DEFAULT')
@ -86,8 +81,6 @@ class Application(Gtk.Application):
logging.verbose('Create dir:', path)
path.mkdir()
def setup_database(self, profile):
self.cfg = JsonConfig(self.path.config,
name = self.profile,
description = '',
@ -107,10 +100,7 @@ class Application(Gtk.Application):
self.db = get_database(self)
self.cookies = get_cookie_db(self)
def setup_misc(self, profile):
proto = 'pyweb'
self.context = WebContext(self)
proxy_resolver = Gio.ProxyResolver.get_default()
proxies = proxy_resolver.lookup('https://www.barkshark.xyz', None)
@ -136,20 +126,6 @@ class Application(Gtk.Application):
})
def create_tabs(self):
with self.db.session as s:
for row in s.fetch('tabs', orderby='order'):
self.window.new_tab(row=row)
for url in self.startup_urls:
self.window.new_tab(url, switch=True)
if len(self.window.tabdata.keys()) < 1:
self.window.new_tab(None, switch=False)
self.window.startup = False
def get_account_by_handle(self, username, domain=None):
if not len(self.accounts):
raise NoAccountsError('No accounts')
@ -204,14 +180,29 @@ class Application(Gtk.Application):
super().quit()
def handle_activate(self, *args):
def handle_activate(self, urls):
self.window = Window(self)
self.dbus = dbus.Server(self.window)
self.add_window(self.window)
self.create_tabs()
self.http_client.set_useragent(list(self.window.tabdata.values())[0].webview.get_settings().get_user_agent())
with self.db.session as s:
self.accounts = s.fetch('accounts').all()
for row in s.fetch('tabs', orderby='order'):
self.window.tab_new(row=row, switch=row.active)
for url in urls:
self.window.tab_new(url, switch=True)
if len(self.window.tabdata.keys()) < 1:
self.window.tab_new(None, switch=False)
tab = list(self.window.tabdata.values())[0]
self.http_client.set_useragent(tab.settings['user-agent'])
self.window.show()
self.window.startup = False
def handle_clipboard_clear_password(self, password):
@ -230,30 +221,30 @@ class Application(Gtk.Application):
self.keybinds = {
'Close current tab': ('close_tab', ['<Ctrl>W']),
'Focus url bar': ('focus_urlbar', ['<Ctrl>L']),
'Force reload page': ('force_reload', ['<Ctrl><Shift>R', '<Shift>F5']),
'Force refresh page': ('force_refresh', ['<Ctrl><Shift>R', '<Shift>F5']),
'Add or edit Bookmark': ('new_bookmark', ['<Ctrl>B']),
'Open new tab': ('new_tab', ['<Ctrl>T']),
'Open file': ('open_file', ['<Ctrl>O']),
'Open help page': ('open_help', ['<Ctrl>H', 'F1']),
'Open library': ('open_library', ['<Ctrl>U']),
'Open web inspector': ('open_inspector', ['<Ctrl><Shift>I', 'F12']),
'Open page search bar': ('open_search', ['<Ctrl>F', 'F3']),
'Print page': ('print', ['<Ctrl>P']),
'Quit app': ('quit', ['<Ctrl>Q']),
'Reload page': ('reload', ['<Ctrl>R', 'F5']),
'Refresh page': ('refresh', ['<Ctrl>R', 'F5']),
'Reopen last closed tab': ('reopen', ['<Ctrl><Shift>T']),
'Save page as MHTML': ('save_page', ['<Ctrl>S']),
'Toggle fullscreen state': ('toggle_fullscreen', ['<Alt>Return', 'F11']),
'Open or close library': ('toggle_library', ['<Ctrl>U'])
'Toggle search bar': ('toggle_search', ['<Ctrl>F', 'F3'])
}
for action, keybind in self.keybinds.values():
self.set_accels_for_action(f'app.accel::{action}', keybind)
for idx, page in enumerate(self.library_pages):
self.set_accels_for_action(f'app.accel::library_page_{page}', [f'<Ctrl>{idx+1}'])
for idx, page in enumerate(LibraryPage):
if page == LibraryPage.HOME:
continue
with self.db.session as s:
self.accounts = s.fetch('accounts').all()
self.set_accels_for_action(f'app.accel::library_page_{page.value}', [f'<Ctrl>{idx}'])
def handle_template_context(self, context):
@ -268,91 +259,62 @@ class Application(Gtk.Application):
tab = self.window.active_tab
if action.startswith('library_page'):
self.handle_library_page(action, tab)
return
self.window.library_open(action.replace('library_page_', ''))
try:
func = getattr(self, f'handle_accel_{action}')
elif action == 'close_tab':
tab.close()
except AttributeError:
elif action == 'focus_urlbar':
tab['navbar-url'].grab_focus()
elif action == 'force_refresh':
tab.page_action('refresh', ignore_cache=True)
elif action == 'new_bookmark':
widget = self.window.statusbar['bookmark']
if widget.get_sensitive():
widget.activate()
elif action == 'new_tab':
self.window.tab_new(switch=True)
elif action == 'open_file':
self.window.file_open()
elif action == 'help':
self.window.library_open('help')
elif action == 'open_inspector':
tab.inspector_toggle()
elif action == 'toggle_search':
tab.search_action('toggle')
elif action == 'print':
tab.page_action('print')
elif action == 'quit':
self.quit()
elif action == 'refresh':
tab.page_action('refresh')
elif action == 'reopen':
if not len(self.window.closed):
return
last_tab = list(self.window.closed.keys())[-1]
self.window.tab_reopen(last_tab)
elif action == 'save_page':
tab.page_action('save')
elif action == 'toggle_fullscreen':
self.window.fullscreen_set()
elif action == 'open_library':
self.window.library_open()
else:
logging.error('Unhandled keyboard shortcut action:', action)
return
func(action, tab)
def handle_accel_close_tab(self, action, tab):
tab.close_tab()
def handle_accel_focus_urlbar(self, action, tab):
self.window['navbar-url'].grab_focus()
def handle_accel_force_reload(self, action, tab):
tab.page_action('reload', ignore_cache=True)
def handle_library_page(self, action, tab):
page = action.replace('library_page_', '')
self.window.open_library(page)
def handle_accel_new_bookmark(self, action, tab):
widget = self.window['statusbar-bookmark']
if widget.get_sensitive():
widget.activate()
def handle_accel_new_tab(self, action, tab):
self.window.new_tab(switch=True)
def handle_accel_open_file(self, action, tab):
self.window.open_file()
def handle_accel_open_help(self, action, tab):
self.window.open_library('help')
def handle_accel_open_inspector(self, action, tab):
tab.toggle_inspector()
def handle_accel_open_search(self, action, tab):
tab.search_toggle()
def handle_accel_print(self, action, tab):
tab.page_print()
def handle_accel_quit(self, action, tab):
self.quit()
def handle_accel_reload(self, action, tab):
tab.page_action('reload')
def handle_accel_reopen(self, action, tab):
if not len(self.window.closed.keys()):
return
last_tab = list(self.window.closed.keys())[-1]
self.window.reopen_tab(last_tab)
def handle_accel_save_page(self, action, tab):
tab.page_save()
def handle_accel_toggle_fullscreen(self, action, tab):
self.window.fullscreen_toggle()
def handle_accel_toggle_library(self, action, tab):
self.window.library_toggle()

View file

@ -1,94 +0,0 @@
from .. import var
class MenuBar:
def __init__(self, window):
self.app = window.app
self.window = window
self.connect = window.Connect
self.setup_signals()
def setup_signals(self):
signals = {
'file': {
'new_tab': {'callback': self.window.new_tab, 'kwargs': {'switch': True}},
'close_tab': {'callback': self.window.close_tab},
'open': {'callback': self.window.open_file},
'save': {'callback': self.handle_save_page},
'print': {'callback': self.handle_print_page},
'quit': {'callback': self.window.app.quit}
},
'edit': ['cut', 'copy', 'paste', 'delete', 'undo', 'redo', 'select', 'unselect', 'source', 'inspect'],
'library': ['bookmarks', 'downloads', 'history', 'passwords', 'search', 'fediverse', 'extensions', 'preferences'],
'help': {
'usage': {'callback': print, 'args': ['UvU']},
'about': {'callback': self.window.open_library, 'args': ['help']}
}
}
for cat, names in signals.items():
if cat == 'edit':
for name in names:
if name == 'inspect':
self.connect(f'menubar-edit-inspect', 'activate', self.handle_inspector_toggle)
elif name == 'source':
self.connect(f'menubar-edit-source', 'activate', self.handle_page_source)
else:
self.connect(f'menubar-edit-{name}', 'activate', self.handle_edit_action, name)
elif cat == 'library':
for name in names:
self.connect(f'menubar-library-{name}', 'activate', self.handle_library_page, name)
else:
for name, data in names.items():
widget_name = f'menubar-{cat}-{name}'
self.connect(widget_name, 'activate', data['callback'], *data.get('args', []), **data.get('kwargs', {}))
def handle_save_page(self):
self.window.active_tab.page_save()
def handle_print_page(self):
self.window.active_tab.page_print()
def handle_page_source(self):
self.window.active_tab.page_view_source()
def handle_inspector_toggle(self):
self.window.active_tab.toggle_inspector()
def handle_edit_action(self, action):
tab = self.window.active_tab
if action == 'unselect':
tab.run_js('document.getSelection().removeAllRanges()')
elif action == 'delete':
tab.run_js('document.activeElement.setRangeText("")')
else:
tab.editing_action(action)
def handle_library_page(self, name):
tab = self.window.active_tab
url = var.local + name
if tab.url.proto == var.local_proto:
return tab.load_url(url)
self.window.new_tab(url, switch=True)
def handle_about_dialog(self):
self.window['about-buttons']
self.window['about-dialog'].show()

View file

@ -1,16 +1,28 @@
from .. import cache, var
from ..database import default_permissions
from ..exceptions import AccountNotFoundError, NoAccountsError
from ..functions import SignalBlock, connect, get_buffer_text
from ..objects.login_rows import SavedLoginRow
from ..functions import ComponentBase, connect, get_buffer_text
from ..objects import SavedLoginRow
class StatusBar:
SITEOPTIONS = [
'instance',
'adblock',
'fullscreen',
'autoplay',
'dialog',
'notification',
'microphone',
'location',
'camera'
]
class StatusBar(ComponentBase):
def __init__(self, window):
self.app = window.app
self.window = window
self.db = window.app.db
ComponentBase.__init__(self, 'statusbar')
self.window = window
self.siteoptions_handler_ids = []
self.bookmark_row = None
self.theme_enabled = False
@ -24,10 +36,6 @@ class StatusBar:
self.setup()
def __getitem__(self, key):
return self.window[f'statusbar-{key}']
def bookmark_get_data(self):
data = DotDict()
@ -207,15 +215,11 @@ class StatusBar:
#self.window.active_tab.editing_action('copy')
self.window.notification('Merp!', 'INFO', 0)
with self.app.cookies.session as s:
for row in s.get_domain('cnn.com'):
print(row.host, row.name)
if self.window.themes.current:
self.window.themes.unset()
#if self.window.themes.current_theme:
#self.window.themes.UnloadTheme()
#else:
#self.window.themes.LoadTheme('test')
else:
self.window.themes.set('test')
elif name == 'bookmark':
if not tab.url:
@ -254,7 +258,7 @@ class StatusBar:
self['siteoptions-reset'].set_sensitive(True if row != None else False)
self.siteoptions_row = row
with SignalBlock(self.siteoptions_handler_ids):
with self.block_signals(*[f'siteoptions-{name}' for name in SITEOPTIONS]):
for k, v in default_permissions.items():
value = row.get(k, v)
self[f'siteoptions-{k}'].set_state(value)
@ -296,15 +300,14 @@ class StatusBar:
self['bookmark-popover'].popdown()
def handle_bookmark_category_button(self, entry, icon_pos, event, _):
if icon_pos == Gtk.EntryIconPosition.SECONDARY:
menu = self['bookmark-category-menu']
menu.popup_at_widget(
widget = self['bookmark-category'],
widget_anchor = Gdk.Gravity.NORTH_EAST,
menu_anchor = Gdk.Gravity.SOUTH_EAST,
trigger_event = None
)
def handle_bookmark_category_button(self):
menu = self['bookmark-category-menu']
menu.popup_at_widget(
widget = self['bookmark-category'],
widget_anchor = Gdk.Gravity.NORTH_EAST,
menu_anchor = Gdk.Gravity.SOUTH_EAST,
trigger_event = None
)
def handle_bookmark_set_category(self, category):
@ -360,15 +363,18 @@ class StatusBar:
cache.posts.store(self.window.active_tab.url, post)
def handle_siteoptions_switch(self, name, switch):
tab = self.window.active_tab
def handle_siteoptions_switch(self, name):
active = self[f'siteoptions-{name}'].get_active()
try:
domain = self.window.active_tab.url.domain
if not tab.webview:
except:
return
with self.db.session as s:
s.put_permission(tab.url.domain, name, switch.get_active())
s.put_permission(domain, name, active)
logging.verbose(f'siteoptions: set {name}: {active}')
self['siteoptions-reset'].set_sensitive(True)
@ -437,81 +443,39 @@ class StatusBar:
saved = self['logins-saved-list']
)
signals = {
'bookmark': [
{'signal': 'toggled', 'callback': self.handle_status_button}
],
'bookmark-save': [
{'signal': 'clicked', 'callback': self.handle_bookmark_button}
],
'bookmark-delete': [
{'signal': 'clicked', 'callback': self.handle_bookmark_button}
],
'bookmark-close': [
{'signal': 'clicked', 'callback': self.handle_bookmark_button}
],
'bookmark-category': [
{'signal': 'icon-press', 'callback': self.handle_bookmark_category_button, 'kwargs': {'original_args': True}}
],
'favorite': [
{'signal': 'clicked', 'callback': self.handle_fedi_button}
],
'boost': [
{'signal': 'clicked', 'callback': self.handle_fedi_button}
],
'reply': [
{'signal': 'clicked', 'callback': self.handle_fedi_button}
],
'debug': [
{'signal': 'clicked', 'callback': self.handle_status_button}
],
'toot': [
{'signal': 'toggled', 'callback': self.handle_status_button}
],
'siteoptions': [
{'signal': 'toggled', 'callback': self.handle_status_button}
],
'logins': [
{'signal': 'toggled', 'callback': self.handle_status_button}
],
'logins-close': [
{'signal': 'clicked', 'callback': self.handle_logins_button}
],
'logins-unsaved-clear': [
{'signal': 'clicked', 'callback': self.handle_logins_button}
],
'siteoptions-reset': [
{'signal': 'clicked', 'callback': self.handle_siteoptions_button}
],
'siteoptions-close': [
{'signal': 'clicked', 'callback': self.handle_siteoptions_button}
],
'toot-spoiler': [
{'signal': 'changed', 'callback': self.toot_update_count},
{'signal': 'activate', 'callback': self.handle_toot_spoiler_activate}
],
'toot-reset': [
{'signal': 'clicked', 'callback': self.toot_clear}
],
'toot-send': [
{'signal': 'clicked', 'callback': self.toot_send}
],
'toot-close': [
{'signal': 'clicked', 'callback': self.toot_close}
],
'toot-content': [
{'signal': 'key-press-event', 'callback': self.handle_toot_key_press, 'kwargs': {'original_args': True}}
]
}
for name, sigs in signals.items():
for sig in sigs:
full_name = f'statusbar-{name}'
short_name = name.split('-')[-1]
self.window.Connect(full_name, sig['signal'], sig['callback'], short_name, *sig.get('args', []), **sig.get('kwargs', {}))
## Status bar
self.connect('debug', 'clicked', self.handle_status_button, 'debug')
self.connect('toot', 'toggled', self.handle_status_button, 'toot')
self.connect('reply', 'clicked', self.handle_fedi_button, 'reply')
self.connect('boost', 'clicked', self.handle_fedi_button, 'boost')
self.connect('favorite', 'clicked', self.handle_fedi_button, 'favorite')
self.connect('logins', 'toggled', self.handle_status_button, 'logins')
self.connect('bookmark', 'toggled', self.handle_status_button, 'bookmark')
self.connect('siteoptions', 'toggled', self.handle_status_button, 'siteoptions')
## Toot
self.connect('toot-spoiler', 'changed', self.toot_update_count)
self.connect('toot-spoiler', 'activate', self.handle_toot_spoiler_activate)
self.connect('toot-reset', 'clicked', self.toot_clear)
self.connect('toot-send', 'clicked', self.toot_send)
self.connect('toot-close', 'clicked', self.toot_close)
self.connect('toot-content', 'key-press-event', self.handle_toot_key_press)
connect(self['toot-content'].get_buffer(), 'changed', self.toot_update_count)
## Logins
self.connect('logins-close', 'clicked', self.handle_logins_button, 'close')
self.connect('logins-unsaved-clear', 'clicked', self.handle_logins_button, 'clear')
## Bookmarks
self.connect('bookmark-save', 'clicked', self.handle_bookmark_button, 'save')
self.connect('bookmark-delete', 'clicked', self.handle_bookmark_button, 'delete')
self.connect('bookmark-close', 'clicked', self.handle_bookmark_button, 'close')
self.connect('bookmark-category', 'icon-press', self.handle_bookmark_category_button)
## Siteoptions
self.connect('siteoptions-reset', 'clicked', self.handle_siteoptions_button, 'reset')
self.connect('siteoptions-close', 'clicked', self.handle_siteoptions_button, 'close')
# extra work needs to be done to toggle webview settings
self.permissions = DotDict({
'instance': False,
@ -530,8 +494,6 @@ class StatusBar:
})
for name in self.permissions:
widget = self[f'siteoptions-{name}']
sigid = self.window.Connect(f'statusbar-siteoptions-{name}', 'state-set', self.handle_siteoptions_switch, name, widget)
self.siteoptions_handler_ids.append([sigid, widget])
self.connect(f'siteoptions-{name}', 'state-set', self.handle_siteoptions_switch, name)
self.bookmark_refresh_categories()

View file

@ -5,14 +5,13 @@ from ..widgets import FileChooser
class WebContext(WebKit2.WebContext):
def __init__(self, window):
self.app = window.app
self.window = window
def __init__(self, app):
self.app = app
## Setup storage
self.storage = WebKit2.WebsiteDataManager(
base_data_directory = self.app.path.storage,
base_cache_directory = self.app.path.cache
base_data_directory = app.path.storage,
base_cache_directory = app.path.cache
)
super().__init__(website_data_manager = self.storage)
@ -22,14 +21,14 @@ class WebContext(WebKit2.WebContext):
self.set_property('use-system-appearance-for-scrollbars', True)
#self.set_web_extensions_initialization_user_data(GLib.Variant.new_string('heck'))
self.set_spell_checking_enabled(True)
self.set_favicon_database_directory(self.app.path.favicon)
self.set_web_extensions_directory(self.app.path.script.join('bin'))
self.set_favicon_database_directory(app.path.favicon)
self.set_web_extensions_directory(app.path.script.join('bin'))
self.set_cache_model(WebKit2.CacheModel(1))
self.set_use_system_appearance_for_scrollbars(True)
## Setup Cookie storage
self.cookiemanager = self.get_cookie_manager()
self.cookiemanager.set_persistent_storage(self.app.path.cookies, WebKit2.CookiePersistentStorage(1))
self.cookiemanager.set_persistent_storage(app.path.cookies, WebKit2.CookiePersistentStorage(1))
self.cookiemanager.set_accept_policy(WebKit2.CookieAcceptPolicy(2))
## Register local uri schemes
@ -53,6 +52,11 @@ class WebContext(WebKit2.WebContext):
self.connect('user-message-received', self.extensions.handle_command)
@property
def window(self):
return self.app.window
def handle_new_download(self, context, download):
NewDownload(self, download)

View file

@ -0,0 +1,563 @@
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()
)
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')

View file

@ -317,10 +317,17 @@ class WebSettings(WebKit2.Settings):
def __getitem__(self, key):
if key == 'user-agent':
return self.get_user_agent()
return self.get_property(key)
def __setitem__(self, key, value):
if key == 'user-agent':
self.set_user_agent(value)
return
if key not in default_settings:
logging.error('Invalid setting:', key)
return

View file

@ -7,53 +7,54 @@ from urllib.parse import urlparse, quote, unquote
from ssl import SSLCertVerificationError
from .. import cache, var
from ..functions import Thread, connect, run_in_gui_thread
from ..enums import EditAction, Javascript, WebviewContextActions
from ..functions import ComponentBase, Thread, connect, run_in_gui_thread
from ..widgets import FileChooser
context_menu_actions = {
'audio_copy': WebKit2.ContextMenuAction.COPY_AUDIO_LINK_TO_CLIPBOARD,
'audio_download': WebKit2.ContextMenuAction.DOWNLOAD_AUDIO_TO_DISK,
'frame_open': WebKit2.ContextMenuAction.OPEN_FRAME_IN_NEW_WINDOW,
'audio_tab': WebKit2.ContextMenuAction.OPEN_AUDIO_IN_NEW_WINDOW,
'go_back': WebKit2.ContextMenuAction.GO_BACK,
'go_forward': WebKit2.ContextMenuAction.GO_FORWARD,
'go_reload': WebKit2.ContextMenuAction.RELOAD,
'go_stop': WebKit2.ContextMenuAction.STOP,
'image_copy': WebKit2.ContextMenuAction.COPY_IMAGE_URL_TO_CLIPBOARD,
'image_copy-full': WebKit2.ContextMenuAction.COPY_IMAGE_TO_CLIPBOARD,
'image_download': WebKit2.ContextMenuAction.DOWNLOAD_IMAGE_TO_DISK,
'image_tab': WebKit2.ContextMenuAction.OPEN_IMAGE_IN_NEW_WINDOW,
'inspect_element': WebKit2.ContextMenuAction.INSPECT_ELEMENT,
'link_copy': WebKit2.ContextMenuAction.COPY_LINK_TO_CLIPBOARD,
'link_download': WebKit2.ContextMenuAction.DOWNLOAD_LINK_TO_DISK,
'link_open': WebKit2.ContextMenuAction.OPEN_LINK,
'link_tab': WebKit2.ContextMenuAction.OPEN_LINK_IN_NEW_WINDOW,
'media_control': WebKit2.ContextMenuAction.TOGGLE_MEDIA_CONTROLS,
'media_mute': WebKit2.ContextMenuAction.MEDIA_MUTE,
'media_loop': WebKit2.ContextMenuAction.TOGGLE_MEDIA_LOOP,
'media_pause': WebKit2.ContextMenuAction.MEDIA_PAUSE,
'spell_add': WebKit2.ContextMenuAction.LEARN_SPELLING,
'media_play': WebKit2.ContextMenuAction.MEDIA_PLAY,
'spell_guess': WebKit2.ContextMenuAction.SPELLING_GUESS,
'spell_ignore-grammar': WebKit2.ContextMenuAction.IGNORE_GRAMMAR,
'spell_ignore-spell': WebKit2.ContextMenuAction.IGNORE_SPELLING,
'spell_none': WebKit2.ContextMenuAction.NO_GUESSES_FOUND,
'text_bold': WebKit2.ContextMenuAction.BOLD,
'text_copy': WebKit2.ContextMenuAction.COPY,
'text_cut': WebKit2.ContextMenuAction.CUT,
'text_delete': WebKit2.ContextMenuAction.DELETE,
'text_emoji': WebKit2.ContextMenuAction.INSERT_EMOJI,
'text_italic': WebKit2.ContextMenuAction.ITALIC,
'text_paste': WebKit2.ContextMenuAction.PASTE,
'text_paste-plain': WebKit2.ContextMenuAction.PASTE_AS_PLAIN_TEXT,
'text_select': WebKit2.ContextMenuAction.SELECT_ALL,
'text_underline': WebKit2.ContextMenuAction.UNDERLINE,
'video_copy': WebKit2.ContextMenuAction.COPY_VIDEO_LINK_TO_CLIPBOARD,
'video_download': WebKit2.ContextMenuAction.DOWNLOAD_VIDEO_TO_DISK,
'video_fullscreen': WebKit2.ContextMenuAction.ENTER_VIDEO_FULLSCREEN,
'video_tab': WebKit2.ContextMenuAction.OPEN_VIDEO_IN_NEW_WINDOW,
}
#context_menu_actions = {
#'audio_copy': WebKit2.ContextMenuAction.COPY_AUDIO_LINK_TO_CLIPBOARD,
#'audio_download': WebKit2.ContextMenuAction.DOWNLOAD_AUDIO_TO_DISK,
#'frame_open': WebKit2.ContextMenuAction.OPEN_FRAME_IN_NEW_WINDOW,
#'audio_tab': WebKit2.ContextMenuAction.OPEN_AUDIO_IN_NEW_WINDOW,
#'go_back': WebKit2.ContextMenuAction.GO_BACK,
#'go_forward': WebKit2.ContextMenuAction.GO_FORWARD,
#'go_reload': WebKit2.ContextMenuAction.RELOAD,
#'go_stop': WebKit2.ContextMenuAction.STOP,
#'image_copy': WebKit2.ContextMenuAction.COPY_IMAGE_URL_TO_CLIPBOARD,
#'image_copy-full': WebKit2.ContextMenuAction.COPY_IMAGE_TO_CLIPBOARD,
#'image_download': WebKit2.ContextMenuAction.DOWNLOAD_IMAGE_TO_DISK,
#'image_tab': WebKit2.ContextMenuAction.OPEN_IMAGE_IN_NEW_WINDOW,
#'inspect_element': WebKit2.ContextMenuAction.INSPECT_ELEMENT,
#'link_copy': WebKit2.ContextMenuAction.COPY_LINK_TO_CLIPBOARD,
#'link_download': WebKit2.ContextMenuAction.DOWNLOAD_LINK_TO_DISK,
#'link_open': WebKit2.ContextMenuAction.OPEN_LINK,
#'link_tab': WebKit2.ContextMenuAction.OPEN_LINK_IN_NEW_WINDOW,
#'media_control': WebKit2.ContextMenuAction.TOGGLE_MEDIA_CONTROLS,
#'media_mute': WebKit2.ContextMenuAction.MEDIA_MUTE,
#'media_loop': WebKit2.ContextMenuAction.TOGGLE_MEDIA_LOOP,
#'media_pause': WebKit2.ContextMenuAction.MEDIA_PAUSE,
#'spell_add': WebKit2.ContextMenuAction.LEARN_SPELLING,
#'media_play': WebKit2.ContextMenuAction.MEDIA_PLAY,
#'spell_guess': WebKit2.ContextMenuAction.SPELLING_GUESS,
#'spell_ignore-grammar': WebKit2.ContextMenuAction.IGNORE_GRAMMAR,
#'spell_ignore-spell': WebKit2.ContextMenuAction.IGNORE_SPELLING,
#'spell_none': WebKit2.ContextMenuAction.NO_GUESSES_FOUND,
#'text_bold': WebKit2.ContextMenuAction.BOLD,
#'text_copy': WebKit2.ContextMenuAction.COPY,
#'text_cut': WebKit2.ContextMenuAction.CUT,
#'text_delete': WebKit2.ContextMenuAction.DELETE,
#'text_emoji': WebKit2.ContextMenuAction.INSERT_EMOJI,
#'text_italic': WebKit2.ContextMenuAction.ITALIC,
#'text_paste': WebKit2.ContextMenuAction.PASTE,
#'text_paste-plain': WebKit2.ContextMenuAction.PASTE_AS_PLAIN_TEXT,
#'text_select': WebKit2.ContextMenuAction.SELECT_ALL,
#'text_underline': WebKit2.ContextMenuAction.UNDERLINE,
#'video_copy': WebKit2.ContextMenuAction.COPY_VIDEO_LINK_TO_CLIPBOARD,
#'video_download': WebKit2.ContextMenuAction.DOWNLOAD_VIDEO_TO_DISK,
#'video_fullscreen': WebKit2.ContextMenuAction.ENTER_VIDEO_FULLSCREEN,
#'video_tab': WebKit2.ContextMenuAction.OPEN_VIDEO_IN_NEW_WINDOW,
#}
cert_error_msg = {
'unknown-ca': 'Unknown certificate authority for url: {url}',
@ -61,121 +62,139 @@ cert_error_msg = {
}
class WebviewHandler:
class WebviewHandler(ComponentBase):
def __init__(self, tab):
self.app = tab.window.app
self.window = tab.window
ComponentBase.__init__(self)
self.tab = tab
self.webview = tab.webview
self.inspector = tab.webview.get_inspector()
self.inspector_open = False
signals = {
'close': {'handler': self.tab.close_tab},
'close': {'handler': self.tab.close},
'context-menu': {'handler': self.handle_context_menu},
'create': {'handler': self.handle_new_window},
'decide-policy': {'handler': self.handle_decide_policy},
'enter-fullscreen': {'handler': self.handle_fullscreen, 'args': ['enter']},
'insecure-content-detected': {'handler': self.handle_insecure_content},
#'insecure-content-detected': {'handler': self.handle_insecure_content},
'leave-fullscreen': {'handler': self.handle_fullscreen, 'args': ['exit']},
'load-changed': {'handler': self.handle_load_changed},
'load-failed-with-tls-errors': {'handler': self.handle_tls_error},
'mouse-target-changed': {'handler': self.handle_mouse_hover},
'resource-load-started': {'handler': self.handle_resource_load},
'script-dialog': {'handler': self.handle_script_dialog},
'show-notification': {'handler': self.handle_notification},
#'show-notification': {'handler': self.handle_notification},
#'submit-form': {'handler': self.handle_submit_form},
'permission-request': {'handler': self.handle_permission_request},
'web-process-terminated': {'handler': self.handle_tab_crashed},
'notify::estimated-load-progress': {'handler': self.handle_load_progress},
'notify::favicon': {'handler': self.handle_set_favicon},
'notify::title': {'handler': self.handle_set_title},
'notify::uri': {'handler': self.handle_set_url}
'notify::favicon': {'handler': self.tab.set_favicon, 'kwargs': {'original_args': False}},
'notify::title': {'handler': self.tab.set_title, 'kwargs': {'original_args': False}},
'notify::uri': {'handler': self.tab.set_url, 'kwargs': {'original_args': False}}
}
for signal, data in signals.items():
connect(self.webview, signal, data['handler'], *data.get('args', ''), **data.get('kwargs', {}), original_args=True)
if data.get('kwargs', {}).get('original_args') == None:
data['kwargs'] = {'original_args': True}
connect(self.webview, signal, data['handler'], *data.get('args', ''), **data['kwargs'])
connect(self.inspector, 'attach', self.handle_inspector, 'attach', original_args=True)
connect(self.inspector, 'detach', self.handle_inspector, 'detach', original_args=True)
connect(self.inspector, 'closed', self.handle_inspector, 'close', original_args=True)
@property
def inspector(self):
return self.webview.get_inspector()
@property
def settings(self):
return self.tab.settings
@property
def webview(self):
return self.tab.webview
def handle_context_menu(self, webview, context_menu, event, hit):
'''
context_is_editable
context_is_image
context_is_link
context_is_media
context_is_scrollbar
context_is_selection
'''
url = webview.get_uri()
link_url = hit.get_link_uri()
link_text = hit.get_link_label()
image_url = hit.get_image_uri()
media_url = hit.get_media_uri()
menu = ContextMenuClass(context_menu, self.tab)
url = DotDict(
page = webview.get_uri(),
link = hit.get_link_uri(),
image = hit.get_image_uri(),
media = hit.get_media_uri()
)
data = DotDict(
link_text = hit.get_link_label(),
selection = hit.context_is_selection(),
editable = hit.context_is_editable(),
link = hit.context_is_link(),
media = hit.context_is_media(),
image = hit.context_is_image(),
scrollbar = hit.context_is_scrollbar()
)
selection = hit.context_is_selection()
editable = hit.context_is_editable()
link = hit.context_is_link()
media = hit.context_is_media()
image = hit.context_is_image()
for key, value in url.items():
if not value:
continue
if link:
menu.new_action('link_open', 'Open Link', self.tab.load_url, link_url)
menu.new_action('link_new', 'Open Link In New Tab', self.window.new_tab, link_url, switch=True)
menu.new_action('link_bg', 'Open Link In Background Tab', self.window.new_tab, link_url)
menu.new_stock('link_copy', 'Copy Link Url', 'link_copy')
menu.new_stock('link_dl', 'Download Link', 'link_download')
url[key] = Url(value)
if data.link:
menu.new_action('link_open', 'Open Link', self.tab.load_url, url.link)
menu.new_action('link_new', 'Open Link In New Tab', self.window.tab_new, url.link, switch=True)
menu.new_action('link_bg', 'Open Link In Background Tab', self.window.tab_new, url.link)
menu.new_stock('link_copy', 'Copy Link Url', WebviewContextActions.LINK_COPY)
menu.new_stock('link_dl', 'Download Link', WebviewContextActions.LINK_DOWNLOAD)
menu.new_sep()
if media:
menu.new_stock('media_play', 'Play', 'media_play')
menu.new_stock('media_pause', 'Pause', 'media_pause')
menu.new_stock('media_loop', 'Toggle Loop', 'media_loop')
menu.new_stock('media_ctrl', 'Toggle Controls', 'media_control')
menu.new_action('media_open', 'Open Media', self.tab.load_url, media_url)
menu.new_action('media_new', 'Open Media In New Tab', self.window.new_tab, media_url, switch=True)
menu.new_action('media_bg', 'Open Media In Background Tab', self.window.new_tab, media_url)
menu.new_action('media_dl', 'Download Media', self.window.context.download_uri, media_url)
if data.media:
menu.new_stock('media_play', 'Play', WebviewContextActions.MEDIA_PLAY)
menu.new_stock('media_pause', 'Pause', WebviewContextActions.MEDIA_PAUSE)
menu.new_stock('media_loop', 'Toggle Loop', WebviewContextActions.MEDIA_LOOP)
menu.new_stock('media_ctrl', 'Toggle Controls', WebviewContextActions.MEDIA_TOGGLE)
menu.new_action('media_open', 'Open Media', self.tab.load_url, url.media)
menu.new_action('media_new', 'Open Media In New Tab', self.window.tab_new, url.media, switch=True)
menu.new_action('media_bg', 'Open Media In Background Tab', self.window.tab_new, url.media)
menu.new_action('media_dl', 'Download Media', self.app.context.download_uri, url.media)
menu.new_sep()
if image:
menu.new_action('image_open', 'Open Image', self.tab.load_url, image_url)
menu.new_action('image_new', 'Open Image In New Tab', self.window.new_tab, image_url, switch=True)
menu.new_action('image_bg', 'Open Image In Background Tab', self.window.new_tab, image_url)
menu.new_stock('image_dl', 'Download Image', 'image_download')
menu.new_stock('image_url', 'Copy Image Url', 'image_copy')
menu.new_stock('image_copy', 'Copy Image', 'image_copy-full')
if data.image:
menu.new_action('image_open', 'Open Image', self.tab.load_url, url.image)
menu.new_action('image_new', 'Open Image In New Tab', self.window.tab_new, url.image, switch=True)
menu.new_action('image_bg', 'Open Image In Background Tab', self.window.tab_new, url.image)
menu.new_stock('image_dl', 'Download Image', WebviewContextActions.IMAGE_DOWNLOAD)
menu.new_stock('image_url', 'Copy Image Url', WebviewContextActions.IMAGE_COPY)
menu.new_stock('image_copy', 'Copy Image', WebviewContextActions.IMAGE_COPY_FULL)
menu.new_sep()
if editable:
if data.editable:
menu.new_webview_action('edit_undo', 'Undo', 'Undo')
menu.new_webview_action('edit_redo', 'Redo', 'Redo')
menu.new_stock('edit_paste', 'Paste', 'text_paste')
menu.new_stock('edit_plain', 'Paste Plain Text', 'text_paste-plain')
menu.new_stock('edit_paste', 'Paste', WebviewContextActions.TEXT_PASTE)
menu.new_stock('edit_plain', 'Paste Plain Text', WebviewContextActions.TEXT_PASTE_PLAIN)
if selection:
menu.new_stock('edit_copy', 'Copy', 'text_copy')
menu.new_stock('edit_cut', 'Cut', 'text_cut')
if data.selection:
menu.new_stock('edit_copy', 'Copy', WebviewContextActions.TEXT_COPY)
if selection and editable:
menu.new_stock('edit_delete', 'Delete', 'text_delete')
if data.selection and data.editable:
menu.new_stock('edit_cut', 'Cut', WebviewContextActions.TEXT_CUT)
menu.new_stock('edit_delete', 'Delete', WebviewContextActions.TEXT_DELETE)
menu.new_stock('edit_select', 'Select All', 'text_select')
menu.new_stock('edit_select', 'Select All', WebviewContextActions.TEXT_SELECT)
if selection or editable:
if data.selection or data.editable:
menu.new_sep()
if selection:
if data.selection:
menu.new_action(
'search',
'Search or Go',
self.tab.run_js,
self.tab.js_functions.get_selection,
Javascript.SELECTION,
self.callback_new_tab
)
@ -189,20 +208,20 @@ class WebviewHandler:
row.keyword,
row.name,
self.tab.run_js,
self.tab.js_functions.get_selection,
Javascript.SELECTION,
self.callback_search,
row.keyword
)
menu.new_sep()
if not url.startswith(var.local):
menu.new_action('print', 'Print Page', self.tab.page_print)
if url.page.proto != var.local:
menu.new_action('print', 'Print Page', self.tab.page_action, 'print')
if any(map(url.startswith, ['https://', 'http://'])):
menu.new_action('source', 'View Page Source', self.tab.page_view_source)
if url.page.proto in ['http', 'https']:
menu.new_action('source', 'View Page Source', self.tab.page_action, 'source')
menu.new_stock('inspect', 'Inspect Element', 'inspect_element')
menu.new_stock('inspect', 'Inspect Element', WebviewContextActions.INSPECT)
return False
@ -212,10 +231,12 @@ class WebviewHandler:
if decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION:
if url.proto == 'file':
decision.ignore()
webview.load_uri(url.replace_property('proto', 'local'))
return True
elif url.proto == 'ftp':
decision.ignore()
webview.load_uri(url.replace_property('proto', 'filetp'))
return True
@ -223,6 +244,7 @@ class WebviewHandler:
if url.startswith('https://pyweb-oauth'):
url = url.replace('https://pyweb-oauth', 'oauth://', 1)
webview.load_uri(url)
decision.ignore()
return True
try:
@ -241,7 +263,7 @@ class WebviewHandler:
def handle_fullscreen(self, webview, state):
domain = self.tab.url.domain
navbar = self.window['navbar']
navbar = self.tab['navbar']
statusbar = self.window['statusbar']
tabs = self.window['tabs']
@ -293,13 +315,11 @@ class WebviewHandler:
def handle_load_changed(self, webview, event):
url = self.tab.url
if not url:
if not (url := self.tab.url):
return
## Only enable the page cache for http(s) urls
self.tab.settings['enable-page-cache'] = url.proto in ['http', 'https']
self.settings.set('enable-page-cache', url.proto in ['http', 'https'])
try:
host = url.hostname()
@ -310,23 +330,22 @@ class WebviewHandler:
permissions = s.get_permission(host)
if event.value_nick == 'started':
self.tab.settings['media-playback-requires-user-gesture'] = not permissions.autoplay
self.tab._data.favicon = False
self.settings.set('media-playback-requires-user-gesture', not permissions.autoplay)
if self.window.active_tab == self.tab:
self.window['navbar-stop'].set_sensitive(True)
self.window['navbar-refresh'].set_sensitive(False)
self.tab.current_url = url
self.tab.search_close()
self.tab.idle = False
self.tab['navbar-stop'].set_sensitive(True)
self.tab['navbar-refresh'].set_sensitive(False)
self.tab.search_action('close')
elif event.value_nick == 'committed':
self.tab.set_button_state()
elif event.value_nick == 'redirected':
logging.debug(f'handle_load_changed: redirect {url}')
pass
elif event.value_nick in ['finished', 'redirected']:
self.tab.idle = True
if event.value_nick == 'finished' and isinstance(url, Url):
elif event.value_nick == 'finished':
if isinstance(url, Url):
if url.proto == 'https':
FediverseCheck(None, self.tab, permissions).start()
@ -335,7 +354,10 @@ class WebviewHandler:
s.put_history_from_tab(self.tab)
if self.window.active_tab == self.tab:
self.window.set_button_state(self.tab)
self.window.set_button_state(self.tab.id)
if not self.tab._data.favicon:
self.tab.set_favicon()
def handle_load_failed(self, webview, event, uri, error):
@ -343,9 +365,12 @@ class WebviewHandler:
def handle_load_progress(self, webview, _):
if self.window.active_tab == self.tab:
progress = webview.get_estimated_load_progress()
self.window['navbar-url'].set_progress_fraction(0 if progress == 1.0 else progress)
progress = webview.get_estimated_load_progress()
finished = True if progress == 1.0 else False
self.tab['navbar-url'].set_progress_fraction(0 if progress == 1.0 else progress)
self.tab['navbar-stop'].set_sensitive(not finished)
self.tab['navbar-refresh'].set_sensitive(finished)
def handle_mouse_hover(self, webview, hit, _):
@ -354,14 +379,29 @@ class WebviewHandler:
def handle_resource_load(self, webview, resource, request):
if logging.get_config('level') == logging.LogLevel.DEBUG:
resource.connect('failed', self.handle_resource_failed)
resource.connect('finished', self.handle_resource_finished)
resource.connect('failed', self.handle_resource_failed)
resource.connect('finished', self.handle_resource_finished)
def handle_resource_failed(self, resource, error):
try:
url = Url(resource.get_uri())
except Exception as e:
print(f'{class_name(e)}: {e}')
return
if error.domain == 'WebKitPolicyError' and 'interrupted' in error.message.lower():
if not url.mimetype:
self.window.notification(f'Cannot open url: {url}')
elif not self.webview.can_show_mime_type(url.mimetype):
self.app.context.download_uri(url)
return
if error.domain not in ['HandlerError', 'soup-http-error-quark']:
if error.domain == 'WebKitNetworkError' and error.message.lower == 'load request cancelled':
if error.domain == 'WebKitNetworkError' and error.message.lower() == 'load request cancelled':
return
logging.debug(f'''Resource failed to load: {resource.get_uri()}
@ -379,11 +419,11 @@ class WebviewHandler:
url = action.get_request().get_uri()
if not user_action:
logging.debug('handle_new_window: Not a user action')
logging.debug('handle_new_window: Not a user action: "{url}"')
return
if navtype in [WebKit2.NavigationType.FORM_SUBMITTED, WebKit2.NavigationType.LINK_CLICKED, WebKit2.NavigationType.OTHER]:
self.window.new_tab(url, switch=True)
self.window.tab_new(url, switch=True)
else:
logging.debug('handle_new_window: unhandled nav type:', navtype)
@ -414,7 +454,7 @@ class WebviewHandler:
permrequest.deny()
return
with self.app.db.session as s:
with self.db.session as s:
row = s.get_permission(domain)
if type(permrequest) == WebKit2.NotificationPermissionRequest and row.notification:
@ -439,24 +479,14 @@ class WebviewHandler:
permrequest.deny()
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_script_dialog(self, webview, dialog):
domain = self.tab.url.domain
try:
domain = self.tab.url.domain
with self.app.db.session as s:
except:
return True
with self.db.session as s:
row = s.get_permission(domain)
if not row.dialog:
@ -492,7 +522,7 @@ class WebviewHandler:
url = Url(url)
err = error.first_value_nick
with self.app.db.session as s:
with self.db.session as s:
if url.address.is_type('private') and s.get_config('allow_local_unsigned'):
self.tab.context.allow_tls_certificate_for_host(cert, url.domain)
webview.load_uri(url)
@ -504,14 +534,13 @@ class WebviewHandler:
logging.debug('handlers.signal.TlsError: Unhandled error:', err)
context = {'error_message': msg.format(url=url), 'title': 'TLS Error'}
webview.load_html(self.app.template.render('error.haml', context), url)
webview.load_html(self.app.template.render('error.haml', context))
return True
def handle_set_url(self, *_):
if self.window.active_tab == self.tab:
self.window.set_navbar_url(self.tab.url)
def handle_set_url(self, webview, url):
self.tab.set_url()
def handle_set_favicon(self, *_):
@ -577,10 +606,11 @@ class ContextMenuClass(DotDict):
def new_stock(self, name, label, action):
if action not in context_menu_actions:
logging.error('Not a valid context menu action:', action)
if not isinstance(action, WebviewContextActions):
raise TypeError(f'Webview context action must be a WebviewContextActions, not {class_name(action)}')
#action = WebviewContextActions[action.upper()]
item = WebKit2.ContextMenuItem.new_from_stock_action_with_label(context_menu_actions[action], label)
item = WebKit2.ContextMenuItem.new_from_stock_action_with_label(action.value, label)
self[name] = item
self.menu.append(item)
@ -613,11 +643,11 @@ class ContextMenuClass(DotDict):
class FediverseCheck(Thread):
def run_func(self, tab, permissions):
acct = tab.app.get_default_account()
url = tab.url
post = {}
with tab.db.session as s:
acct = s.get_account()
history_row = s.get_history(url)
is_post = history_row.post if history_row else None
nodeinfo = None
@ -658,12 +688,17 @@ class FediverseCheck(Thread):
permissions = s.put_permission(permissions.domain, 'instance', is_instance)
if s.get_config('post_check') and permissions and permissions.instance and (is_post in [None, True]):
if not (post := cache.posts.fetch(url)):
if url.path.startswith('/web'):
is_post = False
elif not (post := cache.posts.fetch(url)):
post = acct.fetch_post(url)
cache.posts.store(url, post)
is_post = True if is_post else False
is_post = True if post else False
logging.verbose(f'Result of post check for {url}: {is_post}')
s.put_history(url, tab.title, is_post)
run_in_gui_thread(tab.set_current_post, post)
tab._data.post = post

View file

@ -4,7 +4,7 @@ from bs4 import BeautifulSoup
from izzylib.exceptions import DNSResolverError
from .web_settings import WebSettings
from .web_view_handler import WebviewHandler
from .web_tab_webview_handler import WebviewHandler
from ..functions import run_in_gui_thread, surface_to_pixbuf
from ..widgets import Box, FileChooser
@ -28,15 +28,13 @@ class Webview(Gtk.Box):
self.app = window.app
self.window = window
self.db = window.app.db
self.context = window.context
#self.user_content = WebContentManager(self)
self.settings = WebSettings(self)
self.webview = None
self.tabid = tabid
self.tabid = tabid or row.tabid
self.row = row
self.iconsize = window.iconsize
self.client = None
## Initiate variables that'll get used later
self.history = {}
@ -61,6 +59,11 @@ class Webview(Gtk.Box):
self.load_url(starturl)
@property
def context(self):
self.window.context
@property
def title(self):
if self.state:
@ -490,11 +493,11 @@ class SearchBar(Box):
self.handle_search(selection)
def handle_search(self, text=None):
options = 16
def handle_search(self, text=None, insensitive=True):
options = WebKit2.FindOptions.WRAP_AROUND
if self.insensitive:
options += 1
if insensitive:
options += WebKit2.FindOptions.CASE_INSENSITIVE
self.find.search(text or self['text'].get_text(), options, 1000)

View file

@ -1,41 +1,19 @@
import json, sys, threading, time
from .menu_bar import MenuBar
from izzylib_sql import Row
from .status_bar import StatusBar
from .web_context import WebContext
from .web_view import Webview
from .web_tab import WebTab
from .. import var, __software__
from ..functions import Thread, run_in_gui_thread, connect, get_app, icon_set
from ..enums import LibraryPage
from ..functions import BuilderBase, Thread, run_in_gui_thread, connect, get_app, icon_set
from ..objects import WebviewState
from ..themes import Themes
from ..widgets import FileChooser, Menu, MenuButtonRefresh
page_widget_focus = {
'bookmarks': 'library-bookmarks-search',
'downloads': None,
'history': 'library-history-search',
'passwords': 'library-passwords-search',
'search': None,
'fediverse': 'library-fediverse-domain',
'extensions': None,
'preferences': None
}
class Window(Gtk.ApplicationWindow):
webview_editing_actions = {
'copy': WebKit2.EDITING_COMMAND_COPY,
'link': WebKit2.EDITING_COMMAND_CREATE_LINK,
'cut': WebKit2.EDITING_COMMAND_CUT,
'insert_image': WebKit2.EDITING_COMMAND_INSERT_IMAGE,
'paste': WebKit2.EDITING_COMMAND_PASTE,
'paste_plain': WebKit2.EDITING_COMMAND_PASTE_AS_PLAIN_TEXT,
'redo': WebKit2.EDITING_COMMAND_REDO,
'select': WebKit2.EDITING_COMMAND_SELECT_ALL,
'undo': WebKit2.EDITING_COMMAND_UNDO,
}
class Window(BuilderBase, Gtk.ApplicationWindow):
iconsize = Gtk.IconSize.BUTTON
is_fullscreen = False
startup = True
@ -43,7 +21,8 @@ class Window(Gtk.ApplicationWindow):
autocomplete_timer = None
def __init__(self, app):
super().__init__(application=app, title=__software__)
Gtk.ApplicationWindow.__init__(self, application=app, title=__software__)
BuilderBase.__init__(self, app.path.resources.join('main.ui'))
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self.tabdata = {}
@ -51,7 +30,6 @@ class Window(Gtk.ApplicationWindow):
self.accounts = {}
self.closed = {}
self.ui = Gtk.Builder.new_from_file(app.path.resources.join('main.ui'))
self.set_resizable(True)
self.set_position(Gtk.WindowPosition.CENTER)
self.set_icon_from_file(app.path.resources.join('icon.png'))
@ -67,7 +45,7 @@ class Window(Gtk.ApplicationWindow):
self.move(*self.window_location_check(config.location))
self.set_fullscreen(config.fullscreen)
self.fullscreen_set(config.fullscreen)
self.set_default_size(*self.window_size_check(config.size))
self['tabs'].set_tab_pos(getattr(Gtk.PositionType, config.tab_side.upper()))
@ -75,28 +53,8 @@ class Window(Gtk.ApplicationWindow):
self.setup_signals()
## Connect window signals
self.connect('window-state-event', self.handle_window_state)
self.connect('delete-event', self.handle_window_close)
def __getitem__(self, name):
widget = self.ui.get_object(name)
if not widget:
raise KeyError(f'Widget with ID "{name}" does not exist.')
return widget
def __setitem__(self, name, widget):
try:
self[name]
raise KeyError(f'Widget ID already exists: {name}')
except KeyError:
self.ui.expose_object(name, widget)
return widget
Gtk.ApplicationWindow.connect(self, 'window-state-event', self.handle_window_state)
Gtk.ApplicationWindow.connect(self, 'delete-event', self.handle_window_close)
@property
@ -105,17 +63,6 @@ class Window(Gtk.ApplicationWindow):
return tabs.get_nth_page(tabs.get_current_page())
@active_tab.setter
def active_tab(self, tabid):
page_num = self['tabs'].page_num(self.tabdata[tabid])
self['tabs'].set_current_page(page_num)
@property
def app(self):
return self.get_application()
@property
def dimensions(self):
width, height = self.get_size()
@ -134,101 +81,50 @@ class Window(Gtk.ApplicationWindow):
return DotDict(width=screen.get_width(), height=screen.get_height())
def Connect(self, name, signal, callback, *args, **kwargs):
widget = self[name]
return connect(widget, signal, callback, *args, **kwargs)
def close_tab(self, tabid=None):
if not tabid:
tabid = self.active_tab.tabid
tab = self.tabdata[tabid]
def file_open(self):
with self.app.db.session as s:
tab_limit = s.get_config('closed_tabs_limit')
s.remove('tabs', tabid=tabid)
download_dir = s.get_config('download_dir')
if tab_limit:
while len(self.closed) >= tab_limit:
self.remove_closed_tab(list(self.closed.keys())[0])
fc = FileChooser(self, download_dir, save=False)
fc.new_filter('HTML Document', '*htm', '*.html', '*.xhtml', '*.mhtml')
fc.new_filter('XML', '*.xml', '*.xslt')
fc.new_filter('Text', '*.txt')
path = fc.run_dialog()
fc.destroy()
self.closed[tabid] = DotDict(
state = tab.webview.get_session_state(),
title = tab.title,
url = tab.url
)
if not path:
logging.verbose('Canceled web page download')
return
self['tabs-closed-menu'].new_action_at_pos(2, tabid, (tab.title or 'Untitled'), self.reopen_tab, tabid)
self['tabs-closed-menu'][tabid].set_tooltip_text(tab.url)
if len(self.tabdata) > 1 and self.active_tab == tab:
new_tabid = self.taborder[self.taborder.index(tabid)+1]
try:
self.active_tab = new_tabid
except KeyError:
logging.debug('window.close_tab: Cannot find tabid:', new_tabid)
tab.webview.destroy()
tab.destroy()
del self.tabdata[tabid]
self.taborder.remove(tabid)
if self['tabs'].get_current_page() < 0:
self.new_tab()
self.new_tab(path, switch=True)
def fullscreen_toggle(self):
self.set_fullscreen(not self.is_fullscreen)
def fullscreen_set(self, is_fullscreen=None):
if is_fullscreen == None:
is_fullscreen = not self.is_fullscreen
def new_tab(self, url=None, row=None, switch=False):
if type(row) == str:
row = None
if not row:
while (tabid := random_str()) in self.tabdata:
continue
if is_fullscreen:
self.fullscreen()
else:
tabid = row.tabid
self.unfullscreen()
tab = Webview(window=self, starturl=url, row=row, tabid=tabid)
## Re-initialize a saved webview and switch if it was the active tab
with self.app.db.session as s:
if row:
switch = row.active
def library_open(self, page=LibraryPage.HOME):
if not isinstance(page, LibraryPage):
try:
page = LibraryPage[page.upper()]
if not switch and not s.get_config('load_tabs'):
tab.label.set_favicon('reload')
except KeyError:
raise KeyError(f'Invalid library page: {page}')
elif not url:
tab.homepage()
url = var.local + '/' + page.value
if not self.startup and s.get_config('tab_after_current'):
page_num = self['tabs'].insert_page(tab, tab.label, self['tabs'].get_current_page()+1)
#for tab in self.tabdata.values():
#if tab.url.startswith(url):
#return self.switch_tab(tab)
else:
page_num = self['tabs'].append_page(tab, tab.label)
self.tabdata[tab.tabid] = tab
self.taborder.append(tab.tabid)
self['tabs'].set_tab_reorderable(tab, True)
#move this to tab.label
self['tabs'].set_menu_label(tab, tab.label.menu_label)
if switch:
self.active_tab = tab.tabid
if not self.startup:
self.save_tabs()
logging.verbose('New tab:', tab.tabid, url if not row else 'from saved state')
return tab
self.tab_new(url, switch=True)
def notification(self, text, level='INFO', timeout=5, system=False):
@ -249,114 +145,20 @@ class Window(Gtk.ApplicationWindow):
logging.log(logging.LogLevel[level.upper()], text)
def open_file(self):
with self.app.db.session as s:
download_dir = s.get_config('download_dir')
def set_button_state(self, tabid=None):
try:
tab = self.active_tab if not tabid else self.tabdata[tabid]
fc = FileChooser(self, download_dir, save=False)
fc.new_filter('HTML Document', '*htm', '*.html', '*.xhtml', '*.mhtml')
fc.new_filter('XML', '*.xml', '*.xslt')
fc.new_filter('Text', '*.txt')
path = fc.run_dialog()
fc.destroy()
except KeyError:
if not self.startup:
logging.warning('window.set_button_state: Cannot find tab with id:', tabid)
if not path:
logging.verbose('Canceled web page download')
return
self.new_tab(path, switch=True)
active = tab.url and not any(map(tab.url.startswith, [var.local, 'source']))
def open_library(self, page=''):
url = var.local + '/' + page
for tab in self.tabdata.values():
if tab.url.startswith(url):
return self.switch_tab(tab)
self.new_tab(url, switch=True)
def remove_closed_tab(self, tabid):
del self.closed[tabid]
self['tabs-closed-menu'].remove_item(tabid)
def reopen_tab(self, tabid):
data = self.closed[tabid]
tab = Webview(window=self, tabid=tabid)
tab.webview.restore_session_state(data.state)
tab.page_action('reload')
## Re-initialize a saved webview and switch if it was the active tab
with self.app.db.session as s:
if s.get_config('tab_after_current'):
page_num = self['tabs'].insert_page(tab, tab.label, self['tabs'].get_current_page()+1)
else:
page_num = self['tabs'].append_page(tab, tab.label)
self.tabdata[tab.tabid] = tab
self.taborder.append(tab.tabid)
self['tabs'].set_tab_reorderable(tab, True)
#move this to tab.label
self['tabs'].set_menu_label(tab, tab.label.menu_label)
self.switch_tab(tab)
self.save_tabs()
self.remove_closed_tab(tabid)
logging.verbose('Restored tab:', tab.tabid, data.url)
def save_tabs(self):
active_tab = self.active_tab
active_tab_exists = False
## save tab state
for tabid, tab in self.tabdata.items():
if getattr(tab, 'webview', None) and tab.url not in [None, 'about:blank']:
tab.state_store()
if active_tab == tab:
active_tab_exists = True
with self.app.db.session as s:
if not active_tab_exists:
row = s.fetch('tabs').one()
if row:
s.update_row(row, active=True)
## removed closed tabs from the database
for row in s.fetch('tabs',):
if row.tabid not in self.tabdata:
s.remove_row(row)
def set_button_state(self, tab):
settings = self['statusbar-siteoptions']
bookmark = self['statusbar-bookmark']
password = self['statusbar-logins']
self.set_navbar_url(tab.url)
self['navbar-stop'].set_sensitive(not tab.idle)
self['navbar-refresh'].set_sensitive(tab.idle)
for button, action in {'navbar-prev': tab.webview.can_go_back(), 'navbar-next': tab.webview.can_go_forward()}.items():
self[button].set_sensitive(action)
if tab.url and not any(map(tab.url.startswith, [var.local, 'source'])):
settings.set_sensitive(True)
bookmark.set_sensitive(True)
password.set_sensitive(True)
else:
settings.set_sensitive(False)
bookmark.set_sensitive(False)
password.set_sensitive(False)
for widget_name in ['siteoptions', 'bookmark', 'logins']:
self[f'statusbar-{widget_name}'].set_sensitive(active)
widgets = ['reply', 'boost', 'favorite']
@ -366,26 +168,160 @@ class Window(Gtk.ApplicationWindow):
self['statusbar-toot'].set_sensitive(acct_count > 0)
for widget in widgets:
self[f'statusbar-{widget}'].set_sensitive(tab.fedi_post and acct_count > 0)
self[f'statusbar-{widget}'].set_sensitive(tab._data.post and acct_count > 0)
def set_navbar_url(self, url):
navurl = self['navbar-url']
def tab_close(self, tabid=None):
tab = self.active_tab if not tabid else self.tabdata[tabid]
if not navurl.has_focus():
navurl.set_text(url or '')
with self.app.db.session as s:
tab_limit = s.get_config('closed_tabs_limit')
s.remove('tabs', tabid=tab.id)
if tab_limit:
while len(self.closed) >= tab_limit:
self.remove_closed_tab(list(self.closed.keys())[0])
self.closed[tab.id] = tab.state_unload()
self['tabs-closed-menu'].new_action_at_pos(2, tab.id, (tab.title or 'Untitled'), self.tab_reopen, tab.id)
self['tabs-closed-menu'][tab.id].set_tooltip_text(tab.url)
if len(self.tabdata) > 1 and self.active_tab == tab:
new_tabid = self.taborder[self.taborder.index(tab.id)+1]
try:
self.tab_switch(new_tabid)
except KeyError:
logging.warning('window.close_tab: Cannot find tabid:', new_tabid)
tab.destroy()
del self.tabdata[tabid]
self.taborder.remove(tabid)
if not self['tabs'].get_n_pages():
self.new_tab()
def set_fullscreen(self, isfull):
if isfull:
self.fullscreen()
def tab_new(self, url=None, row=None, switch=False):
if type(row) == str:
logging.warning(f'window.tab_new: row set to a string: {repr(row)}')
row = None
else:
self.unfullscreen()
with self.db.session as s:
if url == None:
url = s.get_config('homepage')
if isinstance(row, WebviewState):
tab = WebTab.new_from_state(row, s.get_config('load_tabs'))
elif isinstance(row, Row):
tab = WebTab.new_from_row(row, s.get_config('load_tabs'))
else:
tab = WebTab(url)
self['tabs'].append_page_menu(tab, tab['label'], tab['menu'])
if not self.startup and s.get_config('tab_after_current'):
self['tabs'].reorder_child(tab, self['tabs'].get_current_page() + 1)
self.tabdata[tab.id] = tab
self.taborder.append(tab.id)
self['tabs'].set_tab_reorderable(tab, True)
if switch:
self.tab_switch(tab.id)
if not self.startup:
self.tabs_save()
logging.verbose('New tab:', tab.id, url if not row else 'from saved state')
return tab
def switch_tab(self, tab):
self.active_tab = tab.tabid
def tab_new_id(self):
while (tabid := random_str()) in self.tabdata:
continue
return tabid
def tab_remove_closed(self, tabid):
del self.closed[tabid]
self['tabs-closed-menu'].remove_item(tabid)
def tab_reopen(self, tabid):
data = self.closed[tabid]
tab = WebTab.new_from_state(data, True)
## Re-initialize a saved webview and switch if it was the active tab
with self.app.db.session as s:
self['tabs'].append_page_menu(tab, tab['label'], tab['menu'])
if s.get_config('tab_after_current'):
self['tabs'].reorder_child(tab, self['tabs'].get_current_page() + 1)
self.tabdata[tab.id] = tab
self.taborder.append(tab.id)
self['tabs'].set_tab_reorderable(tab, True)
self.tab_switch(tab)
self.tabs_save()
self.tab_remove_closed(tabid)
logging.verbose('Restored tab:', tab.id, data.url)
def tab_switch(self, tabid):
if isinstance(tabid, WebTab):
tabid = tabid.id
page_num = self['tabs'].page_num(self.tabdata[tabid])
self['tabs'].set_current_page(page_num)
def tabs_save(self):
active_tab_exists = False
with self.db.session as s:
for tab in self.tabdata.values():
state = tab.get_state()
index = self['tabs'].page_num(tab)
active = self['tabs'].get_current_page() == index
if index == -1:
logging.debug('tabs_save: failed to get tab index', tab.id)
index = 0
if active:
active_tab_exists = True
with self.db.session as s:
if state.url in [None, 'about:blank']:
continue
s.put_tab(
tab.id,
state.title,
state.url,
state.to_bytes(),
active,
index
)
for idx, row in enumerate(s.fetch('tabs')):
if row.tabid not in self.tabdata:
s.remove_row(row)
if not active_tab_exists:
if (first_tab := s.fetch('tabs').one()):
s.update_row(first_tab, active=True)
def window_location_check(self, location):
@ -421,68 +357,6 @@ class Window(Gtk.ApplicationWindow):
return width, height
def handle_button(self, name, arg=None):
tab = self.active_tab
if arg == 'menu':
self['navbar-menu-popover'].popdown()
if name == 'library':
self.new_tab(var.local, switch=True)
elif name == 'about':
self.open_library('help')
elif name == 'fullscreen':
self.fullscreen_toggle()
elif name == 'newtab':
self.new_tab(switch=True)
elif name == 'reply':
pass
elif name == 'boost':
pass
elif name == 'favorite':
pass
elif name == 'quit':
self.app.quit()
elif tab and tab.webview:
if name == 'go':
tab.load_url(self['navbar-url'].get_text())
elif name == 'prev':
tab.page_action('back')
elif name == 'next':
tab.page_action('forward')
elif name == 'stop':
tab.page_action('stop')
elif name == 'refresh':
tab.page_action('reload')
elif name == 'search':
tab.search_toggle()
elif name == 'home':
tab.homepage()
else:
logging.verbose('window.handle_button: Button not handled:', name)
if name == 'search':
tab.search.items.text.grab_focus()
else:
tab.webview.grab_focus()
def handle_clear_closed_tabs(self):
for tabid in list(self.closed.keys()):
del self.closed[tabid]
@ -493,64 +367,77 @@ class Window(Gtk.ApplicationWindow):
self['tabs-closed-menu']['clear'].set_sensitive(not len(self.closed) == 0)
def handle_page_switch(self, notebook, tab, pagenum):
with self.app.db.session as s:
if s.get_config('load_switch') and not self.startup and tab.webview and tab.state:
tab.page_action('reload')
def handle_menu_button(self, name, arg=None):
tab = self.active_tab
if tab.tabid in self.taborder:
self.taborder.remove(tab.tabid)
if name == 'new_tab':
with self.db.session as s:
self.tab_new(s.get_config('homepage'), switch=True)
self.taborder.insert(0, tab.tabid)
self.set_button_state(tab)
elif name == 'search':
self.active_tab.search_action('open')
progress = tab.webview.get_estimated_load_progress()
self['navbar-url'].set_progress_fraction(0 if progress == 1.0 else progress)
elif name == 'fullscreen':
self.fullscreen_set()
elif name == 'library':
self.library_open()
def handle_url_keys(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
widget.select_region(0,0)
widget.set_text(self.active_tab.url or '')
return self.active_tab.webview.grab_focus()
elif name == 'about':
self.library_open('help')
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()
elif name == 'quit':
self.app.quit()
else:
self.autocomplete_timer.refresh()
raise KeyError('window.handle_button: Button not handled:', name)
self['tabs-menu-popover'].popdown()
def handle_url_popup(self, entry, menu):
if not self.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_menubar_file_action(self, action):
self.active_tab.page_action(action)
def handle_url_popup_go(self, item):
self['navbar-url'].set_text(self.clipboard.wait_for_text())
self.handle_button('go')
def handle_menubar_edit_action(self, action):
tab = self.active_tab
if action == 'unselect':
tab.run_js(Javascript.Deselect)
elif action == 'delete':
tab.run_js(Javascript.DeleteSelected)
elif action == 'inspect':
tab.inspector_toggle()
elif action == 'source':
tab.page_action('source')
else:
tab.editing_action(action)
def handle_page_switch(self, notebook, tab, pagenum):
with self.app.db.session as s:
if tab._data.state and not self.startup and s.get_config('load_switch'):
tab.page_action('refresh')
if tab.id in self.taborder:
self.taborder.remove(tab.id)
self.taborder.insert(0, tab.id)
self.set_button_state(tab.id)
#progress = tab.webview.get_estimated_load_progress()
#self['navbar-url'].set_progress_fraction(0 if progress == 1.0 else progress)
## save tab urls and titles on close to reopen them on startup
def handle_window_close(self, *args):
logging.verbose('Saving data')
self.save_tabs()
self.tabs_save()
with self.app.db.session as s:
s.put_config('maximized', self.is_maximized())
@ -571,14 +458,8 @@ class Window(Gtk.ApplicationWindow):
def set_icons(self):
## navigation bar
icon_set(self['navbar-prev-icon'], 'previous', 24)
icon_set(self['navbar-next-icon'], 'next', 24)
icon_set(self['navbar-stop-icon'], 'stop', 24)
icon_set(self['navbar-refresh-icon'], 'refresh', 24)
icon_set(self['navbar-home-icon'], 'home', 24)
icon_set(self['navbar-go-icon'], 'go', 24)
icon_set(self['navbar-menu-icon'], 'menu', 24)
## tab bar
self.set_icon_from_resource('navbar-menu-icon', 'menu.svg', 24)
## status bar
icon_set(self['statusbar-debug-icon'], 'debug', 16)
@ -595,69 +476,41 @@ class Window(Gtk.ApplicationWindow):
def setup_signals(self):
signals = DotDict({
'notification-close': [
{'signal': 'clicked', 'callback': self['notification'].set_reveal_child, 'args': [False]}
],
'navbar-menu-newtab': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['newtab']}
],
'navbar-menu-search': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['search']}
],
'navbar-menu-fullscreen': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['fullscreen']}
],
'navbar-menu-library': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['library']}
],
'navbar-menu-about': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['about']}
],
'navbar-menu-quit': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['quit']}
],
'navbar-prev': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['prev']}
],
'navbar-next': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['next']}
],
'navbar-stop': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['stop']}
],
'navbar-refresh': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['refresh']}
],
'navbar-home': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['home']}
],
'navbar-url': [
{'signal': 'key-press-event', 'callback': self.handle_url_keys, 'kwargs': {'original_args': True}},
{'signal': 'populate-popup', 'callback': self.handle_url_popup, 'kwargs': {'original_args': True}}
],
'navbar-go': [
{'signal': 'clicked', 'callback': self.handle_button, 'args': ['go']}
],
#'navbar-menu': {'signal': 'clicked', 'callback': self.handle_button},
'tabs': [
{'signal': 'switch-page', 'callback': self.handle_page_switch, 'kwargs': {'original_args': True}}
],
'tabs-new': [
{'signal': 'clicked', 'callback': self.new_tab, 'kwargs': {'switch': True}}
],
'tabs-closed-menu': [
{'signal': 'show', 'callback': self.handle_closed_tabs_show}
]
})
## Misc
self.connect('notification-close', 'clicked', self['notification'].set_reveal_child, False)
for name, sigs in signals.items():
for data in sigs:
data = DotDict(data)
if name.startswith('menu'):
data.args.append('menu')
## Tab bar
self.connect('tabs', 'switch-page', self.handle_page_switch, original_args=True)
self.connect('tabs-new', 'clicked', self.tab_new, switch=True)
self.connect('tabs-closed-menu', 'show', self.handle_closed_tabs_show)
self.Connect(name, data.signal, data.callback, *data.get('args', []), **data.get('kwargs', {}))
## Tab bar menu
self.connect('tabs-menu-new_tab', 'clicked', self.handle_menu_button, 'new_tab')
self.connect('tabs-menu-search', 'clicked', self.handle_menu_button, 'search')
self.connect('tabs-menu-fullscreen', 'clicked', self.handle_menu_button, 'fullscreen')
self.connect('tabs-menu-library', 'clicked', self.handle_menu_button, 'library')
self.connect('tabs-menu-about', 'clicked', self.handle_menu_button, 'about')
self.connect('tabs-menu-quit', 'clicked', self.handle_menu_button, 'quit')
## File menu
self.connect('menubar-file-new_tab', 'activate', self.tab_new, switch=True)
self.connect('menubar-file-close_tab', 'activate', self.tab_close)
self.connect('menubar-file-open', 'activate', self.file_open)
self.connect('menubar-file-save', 'activate', self.handle_menubar_file_action, 'save')
self.connect('menubar-file-print', 'activate', self.handle_menubar_file_action, 'print')
self.connect('menubar-file-quit', 'activate', self.app.quit)
## Edit menu
for name in ['cut', 'copy', 'paste', 'delete', 'undo', 'redo', 'select', 'unselect', 'source', 'inspect']:
self.connect(f'menubar-edit-{name}', 'activate', self.handle_menubar_edit_action, name)
## Library menu
for name in ['bookmarks', 'downloads', 'history', 'passwords', 'search', 'fediverse', 'extensions', 'preferences']:
self.connect(f'menubar-library-{name}', 'activate', self.library_open, name)
## Help menu
self.connect('menubar-help-usage', 'activate', self.library_open, 'help')
self.connect('menubar-help-about', 'activate', self.library_open, 'help')
def setup_widgets(self):
@ -671,9 +524,7 @@ class Window(Gtk.ApplicationWindow):
self.add(self['main-overlay'])
self.themes = Themes(self)
self.menubar = MenuBar(self)
self.statusbar = StatusBar(self)
self.context = WebContext(self)
self.set_icons()
@ -724,8 +575,6 @@ class AutocompleteTimeout(threading.Thread):
if self.current_time <= 0:
break
print('loop', self.current_time)
text = self.url_entry.get_text()
with self.app.db.session as s:

78
barkshark_web/enums.py Normal file
View file

@ -0,0 +1,78 @@
from enum import Enum
class EditAction(Enum):
COPY = WebKit2.EDITING_COMMAND_COPY
LINK = WebKit2.EDITING_COMMAND_CREATE_LINK
CUT = WebKit2.EDITING_COMMAND_CUT
IMAGE = WebKit2.EDITING_COMMAND_INSERT_IMAGE
PASTE = WebKit2.EDITING_COMMAND_PASTE
PLAIN = WebKit2.EDITING_COMMAND_PASTE_AS_PLAIN_TEXT
REDO = WebKit2.EDITING_COMMAND_REDO
SELECT = WebKit2.EDITING_COMMAND_SELECT_ALL
UNDO = WebKit2.EDITING_COMMAND_UNDO
class Javascript(Enum):
SELECTION = 'window.getSelection().toString()'
EXEC = 'window.execCommand("{}")'
PRINT = 'window.print()'
DESELECT = 'document.getSelection().removeAllRanges()'
DELETE = 'document.activeElement.setRangeText("")'
class LibraryPage(Enum):
HOME = ''
BOOKMARKS = 'bookmarks'
DOWNLOADS = 'downloads'
HISTORY = 'history'
PASSWORDS = 'passwords'
SEARCH = 'search'
FEDIVERSE = 'fediverse'
EXTENSIONS = 'extensions'
PREFERENCES = 'preferences'
HELP = 'help'
class WebviewContextActions(Enum):
AUDIO_COPY = WebKit2.ContextMenuAction.COPY_AUDIO_LINK_TO_CLIPBOARD
AUDIO_DOWNLOAD = WebKit2.ContextMenuAction.DOWNLOAD_AUDIO_TO_DISK
FRAME_OPEN = WebKit2.ContextMenuAction.OPEN_FRAME_IN_NEW_WINDOW
AUDIO_TAB = WebKit2.ContextMenuAction.OPEN_AUDIO_IN_NEW_WINDOW
GO_BACK = WebKit2.ContextMenuAction.GO_BACK
GO_FORWARD = WebKit2.ContextMenuAction.GO_FORWARD
GO_RELOAD = WebKit2.ContextMenuAction.RELOAD
GO_STOP = WebKit2.ContextMenuAction.STOP
IMAGE_COPY = WebKit2.ContextMenuAction.COPY_IMAGE_URL_TO_CLIPBOARD
IMAGE_COPY_FULL = WebKit2.ContextMenuAction.COPY_IMAGE_TO_CLIPBOARD
IMAGE_DOWNLOAD = WebKit2.ContextMenuAction.DOWNLOAD_IMAGE_TO_DISK
IMAGE_TAB = WebKit2.ContextMenuAction.OPEN_IMAGE_IN_NEW_WINDOW
INSPECT = WebKit2.ContextMenuAction.INSPECT_ELEMENT
LINK_COPY = WebKit2.ContextMenuAction.COPY_LINK_TO_CLIPBOARD
LINK_DOWNLOAD = WebKit2.ContextMenuAction.DOWNLOAD_LINK_TO_DISK
LINK_OPEN = WebKit2.ContextMenuAction.OPEN_LINK
LINK_TAB = WebKit2.ContextMenuAction.OPEN_LINK_IN_NEW_WINDOW
MEDIA_TOGGLE = WebKit2.ContextMenuAction.TOGGLE_MEDIA_CONTROLS
MEDIA_MUTE = WebKit2.ContextMenuAction.MEDIA_MUTE
MEDIA_LOOP = WebKit2.ContextMenuAction.TOGGLE_MEDIA_LOOP
MEDIA_PAUSE = WebKit2.ContextMenuAction.MEDIA_PAUSE
MEDIA_PLAY = WebKit2.ContextMenuAction.MEDIA_PLAY
SPELL_ADD = WebKit2.ContextMenuAction.LEARN_SPELLING
SPELL_GUESS = WebKit2.ContextMenuAction.SPELLING_GUESS
SPELL_IGNORE_GRAMMAR = WebKit2.ContextMenuAction.IGNORE_GRAMMAR
SPELL_IGNORE = WebKit2.ContextMenuAction.IGNORE_SPELLING
SPELL_NO_GUESS = WebKit2.ContextMenuAction.NO_GUESSES_FOUND
TEXT_BOLD = WebKit2.ContextMenuAction.BOLD
TEXT_COPY = WebKit2.ContextMenuAction.COPY
TEXT_CUT = WebKit2.ContextMenuAction.CUT
TEXT_DELETE = WebKit2.ContextMenuAction.DELETE
TEXT_EMOJI = WebKit2.ContextMenuAction.INSERT_EMOJI
TEXT_ITALIC = WebKit2.ContextMenuAction.ITALIC
TEXT_PASTE = WebKit2.ContextMenuAction.PASTE
TEXT_PASTE_PLAIN = WebKit2.ContextMenuAction.PASTE_AS_PLAIN_TEXT
TEXT_SELECT = WebKit2.ContextMenuAction.SELECT_ALL
TEXT_UNDERLINE = WebKit2.ContextMenuAction.UNDERLINE
VIDEO_COPY = WebKit2.ContextMenuAction.COPY_VIDEO_LINK_TO_CLIPBOARD
VIDEO_DOWNLOAD = WebKit2.ContextMenuAction.DOWNLOAD_VIDEO_TO_DISK
VIDEO_FULLSCREEN = WebKit2.ContextMenuAction.ENTER_VIDEO_FULLSCREEN
VIDEO_TAB = WebKit2.ContextMenuAction.OPEN_VIDEO_IN_NEW_WINDOW

View file

@ -1,4 +1,9 @@
import multiprocessing, socket, threading, time, traceback
import cairo
import multiprocessing
import socket
import threading
import time
import traceback
from ctypes import cdll, create_string_buffer, byref
from izzylib.http_client import HttpClient
@ -153,26 +158,283 @@ def set_proc_name(name='pyweb'):
return ret
def surface_to_pixbuf(surface, new_size):
if not surface:
return
width = surface.get_width()
height = surface.get_height()
def scale_pixbuf(pixbuf, new_size=16, keep='height'):
width = pixbuf.get_width()
height = pixbuf.get_height()
## (size ratio) * width * width to height ratio
new_width = (new_size / width) * width * (width/height)
new_width = (new_size / width) * width
new_height = (new_size / height) * height
image = Gdk.pixbuf_get_from_surface(surface, 0, 0, width, height)
image = image.scale_simple(new_width, new_height, GdkPixbuf.InterpType.BILINEAR)
if keep == 'width':
new_height *= height/width
return image.copy()
elif keep == 'height':
new_width *= width/height
return pixbuf.scale_simple(new_width, new_height, GdkPixbuf.InterpType.BILINEAR)
def set_image(widget, image, size=16):
try:
image = new_pixbuf(image, size)
widget.set_from_pixbuf(image)
return widget
except TypeError:
pass
if isinstance(image, str):
widget.set_from_icon_name(image, Gtk.IconSize.SMALL_TOOLBAR)
widget.set_pixel_size(size)
else:
raise TypeError('Image is not a Cairo Surface, Pixbuf, Path object, or an icon name (str)')
return widget
def new_pixbuf(image, size=16):
if isinstance(image, (cairo.Surface, cairo.ImageSurface)):
image = Gdk.pixbuf_get_from_surface(image, 0, 0, image.get_width(), image.get_height())
elif isinstance(image, GdkPixbuf.Pixbuf):
pass
elif isinstance(image, Path):
return GdkPixbuf.Pixbuf.new_from_file_at_scale(image, -1, size, True)
else:
raise TypeError('Image is not a Cairo Surface, Pixbuf, or Path object')
return scale_pixbuf(image, size)
def surface_to_pixbuf(surface, new_size):
logging.warning('DeprecationWarning: use new_pixbuf instead of surface_to_pixbuf')
return new_pixbuf(surface, new_size)
class BuilderBase:
def __init__(self, path):
self._path = path
self._builder = Gtk.Builder.new_from_file(path)
self._connected_sigs = {}
def __getitem__(self, name):
if not (widget := self._builder.get_object(name)):
raise KeyError(f'Widget with ID "{name}" does not exist.')
return widget
def __setitem__(self, name, widget):
try:
self[name]
raise KeyError(f'Widget ID already exists: {name}')
except KeyError:
self._builder.expose_object(name, widget)
return widget
def __delitem__(self, name):
self[name].destroy()
def __del__(self):
self.destroy()
@property
def app(self):
return Gio.Application.get_default()
@property
def db(self):
return self.app.db
@property
def path(self):
return self._path
def block_signals(self, *names):
signals = {name: self.get_signals(name) for name in names}
return SignalBlock(self, signals)
def connect(self, name, signal, callback, *args, **kwargs):
widget = self[name]
if not self._connected_sigs.get(name):
self._connected_sigs[name] = {}
if signal in self._connected_sigs[name]:
raise KeyError(f'Signal for "{name}" already connected: {signal}')
self._connected_sigs[name][signal] = connect(widget, signal, callback, *args, **kwargs)
return self._connected_sigs[name][signal]
def disconnect(self, name, signal):
try:
sigid = self._connected_sigs[name].pop(signal)
self[name].disconnect(sigid)
except KeyError:
raise KeyError(f'Signal for "{name}" not connected: {signal}')
def destroy(self):
for widget in self._builder.get_objects():
try: widget.destroy()
except: pass
self._connected_sigs = {}
def get_signals(self, name):
return self._connected_sigs[name]
def reload(self):
self.destroy(False)
self._builder.add_from_file(self.path)
def set_icon(self, name, obj, size=16):
widget = self[name]
if not isinstance(widget, Gtk.Image):
raise TypeError(f'Widget is not a Gtk.Image object: {name}')
if isinstance(obj, cairo.Surface):
image = surface_to_pixbuf(obj, size)
widget.set_from_pixbuf(image)
elif isinstance(obj, GdkPixbuf.Pixbuf):
image = scale_pixbuf(obj, size)
widget.set_from_pixbuf(image)
elif isinstance(obj, Path):
image = GdkPixbuf.Pixbuf.new_from_file_at_scale(obj, -1, size, True)
widget.set_from_pixbuf(image)
elif isinstance(obj, str):
widget.set_from_icon_name(obj, Gtk.IconSize.SMALL_TOOLBAR)
widget.set_pixel_size(size)
else:
raise TypeError(f'Invalid icon type for "{name}": {class_name(obj)}')
def set_icon_from_resource(self, name, filename, size=16):
path = self.app.path.resources.join('icons').join(f'{filename}')
self.set_icon(name, path, size)
class ComponentBase:
signals = {}
def __init__(self, prefix=None):
self._prefix = prefix
self._app = None
self._db = None
self._window = None
def __getitem__(self, key):
key = f'{self._prefix}-{key}' if self._prefix else key
return self.window[key]
def __setitem__(self, key, widget):
key = f'{self._prefix}-{key}' if self._prefix else key
self.window[key] = widget
def __delitem__(self, key):
key = f'{self._prefix}-{key}' if self._prefix else key
del self.window[key]
@property
def app(self):
return self._app or Gio.Application.get_default()
@app.setter
def app(self, value):
self._app = value
@property
def db(self):
return self._db or self.app.db
@db.setter
def db(self, value):
self._db = value
@property
def window(self):
return self._window or self.app.window
@window.setter
def window(self, value):
self._window = value
def block_signals(self, *names):
return BuilderBase.block_signals(self, *names)
def connect(self, name, signal, callback, *args, **kwargs):
name = f'{self._prefix}-{name}' if self._prefix else name
return self.window.connect(name, signal, callback, *args, **kwargs)
def disconnect(self, name, signal):
name = f'{self._prefix}-{name}' if self._prefix else name
return self.window.disconnect(name, signal)
def get_signals(self, name):
return self._window._connected_sigs[f'{self._prefix}-{name}']
def set_icon(self, name, obj, size=16):
BuilderBase.set_icon(self, name, obj, size)
def set_icon_from_resource(self, name, filename, size=16):
Builder.set_icon_from_resource(self, name, filename, size)
def setup_signals(self):
for name, sigs in self.signals.items():
for data in sigs:
data = DotDict(data)
if name.startswith('menu'):
data.args.append('menu')
self.connect(name, data.signal, data.callback, *data.get('args', []), **data.get('kwargs', {}))
class SignalBlock:
def __init__(self, signal_pairs):
self.widgets = signal_pairs
def __init__(self, parent, signals):
self.parent = parent
self.signals = signals
def __enter__(self):
@ -184,12 +446,13 @@ class SignalBlock:
def set_block(self, block):
for signal, widget in self.widgets:
if block:
widget.handler_block(signal)
for name, signals in self.signals.items():
for signame, sigid in signals.items():
if block:
self.parent[name].handler_block(sigid)
else:
widget.handler_unblock(signal)
else:
self.parent[name].handler_unblock(sigid)
class Thread(threading.Thread):

View file

@ -17,16 +17,17 @@
%img.image.grid-item src='{{var.local}}/icon/menu.svg' title='Toggle Text'
%span.menu-item-text.grid-item style='display:none;' << Menu
=menu_item('Home', '/', 'home')
=menu_item('Bookmarks', '/bookmarks', 'bookmark')
=menu_item('Downloads', '/downloads', 'download')
=menu_item('History', '/history', 'history')
=menu_item('Passwords', '/passwords', 'password')
=menu_item('Search', '/search', 'search')
=menu_item('Fediverse', '/fediverse', 'toot')
=menu_item('Extensions', '/extensions', 'extension')
=menu_item('Preferences','/preferences', 'settings')
=menu_item('Help', '/help', 'help')
=menu_item('Home', '/', 'home.svg')
=menu_item('Bookmarks', '/bookmarks', 'bookmark.svg')
=menu_item('Downloads', '/downloads', 'download.svg')
=menu_item('History', '/history', 'history.svg')
=menu_item('Passwords', '/passwords', 'password.svg')
=menu_item('Search', '/search', 'search.svg')
=menu_item('Fediverse', '/fediverse', 'toot.svg')
=menu_item('Extensions', '/extensions', 'extension.svg')
=menu_item('Preferences','/preferences', 'settings.svg')
=menu_item('Help', '/help', 'help.svg')
=menu_item('About', '/about', 'app.png')
#content-body
#header.section

View file

@ -22,6 +22,6 @@
-macro menu_item(label, path, icon):
%a.menu-item.grid-container href='{{var.local}}{{path}}'
%img.image.grid-item src='{{var.local}}/icon/{{icon}}.svg' title='{{label}}'
%img.image.grid-item src='{{var.local}}/icon/{{icon}}' title='{{label}}'
%span.menu-item-text.grid-item style='display:none;' -> =label

View file

@ -0,0 +1,22 @@
-set page = 'About'
-extends 'base.haml'
-block meta
%link rel='stylesheet' type='text/css' href='{{var.local}}/css/about.css'
-block content
%center
.info
.title -> %a href='https://git.barkshark.xyz/izaliamae/barkshark-web' << Barkshark Web
.version << Version {{version}}
.icon -> %img src='{{var.local}}/icon/app.png' width='320' height='320'
.credits
.title << Credits
.icon
Application icon by
%a href='https://chitter.xyz/users/efi' << Efi@Chitter
.font
%a href='https://github.com/googlefonts/NunitoSans' << Nunito Sans
&copy; Vernon Adams

View file

@ -1,6 +1,13 @@
from izzylib import ObjectBase
from mastodon import Mastodon
from .widgets import Box
from .functions import (
TimeoutCallback,
connect,
get_app,
run_in_gui_thread
)
class Account:
def __init__(self, row):
@ -190,3 +197,162 @@ class AccountRow(Box):
def fetch_post(self, url):
data = self.api.search_v2(url, resolve=True, result_type='statuses')['statuses']
return data[0] if len(data) else None
class LoginRowBase:
def __getitem__(self, key):
return self.ui.get_object(key)
@property
def app(self):
return get_app()
@property
def db(self):
return self.app.db
@property
def window(self):
return self.app.window
def connect(self, name, signal, callback, *args, original_args=False, **kwargs):
if original_args:
connect(self[name], signal, lambda *origargs: callback(*origargs, *args, **kwargs))
else:
connect(self[name], signal, lambda *origargs: callback(*args, **kwargs))
class SavedLoginRow(LoginRowBase):
def __init__(self, row, page_url):
self.ui = Gtk.Builder.new_from_file(self.app.path.resources.join('password_saved.ui'))
self.row = row
self['username'].set_text(self.row['username'])
self.connect('fill', 'clicked', self.handle_fill_password)
self.connect('copy-password', 'clicked', self.handle_copy_password)
self.connect('copy-username', 'clicked', self.row.copy_username)
self['container'].show_all()
@property
def tab(self):
return self.window.active_tab
def handle_copy_password(self):
self.row.copy_password(60)
self.window.notification('Copied password to clipboard for 5 seconds')
self.window['statusbar-logins-popover'].popdown()
def handle_fill_password(self):
with dirs.resources.join('ext_js/autofill.js').open() as fd:
self.tab.run_js(fd.read().replace('USERNAME_VALUE', self.row.username).replace('PASSWORD_VALUE', self.row.password))
self.window['statusbar-logins-popover'].popdown()
class UnsavedLoginRow(LoginRowBase):
def __init__(self, ext, form):
self.ui = Gtk.Builder.new_from_file(self.app.path.resources.join('password_unsaved.ui'))
self.ext = ext
self.form = form
self.set_data(form.username[1], form.password[1])
self.connect('save', 'clicked', self.handle_save)
self.connect('cancel', 'clicked', self.handle_cancel)
self.connect('password', 'icon-press', self.handle_pass_visibility)
self['container'].show_all()
def get_data(self):
url = Url(self.form.url)
data = DotDict(
url = url,
form_url = url.without_query,
username = self.form.username[1],
password = self.form.password[1],
userfield = self.form.username[0],
passfield = self.form.password[0],
note = None
)
if data.url.endswith('/'):
data.url = data.url[:-1]
return data
def set_data(self, username, password):
self['username'].set_text(username or '')
self['password'].set_text(password or '')
def handle_cancel(self):
self.window['statusbar-logins-unsaved-list'].remove(self['container'])
self['container'].destroy()
del self.ext.unsaved_forms[self.form.form_url]
def handle_pass_visibility(self):
self['password'].set_visibility(not self['password'].get_visibility())
def handle_save(self):
data = self.get_data()
with get_app().db.session as s:
s.put_password(data.username, data.password, data.url, name=None, note=data.note)
self.handle_cancel()
class WebviewState:
def __init__(self, tabid, title, url, session):
self.tabid = tabid
self.title = title
self.url = url
self.session = session
def __repr__(self):
return f'WebViewState(tabid="{self.tabid}", title="{self.title}", url="{self.url}")'
@classmethod
def new_from_row(cls, row):
return cls(
row.tabid,
row.title,
row.url,
WebKit2.WebViewSessionState.new(GLib.Bytes.new(row.state))
)
@classmethod
def new_from_tab(cls, tab):
return cls(
tab.id,
tab.title,
tab.url,
tab.webview.get_session_state()
)
@property
def app(self):
return get_app()
def to_bytes(self):
return self.session.serialize().get_data()

View file

@ -1,119 +0,0 @@
from ..functions import TimeoutCallback, connect, get_app, run_in_gui_thread
class LoginRowBase:
def __getitem__(self, key):
return self.ui.get_object(key)
@property
def app(self):
return get_app()
@property
def db(self):
return self.app.db
@property
def window(self):
return self.app.window
def connect(self, name, signal, callback, *args, original_args=False, **kwargs):
if original_args:
connect(self[name], signal, lambda *origargs: callback(*origargs, *args, **kwargs))
else:
connect(self[name], signal, lambda *origargs: callback(*args, **kwargs))
class SavedLoginRow(LoginRowBase):
def __init__(self, row, page_url):
self.ui = Gtk.Builder.new_from_file(self.app.path.resources.join('password_saved.ui'))
self.row = row
self['username'].set_text(self.row['username'])
self.connect('fill', 'clicked', self.handle_fill_password)
self.connect('copy-password', 'clicked', self.handle_copy_password)
self.connect('copy-username', 'clicked', self.row.copy_username)
self['container'].show_all()
@property
def tab(self):
return self.window.active_tab
def handle_copy_password(self):
self.row.copy_password(60)
self.window.notification('Copied password to clipboard for 5 seconds')
self.window['statusbar-logins-popover'].popdown()
def handle_fill_password(self):
with dirs.resources.join('ext_js/autofill.js').open() as fd:
self.tab.run_js(fd.read().replace('USERNAME_VALUE', self.row.username).replace('PASSWORD_VALUE', self.row.password))
self.window['statusbar-logins-popover'].popdown()
class UnsavedLoginRow(LoginRowBase):
def __init__(self, ext, form):
self.ui = Gtk.Builder.new_from_file(self.app.path.resources.join('password_unsaved.ui'))
self.ext = ext
self.form = form
self.set_data(form.username[1], form.password[1])
self.connect('save', 'clicked', self.handle_save)
self.connect('cancel', 'clicked', self.handle_cancel)
self.connect('password', 'icon-press', self.handle_pass_visibility)
self['container'].show_all()
def get_data(self):
url = Url(self.form.url)
data = DotDict(
url = url,
form_url = url.without_query,
username = self.form.username[1],
password = self.form.password[1],
userfield = self.form.username[0],
passfield = self.form.password[0],
note = None
)
if data.url.endswith('/'):
data.url = data.url[:-1]
return data
def set_data(self, username, password):
self['username'].set_text(username or '')
self['password'].set_text(password or '')
def handle_cancel(self):
self.window['statusbar-logins-unsaved-list'].remove(self['container'])
self['container'].destroy()
del self.ext.unsaved_forms[self.form.form_url]
def handle_pass_visibility(self):
self['password'].set_visibility(not self['password'].get_visibility())
def handle_save(self):
data = self.get_data()
with get_app().db.session as s:
s.put_password(data.username, data.password, data.url, name=None, note=data.note)
self.handle_cancel()

View file

@ -65,6 +65,11 @@ def search_web(handler, request):
return request.redirect(row.compile(text))
@Local.route('/about')
def about(handler, request):
return request.page('about')
### Bookmarks ###
@Local.route('/bookmarks')
def bookmarks_home(handler, request):
@ -462,39 +467,7 @@ def search_default(handler, request, searchid):
return request.ok_or_redirect('Set default search')
### Resources ###
@Local.route('/css/{filename}')
def css(handler, request, filename):
context = DotDict(
primary = Color('#e7a'),
secondary = Color('#e7a'),
background = Color('#191919'),
positive = Color('#ada'),
negative = Color('#daa'),
speed = 350
)
return request.template(f'css/{filename}', context)
@Local.route('/js/{filename}')
def javascript(handler, request, filename):
with handler.app.path.localweb.join('js', filename).open() as fd:
return request.response(fd.read(), 'application/javascript')
@Local.route('/font/{name}/{filename}')
def font(handler, request, name, filename):
with handler.app.path.localweb.join('fonts', name, filename).open('rb') as fd:
return request.response(fd.read(), request.mimetype)
@Local.route('/icon/{name}')
def icon(handler, request, name):
with handler.app.path.resources.join('icons', name).open('rb') as fd:
return request.response(fd.read(), 'image/svg+xml')
## Debug
@Local.route('/debug')
def debug_home(handler, request):
return request.page('debug')
@ -527,3 +500,44 @@ def debug_leaking(handler, request):
)
return request.page('debug', context)
### Resources ###
@Local.route('/css/{filename}')
def css(handler, request, filename):
context = DotDict(
primary = Color('#e7a'),
secondary = Color('#e7a'),
background = Color('#191919'),
positive = Color('#ada'),
negative = Color('#daa'),
speed = 350
)
return request.template(f'css/{filename}', context)
@Local.route('/js/{filename}')
def javascript(handler, request, filename):
with handler.app.path.localweb.join('js', filename).open() as fd:
return request.response(fd.read(), 'application/javascript')
@Local.route('/font/{name}/{filename}')
def font(handler, request, name, filename):
with handler.app.path.localweb.join('fonts', name, filename).open('rb') as fd:
return request.response(fd.read(), request.mimetype)
@Local.route('/icon/{name}')
def icon(handler, request, name):
if name == 'app.png':
path = handler.app.path.resources.join('icon.png')
mime = 'image/png'
else:
path = handler.app.path.resources.join('icons', name)
mime = 'image/svg+xml'
with path.open('rb') as fd:
return request.response(fd.read(), mime)

View file

@ -1,17 +1,41 @@
@define-color pyweb_border_color shade(@theme_bg_color, 1.25);
@define-color barkshark_border_color shade(black, 1.25);
@define-color barkshark_button_color shade(@theme_bg_color, 1.15);
list {
entry {
padding-left: 5px;
padding-right: 5px;
border-radius: 5px;
}
entry image {
padding: 5px;
border: 1px solid transparent;
border-radius: 5px;
transition-property: border-color;
transition-property: background-image;
transition-duration: 0.5s;
transition-timing-function: ease-out;
}
entry image:hover {
border-color: @barkshark_border_color;
background-image: radial-gradient(circle farthest-side at center, @barkshark_button_color, @theme_bg_color);
}
.entry-recolor {
background-color: @theme_bg_color;
border: 1px solid @theme_bg_color;
transition: background-color 0.5s ease-out;
}
.entry-recolor:focus {
background-color: @theme_base_color;
border: 1px solid @pyweb_border_color;
}
popover {
border-radius: 3px;
}
popover button {
border-radius: 3px;
}
.notification-container {
background-color: @theme_base_color;
@ -20,71 +44,8 @@ popover button {
border-radius: 8px 8px 0 0;
}
.dropdown-shadow {
-gtk-icon-shadow: 2px 2px rgba(0, 0, 0, 0.25);
}
.dropdown-shadow:hover {
background-color: @theme_base_color;
border: 1px solid @pyweb_border_color;
-gtk-icon-effect: highlight;
-gtk-icon-shadow: 3px 3px black;
}
.notebook-button {
border-radius: 2px;
padding: 5px;
}
/*.notebook-button:hover, button {
border-radius: 2px;
border-width: 1px;
}*/
/*.overlay-widget {
background-color: @theme_base_color;
}*/
.scrolled-no-border {
border: 0px solid transparent;
}
/*.stack-switcher .radio {
margin: 0px 0px 5px 0px;
}*/
.statusbar-login-list {
background-color: @theme_base_color;
padding: 5px;
}
.tab-favicon, .tab-close {
border: 1px solid @theme_bg_color;
background-color: @theme_bg_color;
padding: 2px;
}
.tab-favicon {
border-radius: 2px;
}
.tab-close {
border-radius: 10px;
}
.tab-favicon:hover, .tab-close:hover{
background-color: shade(@theme_bg_color,0.50);
border-color: shade(@theme_bg_color, 1.25);
}
.urlbar {
padding-left: 5px;
padding-right: 5px;
background: @theme_bg_color;
border: 1px solid transparent;
}
.urlbar:focus {
border: 1px solid shade(@theme_bg_color, 1.25);
background: @theme_base_color;
}

View file

@ -7,19 +7,6 @@
<property name="can-focus">False</property>
<property name="icon-name">user-bookmarks</property>
</object>
<object class="GtkImage" id="navbar-go-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">system-run</property>
</object>
<object class="GtkImage" id="navbar-home-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">go-home</property>
<property name="icon_size">0</property>
</object>
<object class="GtkImage" id="navbar-menu-about-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
@ -56,7 +43,7 @@
<property name="can-focus">False</property>
<property name="icon-name">edit-find</property>
</object>
<object class="GtkPopover" id="navbar-menu-popover">
<object class="GtkPopover" id="tabs-menu-popover">
<property name="can-focus">False</property>
<child>
<object class="GtkBox">
@ -68,12 +55,13 @@
<property name="margin-bottom">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkButton" id="navbar-menu-newtab">
<object class="GtkButton" id="tabs-menu-new_tab">
<property name="label" translatable="yes">New Tab</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">False</property>
<property name="tooltip-text" translatable="yes">Opens a new tab</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="image">navbar-menu-newtab-icon</property>
@ -86,12 +74,13 @@
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-menu-search">
<object class="GtkButton" id="tabs-menu-search">
<property name="label" translatable="yes">Search</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">False</property>
<property name="tooltip-text" translatable="yes">Toggle the current tab's search bar</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="image">navbar-menu-search-icon</property>
@ -104,12 +93,13 @@
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-menu-fullscreen">
<object class="GtkButton" id="tabs-menu-fullscreen">
<property name="label" translatable="yes">Fullscreen</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">False</property>
<property name="tooltip-text" translatable="yes">Toggle fullscreen mode</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="image">navbar-menu-fullscreen-icon</property>
@ -133,12 +123,13 @@
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-menu-library">
<object class="GtkButton" id="tabs-menu-library">
<property name="label" translatable="yes">Library</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">False</property>
<property name="tooltip-text" translatable="yes">Open the library in a new tab</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="image">navbar-menu-preferences-icon</property>
@ -162,12 +153,13 @@
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-menu-about">
<object class="GtkButton" id="tabs-menu-about">
<property name="label" translatable="yes">About</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">False</property>
<property name="tooltip-text" translatable="yes">Open the about page in a new tab</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="image">navbar-menu-about-icon</property>
@ -180,12 +172,13 @@
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-menu-quit">
<object class="GtkButton" id="tabs-menu-quit">
<property name="label" translatable="yes">Quit</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">False</property>
<property name="tooltip-text" translatable="yes">Quit the browser</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="image">navbar-menu-quit-icon</property>
@ -200,34 +193,6 @@
</object>
</child>
</object>
<object class="GtkImage" id="navbar-next-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">go-next</property>
<property name="icon_size">3</property>
</object>
<object class="GtkImage" id="navbar-prev-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">go-previous</property>
<property name="icon_size">3</property>
</object>
<object class="GtkImage" id="navbar-refresh-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">view-refresh</property>
<property name="icon_size">3</property>
</object>
<object class="GtkImage" id="navbar-stop-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">process-stop</property>
<property name="icon_size">3</property>
</object>
<object class="GtkImage" id="notification-close-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
@ -385,6 +350,11 @@
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="wrap-mode">word</property>
<property name="left-margin">8</property>
<property name="right-margin">8</property>
<property name="top-margin">8</property>
<property name="bottom-margin">8</property>
<property name="accepts-tab">False</property>
</object>
</child>
<child type="label_item">
@ -1390,172 +1360,6 @@
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="navbar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkButton" id="navbar-prev">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Previous</property>
<property name="image">navbar-prev-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-next">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Forward</property>
<property name="image">navbar-next-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-stop">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Stop</property>
<property name="image">navbar-stop-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-refresh">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Refresh</property>
<property name="image">navbar-refresh-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-home">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Go Home</property>
<property name="image">navbar-home-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="navbar-url">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="max-length">2048</property>
<property name="has-frame">False</property>
<property name="secondary-icon-name">edit-clear</property>
<property name="secondary-icon-tooltip-text" translatable="yes">Clear text</property>
<property name="placeholder-text" translatable="yes">URL</property>
<property name="input-purpose">url</property>
<style>
<class name="urlbar"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-go">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Go</property>
<property name="image">navbar-go-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="navbar-menu">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Menu</property>
<property name="image">navbar-menu-icon</property>
<property name="relief">none</property>
<property name="popover">navbar-menu-popover</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkNotebook" id="tabs">
<property name="visible">True</property>
@ -1599,17 +1403,49 @@
</packing>
</child>
<child type="action-end">
<object class="GtkButton" id="tabs-new">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">New tab</property>
<property name="valign">center</property>
<property name="image">tabs-new-icon</property>
<property name="relief">none</property>
<style>
<class name="notebook-button"/>
</style>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="tabs-new">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">New tab</property>
<property name="valign">center</property>
<property name="image">tabs-new-icon</property>
<property name="relief">none</property>
<style>
<class name="tab-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="navbar-menu">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Menu</property>
<property name="image">navbar-menu-icon</property>
<property name="relief">none</property>
<property name="popover">tabs-menu-popover</property>
<style>
<class name="tab-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="tab-fill">False</property>
@ -1619,7 +1455,7 @@
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="position">1</property>
</packing>
</child>
<child>
@ -1828,7 +1664,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
<property name="position">2</property>
</packing>
</child>
</object>

View file

@ -0,0 +1,386 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkImage" id="label-close-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">window-close</property>
</object>
<object class="GtkImage" id="label-favicon-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">image-x-generic</property>
</object>
<object class="GtkBox" id="label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="label-favicon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">False</property>
<property name="image">label-favicon-icon</property>
<property name="relief">none</property>
<style>
<class name="tab-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label-title">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Untitled Tab</property>
<property name="justify">fill</property>
<property name="ellipsize">end</property>
<property name="single-line-mode">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="label-close">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="image">label-close-icon</property>
<property name="relief">none</property>
<style>
<class name="tab-button"/>
<class name="tab-close"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<object class="GtkBox" id="menu">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="menu-favicon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon_size">1</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="menu-title">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<object class="GtkImage" id="navbar-go-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">system-run</property>
</object>
<object class="GtkImage" id="navbar-home-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">go-home</property>
<property name="icon_size">0</property>
</object>
<object class="GtkImage" id="navbar-next-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">go-next</property>
<property name="icon_size">3</property>
</object>
<object class="GtkImage" id="navbar-prev-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">go-previous</property>
<property name="icon_size">3</property>
</object>
<object class="GtkImage" id="navbar-refresh-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">view-refresh</property>
<property name="icon_size">3</property>
</object>
<object class="GtkImage" id="navbar-stop-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixel-size">24</property>
<property name="icon-name">process-stop</property>
<property name="icon_size">3</property>
</object>
<object class="GtkBox" id="navbar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="navbar-prev">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Previous</property>
<property name="image">navbar-prev-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-next">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Forward</property>
<property name="image">navbar-next-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-stop">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Stop</property>
<property name="image">navbar-stop-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-refresh">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Refresh</property>
<property name="image">navbar-refresh-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-home">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Go Home</property>
<property name="image">navbar-home-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="navbar-url">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="max-length">2048</property>
<property name="has-frame">False</property>
<property name="secondary-icon-name">edit-clear</property>
<property name="secondary-icon-tooltip-text" translatable="yes">Clear text</property>
<property name="secondary-icon-tooltip-markup" translatable="yes">Clear text</property>
<property name="placeholder-text" translatable="yes">URL</property>
<property name="input-purpose">url</property>
<style>
<class name="entry-recolor"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkButton" id="navbar-go">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Go</property>
<property name="image">navbar-go-icon</property>
<property name="relief">none</property>
<style>
<class name="dropdown-shadow"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<style>
<class name="navbar"/>
</style>
</object>
<object class="GtkImage" id="search-close-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">window-close</property>
</object>
<object class="GtkImage" id="search-find-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-find</property>
</object>
<object class="GtkImage" id="search-next-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-next</property>
</object>
<object class="GtkImage" id="search-previous-icon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous</property>
</object>
<object class="GtkBox" id="search">
<property name="can-focus">False</property>
<property name="no-show-all">True</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="search-previous">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="image">search-previous-icon</property>
<property name="relief">none</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="search-next">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="image">search-next-icon</property>
<property name="relief">none</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="search-find">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="image">search-find-icon</property>
<property name="relief">none</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="search-text">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="has-frame">False</property>
<property name="secondary-icon-name">edit-clear</property>
<style>
<class name="entry-recolor"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="search-close">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="image">search-close-icon</property>
<property name="relief">none</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
</interface>

View file

@ -16,8 +16,8 @@ class Themes(DotDict):
@property
def window(self):
return self._window
def current(self):
return self._current_theme
@property
@ -25,6 +25,11 @@ class Themes(DotDict):
return Gdk.Screen.get_default()
@property
def window(self):
return self._window
def load(self, name):
logging.verbose('Loading theme:', name)

View file

@ -79,6 +79,7 @@
"LOG_LEVEL": "DEBUG"
},
"ext": [
"css",
"ui",
"py",
"pyx",
@ -95,4 +96,4 @@
"pyvenv.py"
]
}
}
}