Last active
December 26, 2015 13:49
-
-
Save jsoffer/7161376 to your computer and use it in GitHub Desktop.
Navegador mínimo en Python y Webkit, toma 2. Incluye tabs. Basado en foobrowser (https://code.google.com/p/foobrowser/)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
""" | |
minimalistic browser levering off of Python, PyQt and Webkit | |
Original: https://code.google.com/p/foobrowser/ | |
[email protected] | |
+ tabs | |
+ descargar con click derecho + menú: requiere auxiliar | |
+ proxy | |
+ actualizar dirección al cambiar de página | |
+ ctrl-l: address bar | |
+ ctrl-q: salir | |
+ dirección destino, [title], ¿status bar? | |
+ ^J como Enter en address bar | |
+ zoom | |
+ buscar en página | |
+ detecta si es búsqueda o completa el http:// | |
+ movimiento con jk (falta hl) | |
+ paste (y visita sitio) con 'y' | |
+ verifica no tráfico no solicitado | |
Pendiente: | |
* navegación con teclado | |
* historia en address bar [medio - falta sesiones anteriores] | |
* ^H en address bar es backspace | |
Copyright (c) 2012, Davyd McColl; 2013, Jaime Soffer | |
All rights reserved. | |
Redistribution and use in source and binary forms, with or without | |
modification, are permitted provided that the following conditions are met: | |
Redistributions of source code must retain the above copyright notice, this | |
list of conditions and the following disclaimer. | |
Redistributions in binary form must reproduce the above copyright notice, | |
this list of conditions and the following disclaimer in the documentation | |
and/or other materials provided with the distribution. | |
Neither the name of the involved organizations nor the names of its | |
contributors may be used to endorse or promote products derived from this | |
software without specific prior written permission. | |
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | |
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF | |
THE POSSIBILITY OF SUCH DAMAGE. | |
""" | |
from PyQt4 import Qt, QtGui, QtCore, QtWebKit, QtNetwork | |
import os | |
import sys | |
import socket | |
# clipboard; global, feo pero parece apropiado (es global a todo el sistema) | |
cb = None | |
def log(s): | |
print(s) | |
def registerShortcuts(actions, defaultOwner): | |
for action in actions: | |
shortcut = actions[action][1] | |
if shortcut.lower() == "none": | |
continue | |
# allow multiple shortcuts with keys delimited by | | |
shortcuts = shortcut.split("|") | |
for shortcut in shortcuts: | |
shortcut = shortcut.strip() | |
if shortcut == "": | |
continue | |
callback = actions[action][0] | |
if len(actions[action]) == 2: | |
owner = defaultOwner | |
else: | |
if type(actions[action][2]) != str: | |
owner = actions[action][2] | |
elif len(actions[action]) == 4: | |
owner = actions[action][3] | |
else: | |
owner = defaultOwner | |
QtGui.QShortcut(shortcut, owner, callback) | |
class WebView(QtWebKit.QWebView): | |
def __init__(self, parent = None): | |
self.parent = parent | |
QtWebKit.QWebView.__init__(self, parent) | |
def createWindow(self, type): | |
return self.parent.browser.addTab().webkit | |
class WebTab(QtGui.QWidget): | |
def __init__(self, browser, actions=None, parent=None, showStatusBar=False): | |
QtGui.QWidget.__init__(self, parent) | |
self.actions = dict() | |
self.grid = QtGui.QGridLayout(self) | |
self.grid.setSpacing(0) | |
self.grid.setContentsMargins(0,0,0,0) | |
self.cmb = QtGui.QComboBox() | |
self.cmb.setEditable(True) | |
self.browser = browser | |
self.webkit = WebView(self) | |
self.webkit.page().setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks) | |
self.webkit.linkClicked.connect(self.onLinkClick) | |
self.webkit.settings().setAttribute(QtWebKit.QWebSettings.PluginsEnabled,False) | |
self.webkit.settings().setAttribute(QtWebKit.QWebSettings.JavascriptEnabled,False) | |
#self.webkit.settings().setAttribute(QtWebKit.QWebSettings.CookiesEnabled,False) | |
#self.webkit.settings().setAttribute(QtWebKit.QWebSettings.SpatialNavigationEnabled,True) | |
#self.webkit.settings().setAttribute(QtWebKit.QWebSettings.DeveloperExtrasEnabled,True) | |
self.pbar = QtGui.QProgressBar() | |
self.pbar.setRange(0, 100) | |
self.pbar.setTextVisible(False) | |
self.grid.addWidget(self.cmb, 0, 0) | |
self.grid.addWidget(self.pbar, 1, 0, 1, self.grid.columnCount()) | |
self.grid.addWidget(self.webkit, 2, 0, 1, self.grid.columnCount()) | |
self.pbar.setVisible(False) | |
self.pbar.setMaximumHeight(7) | |
self.fraSearch = QtGui.QFrame() | |
self.searchGrid = QtGui.QGridLayout(self.fraSearch) | |
self.searchGrid.setSpacing(0) | |
self.lblSearch = QtGui.QLabel("Find text in page:") | |
self.txtSearch = QtGui.QLineEdit() | |
self.btnClearSearch = QtGui.QPushButton("[X]") | |
self.searchGrid.addWidget(self.lblSearch, 0, 0) | |
self.searchGrid.addWidget(self.txtSearch, 0, 1) | |
self.searchGrid.addWidget(self.btnClearSearch, 0, 2) | |
self.statusbar = QtGui.QStatusBar() | |
self.statusbar.setVisible(showStatusBar) | |
self.statusbar.setMaximumHeight(25) | |
self.grid.addWidget(self.statusbar, self.grid.rowCount(), 0, 1, self.grid.columnCount()) | |
for i in range(2): | |
self.searchGrid.setColumnStretch(i, i % 2) | |
self.fraSearch.setVisible(False) | |
self.grid.addWidget(self.fraSearch, self.grid.rowCount() + 1, 0, 1, self.grid.columnCount()) | |
for c in range(self.grid.columnCount() + 1): | |
self.grid.setColumnStretch(c, 0) | |
for r in range(self.grid.rowCount() + 1): | |
self.grid.setRowStretch(r, 0) | |
self.grid.setRowStretch(2, 1) | |
self.grid.setColumnStretch(0, 1) | |
self.connect(self.cmb, QtCore.SIGNAL("currentIndexChanged(int)"), self.navigate) | |
self.connect(self.webkit, QtCore.SIGNAL("loadStarted()"), self.loadStarted) | |
self.connect(self.webkit, QtCore.SIGNAL("loadFinished(bool)"), self.loadFinished) | |
self.connect(self.webkit, QtCore.SIGNAL("titleChanged(QString)"), self.setTitle) | |
self.connect(self.webkit, QtCore.SIGNAL("loadProgress(int)"), self.loadProgress) | |
self.connect(self.webkit, QtCore.SIGNAL("urlChanged(QUrl)"), self.setURL) | |
self.connect(self.webkit.page(), QtCore.SIGNAL("linkHovered(QString, QString, QString)"), self.onLinkHovered) | |
page = self.webkit.page() | |
page.downloadRequested.connect(self.onDownloadRequested) | |
page.setForwardUnsupportedContent(True) | |
page.unsupportedContent.connect(self.onUnsupportedContent) | |
self.connect(self.btnClearSearch, QtCore.SIGNAL("clicked()"), self.stopOrHideSearch) | |
self.connect(self.txtSearch, QtCore.SIGNAL("textChanged(QString)"), self.doSearch) | |
self.registerActions() | |
registerShortcuts(self.actions, self) | |
self.cmb.setFocus() | |
self.showHideMessage() | |
self.netmanager = InterceptNAM(app) | |
page.setNetworkAccessManager(self.netmanager); | |
def onLinkClick(self, qurl): | |
self.navigate(qurl.toString()) | |
def registerActions(self): | |
self.actions["addressnav"] = [self.navigate, "Ctrl+J|Enter", self.cmb, "Navigate to the url in the address bar"] | |
self.actions["reload"] = [self.reload, "F5|Ctrl+R", "Reload the current page"] | |
self.actions["back"] = [self.back, "Alt+Left", "Go back in history"] | |
self.actions["fwd"] = [self.fwd, "Alt+Right", "Go forward in history"] | |
self.actions["smartsearch"] = [self.smartSearch, "G", "Smart search (find next or start search)"] | |
self.actions["stopsearch"] = [self.stopOrHideSearch, "Escape", self.fraSearch, "Stop current load or searching"] | |
self.actions["findnext"] = [self.doSearch, "Return", self.txtSearch, "Next match for current search"] | |
self.actions["togglestatus"]= [self.toggleStatus, "Ctrl+Space", "Toggle visibility of status bar"] | |
# el scroll debería ser el mismo de apretar flecha arriba / flecha abajo | |
self.actions["scrolldown"] = [lambda: self.webkit.page().mainFrame().scroll(0,40), "J", "Scrolls down"] | |
self.actions["scrollup"] = [lambda: self.webkit.page().mainFrame().scroll(0,-40), "K", "Scrolls down"] | |
self.actions["paste"] = [lambda: self.navigate(cb.text(Qt.QClipboard.Selection)), "Y", "Access to clipboard"] | |
self.actions["togglejs"] = [self.toggleScript, "Q", "Switches javascript on/off"] | |
def toggleScript(self): | |
if self.webkit.settings().testAttribute(QtWebKit.QWebSettings.JavascriptEnabled): | |
self.webkit.settings().setAttribute(QtWebKit.QWebSettings.JavascriptEnabled,False) | |
self.cmb.setStyleSheet("QComboBox { background-color: #fff; }") | |
else: | |
self.webkit.settings().setAttribute(QtWebKit.QWebSettings.JavascriptEnabled,True) | |
self.cmb.setStyleSheet("QComboBox { background-color: #eef; }") | |
def toggleStatus(self): | |
if self.browser: | |
self.browser.toggleStatusVisiblity() | |
else: | |
self.statusbar.setVisible(not self.statusBar.isVisible()) | |
def setStatusVisibility(self, visible): | |
self.statusbar.setVisible(visible) | |
def onUnsupportedContent(self, reply): | |
log("Unsupported content %s" % (reply.url().toString())) | |
def onDownloadRequested(self, request): | |
log("Download Request: " + str(request.url())) | |
def doSearch(self, s = None): | |
if s is None: s = self.txtSearch.text() | |
self.webkit.findText(s, QtWebKit.QWebPage.FindWrapsAroundDocument) | |
def stopOrHideSearch(self): | |
if self.fraSearch.isVisible(): | |
self.fraSearch.setVisible(False) | |
self.webkit.setFocus() | |
else: | |
self.webkit.stop() | |
def showSearch(self): | |
self.txtSearch.setText("") | |
self.fraSearch.setVisible(True) | |
self.txtSearch.setFocus() | |
def zoom(self, lvl): | |
self.webkit.setZoomFactor(self.webkit.zoomFactor() + (lvl * 0.25)) | |
def stop(self): | |
self.webkit.stop() | |
def URL(self): | |
return self.cmb.currentText() | |
def loadProgress(self, val): | |
if self.pbar.isVisible(): | |
self.pbar.setValue(val) | |
def setTitle(self, title): | |
if self.browser: | |
self.browser.setTabTitle(self, title) | |
def setURL(self, url): | |
self.cmb.setEditText(url.toString()) | |
def refresh(self): | |
self.navigate(self.URL()) | |
self.webkit.reload() | |
def loadStarted(self): | |
self.showProgressBar() | |
def loadFinished(self, success): | |
self.hideProgressBar() | |
if self.cmb.hasFocus(): | |
self.webkit.setFocus() | |
def showProgressBar(self): | |
self.pbar.setValue(0) | |
self.pbar.setVisible(True) | |
def hideProgressBar(self, success = False): | |
self.pbar.setVisible(False) | |
def reload(self): | |
self.webkit.reload() | |
def smartSearch(self): | |
if self.fraSearch.isVisible(): | |
self.doSearch() | |
else: | |
self.showSearch() | |
def mkShortcuts(self): | |
if self.browser: | |
self.bro | |
def fwd(self): | |
self.webkit.history().forward() | |
def back(self): | |
self.webkit.history().back() | |
def navigate(self, url = None): | |
# 'not url' para keybinding; 'int' para sin http://? | |
if not url or type(url) == int: url = str(self.cmb.currentText()) # ??? TODO | |
url = QtCore.QUrl(self.browser.fixUrl(url)) | |
self.setTitle("Loading...") | |
self.webkit.load(url) | |
def oldnavigate(self, url = None): | |
if url and type(url) == str: | |
u = url | |
else: | |
u = str(self.cmb.currentText()) | |
parts = u.split(":") | |
if len(parts) == 2 and parts[0] == "about": | |
self.navabout(parts[1].strip().lower()) | |
return | |
if u.strip() == "": | |
return | |
if self.browser is not None: | |
u = self.browser.fixUrl(u) | |
self.cmb.setEditText(u) | |
url = QtCore.QUrl(u) | |
self.setTitle("Loading...") | |
self.webkit.load(url) | |
def onStatusBarMessage(self, s): | |
if s: | |
self.statusbar.showMessage(s) | |
else: | |
self.showHideMessage() | |
def showHideMessage(self): | |
self.statusbar.showMessage("(press %s to hide this)" % (self.actions["togglestatus"][1])) | |
def onLinkHovered(self, link, title, content): | |
if link or title: | |
if title and not link: | |
self.statusbar.showMessage(title) | |
elif link and not title: | |
self.statusbar.showMessage(link) | |
elif link and title: | |
self.statusbar.showMessage("%s (%s)" % (title, link)) | |
else: | |
self.showHideMessage() | |
class MainWin(QtGui.QMainWindow): | |
def __init__(self): | |
QtGui.QMainWindow.__init__(self, None) | |
self.downloader = None | |
self.actions = dict() | |
self.tabactions = dict() | |
self.tabactions = dict() | |
tmp = WebTab(None, None) | |
self.tabactions = tmp.actions | |
self.registerActions() | |
self.showStatusBar = False | |
self.appname = "Eilat Browser" | |
self.maxHistory = 4096 | |
self.tabs = [] | |
self.historyDateFormat = "%Y-%m-%d %H:%M:%S" | |
self.maxTitleLen = 40 | |
tmp.deleteLater() | |
self.mkGui() | |
registerShortcuts(self.actions, self) | |
def toggleStatusVisiblity(self): | |
self.showStatusBar = not self.showStatusBar | |
for t in self.tabs: | |
t.setStatusVisibility(self.showStatusBar) | |
def registerActions(self): | |
self.actions["newtab"] = [self.addTab, "Ctrl+T", "Open new tab"] | |
self.actions["closetab"] = [self.delTab, "Ctrl+W", "Close current tab"] | |
self.actions["tabprev"] = [self.decTab, "N|Ctrl+PgUp", "Switch to previous tab"] | |
self.actions["tabnext"] = [self.incTab, "M|Ctrl+PgDown", "Switch to next tab"] | |
self.actions["go"] = [self.currentTabGo, "Ctrl+L", "Focus address bar"] | |
self.actions["close"] = [self.close, "Ctrl+Q", "Close application"] | |
self.actions["zoomin"] = [self.zoomIn, "Ctrl+Up", "Zoom into page"] | |
self.actions["zoomout"] = [self.zoomOut, "Ctrl+Down", "Zoom out of page"] | |
def addWin(self): | |
MainWin().show() | |
def currentTabGo(self): | |
self.tabs[self.tabWidget.currentIndex()].cmb.setFocus() | |
def zoomIn(self): | |
self.zoom(1) | |
def zoomOut(self): | |
self.zoom(-1) | |
def zoom(self, lvl): | |
self.tabs[self.tabWidget.currentIndex()].zoom(lvl) | |
def decTab(self): | |
self.incTab(-1) | |
def incTab(self, incby = 1): | |
if self.tabWidget.count() < 2: | |
return | |
idx = self.tabWidget.currentIndex() | |
idx += incby | |
if idx < 0: | |
idx = self.tabWidget.count()-1; | |
elif idx >= self.tabWidget.count(): | |
idx = 0 | |
self.tabWidget.setCurrentIndex(idx) | |
def setTabTitle(self, tab, title): | |
idx = self.getTabIndex(tab) | |
if idx > -1: | |
if len(title) > self.maxTitleLen: | |
title = title[:self.maxTitleLen-3] + "..." | |
self.tabWidget.setTabText(idx, title) | |
def getTabIndex(self, tab): | |
for i in range(len(self.tabs)): | |
if tab == self.tabs[i]: | |
return i | |
return -1 | |
def closeEvent(self, e): | |
e.accept() | |
self.close() | |
def mkGui(self): | |
self.setWindowTitle(self.appname) | |
self.tabWidget = QtGui.QTabWidget(self) | |
self.tabWidget.tabBar().setMovable(True) | |
self.setCentralWidget(self.tabWidget) | |
self.tabWidget.setTabsClosable(True) | |
self.connect(self.tabWidget, QtCore.SIGNAL("tabCloseRequested(int)"), self.delTab) | |
self.connect(self, QtCore.SIGNAL("refreshAll()"), self.refreshAll) | |
self.addTab() | |
def addTab(self, url = None): | |
tab = WebTab(browser=self, actions=self.tabactions, showStatusBar = self.showStatusBar) | |
self.tabWidget.addTab(tab, "New tab") | |
self.tabs.append(tab) | |
self.tabWidget.setCurrentWidget(tab) | |
if url: | |
tab.navigate(url) | |
else: | |
self.currentTabGo() | |
return self.tabs[self.tabWidget.currentIndex()] | |
def fixUrl(self, url): # FIXME | |
# look for "smart" search | |
search = False | |
if url[:4] == 'http': | |
return url | |
else: | |
try: | |
socket.gethostbyname(url.split('/')[0]) # ingenioso pero feo; con 'bind' local es barato | |
except Exception as e: | |
print e | |
search = True | |
if search: | |
return "http://localhost:8000/?q=%s" % (url.replace(" ", "+")) | |
else: | |
return "http://" + url | |
def delTab(self, idx = -1): | |
if idx >= len(self.tabs): | |
return | |
if idx == -1: | |
idx = self.tabWidget.currentIndex() | |
t = self.tabs.pop(idx) | |
t.stop() | |
self.tabWidget.removeTab(idx) | |
t.deleteLater() | |
if len(self.tabs) == 0: | |
self.close() | |
def load(self, url): | |
if self.tabs[-1].URL() == "": | |
self.tabs[-1].navigate(url) | |
else: | |
self.addTab(url) | |
def refreshAll(self): | |
for t in self.tabs: | |
t.refresh() | |
class InterceptNAM(QtNetwork.QNetworkAccessManager): | |
def createRequest(self, operation, request, data): | |
print "<<< " + str(request.url().toString()) | |
return QtNetwork.QNetworkAccessManager.createRequest(self, operation, request, data) | |
if __name__ == "__main__": | |
# Proxy | |
proxy = QtNetwork.QNetworkProxy() | |
proxy.setType(QtNetwork.QNetworkProxy.HttpProxy) | |
proxy.setHostName('localhost'); | |
proxy.setPort(3128) | |
QtNetwork.QNetworkProxy.setApplicationProxy(proxy); | |
app = QtGui.QApplication([]) | |
cb = app.clipboard() | |
app.setApplicationName("Eilat") | |
app.setApplicationVersion("0.001") | |
mainwin = MainWin() | |
mainwin.show() | |
for arg in sys.argv[1:]: | |
if arg not in ["-debug"]: | |
mainwin.load(arg) | |
app.exec_() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment