Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 666 lines (548 sloc) 23.9 KB
#!/usr/bin/env python
# quickbrowse: a fast, minimal, anonymous browser in python-webkit-gtk.
# Copyright (C) 2016 by Akkana Peck.
# Originally based on /usr/share/doc/python-webkit/examples/browser.py
# written by Jan Michael Alonzo <jmalonzo@gmai.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from gettext import gettext as _
import sys, os
import gobject
import gtk
import pango
import webkit
# from inspector import Inspector
import signal
ABOUT_PAGE = """
<html><head><title>Hello, world</title></head><body>
<h1>Welcome to Quickbrowse</h1>
</body></html>
"""
# The file used to remotely trigger the browser to open more tabs.
# The %d will be the process ID of the running browser.
URL_FILE = "/tmp/quickbrowse-urls-%d"
class BrowserView(webkit.WebView):
'''A WebView in a single tab.
'''
def __init__(self, contentPane):
webkit.WebView.__init__(self)
self.contentPaneParent = contentPane
settings = self.get_settings()
settings.set_property("enable-developer-extras", True)
self.verbose = False
# scale other content besides from text as well
self.set_full_content_zoom(True)
# make sure the items will be added in the end
# hence the reason for the connect_after
self.connect_after("populate-popup", self.populate_popup)
self.connect('navigation-policy-decision-requested',
self.navigation_policy_decision)
self.connect("key-press-event", self.key_press_event)
# Block the rediculously chatty stream of console messages.
# Alas, it doesn't block "Vector smash protection is enabled."
self.connect("console-message", self.console_message_handler)
def key_press_event(self, widget, event):
# Can't trap ctrl-q to quit here -- it never comes through.
# That has to be in a separate key_press_handler on the Window.
if (event.keyval == gtk.keysyms.Left and \
event.state & gtk.gdk.MOD1_MASK) :
self.go_back();
return True
elif (event.keyval == gtk.keysyms.Right and \
event.state & gtk.gdk.MOD1_MASK) :
self.go_forward();
return True
return False # False means we haven't handled, pass event on
def console_message_handler(self, view, message, line, sourceid):
if self.verbose:
if sourceid:
print message, "sourceid:", sourceid
else:
print message
return True
def populate_popup(self, view, menu):
# These don't work, because they call callbacks that are
# members of a different class, WebToolbar.
# Need to add all the elaborate "emit" structure
# before any of these will work.
# zoom buttons
zoom_in = gtk.ImageMenuItem(gtk.STOCK_ZOOM_IN)
zoom_in.connect('activate', self.zoom_in_cb, view)
menu.append(zoom_in)
zoom_out = gtk.ImageMenuItem(gtk.STOCK_ZOOM_OUT)
zoom_out.connect('activate', self.zoom_out_cb, view)
menu.append(zoom_out)
zoom_hundred = gtk.ImageMenuItem(gtk.STOCK_ZOOM_100)
zoom_hundred.connect('activate', self.zoom_hundred_cb, view)
menu.append(zoom_hundred)
printitem = gtk.ImageMenuItem(gtk.STOCK_PRINT)
menu.append(printitem)
printitem.connect('activate', self.print_cb, view)
page_properties = gtk.ImageMenuItem(gtk.STOCK_PROPERTIES)
menu.append(page_properties)
page_properties.connect('activate', self.page_properties_cb, view)
menu.append(gtk.SeparatorMenuItem())
aboutitem = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
menu.append(aboutitem)
aboutitem.connect('activate', self.about_pywebkitgtk_cb, view)
menu.show_all()
return False
def navigation_policy_decision(self, wv, fr, req, action, decision):
if action.get_button() == 2:
# https://webkitgtk.org/reference/webkitgtk/stable/WebKitWebNavigationAction.html
decision.ignore()
self.contentPaneParent.new_tab(action.get_original_uri())
else:
decision.use()
class TabLabel (gtk.HBox):
"""A class for Tab labels"""
__gsignals__ = {
"close": (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
(gobject.TYPE_OBJECT,))
}
def __init__ (self, title, child):
"""initialize the tab label"""
gtk.HBox.__init__(self, False, 4)
self.title = title
self.child = child
self.label = gtk.Label(title)
self.label.props.max_width_chars = 30
self.label.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
self.label.set_alignment(0.0, 0.5)
icon = gtk.image_new_from_stock(gtk.STOCK_ORIENTATION_PORTRAIT,
gtk.ICON_SIZE_BUTTON)
close_image = gtk.image_new_from_stock(gtk.STOCK_CLOSE,
gtk.ICON_SIZE_MENU)
close_button = gtk.Button()
close_button.set_relief(gtk.RELIEF_NONE)
close_button.connect("clicked", self._close_tab, child)
close_button.set_image(close_image)
self.pack_start(icon, False, False, 0)
self.pack_start(self.label, True, True, 0)
self.pack_start(close_button, False, False, 0)
self.set_data("label", self.label)
self.set_data("close-button", close_button)
self.connect("style-set", self.tab_label_style_set_cb)
def set_label (self, text):
"""sets the text of this label"""
self.label.set_label(text)
def _close_tab (self, widget, child):
self.emit("close", child)
def tab_label_style_set_cb (self, tab_label, style):
context = tab_label.get_pango_context()
metrics = context.get_metrics(tab_label.style.font_desc,
context.get_language())
char_width = metrics.get_approximate_digit_width()
(width, height) = gtk.icon_size_lookup(gtk.ICON_SIZE_MENU)
tab_label.set_size_request(20 * pango.PIXELS(char_width) + 2 * width,
pango.PIXELS(metrics.get_ascent() +
metrics.get_descent()) + 8)
class ContentPane (gtk.Notebook):
__gsignals__ = {
"focus-view-title-changed": (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
(gobject.TYPE_OBJECT,
gobject.TYPE_STRING,)),
"new-window-requested": (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
(gobject.TYPE_OBJECT,))
}
def __init__ (self):
"""initialize the content pane"""
gtk.Notebook.__init__(self)
self.props.scrollable = True
self.props.homogeneous = True
self.connect("switch-page", self._switch_page)
self.show_all()
self._hovered_uri = None
def current_view(self):
'''Returns the BrowserView for the current tab'''
return self.get_nth_page(self.get_current_page()).get_child()
def load (self, text):
"""load the given uri in the current web view"""
child = self.get_nth_page(self.get_current_page())
view = child.get_child()
view.open(text)
def new_tab_with_webview (self, webview):
"""creates a new tab with the given webview as its child"""
self._construct_tab_view(webview)
def new_tab (self, url=None):
"""creates a new page in a new tab"""
# create the tab content
browser = BrowserView(self)
self._construct_tab_view(browser, url)
def _construct_tab_view (self, web_view, url=None):
web_view.connect("hovering-over-link", self._hovering_over_link_cb)
web_view.connect("populate-popup", self._populate_page_popup_cb)
web_view.connect("load-finished", self._view_load_finished_cb)
web_view.connect("create-web-view", self._new_web_view_request_cb)
web_view.connect("title-changed", self._title_changed_cb)
# inspector = Inspector(web_view.get_web_inspector())
# web_view.connect("button-press", self.button_press_cb)
scrolled_window = gtk.ScrolledWindow()
scrolled_window.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC
scrolled_window.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC
scrolled_window.add(web_view)
scrolled_window.show_all()
# create the tab
label = TabLabel(url, scrolled_window)
label.connect("close", self._close_tab)
label.show_all()
new_tab_number = self.append_page(scrolled_window, label)
self.set_tab_label_packing(scrolled_window, False, False,
gtk.PACK_START)
self.set_tab_label(scrolled_window, label)
# hide the tab if there's only one
self.set_show_tabs(self.get_n_pages() > 1)
self.show_all()
self.set_current_page(new_tab_number)
# Make sure it has a scheme -- add http:// if none already.
url = WebUtils.urlify(url)
# load the content
self._hovered_uri = None
if url:
web_view.load_uri(url)
else:
web_view.load_string(ABOUT_PAGE, "text/html", "iso-8859-15",
"about")
def button_press_cb(self, thing):
print type(thing)
print thing
def _populate_page_popup_cb(self, view, menu):
# misc
if self._hovered_uri:
open_in_new_tab = gtk.MenuItem(_("Open Link in New Tab"))
open_in_new_tab.connect("activate", self._open_in_new_tab, view)
menu.insert(open_in_new_tab, 0)
menu.show_all()
def _open_in_new_tab (self, menuitem, view):
self.new_tab(self._hovered_uri)
def _close_tab (self, label, child):
page_num = self.page_num(child)
if page_num != -1:
view = child.get_child()
view.destroy()
self.remove_page(page_num)
self.set_show_tabs(self.get_n_pages() > 1)
def _switch_page (self, notebook, page, page_num):
child = self.get_nth_page(page_num)
view = child.get_child()
frame = view.get_main_frame()
self.emit("focus-view-title-changed", frame, frame.props.title)
def _hovering_over_link_cb (self, view, title, uri):
# if uri:
# print "hovering over", uri
self._hovered_uri = uri
def _title_changed_cb (self, view, frame, title):
child = self.get_nth_page(self.get_current_page())
label = self.get_tab_label(child)
label.set_label(title)
self.emit("focus-view-title-changed", frame, title)
def _view_load_finished_cb(self, view, frame):
child = self.get_nth_page(self.get_current_page())
label = self.get_tab_label(child)
title = frame.get_title()
if not title:
title = frame.get_uri()
if title:
label.set_label(title)
def _new_web_view_request_cb (self, web_view, web_frame):
scrolled_window = gtk.ScrolledWindow()
scrolled_window.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC
scrolled_window.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC
view = BrowserView(self)
scrolled_window.add(view)
scrolled_window.show_all()
vbox = gtk.VBox(spacing=1)
vbox.pack_start(scrolled_window, True, True)
window = gtk.Window()
window.add(vbox)
view.connect("web-view-ready", self._new_web_view_ready_cb)
return view
def _new_web_view_ready_cb (self, web_view):
self.emit("new-window-requested", web_view)
class WebToolbar(gtk.Toolbar):
__gsignals__ = {
"load-requested": (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
(gobject.TYPE_STRING,)),
"go-back-requested": (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE, ()),
"go-forward-requested": (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE, ()),
"new-tab-requested": (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE, ()),
"view-source-mode-requested": (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
(gobject.TYPE_BOOLEAN, ))
}
def __init__(self, location_enabled=True, toolbar_enabled=True):
gtk.Toolbar.__init__(self)
backButton = gtk.ToolButton(gtk.STOCK_GO_BACK)
backButton.connect("clicked", self.back_cb)
self.insert(backButton, -1)
backButton.show()
forwardButton = gtk.ToolButton(gtk.STOCK_GO_FORWARD)
forwardButton.connect("clicked", self.forward_cb)
self.insert(forwardButton, -1)
forwardButton.show()
# location entry
if location_enabled:
self._entry = gtk.Entry()
self._entry.connect('activate', self._entry_activate_cb)
entry_item = gtk.ToolItem()
entry_item.set_expand(True)
entry_item.add(self._entry)
self._entry.show()
self.insert(entry_item, -1)
entry_item.show()
# add tab button
if toolbar_enabled:
addTabButton = gtk.ToolButton(gtk.STOCK_ADD)
addTabButton.connect("clicked", self._add_tab_cb)
self.insert(addTabButton, -1)
addTabButton.show()
viewSourceItem = gtk.ToggleToolButton(gtk.STOCK_PROPERTIES)
viewSourceItem.set_label("View Source Mode")
viewSourceItem.connect('toggled', self._view_source_mode_cb)
self.insert(viewSourceItem, -1)
viewSourceItem.show()
def back_cb(self, w):
self.emit("go-back-requested")
def forward_cb(self, w):
self.emit("go-forward-requested")
def location_set_text (self, text):
self._entry.set_text(text)
def _entry_activate_cb(self, entry):
self.emit("load-requested", WebUtils.urlify(entry.props.text))
def _add_tab_cb(self, button):
self.emit("new-tab-requested")
def _view_source_mode_cb(self, button):
self.emit("view-source-mode-requested", button.get_active())
# context menu item callbacks
def about_pywebkitgtk_cb(self, menu_item, web_view):
web_view.open("http://live.gnome.org/PyWebKitGtk")
def zoom_in_cb(self, menu_item, web_view):
"""Zoom into the page"""
web_view.zoom_in()
def zoom_out_cb(self, menu_item, web_view):
"""Zoom out of the page"""
web_view.zoom_out()
def zoom_hundred_cb(self, menu_item, web_view):
"""Zoom 100%"""
if not (web_view.get_zoom_level() == 1.0):
web_view.set_zoom_level(1.0)
def print_cb(self, menu_item, web_view):
mainframe = web_view.get_main_frame()
mainframe.print_full(gtk.PrintOperation(),
gtk.PRINT_OPERATION_ACTION_PRINT_DIALOG);
def page_properties_cb(self, menu_item, web_view):
mainframe = web_view.get_main_frame()
datasource = mainframe.get_data_source()
main_resource = datasource.get_main_resource()
window = gtk.Window()
window.set_default_size(100, 60)
vbox = gtk.VBox()
hbox = gtk.HBox()
hbox.pack_start(gtk.Label("MIME Type :"), False, False)
hbox.pack_end(gtk.Label(main_resource.get_mime_type()), False, False)
vbox.pack_start(hbox, False, False)
hbox2 = gtk.HBox()
hbox2.pack_start(gtk.Label("URI : "), False, False)
hbox2.pack_end(gtk.Label(main_resource.get_uri()), False, False)
vbox.pack_start(hbox2, False, False)
hbox3 = gtk.HBox()
hbox3.pack_start(gtk.Label("Encoding : "), False, False)
hbox3.pack_end(gtk.Label(main_resource.get_encoding()), False, False)
vbox.pack_start(hbox3, False, False)
window.add(vbox)
window.show_all()
window.present()
class WebBrowserWindow(gtk.Window):
'''The winddow containing the browser.
'''
def __init__(self):
gtk.Window.__init__(self)
toolbar = WebToolbar()
self.content_tabs = ContentPane()
self.content_tabs.connect("focus-view-title-changed",
self._title_changed_cb, toolbar)
self.content_tabs.connect("new-window-requested",
self._new_window_requested_cb)
toolbar.connect("load-requested", self.load_requested_cb,
self.content_tabs)
toolbar.connect("go-back-requested", self.go_back_requested_cb,
self.content_tabs)
toolbar.connect("go-forward-requested", self.go_forward_requested_cb,
self.content_tabs)
toolbar.connect("new-tab-requested", self.new_tab_requested_cb,
self.content_tabs)
toolbar.connect("view-source-mode-requested",
self.view_source_mode_requested_cb, self.content_tabs)
vbox = gtk.VBox(spacing=1)
vbox.pack_start(toolbar, expand=False, fill=False)
vbox.pack_start(self.content_tabs)
self.add(vbox)
self.set_default_size(1024, 768)
self.connect('destroy', self.destroy_cb, self.content_tabs)
# Key events independent of any tab, like ctrl-q to quit,
# have to happen on the window or they won't be seen.
self.connect("key-press-event", self.key_press_event)
self.show_all()
def key_press_event(self, widget, event):
if (event.keyval == gtk.keysyms.q and
event.state & gtk.gdk.CONTROL_MASK) :
gtk.main_quit()
return False # False means we haven't handled, pass event on
def destroy_cb(self, window, content_pane):
"""destroy window resources"""
num_pages = content_pane.get_n_pages()
while num_pages != -1:
child = content_pane.get_nth_page(num_pages)
if child:
view = child.get_child()
num_pages = num_pages - 1
window.destroy()
gtk.main_quit()
def new_tab(self, url):
self.content_tabs.new_tab(url)
def signal_handler(self, signal, frame):
with open(URL_FILE % os.getpid()) as url_fp:
for url in url_fp:
self.new_tab(url.strip())
def new_tab_requested_cb (self, toolbar, content_pane):
content_pane.new_tab("about:blank")
def _new_window_requested_cb (self, content_pane, view):
features = view.get_window_features()
window = view.get_toplevel()
scrolled_window = view.get_parent()
if features.get_property("scrollbar-visible"):
scrolled_window.props.hscrollbar_policy = gtk.POLICY_NEVER
scrolled_window.props.vscrollbar_policy = gtk.POLICY_NEVER
isLocationbarVisible = features.get_property("locationbar-visible")
isToolbarVisible = features.get_property("toolbar-visible")
if isLocationbarVisible or isToolbarVisible:
toolbar = WebToolbar(isLocationbarVisible, isToolbarVisible)
scrolled_window.get_parent().pack_start(toolbar, False, False, 0)
window.set_default_size(features.props.width, features.props.height)
window.move(features.props.x, features.props.y)
window.show_all()
return True
def _title_changed_cb (self, tabbed_pane, frame, title, toolbar):
if not title:
title = frame.get_uri()
self.set_title(_("quickbrowse - %s") % title)
uri = frame.get_uri()
if uri:
toolbar.location_set_text(uri)
def load_requested_cb (self, widget, text, content_pane):
if not text:
return
content_pane.load(text)
def go_back_requested_cb (self, widget, content_pane):
content_pane.current_view().go_back()
def go_forward_requested_cb (self, widget, content_pane):
content_pane.current_view().go_forward()
def view_source_mode_requested_cb(self, widget, is_active, content_pane):
currentTab = content_pane.get_nth_page(content_pane.get_current_page())
childView = currentTab.get_child()
childView.set_view_source_mode(is_active)
childView.reload()
class WebUtils:
@staticmethod
def urlify(url):
if ':' in url:
return url
if url.startswith('/'):
return 'file://' + url
return 'http://' + url
@staticmethod
def get_from_clipboard(self, which=gtk.gdk.SELECTION_PRIMARY):
clip = gtk.clipboard_get(which)
if not clip.wait_is_text_available():
return None
return clip.wait_for_text()
@staticmethod
def get_procname(procargs):
'''Return the program name: either the first commandline argument,
or, if that argument is some variant of "python", the second.
'''
basearg = os.path.basename(procargs[0])
if basearg == 'python' or basearg == 'python2' \
or basearg.startswith('python2.'):
basearg = os.path.basename(procargs[1])
return basearg
@staticmethod
def find_proc_by_name(name):
"""Looks for a process with the given basename, ignoring a possible
"python" prefix in case it's another Python script.
Will ignore the current running process.
Returns the pid, or None.
"""
PROCDIR = '/proc'
for proc in os.listdir(PROCDIR):
if not proc[0].isdigit():
continue
# Race condition: processes can come and go, so we may not be
# able to open something just because it was there when we
# did the listdir.
try:
with open(os.path.join(PROCDIR, proc, 'cmdline')) as procfp:
procargs = procfp.read().split('\0')
basearg = WebUtils.get_procname(procargs)
if basearg == name and int(proc) != os.getpid():
return int(proc)
except Exception, e:
print "exception", e
pass
return None
if __name__ == "__main__":
SIGNAL = signal.SIGUSR1
args = sys.argv[1:]
if len(sys.argv) < 2:
url = WebUtils.get_from_clipboard(gtk.gdk.SELECTION_PRIMARY)
if not url:
url = WebUtils.get_from_clipboard(gtk.gdk.SELECTION_CLIPBOARD)
if url:
urls = [ url ]
else:
urls = []
else:
urls = sys.argv[1:]
if args and args[0] == "--new-tab":
# Try to use an existing instance of quickbrowse
# instead of creating a new window.
urls = args[1:]
progname = WebUtils.get_procname(sys.argv)
pid = WebUtils.find_proc_by_name(progname)
if pid:
# Create the file of URLs:
with open(URL_FILE % pid, 'w') as url_fp:
for url in urls:
print >>url_fp, url
os.kill(int(pid), SIGNAL)
sys.exit(0)
print "No existing %s process: starting a new one." % progname
# Return control to the shell before creating the window:
rc = os.fork()
if rc:
sys.exit(0)
webbrowser = WebBrowserWindow()
for url in urls:
webbrowser.new_tab(url)
# Handle SIGUSR1 signal so we can be signalled to open other tabs:
signal.signal(SIGNAL, webbrowser.signal_handler)
gtk.main()