Om wat te oefenen met het nieuwe Gobject Introspection, heb ik volgend programma geschreven.
Het is een browser met heel beperkte mogelijkheden. Zo is het downloaden en printen uitgezet, en kan ervoor gekozen worden om externe links te blokkeren.
Het is bedoeld om in fullscreen te draaien op een openbare pc, zodat klanten/bezoekers een door de eigenaar opgelegde website kan raadplegen, en niets meer. Bijvoorbeeld handig in een videotheek om IMDb en/of aanverwanten op te hebben staan.
Het programma heeft nog geen naam, dus ideëen zijn welkom

.
Dependencies:
python-gobject
gir1.2-gtk-3.0
gir1.2-webkit-3.0
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) 2011 Timo Vanwynsberghe
# 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 3 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, see <http://www.gnu.org/licenses/>.
"""
This program is a webbrowser with limited functionality. Features like
downloading and printing are disabled. And an option to turn of following
external links. This will make sure that links that are not on the given
domain (or subdomain) will be blocked.
What's its use then?
It is created for public computers, to let the users search the internet and
nothing else. Example:
A videostore can have this program in fullscreen with a couple of tabs open
like IMDb, and customers can lookup some extra info before they buy anything.
NOTE: you have to disable specific OS keybindings yourself.
"""
__version__ = "0.3"
import os.path
import tempfile
import urllib
import urlparse
import logging
from datetime import datetime
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GObject, GdkPixbuf
GObject.threads_init()
try:
from gi.repository import WebKit
except ImportError as exc:
raise SystemExit("GObject Introspection data for Webkit and libsoup are required!")
# ------------------------------------------------------------------------
# Start of user preferences
# ------------------------------------------------------------------------
# Enabling this will keep users on the given website (or subdomains). Links
# to other sites will be blocked.
# Value: True or False
BLOCK_LINKS = True
# The key (or keys) that will switch between fullscreen and windowed mode.
# Value: A valid key, like "F11" or "<Control><Alt>F"
FS_KEY = "F11"
# Wheter to start the program in fullscreen mode
# Value: True or False
START_FS = False
# A timer which will reset all pages to their default after inactivity
# NOTE: this isn't accurate to the second, so don't rely on that.
# Value: Number in minutes or zero to deactivate
RESET_TIMER = 0
# All the webpages that need to be loaded.
# Value: List of tuples in the form of (name, url)
PAGES = [
('Ubuntu', 'http://www.ubuntu.com'),
('Ubuntu-nl', 'http://ubuntu-nl.org'),
]
# Messages shown to the user.
## Blocked download, default: "Downloading is disabled"
BLOCKED_DL = "Downloading is disabled"
## Blocked link, default: "Following external links is disabled"
BLOCKED_LINK = "Following external links is disabled"
# ------------------------------------------------------------------------
# End of user preferences
# ------------------------------------------------------------------------
# Program constants
TEMPDIR = tempfile.gettempdir()
# Setup logging
format = "%(name)s: %(levelname)s: %(message)s"
logging.basicConfig(level=logging.DEBUG, format=format)
logger = logging.getLogger(__file__)
class InfoBar(Gtk.InfoBar):
"""
Custom Gtk.InfoBar with support for multiple labels, buttons and timeout
"""
def __init__(self, parent, primary_text, secondary_text=None,
message_type=Gtk.MessageType.INFO, buttons=(), timeout=0):
"""
InfoBar initializer
@param parent: a parent widget which can hold the infobar
@param primary_text: primary text
@param secondary_text: secondary text or None
@param message_type: one of the supported message types (INFO, WARNING,
QUESTION or ERROR)
@param buttons: list of buttons or empty list
@param timeout: if set, the infobar will disappear after timeout seconds
"""
if not isinstance(parent, Gtk.Box):
raise TypeError("Parent of infobar need to be of type Gtk.Box")
Gtk.InfoBar.__init__(self)
vbox = Gtk.VBox.new(False, 6)
self._primary_label = Gtk.Label()
vbox.pack_start(self._primary_label, True, True, 0)
self._primary_label.set_line_wrap(True)
self._primary_label.set_alignment(0, 0.5)
self._secondary_label = None
if secondary_text:
self._secondary_label = Gtk.Label()
vbox.pack_start(self._secondary_label, True, True, 0)
if buttons:
self._secondary_label.set_line_wrap(True)
self._secondary_label.set_alignment(0, 0.5)
self.update_text(primary_text, secondary_text)
if buttons:
for name,response_id in buttons:
self.add_button(name, response_id)
self.set_message_type(message_type)
self.get_content_area().pack_start(vbox, False, False, 0)
parent.pack_start(self, False, True, 0)
self.show_all()
if timeout:
GObject.timeout_add(timeout*1000, self._infobar_timeout, parent)
def update_text(self, primary_text="", secondary_text=""):
"""
Update the labels in the infobar
@param primary_text: text for the primary label
@param secondary_text: text for the secondary label
"""
if primary_text:
self._primary_label.set_markup("<b>%s</b>" %primary_text)
if secondary_text and self._secondary_label:
self._secondary_label.set_markup("<small>%s</small>" %secondary_text)
def _infobar_timeout(self, parent):
"""
Timeout function that will remove the infobar from its parent
@param parent: parent widget of the infobar
"""
parent.remove(self)
return False
class PageView(Gtk.VBox):
"""
Class that holds the toolbar and webview for each given website
"""
__gsignals__ = {'favicon-ready': (GObject.SIGNAL_RUN_LAST,
None, (str,)),
'loading-page': (GObject.SIGNAL_RUN_LAST,
None, (bool,)),
}
def __init__(self, url):
"""
Page initializer
@param url: the url for this page
"""
Gtk.VBox.__init__(self)
self._url = url
self._root = self._url2root(url)
self._favicon = None
# Initialize the toolbar
self._entry = Gtk.Entry()
self._entry.set_text(url)
self._entry.set_width_chars(80)
self._entry.set_editable(False)
self._address = Gtk.ToolItem()
self._address.add(self._entry)
self._back = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_BACK)
self._back.set_sensitive(False)
self._back.connect('clicked', self.on_back_clicked)
self._forward = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_FORWARD)
self._forward.set_sensitive(False)
self._forward.connect('clicked', self.on_forward_clicked)
self._reload = Gtk.ToolButton.new_from_stock(Gtk.STOCK_REFRESH)
self._reload.connect('clicked', self.on_reload_clicked)
self._stop = Gtk.ToolButton.new_from_stock(Gtk.STOCK_STOP)
self._stop.set_sensitive(False)
self._stop.connect('clicked', self.on_stop_clicked)
self._home = Gtk.ToolButton.new_from_stock(Gtk.STOCK_HOME)
self._home.connect('clicked', self.on_home_clicked)
toolbar = Gtk.Toolbar()
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
toolbar.insert(self._back, -1)
toolbar.insert(self._forward, -1)
toolbar.insert(self._reload, -1)
toolbar.insert(self._stop, -1)
toolbar.insert(self._home, -1)
toolbar.insert(self._address, -1)
self.pack_start(toolbar, False, False, 0)
# Initialize the browser view
self._webview = WebKit.WebView()
self._webframe = self._webview.get_main_frame()
# General WebKit signals
self._webview.connect('console-message', self.on_console_message)
self._webview.connect('notify::load-status', self.on_load_status)
self._webview.connect('icon-loaded', self.on_icon_loaded)
# Signals used for blocking features, add or remove if wanted
self._webview.connect('navigation-policy-decision-requested',
self.on_navigation_request)
self._webview.connect('download-requested', self.on_download_requested)
self._webview.connect('populate-popup', self.on_populate_popup)
self._webview.connect('print-requested', self.on_print_requested)
# Open the webpage
self._webview.load_uri(url)
sw = Gtk.ScrolledWindow()
sw.add(self._webview)
self.pack_start(sw, True, True, 0)
# Toolbar button callbacks
def on_back_clicked(self, toolbutton):
self._webview.go_back()
def on_forward_clicked(self, toolbutton):
self._webview.go_forward()
def on_reload_clicked(self, toolbutton):
self._webview.reload()
def on_stop_clicked(self, toolbutton):
self._webview.stop_loading()
def on_home_clicked(self, toolbutton):
self.show_home_page()
# Webkit callbacks
def on_load_status(self, webview, status):
status = webview.get_load_status()
if status == WebKit.LoadStatus.COMMITTED:
self.emit('loading-page', True)
self._set_loading(True)
self._entry.set_text(self._webview.get_uri())
elif status == WebKit.LoadStatus.FINISHED:
self.emit('loading-page', False)
self._set_loading(False)
def on_icon_loaded(self, webview, icon_uri):
if self._favicon is None:
self._download_favicon(icon_uri)
def on_console_message(self, webview, message, line, source_id):
# Disable console messages by WebKit.WebView
return True
def on_navigation_request(self, webview, frame, request, action, decision):
if BLOCK_LINKS and \
action.get_reason() == WebKit.WebNavigationReason.LINK_CLICKED:
uri = request.get_uri()
if not self._check_valid_link(uri):
logger.info("Link blocked: %s", uri)
InfoBar(self, BLOCKED_LINK, timeout=4)
decision.ignore()
return True
def on_download_requested(self, webview, download):
logger.info("Download blocked: %s", download.get_uri())
InfoBar(self, BLOCKED_DL, timeout=4)
return False
def on_populate_popup(self, webview, menu):
# We disable every item in the menu. Setting the menu insensitive
# will lead to focus issues.
for item in menu.get_children():
item.set_sensitive(False)
def on_print_requested(self, webview, frame):
return True
# Public methods
def show_home_page(self):
self._webview.open(self._url)
# Internal methods
def _set_loading(self, loading):
"""
Set widgets in the correct state when a page is loading or finished
@param loading: bool which indicates if the frame is loading
"""
self._stop.set_sensitive(loading)
self._reload.set_sensitive(not loading)
self._back.set_sensitive(self._webview.can_go_back())
self._forward.set_sensitive(self._webview.can_go_forward())
def _url2root(self, url):
"""
Get the root domain of an url
@param url: a full url
"""
host = urlparse.urlparse(url).hostname
return host if not host.startswith('www.') else host[4:]
def _check_valid_link(self, url):
"""
Check if a link is part of the main website
@param url: the url of the link
"""
linkroot = self._url2root(url)
rootlist = self._root.split('.')
linkrootlist = linkroot.split('.')
if linkrootlist[-len(rootlist):] != rootlist:
return False
return True
def _download_favicon(self, url):
"""
Download and set the favicon
@param url: the url of the favicon
"""
try:
self._favicon = os.path.join(TEMPDIR, "%s.ico" %self._root)
urllib.urlretrieve(url, self._favicon)
except Exception as exc:
logger.error("Downloading favicon failed: %s", exc)
self._favicon = None
else:
self.emit('favicon-ready', self._favicon)
class TabLabel(Gtk.HBox):
"""
The notebooktab label widget
"""
def __init__(self, name, pageview):
"""
Notebook label initializer
@param name: the name for the webpage
@param pageview: the pageview widget for this tab
"""
Gtk.HBox.__init__(self)
self._image = Gtk.Image()
self._spinner = Gtk.Spinner.new()
label = Gtk.Label()
label.set_markup("<big><b>%s</b></big>" %name)
self.set_spacing(4)
self.pack_start(self._spinner, False, False, 0)
self.pack_start(self._image, False, False, 0)
self.pack_start(label, False, False, 0)
self.show_all()
pageview.connect('loading-page', self.on_loading_page)
pageview.connect('favicon-ready', self.on_favicon_ready)
# Callbacks
def on_loading_page(self, pageview, loading):
self._spinner.start() if loading else self._spinner.stop()
self._image.set_property('visible', not loading)
self._spinner.set_property('visible', loading)
def on_favicon_ready(self, pageview, favicon):
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(favicon, 20, 20)
except Exception as exc:
logger.error("Setting favicon failed: %s", exc)
else:
self._image.set_from_pixbuf(pixbuf)
class MainView(Gtk.Window):
"""
Main window class which holds the notebook of webpages
"""
def __init__(self):
Gtk.Window.__init__(self)
self.connect('delete-event', self.quit)
self.connect('motion-notify-event', self.on_motion_notify_event)
self.resize(1050, 750)
self.set_title("Librarian")
self._fullscreen = False
self._can_motion_check = False
self.last_active = datetime.now()
accelgroup = Gtk.AccelGroup()
key, modifier = Gtk.accelerator_parse(FS_KEY)
accelgroup.connect(key, modifier, Gtk.AccelFlags.VISIBLE,
self.on_fullscreen_pressed)
self.add_accel_group(accelgroup)
self._notebook = Gtk.Notebook()
self._build_notebook_tabs()
self.add(self._notebook)
self.show_all()
self.set_fullscreen(START_FS)
if RESET_TIMER > 0:
GObject.timeout_add_seconds(4, self._motion_timer_cb)
GObject.timeout_add_seconds(4, self._reset_timer_cb)
def get_fullscreen(self):
return self._fullscreen
def set_fullscreen(self, fullscreen):
self._fullscreen = fullscreen
self.fullscreen() if fullscreen else self.unfullscreen()
is_fullscreen = GObject.property(get_fullscreen, set_fullscreen, bool, False)
# Callbacks
def quit(self, widget, event):
# Prevent quitting with alt+f4 when in fullscreen
if self.get_fullscreen():
return True
Gtk.main_quit()
def on_motion_notify_event(self, widget, event):
if self._can_motion_check:
self._can_motion_check = False
self.last_active = datetime.now()
return False
def on_fullscreen_pressed(self, accelgroup, acceleratable, keyval, modifier):
self.set_fullscreen(not self.is_fullscreen)
return True
def _motion_timer_cb(self):
self._can_motion_check = True
return True
def _reset_timer_cb(self):
diff = datetime.now() - self.last_active
if diff.seconds/60 == RESET_TIMER:
for page in self._notebook.get_children():
page.show_home_page()
self.last_active = datetime.now()
return True
# Internal methods
def _build_notebook_tabs(self):
"""
Build a notebook tab for each page defined in the preferences
"""
for name, url in PAGES:
logger.info("Setting up page for '%s - %s'", name, url)
view = PageView(url)
label = TabLabel(name, view)
self._notebook.append_page(view, label)
if __name__ == "__main__":
browser = MainView()
Gtk.main()
Edit: Foutjes uitgehaald en volledige GTK 3 ondersteuning.
Edit 2: START_FS en RESET_TIMER toegevoegd