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
2022-09-24 06:57:21 -04:00

625 lines
16 KiB
Python

import json, sys, threading, time
from izzylib_sql import Row
from .status_bar import StatusBar
from .web_tab import WebTab
from .. import var, __software__
from ..base import BuilderBase
from ..enums import LibraryPage, FileChooserResponse
from ..functions import Thread, run_in_gui_thread, connect, get_app, icon_set
from ..objects import Notification, WebviewState
from ..themes import Themes
from ..widgets import FileChooser, Menu
class Window(BuilderBase, Gtk.ApplicationWindow):
iconsize = Gtk.IconSize.BUTTON
is_fullscreen = False
startup = True
shutdown = False
autocomplete_timer = None
def __init__(self, app):
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 = {}
self.taborder = []
self.accounts = {}
self.closed = {}
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.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()))
self.setup_widgets()
self.setup_signals()
## Connect window signals
Gtk.ApplicationWindow.connect(self, 'window-state-event', self.handle_window_state)
Gtk.ApplicationWindow.connect(self, 'delete-event', self.handle_window_close)
@property
def active_tab(self):
tabs = self['tabs']
return tabs.get_nth_page(tabs.get_current_page())
@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 file_open(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')
fc.set_callback(FileChooserResponse.OK, self.handle_file_open)
fc.set_callback(FileChooserResponse.CANCEL, logging.verbose, 'Canceled file open')
fc.run()
def fullscreen_set(self, is_fullscreen=None):
if is_fullscreen == None:
is_fullscreen = not self.is_fullscreen
if is_fullscreen:
self.fullscreen()
else:
self.unfullscreen()
def library_open(self, page=LibraryPage.HOME):
if not isinstance(page, LibraryPage):
try:
page = LibraryPage[page.upper()]
except KeyError:
raise KeyError(f'Invalid library page: {page}')
url = var.local + '/' + page.value
#for tab in self.tabdata.values():
#if tab.url.startswith(url):
#return self.switch_tab(tab)
self.tab_new(url, switch=True)
def notification(self, text, level='INFO', timeout=5, system=False):
if not text:
raise ValueError('Empty text')
if system:
self.app.send_notification(random_str(), Notification(__software__, text))
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 set_button_state(self, tabid=None):
try:
tab = self.active_tab if not tabid else self.tabdata[tabid]
except KeyError:
if not self.startup:
logging.warning('window.set_button_state: Cannot find tab with id:', tabid)
return
active = tab.url and not any(map(tab.url.startswith, [var.local, 'source']))
for widget_name in ['siteoptions', 'bookmark', 'logins']:
self[f'statusbar-{widget_name}'].set_sensitive(active)
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._data.post and acct_count > 0)
def tab_close(self, tabid=None):
tab = self.active_tab if not tabid else self.tabdata[tabid]
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.tab_remove_closed(list(self.closed.keys())[0])
state = tab.state_unload()
self.closed[tab.id] = state
self['tabs-closed-menu'].new_action_at_pos(2, tab.id, state.title or 'Untitled', self.tab_reopen, tab.id)
self['tabs-closed-menu'][tab.id].set_tooltip_text(state.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.tab_new()
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
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 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):
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_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_file_open(self, path):
self.tab_new(f'file://{path}', switch=True)
def handle_menu_button(self, name, arg=None):
tab = self.active_tab
if name == 'new_tab':
with self.db.session as s:
self.tab_new(s.get_config('homepage'), switch=True)
elif name == 'search':
self.active_tab.search_action('open')
elif name == 'fullscreen':
self.fullscreen_set()
elif name == 'library':
self.library_open()
elif name == 'about':
self.library_open('help')
elif name == 'quit':
self.app.quit()
else:
raise KeyError('window.handle_button: Button not handled:', name)
self['tabs-menu-popover'].popdown()
def handle_menubar_file_action(self, action):
self.active_tab.page_action(action)
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.hide()
self.app.password.stop()
self.tabs_save()
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):
## tab bar
self.set_icon_from_resource('navbar-menu-icon', 'menu.svg', 24)
## status bar
icon_set(self['statusbar-debug-icon'], 'debug', 20)
icon_set(self['statusbar-toot-icon'], 'toot', 20)
icon_set(self['statusbar-reply-icon'], 'reply', 20)
icon_set(self['statusbar-boost-icon'], 'boost', 20)
icon_set(self['statusbar-favorite-icon'], 'favorite', 20)
icon_set(self['statusbar-logins-icon'], 'password', 20)
icon_set(self['statusbar-bookmark-icon'], 'bookmark', 20)
icon_set(self['statusbar-siteoptions-icon'], 'permissions', 20)
## tab bar
#icon_set(self['tabs-closed-icon'].get_image(), 'debug', 16)
def setup_signals(self):
## Misc
self.connect('notification-close', 'clicked', self['notification'].set_reveal_child, False)
## 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)
## 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):
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.statusbar = StatusBar(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
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)