# -*- coding: iso-8859-1 -*- # Copyright (C) 2008-2010 Bastian Kleineidam # # 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 Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import sys import webbrowser from PyQt4 import QtCore, QtGui from .linkchecker_ui_main import Ui_MainWindow from .progress import LinkCheckerProgress, StatusLogger from .debug import LinkCheckerDebug from .logger import GuiLogger, GuiLogHandler from .help import HelpWindow from .options import LinkCheckerOptions from .checker import CheckerThread from .contextmenu import ContextMenu from .editor import EditorWindow from .. import configuration, checker, director, add_intern_pattern, \ strformat, fileutil from ..containers import enum from .. import url as urlutil from ..checker import httpheaders DocBaseUrl = "qthelp://bfk.app.linkchecker/doc/" RegistryBase = "Bastian" Status = enum('idle', 'checking') def save_point (qpoint): """Ensure positive X and Y values of point.""" qpoint.setX(max(0, qpoint.x())) qpoint.setY(max(0, qpoint.y())) return qpoint def save_size (qsize): """Ensure minimum width and height values of the given size.""" qsize.setWidth(max(400, qsize.width())) qsize.setHeight(max(400, qsize.height())) return qsize def get_parent_url (itemtext): """Split off line and column information from URL.""" url, col = itemtext.rsplit(",", 1) col = int(col.split()[1]) url, line = url.rsplit(",", 1) line = int(line.split()[1]) return url, line, col class LinkCheckerMain (QtGui.QMainWindow, Ui_MainWindow): def __init__(self, parent=None, url=None): """Initialize UI.""" super(LinkCheckerMain, self).__init__(parent) self.setupUi(self) if url: self.urlinput.setText(url) self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowContextHelpButtonHint) self.setWindowTitle(configuration.App) # init subdialogs self.options = LinkCheckerOptions(parent=self) self.progress = LinkCheckerProgress(parent=self) self.debug = LinkCheckerDebug(parent=self) self.checker = CheckerThread() self.contextmenu = ContextMenu(parent=self) self.editor = EditorWindow(parent=self) # Note: we can't use QT assistant here because of the .exe packaging self.assistant = HelpWindow(self, self.get_qhcpath()) # setup this widget self.init_treewidget() self.read_settings() self.connect_widgets() self.init_config() self.status = Status.idle def get_qhcpath (self): paths = [ # when developing os.path.join(configuration.configdata.install_data, "doc", "html"), # when running under py2exe os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "share", "linkchecker"), # after installing as a package configuration.configdata.config_dir, ] for path in paths: qhcfile = os.path.join(path, "lccollection.qhc") if os.path.isfile(qhcfile): break return qhcfile def read_settings (self): settings = QtCore.QSettings(RegistryBase, configuration.AppName) settings.beginGroup('mainwindow') if settings.contains('size'): self.resize(save_size(settings.value('size').toSize())) self.move(save_point(settings.value('pos').toPoint())) settings.endGroup() def connect_widgets (self): """Connect widget signals. Some signals use the AutoConnect feature. Autoconnected methods have the form on__. """ self.connect(self.checker, QtCore.SIGNAL("finished()"), self.set_status_idle) self.connect(self.checker, QtCore.SIGNAL("terminated()"), self.set_status_idle) self.connect(self.checker, QtCore.SIGNAL("log_url(PyQt_PyObject)"), self.log_url) def init_treewidget (self): self.treeWidget.setColumnHidden(0, True) self.treeWidget.setColumnWidth(1, 200) self.treeWidget.setColumnWidth(2, 200) self.treeWidget.setColumnWidth(3, 150) self.treeWidget.setSortingEnabled(True) self.treeWidget.sortByColumn(0, QtCore.Qt.AscendingOrder) self.num = 0 def get_status (self): return self._status def set_status (self, status): self._status = status if status == Status.idle: self.progress.hide() self.aggregate = None self.controlButton.setEnabled(True) self.set_statusbar(_("Ready.")) elif status == Status.checking: self.num = 0 self.debug.reset() self.progress.reset() self.progress.show() self.controlButton.setEnabled(False) status = property(get_status, set_status) def set_status_idle (self): """Set idle status. Helper function for signal connections.""" self.status = Status.idle @QtCore.pyqtSignature("") def on_actionHelp_triggered (self): """Show help page.""" url = QtCore.QUrl("%sindex.html" % DocBaseUrl) self.assistant.showDocumentation(url) @QtCore.pyqtSignature("") def on_actionOptions_triggered (self): """Show option dialog.""" self.options.exec_() @QtCore.pyqtSignature("") def on_actionQuit_triggered (self): """Quit application.""" self.close() def closeEvent (self, e=None): """Save settings on close.""" settings = QtCore.QSettings(RegistryBase, configuration.AppName) settings.beginGroup('mainwindow') settings.setValue("size", QtCore.QVariant(save_size(self.size()))) settings.setValue("pos", QtCore.QVariant(save_point(self.pos()))) settings.endGroup() settings.sync() if e is not None: e.accept() # remove registered logging handler self.config.remove_loghandler(self.handler) @QtCore.pyqtSignature("") def on_actionAbout_triggered (self): """Display about dialog.""" d = { "app": configuration.App, "appname": configuration.AppName, "copyright": configuration.HtmlCopyright, } QtGui.QMessageBox.about(self, _(u"About %(appname)s") % d, _(u"""

%(appname)s checks HTML documents and websites for broken links.

%(copyright)s

%(app)s is licensed under the GPL Version 2 or later.

""") % d) @QtCore.pyqtSignature("") def on_actionDebug_triggered (self): """Display debug dialog.""" self.debug.show() def on_controlButton_clicked (self): """Start a new check.""" if self.status == Status.idle: self.check() def check (self): """Check given URL.""" self.controlButton.setEnabled(False) self.treeWidget.clear() self.set_config() aggregate = director.get_aggregate(self.config) url = unicode(self.urlinput.text()).strip() if not url: self.set_statusbar(_("Error, empty URL")) self.status = Status.idle return if url.startswith(u"www."): url = u"http://%s" % url elif url.startswith(u"ftp."): url = u"ftp://%s" % url self.set_statusbar(_("Checking '%s'.") % strformat.limit(url, 40)) url_data = checker.get_url_from(url, 0, aggregate) try: add_intern_pattern(url_data, self.config) except UnicodeError: self.set_statusbar(_("Error, invalid URL `%s'.") % strformat.limit(url, 40)) self.status = Status.idle return aggregate.urlqueue.put(url_data) self.aggregate = aggregate # check in background self.checker.check(self.aggregate, self.progress) self.status = Status.checking def init_config (self): """Create a configuration object.""" self.config = configuration.Configuration() self.config.logger_add("gui", GuiLogger) self.config["logger"] = self.config.logger_new('gui', widget=self.checker) self.handler = GuiLogHandler(self.debug) self.config["status"] = True self.config["status_wait_seconds"] = 1 self.config.init_logging(StatusLogger(self.progress), handler=self.handler) def set_config (self): """Set configuration.""" self.config["recursionlevel"] = self.options.recursionlevel.value() self.config["verbose"] = self.options.verbose.isChecked() self.config["timeout"] = self.options.timeout.value() self.config["threads"] = self.options.threads.value() if self.options.debug: self.config.set_debug(["all"]) # make sure at least one thread is used self.config["threads"] = 1 else: self.config.reset_loglevel() def log_url (self, url_data): """Add URL data to tree widget.""" num = u"%09d" % self.num if url_data.parent_url: parent = unicode(url_data.parent_url) + \ (_(", line %d") % url_data.line) + \ (_(", col %d") % url_data.column) else: parent = u"" url = unicode(url_data.url) name = url_data.name if url_data.valid: if url_data.warnings: color = QtCore.Qt.darkYellow else: color = QtCore.Qt.darkGreen result = u"Valid" else: color = QtCore.Qt.darkRed result = u"Error" if url_data.result: result += u": %s" % url_data.result item = QtGui.QTreeWidgetItem((num, parent, url, name, result)) item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) item.setForeground(4, QtGui.QBrush(color)) item.setToolTip(2, url) item.setToolTip(3, name) if url_data.warnings: text = u"\n".join(url_data.warnings) item.setToolTip(4, strformat.wrap(text, 60)) self.treeWidget.addTopLevelItem(item) self.num += 1 def on_treeWidget_itemDoubleClicked (self, item, column): """View property page.""" pass # XXX todo def on_treeWidget_customContextMenuRequested (self, point): """Show item context menu.""" item = self.treeWidget.itemAt(point) if item is not None: self.contextmenu.enableFromItem(item) self.contextmenu.popup(QtGui.QCursor.pos()) @QtCore.pyqtSignature("") def on_actionViewOnline_triggered (self): """View item URL online.""" item = self.treeWidget.currentItem() if item is not None: url = str(item.text(2)) webbrowser.open(url) @QtCore.pyqtSignature("") def on_actionViewParentOnline_triggered (self): """View item parent URL online.""" item = self.treeWidget.currentItem() if item is not None: parenturl, line, col = get_parent_url(str(item.text(1))) webbrowser.open(parenturl) @QtCore.pyqtSignature("") def on_actionViewParentSource_triggered (self): """View item parent URL source in local text editor (read-only).""" item = self.treeWidget.currentItem() if item is not None: # XXX simplify this once a proper model is stored parenturl, line, col = get_parent_url(str(item.text(1))) self.view_source(parenturl, line, col) def view_source (self, url, line, col): self.editor.setWindowTitle(u"View %s" % url) info, data = urlutil.get_content(url, proxy=self.config["proxy"]) if (info, data) == (None, None): self.editor.setText(u"An error occurred retreiving URL `%s'." % url) else: content_type = httpheaders.get_content_type(info) if not content_type: # read function for content type guessing read = lambda: data content_type = fileutil.guess_mimetype(url, read=read) self.editor.setContentType(content_type) self.editor.setText(data, line=line, col=col) self.editor.show() @QtCore.pyqtSignature("") def on_actionCopyToClipboard_triggered (self): """Copy item URL to clipboard.""" item = self.treeWidget.currentItem() if item is not None: url = str(item.text(2)) clipboard = QtGui.QApplication.clipboard() clipboard.setText(url) event = QtCore.QEvent(QtCore.QEvent.Clipboard) QtGui.QApplication.sendEvent(clipboard, event) def set_statusbar (self, msg): """Show status message in status bar.""" self.statusBar.showMessage(msg)