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

770 lines
19 KiB
Python

import json, sys, threading, time
from .menu_bar import MenuBar
from .status_bar import StatusBar
from .web_context import WebContext
from .web_view import Webview
from .. import var, __software__
from ..functions import Thread, run_in_gui_thread, connect, get_app, icon_set
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,
}
iconsize = Gtk.IconSize.BUTTON
is_fullscreen = False
startup = True
shutdown = False
autocomplete_timer = None
def __init__(self, app):
super().__init__(application=app, title=__software__)
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self.tabdata = {}
self.taborder = []
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'))
## Set previous size, location, and maximized states
with app.db.session as s:
config = s.get_config()
if config.maximized:
self.maximize()
else:
self.move(*self.window_location_check(config.location))
self.set_fullscreen(config.fullscreen)
self.set_default_size(*self.window_size_check(config.size))
self['tabs'].set_tab_pos(getattr(Gtk.PositionType, config.tab_side.upper()))
self.setup_widgets()
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
@property
def active_tab(self):
tabs = self['tabs']
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()
return DotDict(width=width, height=height)
@property
def location(self):
x, y = self.get_position()
return DotDict(x=x, y=y)
@property
def screen_size(self):
screen = self.get_screen()
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]
with self.app.db.session as s:
tab_limit = s.get_config('closed_tabs_limit')
s.remove('tabs', tabid=tabid)
if tab_limit:
while len(self.closed) >= tab_limit:
self.remove_closed_tab(list(self.closed.keys())[0])
self.closed[tabid] = DotDict(
state = tab.webview.get_session_state(),
title = tab.title,
url = tab.url
)
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()
def fullscreen_toggle(self):
self.set_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
else:
tabid = row.tabid
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
if not switch and not s.get_config('load_tabs'):
tab.label.set_favicon('reload')
elif not url:
tab.homepage()
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)
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
def notification(self, text, level='INFO', timeout=5, system=False):
if system:
Notify.Notification.new(text).show()
else:
notif = self['notification']
label = self['notification-message']
label.set_text(text)
notif.set_reveal_child(True)
if timeout:
thread = NotificationTimeout(timeout, notif, label)
thread.start()
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')
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()
if not path:
logging.verbose('Canceled web page download')
return
self.new_tab(path, 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.active_tab = tab.tabid
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)
widgets = ['reply', 'boost', 'favorite']
with self.app.db.session as s:
acct_count = s.count('accounts')
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)
def set_navbar_url(self, url):
navurl = self['navbar-url']
if not navurl.has_focus():
navurl.set_text(url or '')
def set_fullscreen(self, isfull):
if isfull:
self.fullscreen()
else:
self.unfullscreen()
def window_location_check(self, location):
screen = self.screen_size
size = self.dimensions
if not location:
return screen.values()
if not location.x or not (0 < location.x + size.width < screen.width):
logging.verbose('Window offscreen horizontally:', location.x)
location.x = screen.width
if not location.y or not (0 < location.y + size.height < screen.height):
logging.verbose('Window offscreen vertically:', location.y)
location.y = screen.height
return location.x, location.y
def window_size_check(self, size):
screen = self.get_screen()
width, height = size.values()
if width > screen.width():
logging.verbose('Window too wide:', width)
width = 1024
if height > screen.height():
logging.verbose('Window too tall:', height)
height = 600
return width, height
def handle_about_link(self, about, url):
about.hide()
self.new_tab(url, switch=True)
return True
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['about-dialog'].show_all()
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]
self['tabs-closed-menu'].remove_item(tabid)
def handle_closed_tabs_show(self):
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')
if tab.tabid in self.taborder:
self.taborder.remove(tab.tabid)
self.taborder.insert(0, tab.tabid)
self.set_button_state(tab)
progress = tab.webview.get_estimated_load_progress()
self['navbar-url'].set_progress_fraction(0 if progress == 1.0 else progress)
def handle_subwindow_close(self, widget, event):
widget.hide()
return True
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 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.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.clipboard.wait_for_text())
self.handle_button('go')
## 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()
with self.app.db.session as s:
s.put_config('maximized', self.is_maximized())
s.put_config('fullscreen', self.is_fullscreen)
if not self.is_maximized():
width, height = self.get_size()
s.put_config('size', {'width': width, 'height': height})
x, y = self.get_position()
s.put_config('location', {'x': x, 'y': y})
logging.verbose('Finished saving data')
def handle_window_state(self, window, state):
self.is_fullscreen = bool(Gdk.WindowState.FULLSCREEN & state.new_window_state)
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)
## status bar
icon_set(self['statusbar-debug-icon'], 'debug', 16)
icon_set(self['statusbar-toot-icon'], 'toot', 16)
icon_set(self['statusbar-reply-icon'], 'reply', 16)
icon_set(self['statusbar-boost-icon'], 'boost', 16)
icon_set(self['statusbar-favorite-icon'], 'favorite', 16)
icon_set(self['statusbar-logins-icon'], 'password', 16)
icon_set(self['statusbar-bookmark-icon'], 'bookmark', 16)
icon_set(self['statusbar-siteoptions-icon'], 'permissions', 16)
## tab bar
#icon_set(self['tabs-closed-icon'].get_image(), 'debug', 16)
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}
]
})
for name, sigs in 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', {}))
connect(self['about-buttons'].get_children()[2], 'clicked', self['about-dialog'].hide)
connect(self['about-dialog'], 'delete-event', self.handle_subwindow_close, original_args=True)
connect(self['about-dialog'], 'activate_link', self.handle_about_link, original_args=True)
def setup_widgets(self):
self['autocomplete-menu'] = Menu()
self['statusbar-bookmark-category-menu'] = Menu()
self['tabs-closed-menu'] = Menu()
self['tabs-closed-menu'].new_action('clear', 'Clear closed tabs', self.handle_clear_closed_tabs)
self['tabs-closed-menu'].new_separator()
self['tabs-closed'].set_popup(self['tabs-closed-menu'])
self.add(self['main-overlay'])
self.themes = Themes(self)
self.menubar = MenuBar(self)
self.statusbar = StatusBar(self)
self.context = WebContext(self)
self.set_icons()
class NotificationTimeout(Thread):
def __init__(self, timeout, bar, label):
super().__init__(None, timeout, bar, label)
def run_func(self, timeout, bar, label):
self.stop_event.wait(timeout)
bar.set_reveal_child(False)
class AutocompleteTimeout(threading.Thread):
def __init__(self, timeout=1):
super().__init__()
self.exit_event = threading.Event()
self.lock = threading.Lock()
self.timeout = timeout
self.current_time = timeout
self.start()
@property
def url_entry(self):
return self.window['navbar-url']
@property
def window(self):
return get_app().window
def load_url(self, url):
self.window.active_tab.load_url(url)
def run(self):
while True:
time.sleep(1)
self.set_current_time(self.current_time - 1)
if self.exit_event.is_set():
return
if self.current_time <= 0:
break
print('loop', self.current_time)
text = self.url_entry.get_text()
with self.app.db.session as s:
rows = s.execute(f"SELECT * FROM history WHERE url LIKE '%{text}%' or title LIKE '%{text}%'")
run_in_gui_thread(self.handle_display_menu, rows)
def refresh(self):
self.set_current_time(self.timeout)
def set_current_time(self, new_time):
with self.lock:
self.current_time = new_time
def handle_display_menu(self, rows):
self.window.autocomplete_timer = None
menu = self.window['autocomplete-menu']
menu.clear_items()
for row in rows:
menu.new_action(row.url, row.title, self.handle_item_select, row)
if not len(menu.items):
logging.verbose('No items found for autocomplete')
return
menu.popup_at_widget(
self.url_entry,
Gdk.Gravity.SOUTH_WEST,
Gdk.Gravity.NORTH_WEST,
Gdk.Event.new(Gdk.EventType.NOTHING)
)
def handle_item_select(self, row, *args):
self.load_url(row.url)