Sun, 03 Apr 2016 16:33:37 +0200
Merged the QtWebEngine branch into the default development branch to finalize the port in here.
install.py | file | annotate | diff | comparison | revisions |
--- a/.hgignore Sun Apr 03 12:29:37 2016 +0200 +++ b/.hgignore Sun Apr 03 16:33:37 2016 +0200 @@ -16,3 +16,4 @@ glob:__pycache__ glob:**.DS_Store glob:**.coverage +glob:GPUCache
--- a/DebugClients/Python/DebugClientThreads.py Sun Apr 03 12:29:37 2016 +0200 +++ b/DebugClients/Python/DebugClientThreads.py Sun Apr 03 16:33:37 2016 +0200 @@ -200,4 +200,4 @@ # # eflag: FileType = Python2 -# eflag: noqa = M601, M702 +# eflag: noqa = M601, M702, E402
--- a/DebugClients/Python3/DebugClientThreads.py Sun Apr 03 12:29:37 2016 +0200 +++ b/DebugClients/Python3/DebugClientThreads.py Sun Apr 03 16:33:37 2016 +0200 @@ -199,4 +199,4 @@ debugClient.main() # -# eflag: noqa = M702 +# eflag: noqa = M702, E402
--- a/E5Gui/E5ErrorMessage.py Sun Apr 03 12:29:37 2016 +0200 +++ b/E5Gui/E5ErrorMessage.py Sun Apr 03 16:33:37 2016 +0200 @@ -44,7 +44,9 @@ "QFont::", "QCocoaMenu::removeMenuItem", "QCocoaMenu::insertNative", - ",type id:" + ",type id:", + "Remote debugging server started successfully", + "Uncaught SecurityError:", ] def __filterMessage(self, message):
--- a/Helpviewer/HelpBrowserWV.py Sun Apr 03 12:29:37 2016 +0200 +++ b/Helpviewer/HelpBrowserWV.py Sun Apr 03 16:33:37 2016 +0200 @@ -10,7 +10,7 @@ from __future__ import unicode_literals try: - str = unicode + str = unicode # __IGNORE_EXCEPTION__ except NameError: pass @@ -1617,8 +1617,8 @@ searchUrl = QUrl(self.page().mainFrame().baseUrl().resolved( QUrl(formElement.attribute("action")))) - if searchUrl.scheme() != "http": - return +## if searchUrl.scheme() != "http": +## return if qVersion() >= "5.0.0": from PyQt5.QtCore import QUrlQuery
--- a/Helpviewer/HelpWindow.py Sun Apr 03 12:29:37 2016 +0200 +++ b/Helpviewer/HelpWindow.py Sun Apr 03 16:33:37 2016 +0200 @@ -71,8 +71,6 @@ helpwindows = [] - maxMenuFilePathLen = 75 - _fromEric = False useQtHelp = QTHELP_AVAILABLE
--- a/PluginManager/PluginManager.py Sun Apr 03 12:29:37 2016 +0200 +++ b/PluginManager/PluginManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -144,7 +144,11 @@ self.__networkManager.sslErrors.connect(self.__sslErrors) self.__replies = [] - self.__ui.onlineStateChanged.connect(self.__onlineStateChanged) + try: + self.__ui.onlineStateChanged.connect(self.__onlineStateChanged) + except AttributeError: + # it was not called from eric + pass def finalizeSetup(self): """
--- a/Preferences/ConfigurationDialog.py Sun Apr 03 12:29:37 2016 +0200 +++ b/Preferences/ConfigurationDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -79,9 +79,10 @@ HelpBrowserMode = 1 TrayStarterMode = 2 HexEditorMode = 3 + WebBrowserMode = 4 def __init__(self, parent=None, fromEric=True, displayMode=DefaultMode, - expandedEntries=[]): + expandedEntries=[], webEngine=False): """ Constructor @@ -89,21 +90,25 @@ @keyparam fromEric flag indicating a dialog generation from within the eric6 ide (boolean) @keyparam displayMode mode of the configuration dialog - (DefaultMode, HelpBrowserMode, TrayStarterMode, HexEditorMode) + (DefaultMode, HelpBrowserMode, TrayStarterMode, HexEditorMode, + WebBrowserMode) @exception RuntimeError raised to indicate an invalid dialog mode @keyparam expandedEntries list of entries to be shown expanded (list of strings) + @keyparam webEngine flag indicating QtWebEngine is used (bool) """ assert displayMode in ( ConfigurationWidget.DefaultMode, ConfigurationWidget.HelpBrowserMode, ConfigurationWidget.TrayStarterMode, ConfigurationWidget.HexEditorMode, + ConfigurationWidget.WebBrowserMode, ) super(ConfigurationWidget, self).__init__(parent) self.fromEric = fromEric self.displayMode = displayMode + self.__webEngine = webEngine self.__setupUi() @@ -315,25 +320,49 @@ [self.tr("Viewmanager"), "preferences-viewmanager.png", "ViewmanagerPage", "0interfacePage", None], } - try: - from PyQt5 import QtWebKit # __IGNORE_WARNING__ + if webEngine: self.configItems.update({ - "helpAppearancePage": + "0webBrowserPage": + [self.tr("Web Browser"), "ericWeb.png", + None, None, None], + "webBrowserAppearancePage": [self.tr("Appearance"), "preferences-styles.png", - "HelpAppearancePage", "0helpPage", None], + "WebBrowserAppearancePage", "0webBrowserPage", None], + "webBrowserPage": + [self.tr("eric6 Web Browser"), "ericWeb.png", + "WebBrowserPage", "0webBrowserPage", None], "helpFlashCookieManagerPage": [self.tr("Flash Cookie Manager"), "flashCookie16.png", - "HelpFlashCookieManagerPage", "0helpPage", None], + "HelpFlashCookieManagerPage", "0webBrowserPage", None], "helpVirusTotalPage": [self.tr("VirusTotal Interface"), "virustotal.png", - "HelpVirusTotalPage", "0helpPage", None], - "helpWebBrowserPage": - [self.tr("eric6 Web Browser"), "ericWeb.png", - "HelpWebBrowserPage", "0helpPage", None], + "HelpVirusTotalPage", "0webBrowserPage", None], }) - except ImportError: - pass + else: + try: + from PyQt5 import QtWebKit # __IGNORE_WARNING__ + self.configItems.update({ + "0helpBrowserPage": + [self.tr("Web Browser"), "ericWeb.png", + None, None, None], + "helpAppearancePage": + [self.tr("Appearance"), "preferences-styles.png", + "HelpAppearancePage", "0helpBrowserPage", None], + "helpWebBrowserPage": + [self.tr("eric6 Web Browser"), "ericWeb.png", + "HelpWebBrowserPage", "0helpBrowserPage", None], + "helpFlashCookieManagerPage": + [self.tr("Flash Cookie Manager"), + "flashCookie16.png", + "HelpFlashCookieManagerPage", "0helpBrowserPage", + None], + "helpVirusTotalPage": + [self.tr("VirusTotal Interface"), "virustotal.png", + "HelpVirusTotalPage", "0helpBrowserPage", None], + }) + except ImportError: + pass self.configItems.update( e5App().getObject("PluginManager").getPluginConfigData()) @@ -359,34 +388,73 @@ [self.tr("Security"), "preferences-security.png", "SecurityPage", None, None], - "0helpPage": - [self.tr("Help"), "preferences-help.png", - None, None, None], "helpDocumentationPage": [self.tr("Help Documentation"), "preferences-helpdocumentation.png", - "HelpDocumentationPage", "0helpPage", None], + "HelpDocumentationPage", None, None], } try: from PyQt5 import QtWebKit # __IGNORE_WARNING__ self.configItems.update({ "helpAppearancePage": [self.tr("Appearance"), "preferences-styles.png", - "HelpAppearancePage", "0helpPage", None], + "HelpAppearancePage", None, None], "helpFlashCookieManagerPage": [self.tr("Flash Cookie Manager"), "flashCookie16.png", - "HelpFlashCookieManagerPage", "0helpPage", None], + "HelpFlashCookieManagerPage", None, None], "helpVirusTotalPage": [self.tr("VirusTotal Interface"), "virustotal.png", - "HelpVirusTotalPage", "0helpPage", None], + "HelpVirusTotalPage", None, None], "helpWebBrowserPage": [self.tr("eric6 Web Browser"), "ericWeb.png", - "HelpWebBrowserPage", "0helpPage", None], + "HelpWebBrowserPage", None, None], }) except ImportError: pass + elif displayMode == ConfigurationWidget.WebBrowserMode: + self.configItems = { + # key : [display string, pixmap name, dialog module name or + # page creation function, parent key, + # reference to configuration page (must always be last)] + # The dialog module must have the module function 'create' to + # create the configuration page. This must have the method + # 'save' to save the settings. + "interfacePage": + [self.tr("Interface"), "preferences-interface.png", + "HelpInterfacePage", None, None], + "networkPage": + [self.tr("Network"), "preferences-network.png", + "NetworkPage", None, None], + "printerPage": + [self.tr("Printer"), "preferences-printer.png", + "PrinterPage", None, None], + "securityPage": + [self.tr("Security"), "preferences-security.png", + "SecurityPage", None, None], + + "helpDocumentationPage": + [self.tr("Help Documentation"), + "preferences-helpdocumentation.png", + "HelpDocumentationPage", None, None], + + "webBrowserAppearancePage": + [self.tr("Appearance"), "preferences-styles.png", + "WebBrowserAppearancePage", None, None], + "webBrowserPage": + [self.tr("eric6 Web Browser"), "ericWeb.png", + "WebBrowserPage", None, None], + + "helpFlashCookieManagerPage": + [self.tr("Flash Cookie Manager"), + "flashCookie16.png", + "HelpFlashCookieManagerPage", None, None], + "helpVirusTotalPage": + [self.tr("VirusTotal Interface"), "virustotal.png", + "HelpVirusTotalPage", None, None], + } + elif displayMode == ConfigurationWidget.TrayStarterMode: self.configItems = { # key : [display string, pixmap name, dialog module name or @@ -446,7 +514,8 @@ if displayMode in [ConfigurationWidget.HelpBrowserMode, ConfigurationWidget.TrayStarterMode, - ConfigurationWidget.HexEditorMode]: + ConfigurationWidget.HexEditorMode, + ConfigurationWidget.WebBrowserMode]: self.configListSearch.hide() if displayMode not in [ConfigurationWidget.TrayStarterMode, @@ -837,6 +906,13 @@ pageName = item.data(0, Qt.UserRole) if pageName not in self.__expandedEntries: self.__expandedEntries.append(pageName) + + def isUsingWebEngine(self): + """ + Public method to get an indication, if QtWebEngine is being used. + """ + return self.__webEngine or \ + self.displayMode == ConfigurationWidget.WebBrowserMode class ConfigurationDialog(QDialog): @@ -854,10 +930,11 @@ HelpBrowserMode = ConfigurationWidget.HelpBrowserMode TrayStarterMode = ConfigurationWidget.TrayStarterMode HexEditorMode = ConfigurationWidget.HexEditorMode + WebBrowserMode = ConfigurationWidget.WebBrowserMode def __init__(self, parent=None, name=None, modal=False, fromEric=True, displayMode=ConfigurationWidget.DefaultMode, - expandedEntries=[]): + expandedEntries=[], webEngine=False): """ Constructor @@ -867,9 +944,11 @@ @keyparam fromEric flag indicating a dialog generation from within the eric6 ide (boolean) @keyparam displayMode mode of the configuration dialog - (DefaultMode, HelpBrowserMode, TrayStarterMode, HexEditorMode) + (DefaultMode, HelpBrowserMode, TrayStarterMode, HexEditorMode, + WebBrowserMode) @keyparam expandedEntries list of entries to be shown expanded (list of strings) + @keyparam webEngine flag indicating QtWebEngine is used (bool) """ super(ConfigurationDialog, self).__init__(parent) if name: @@ -883,7 +962,8 @@ self.cw = ConfigurationWidget(self, fromEric=fromEric, displayMode=displayMode, - expandedEntries=expandedEntries) + expandedEntries=expandedEntries, + webEngine=webEngine) size = self.cw.size() self.layout.addWidget(self.cw) self.resize(size) @@ -951,15 +1031,17 @@ """ Main window class for the standalone dialog. """ - def __init__(self, parent=None): + def __init__(self, parent=None, webEngine=False): """ Constructor @param parent reference to the parent widget (QWidget) + @keyparam webEngine flag indicating QtWebEngine is used (bool) """ super(ConfigurationWindow, self).__init__(parent) - self.cw = ConfigurationWidget(self, fromEric=False) + self.cw = ConfigurationWidget(self, fromEric=False, + webEngine=webEngine) size = self.cw.size() self.setCentralWidget(self.cw) self.resize(size)
--- a/Preferences/ConfigurationPages/HelpViewersPage.py Sun Apr 03 12:29:37 2016 +0200 +++ b/Preferences/ConfigurationPages/HelpViewersPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -9,7 +9,21 @@ from __future__ import unicode_literals +from PyQt5.QtCore import qVersion from PyQt5.QtWidgets import QButtonGroup +try: + from PyQt5 import QtWebKit # __IGNORE_WARNING__ + WEBKIT_AVAILABLE = True +except ImportError: + WEBKIT_AVAILABLE = False +if qVersion() < "5.6.0": + WEBENGINE_AVAILABLE = False +else: + try: + from PyQt5 import QtWebEngineWidgets # __IGNORE_WARNING__ + WEBENGINE_AVAILABLE = True + except ImportError: + WEBENGINE_AVAILABLE = False from E5Gui.E5PathPicker import E5PathPickerModes @@ -40,14 +54,14 @@ self.helpViewerGroup.addButton(self.customViewerButton) # set initial values - hvId = Preferences.getHelp("HelpViewerType") - # check availability of QtWebKit - try: - from PyQt5 import QtWebKit # __IGNORE_WARNING__ - except ImportError: - # not available, reset help viewer to default + if WEBENGINE_AVAILABLE: + hvId = Preferences.getWebBrowser("HelpViewerType") + else: + hvId = Preferences.getHelp("HelpViewerType") + if not WEBENGINE_AVAILABLE and not WEBKIT_AVAILABLE: if hvId == 1: - hvId = Preferences.Prefs.helpDefaults["HelpViewerType"] + hvId = Preferences.Prefs.webBrowserDefaultsDefaults[ + "HelpViewerType"] self.helpBrowserButton.setEnabled(False) if hvId == 1: @@ -74,6 +88,7 @@ elif self.customViewerButton.isChecked(): hvId = 4 Preferences.setHelp("HelpViewerType", hvId) + Preferences.setWebBrowser("HelpViewerType", hvId) Preferences.setHelp( "CustomViewer", self.customViewerPicker.text())
--- a/Preferences/ConfigurationPages/NetworkPage.py Sun Apr 03 12:29:37 2016 +0200 +++ b/Preferences/ConfigurationPages/NetworkPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -25,14 +25,22 @@ """ Class implementing the Network configuration page. """ - def __init__(self): + def __init__(self, configDialog): """ Constructor + + @param configDialog reference to the configuration dialog + (ConfigurationDialog) """ super(NetworkPage, self).__init__() self.setupUi(self) self.setObjectName("NetworkPage") + self.__configDlg = configDialog + self.__displayMode = None + self.__webEngine = False + self.__webKit = False + self.downloadDirPicker.setMode(E5PathPickerModes.DirectoryMode) self.ftpProxyTypeCombo.addItem( @@ -60,17 +68,6 @@ self.downloadDirPicker.setText(Preferences.getUI("DownloadPath")) self.requestFilenameCheckBox.setChecked( Preferences.getUI("RequestDownloadFilename")) - try: - policy = Preferences.getHelp("DownloadManagerRemovePolicy") - from Helpviewer.Download.DownloadManager import DownloadManager - if policy == DownloadManager.RemoveNever: - self.cleanupNeverButton.setChecked(True) - elif policy == DownloadManager.RemoveExit: - self.cleanupExitButton.setChecked(True) - else: - self.cleanupSuccessfulButton.setChecked(True) - except ImportError: - self.cleanupGroup.hide() # HTTP proxy self.httpProxyHostEdit.setText( @@ -111,6 +108,48 @@ self.exceptionsEdit.setText( ", ".join(Preferences.getUI("ProxyExceptions").split(","))) + def setMode(self, displayMode): + """ + Public method to perform mode dependent setups. + + @param displayMode mode of the configuration dialog + (ConfigurationWidget.DefaultMode, + ConfigurationWidget.HelpBrowserMode, + ConfigurationWidget.WebBrowserMode) + """ + from ..ConfigurationDialog import ConfigurationWidget + assert displayMode in ( + ConfigurationWidget.DefaultMode, + ConfigurationWidget.HelpBrowserMode, + ConfigurationWidget.WebBrowserMode + ) + + self.__displayMode = displayMode + if self.__displayMode == ConfigurationWidget.HelpBrowserMode or \ + not self.__configDlg.isUsingWebEngine(): + try: + policy = Preferences.getHelp("DownloadManagerRemovePolicy") + from Helpviewer.Download.DownloadManager import DownloadManager + if policy == DownloadManager.RemoveNever: + self.cleanupNeverButton.setChecked(True) + elif policy == DownloadManager.RemoveExit: + self.cleanupExitButton.setChecked(True) + else: + self.cleanupSuccessfulButton.setChecked(True) + self.__webKit = True + except ImportError: + self.cleanupGroup.hide() + else: + policy = Preferences.getWebBrowser("DownloadManagerRemovePolicy") + from WebBrowser.Download.DownloadManager import DownloadManager + if policy == DownloadManager.RemoveNever: + self.cleanupNeverButton.setChecked(True) + elif policy == DownloadManager.RemoveExit: + self.cleanupExitButton.setChecked(True) + else: + self.cleanupSuccessfulButton.setChecked(True) + self.__webEngine = True + def save(self): """ Public slot to save the Networj configuration. @@ -121,7 +160,7 @@ Preferences.setUI( "RequestDownloadFilename", self.requestFilenameCheckBox.isChecked()) - try: + if self.__webKit: from Helpviewer.Download.DownloadManager import DownloadManager if self.cleanupNeverButton.isChecked(): policy = DownloadManager.RemoveNever @@ -130,9 +169,15 @@ else: policy = DownloadManager.RemoveSuccessFullDownload Preferences.setHelp("DownloadManagerRemovePolicy", policy) - except ImportError: - # ignore it - pass + elif self.__webEngine: + from WebBrowser.Download.DownloadManager import DownloadManager + if self.cleanupNeverButton.isChecked(): + policy = DownloadManager.RemoveNever + elif self.cleanupExitButton.isChecked(): + policy = DownloadManager.RemoveExit + else: + policy = DownloadManager.RemoveSuccessFullDownload + Preferences.setWebBrowser("DownloadManagerRemovePolicy", policy) Preferences.setUI( "UseProxy", @@ -222,5 +267,5 @@ @param dlg reference to the configuration dialog @return reference to the instantiated page (ConfigurationPageBase) """ - page = NetworkPage() + page = NetworkPage(dlg) return page
--- a/Preferences/ConfigurationPages/PrinterPage.py Sun Apr 03 12:29:37 2016 +0200 +++ b/Preferences/ConfigurationPages/PrinterPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -52,6 +52,8 @@ Preferences.getPrinter("TopMargin")) self.bottomMarginSpinBox.setValue( Preferences.getPrinter("BottomMargin")) + self.resolutionSpinBox.setValue( + Preferences.getPrinter("Resolution")) def save(self): """ @@ -84,6 +86,9 @@ Preferences.setPrinter( "BottomMargin", self.bottomMarginSpinBox.value()) + Preferences.setPrinter( + "Resolution", + self.resolutionSpinBox.value()) @pyqtSlot() def on_printheaderFontButton_clicked(self):
--- a/Preferences/ConfigurationPages/PrinterPage.ui Sun Apr 03 12:29:37 2016 +0200 +++ b/Preferences/ConfigurationPages/PrinterPage.ui Sun Apr 03 16:33:37 2016 +0200 @@ -1,56 +1,86 @@ -<ui version="4.0" > +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> <class>PrinterPage</class> - <widget class="QWidget" name="PrinterPage" > - <property name="geometry" > + <widget class="QWidget" name="PrinterPage"> + <property name="geometry"> <rect> <x>0</x> <y>0</y> - <width>446</width> + <width>448</width> <height>568</height> </rect> </property> - <layout class="QVBoxLayout" > + <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QLabel" name="headerLabel" > - <property name="text" > - <string><b>Configure printer settings</b></string> + <widget class="QLabel" name="headerLabel"> + <property name="text"> + <string><b>Configure printer settings</b></string> </property> </widget> </item> <item> - <widget class="Line" name="line7" > - <property name="frameShape" > + <widget class="Line" name="line7"> + <property name="frameShape"> <enum>QFrame::HLine</enum> </property> - <property name="frameShadow" > + <property name="frameShadow"> <enum>QFrame::Sunken</enum> </property> - <property name="orientation" > + <property name="orientation"> <enum>Qt::Horizontal</enum> </property> </widget> </item> <item> - <layout class="QGridLayout" > - <item row="1" column="1" colspan="2" > - <widget class="QFrame" name="frame_2" > - <property name="frameShape" > + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="TextLabel1"> + <property name="text"> + <string>Printername:</string> + </property> + </widget> + </item> + <item row="0" column="1" colspan="3"> + <widget class="QLineEdit" name="printerNameEdit"/> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="TextLabel2"> + <property name="text"> + <string>Colour Mode:</string> + </property> + <property name="alignment"> + <set>Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="1" column="1" colspan="3"> + <widget class="QFrame" name="frame_2"> + <property name="frameShape"> <enum>QFrame::NoFrame</enum> </property> - <layout class="QVBoxLayout" > - <property name="margin" > + <layout class="QVBoxLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> <number>0</number> </property> <item> - <widget class="QRadioButton" name="printerColorButton" > - <property name="text" > + <widget class="QRadioButton" name="printerColorButton"> + <property name="text"> <string>Colour</string> </property> </widget> </item> <item> - <widget class="QRadioButton" name="printerGrayscaleButton" > - <property name="text" > + <widget class="QRadioButton" name="printerGrayscaleButton"> + <property name="text"> <string>Gray Scale</string> </property> </widget> @@ -58,28 +88,47 @@ </layout> </widget> </item> - <item row="2" column="1" colspan="2" > - <widget class="QFrame" name="frame" > - <property name="frameShape" > + <item row="2" column="0"> + <widget class="QLabel" name="TextLabel4"> + <property name="text"> + <string>Page Order:</string> + </property> + <property name="alignment"> + <set>Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="2" column="1" colspan="3"> + <widget class="QFrame" name="frame"> + <property name="frameShape"> <enum>QFrame::NoFrame</enum> </property> - <layout class="QVBoxLayout" > - <property name="spacing" > + <layout class="QVBoxLayout"> + <property name="spacing"> <number>6</number> </property> - <property name="margin" > + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> <number>0</number> </property> <item> - <widget class="QRadioButton" name="printFirstPageFirstButton" > - <property name="text" > + <widget class="QRadioButton" name="printFirstPageFirstButton"> + <property name="text"> <string>First Page First</string> </property> </widget> </item> <item> - <widget class="QRadioButton" name="printFirstPageLastButton" > - <property name="text" > + <widget class="QRadioButton" name="printFirstPageLastButton"> + <property name="text"> <string>Last Page First</string> </property> </widget> @@ -87,88 +136,32 @@ </layout> </widget> </item> - <item row="0" column="1" colspan="2" > - <widget class="QLineEdit" name="printerNameEdit" /> - </item> - <item row="0" column="0" > - <widget class="QLabel" name="TextLabel1" > - <property name="text" > - <string>Printername:</string> - </property> - </widget> - </item> - <item row="3" column="0" > - <widget class="QLabel" name="TextLabel3" > - <property name="text" > + <item row="3" column="0"> + <widget class="QLabel" name="TextLabel3"> + <property name="text"> <string>Magnification:</string> </property> </widget> </item> - <item row="4" column="1" colspan="2" > - <widget class="QLineEdit" name="printheaderFontSample" > - <property name="focusPolicy" > - <enum>Qt::NoFocus</enum> - </property> - <property name="text" > - <string>Header Font</string> - </property> - <property name="alignment" > - <set>Qt::AlignHCenter</set> - </property> - <property name="readOnly" > - <bool>true</bool> - </property> - </widget> - </item> - <item row="4" column="0" > - <widget class="QPushButton" name="printheaderFontButton" > - <property name="toolTip" > - <string>Press to select the font for the page headers</string> - </property> - <property name="text" > - <string>Header Font</string> - </property> - </widget> - </item> - <item row="3" column="1" > - <widget class="QSpinBox" name="printMagnificationSpinBox" > - <property name="minimum" > + <item row="3" column="1"> + <widget class="QSpinBox" name="printMagnificationSpinBox"> + <property name="minimum"> <number>-10</number> </property> - <property name="maximum" > + <property name="maximum"> <number>10</number> </property> - <property name="value" > + <property name="value"> <number>-3</number> </property> </widget> </item> - <item row="1" column="0" > - <widget class="QLabel" name="TextLabel2" > - <property name="text" > - <string>Colour Mode:</string> - </property> - <property name="alignment" > - <set>Qt::AlignTop</set> - </property> - </widget> - </item> - <item row="2" column="0" > - <widget class="QLabel" name="TextLabel4" > - <property name="text" > - <string>Page Order:</string> - </property> - <property name="alignment" > - <set>Qt::AlignTop</set> - </property> - </widget> - </item> - <item row="3" column="2" > + <item row="3" column="2" colspan="2"> <spacer> - <property name="orientation" > + <property name="orientation"> <enum>Qt::Horizontal</enum> </property> - <property name="sizeHint" stdset="0" > + <property name="sizeHint" stdset="0"> <size> <width>251</width> <height>20</height> @@ -176,88 +169,153 @@ </property> </spacer> </item> + <item row="4" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Resolution:</string> + </property> + </widget> + </item> + <item row="4" column="1" colspan="2"> + <widget class="QSpinBox" name="resolutionSpinBox"> + <property name="toolTip"> + <string>Select the printer resolution </string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string> DPI</string> + </property> + <property name="maximum"> + <number>6000</number> + </property> + <property name="singleStep"> + <number>50</number> + </property> + </widget> + </item> + <item row="4" column="3"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>208</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="5" column="0"> + <widget class="QPushButton" name="printheaderFontButton"> + <property name="toolTip"> + <string>Press to select the font for the page headers</string> + </property> + <property name="text"> + <string>Header Font</string> + </property> + </widget> + </item> + <item row="5" column="1" colspan="3"> + <widget class="QLineEdit" name="printheaderFontSample"> + <property name="focusPolicy"> + <enum>Qt::NoFocus</enum> + </property> + <property name="text"> + <string>Header Font</string> + </property> + <property name="alignment"> + <set>Qt::AlignHCenter</set> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> </layout> </item> <item> - <layout class="QHBoxLayout" > + <layout class="QHBoxLayout"> <item> - <widget class="QGroupBox" name="groupBox" > - <property name="title" > + <widget class="QGroupBox" name="groupBox"> + <property name="title"> <string>Margins</string> </property> - <layout class="QGridLayout" > - <item row="0" column="1" > - <widget class="QDoubleSpinBox" name="topMarginSpinBox" > - <property name="toolTip" > + <layout class="QGridLayout"> + <item row="0" column="1"> + <widget class="QDoubleSpinBox" name="topMarginSpinBox"> + <property name="toolTip"> <string>Enter the top margin in cm.</string> </property> - <property name="suffix" > + <property name="suffix"> <string> cm</string> </property> - <property name="decimals" > + <property name="decimals"> <number>1</number> </property> - <property name="maximum" > + <property name="maximum"> <double>9.900000000000000</double> </property> - <property name="singleStep" > + <property name="singleStep"> <double>0.500000000000000</double> </property> </widget> </item> - <item row="1" column="0" > - <widget class="QDoubleSpinBox" name="leftMarginSpinBox" > - <property name="toolTip" > + <item row="1" column="0"> + <widget class="QDoubleSpinBox" name="leftMarginSpinBox"> + <property name="toolTip"> <string>Enter the left margin in cm.</string> </property> - <property name="suffix" > + <property name="suffix"> <string> cm</string> </property> - <property name="decimals" > + <property name="decimals"> <number>1</number> </property> - <property name="maximum" > + <property name="maximum"> <double>9.900000000000000</double> </property> - <property name="singleStep" > + <property name="singleStep"> <double>0.500000000000000</double> </property> </widget> </item> - <item row="1" column="2" > - <widget class="QDoubleSpinBox" name="rightMarginSpinBox" > - <property name="toolTip" > + <item row="1" column="2"> + <widget class="QDoubleSpinBox" name="rightMarginSpinBox"> + <property name="toolTip"> <string>Enter the right margin in cm.</string> </property> - <property name="suffix" > + <property name="suffix"> <string> cm</string> </property> - <property name="decimals" > + <property name="decimals"> <number>1</number> </property> - <property name="maximum" > + <property name="maximum"> <double>9.900000000000000</double> </property> - <property name="singleStep" > + <property name="singleStep"> <double>0.500000000000000</double> </property> </widget> </item> - <item row="2" column="1" > - <widget class="QDoubleSpinBox" name="bottomMarginSpinBox" > - <property name="toolTip" > + <item row="2" column="1"> + <widget class="QDoubleSpinBox" name="bottomMarginSpinBox"> + <property name="toolTip"> <string>Enter the bottom margin in cm.</string> </property> - <property name="suffix" > + <property name="suffix"> <string> cm</string> </property> - <property name="decimals" > + <property name="decimals"> <number>1</number> </property> - <property name="maximum" > + <property name="maximum"> <double>9.900000000000000</double> </property> - <property name="singleStep" > + <property name="singleStep"> <double>0.500000000000000</double> </property> </widget> @@ -267,10 +325,10 @@ </item> <item> <spacer> - <property name="orientation" > + <property name="orientation"> <enum>Qt::Horizontal</enum> </property> - <property name="sizeHint" stdset="0" > + <property name="sizeHint" stdset="0"> <size> <width>40</width> <height>20</height> @@ -282,10 +340,10 @@ </item> <item> <spacer> - <property name="orientation" > + <property name="orientation"> <enum>Qt::Vertical</enum> </property> - <property name="sizeHint" stdset="0" > + <property name="sizeHint" stdset="0"> <size> <width>428</width> <height>61</height> @@ -302,6 +360,7 @@ <tabstop>printFirstPageFirstButton</tabstop> <tabstop>printFirstPageLastButton</tabstop> <tabstop>printMagnificationSpinBox</tabstop> + <tabstop>resolutionSpinBox</tabstop> <tabstop>printheaderFontButton</tabstop> <tabstop>topMarginSpinBox</tabstop> <tabstop>leftMarginSpinBox</tabstop>
--- a/Preferences/ConfigurationPages/SecurityPage.py Sun Apr 03 12:29:37 2016 +0200 +++ b/Preferences/ConfigurationPages/SecurityPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -11,10 +11,6 @@ from PyQt5.QtCore import pyqtSlot from PyQt5.QtWidgets import QDialog -try: - from PyQt5.QtWebKit import QWebSettings -except ImportError: - QWebSettings = None from .ConfigurationPageBase import ConfigurationPageBase from .Ui_SecurityPage import Ui_SecurityPage @@ -38,6 +34,7 @@ self.setObjectName("SecurityPage") self.__configDlg = configDialog + self.__displayMode = None # set initial values self.savePasswordsCheckBox.setChecked( @@ -46,15 +43,41 @@ Preferences.getUser("UseMasterPassword")) self.masterPasswordButton.setEnabled( Preferences.getUser("UseMasterPassword")) - if QWebSettings and hasattr(QWebSettings, "DnsPrefetchEnabled"): - self.dnsPrefetchCheckBox.setChecked( - Preferences.getHelp("DnsPrefetchEnabled")) - else: - self.dnsPrefetchCheckBox.setEnabled(False) self.__newPassword = "" self.__oldUseMasterPassword = Preferences.getUser("UseMasterPassword") + def setMode(self, displayMode): + """ + Public method to perform mode dependent setups. + + @param displayMode mode of the configuration dialog + (ConfigurationWidget.DefaultMode, + ConfigurationWidget.HelpBrowserMode, + ConfigurationWidget.WebBrowserMode) + """ + from ..ConfigurationDialog import ConfigurationWidget + assert displayMode in ( + ConfigurationWidget.DefaultMode, + ConfigurationWidget.HelpBrowserMode, + ConfigurationWidget.WebBrowserMode + ) + + self.__displayMode = displayMode + if self.__displayMode == ConfigurationWidget.HelpBrowserMode: + try: + from PyQt5.QtWebKit import QWebSettings + if QWebSettings and \ + hasattr(QWebSettings, "DnsPrefetchEnabled"): + self.dnsPrefetchCheckBox.setChecked( + Preferences.getHelp("DnsPrefetchEnabled")) + except ImportError: + self.dnsPrefetchCheckBox.setEnabled(False) + else: + if self.__configDlg.isUsingWebEngine(): + self.dnsPrefetchCheckBox.setEnabled(False) + self.dnsGroup.hide() + def save(self): """ Public slot to save the Help Viewers configuration.
--- a/Preferences/ConfigurationPages/SecurityPage.ui Sun Apr 03 12:29:37 2016 +0200 +++ b/Preferences/ConfigurationPages/SecurityPage.ui Sun Apr 03 16:33:37 2016 +0200 @@ -91,7 +91,7 @@ </widget> </item> <item> - <widget class="QGroupBox" name="groupBox_3"> + <widget class="QGroupBox" name="dnsGroup"> <property name="title"> <string>DNS</string> </property>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Preferences/ConfigurationPages/WebBrowserAppearancePage.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2006 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Web Browser configuration page. +""" + +from __future__ import unicode_literals + +from PyQt5.QtGui import QFont +from PyQt5.QtWidgets import QFontDialog + +from E5Gui.E5PathPicker import E5PathPickerModes + +from .ConfigurationPageBase import ConfigurationPageBase +from .Ui_WebBrowserAppearancePage import Ui_WebBrowserAppearancePage + +import Preferences + +try: + MonospacedFontsOption = QFontDialog.MonospacedFonts +except AttributeError: + MonospacedFontsOption = QFontDialog.FontDialogOptions(0x10) + + +class WebBrowserAppearancePage(ConfigurationPageBase, + Ui_WebBrowserAppearancePage): + """ + Class implementing the Web Browser Appearance page. + """ + def __init__(self): + """ + Constructor + """ + super(WebBrowserAppearancePage, self).__init__() + self.setupUi(self) + self.setObjectName("WebBrowserAppearancePage") + + self.styleSheetPicker.setMode(E5PathPickerModes.OpenFileMode) + self.styleSheetPicker.setFilters(self.tr( + "Cascading Style Sheets (*.css);;All files (*)")) + + self.__displayMode = None + + # set initial values + defaultFontSize = Preferences.getWebBrowser("DefaultFontSize") + fixedFontSize = Preferences.getWebBrowser("DefaultFixedFontSize") + self.defaultSizeSpinBox.setValue(defaultFontSize) + self.fixedSizeSpinBox.setValue(fixedFontSize) + self.minSizeSpinBox.setValue( + Preferences.getWebBrowser("MinimumFontSize")) + self.minLogicalSizeSpinBox.setValue( + Preferences.getWebBrowser("MinimumLogicalFontSize")) + + self.standardFontCombo.setCurrentFont( + QFont(Preferences.getWebBrowser("StandardFontFamily"), + defaultFontSize, QFont.Normal, False)) + self.fixedFontCombo.setCurrentFont( + QFont(Preferences.getWebBrowser("FixedFontFamily"), + fixedFontSize, QFont.Normal, False)) + self.serifFontCombo.setCurrentFont( + QFont(Preferences.getWebBrowser("SerifFontFamily"), + defaultFontSize, QFont.Normal, False)) + self.sansSerifFontCombo.setCurrentFont( + QFont(Preferences.getWebBrowser("SansSerifFontFamily"), + defaultFontSize, QFont.Normal, False)) + self.cursiveFontCombo.setCurrentFont( + QFont(Preferences.getWebBrowser("CursiveFontFamily"), + defaultFontSize, QFont.Normal, True)) + self.fantasyFontCombo.setCurrentFont( + QFont(Preferences.getWebBrowser("FantasyFontFamily"), + defaultFontSize, QFont.Normal, False)) + + self.initColour("SaveUrlColor", self.secureURLsColourButton, + Preferences.getWebBrowser) + + self.autoLoadImagesCheckBox.setChecked( + Preferences.getWebBrowser("AutoLoadImages")) + + self.styleSheetPicker.setText( + Preferences.getWebBrowser("UserStyleSheet")) + + self.tabsCloseButtonCheckBox.setChecked( + Preferences.getUI("SingleCloseButton")) + self.warnOnMultipleCloseCheckBox.setChecked( + Preferences.getWebBrowser("WarnOnMultipleClose")) + + def setMode(self, displayMode): + """ + Public method to perform mode dependent setups. + + @param displayMode mode of the configuration dialog + (ConfigurationWidget.DefaultMode, + ConfigurationWidget.HelpBrowserMode, + ConfigurationWidget.TrayStarterMode) + """ + from ..ConfigurationDialog import ConfigurationWidget + assert displayMode in ( + ConfigurationWidget.DefaultMode, + ConfigurationWidget.WebBrowserMode, + ) + + self.__displayMode = displayMode + if self.__displayMode != ConfigurationWidget.WebBrowserMode: + self.tabsGroupBox.hide() + + def save(self): + """ + Public slot to save the Help Viewers configuration. + """ + Preferences.setWebBrowser( + "StandardFontFamily", + self.standardFontCombo.currentFont().family()) + Preferences.setWebBrowser( + "FixedFontFamily", + self.fixedFontCombo.currentFont().family()) + Preferences.setWebBrowser( + "SerifFontFamily", + self.serifFontCombo.currentFont().family()) + Preferences.setWebBrowser( + "SansSerifFontFamily", + self.sansSerifFontCombo.currentFont().family()) + Preferences.setWebBrowser( + "CursiveFontFamily", + self.cursiveFontCombo.currentFont().family()) + Preferences.setWebBrowser( + "FantasyFontFamily", + self.fantasyFontCombo.currentFont().family()) + + Preferences.setWebBrowser( + "DefaultFontSize", + self.defaultSizeSpinBox.value()) + Preferences.setWebBrowser( + "DefaultFixedFontSize", + self.fixedSizeSpinBox.value()) + Preferences.setWebBrowser( + "MinimumFontSize", + self.minSizeSpinBox.value()) + Preferences.setWebBrowser( + "MinimumLogicalFontSize", + self.minLogicalSizeSpinBox.value()) + + Preferences.setWebBrowser( + "AutoLoadImages", + self.autoLoadImagesCheckBox.isChecked()) + + Preferences.setWebBrowser( + "UserStyleSheet", + self.styleSheetPicker.text()) + + self.saveColours(Preferences.setWebBrowser) + + from ..ConfigurationDialog import ConfigurationWidget + if self.__displayMode == ConfigurationWidget.WebBrowserMode: + Preferences.setUI( + "SingleCloseButton", + self.tabsCloseButtonCheckBox.isChecked()) + + Preferences.setWebBrowser( + "WarnOnMultipleClose", + self.warnOnMultipleCloseCheckBox.isChecked()) + + +def create(dlg): + """ + Module function to create the configuration page. + + @param dlg reference to the configuration dialog + @return reference to the instantiated page (ConfigurationPageBase) + """ + page = WebBrowserAppearancePage() + return page
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Preferences/ConfigurationPages/WebBrowserAppearancePage.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,410 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>WebBrowserAppearancePage</class> + <widget class="QWidget" name="WebBrowserAppearancePage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>499</width> + <height>782</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QLabel" name="headerLabel"> + <property name="text"> + <string><b>Configure Web Browser appearance</b></string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line17"> + <property name="frameShape"> + <enum>QFrame::HLine</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Sunken</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_4"> + <property name="title"> + <string>Fonts</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Standard Font:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QFontComboBox" name="standardFontCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the standard font</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Fixed Width Font</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QFontComboBox" name="fixedFontCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the fixed width font</string> + </property> + <property name="fontFilters"> + <set>QFontComboBox::MonospacedFonts</set> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Serif Font:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QFontComboBox" name="serifFontCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the serif font</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Sans Serif Font:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QFontComboBox" name="sansSerifFontCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the sans serif font</string> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Cursive Font:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QFontComboBox" name="cursiveFontCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the cursive font</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Fantasy Font:</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QFontComboBox" name="fantasyFontCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the fantasy font</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_6"> + <property name="title"> + <string>Font Sizes</string> + </property> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="0" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Default Font Size:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="defaultSizeSpinBox"> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="minimum"> + <number>1</number> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Fixed Font Size:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="fixedSizeSpinBox"> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="minimum"> + <number>1</number> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>Minimum Font Size:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="minSizeSpinBox"> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="minimum"> + <number>1</number> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>Minimum Logical Font Size:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QSpinBox" name="minLogicalSizeSpinBox"> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="minimum"> + <number>1</number> + </property> + </widget> + </item> + <item row="3" column="2"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>230</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Colours</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="textLabel1_3"> + <property name="text"> + <string>Background colour of secure URLs:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="secureURLsColourButton"> + <property name="minimumSize"> + <size> + <width>100</width> + <height>0</height> + </size> + </property> + <property name="toolTip"> + <string>Select the background colour for secure URLs.</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="0" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>141</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Images</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QCheckBox" name="autoLoadImagesCheckBox"> + <property name="toolTip"> + <string>Select to load images</string> + </property> + <property name="text"> + <string>Load images</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Style Sheet</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>User Style Sheet:</string> + </property> + </widget> + </item> + <item> + <widget class="E5PathPicker" name="styleSheetPicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the file name of a user style sheet</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="tabsGroupBox"> + <property name="title"> + <string>Tabs</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QCheckBox" name="tabsCloseButtonCheckBox"> + <property name="text"> + <string>Show only one close button instead of one for each tab</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="warnOnMultipleCloseCheckBox"> + <property name="toolTip"> + <string>Select to issue a warning, if multiple tabs are about to be closed</string> + </property> + <property name="text"> + <string>Warn, if multiple tabs are about to be closed</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>479</width> + <height>121</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5PathPicker</class> + <extends>QWidget</extends> + <header>E5Gui/E5PathPicker.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>secureURLsColourButton</tabstop> + <tabstop>autoLoadImagesCheckBox</tabstop> + <tabstop>styleSheetPicker</tabstop> + <tabstop>tabsCloseButtonCheckBox</tabstop> + <tabstop>warnOnMultipleCloseCheckBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Preferences/ConfigurationPages/WebBrowserPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Web Browser configuration page. +""" + +from PyQt5.QtCore import pyqtSlot, QLocale + +from .ConfigurationPageBase import ConfigurationPageBase +from .Ui_WebBrowserPage import Ui_WebBrowserPage + +import Preferences + + +class WebBrowserPage(ConfigurationPageBase, Ui_WebBrowserPage): + """ + Class documentation goes here. + """ + def __init__(self, configDialog): + """ + Constructor + + @param configDialog reference to the configuration dialog + (ConfigurationDialog) + """ + super(WebBrowserPage, self).__init__() + self.setupUi(self) + self.setObjectName("WebBrowserPage") + + self.__configDlg = configDialog + mw = configDialog.parent().parent() + if hasattr(mw, "helpWindow") and mw.helpWindow is not None: + # IDE + self.__browserWindow = mw.helpWindow + elif hasattr(mw, "currentBrowser"): + # Web Browser + self.__browserWindow = mw + else: + self.__browserWindow = None + self.setCurrentPageButton.setEnabled(self.__browserWindow is not None) + + defaultSchemes = ["file://", "http://", "https://"] + self.defaultSchemeCombo.addItems(defaultSchemes) + + # set initial values + self.singleHelpWindowCheckBox.setChecked( + Preferences.getWebBrowser("SingleWebBrowserWindow")) + self.saveGeometryCheckBox.setChecked( + Preferences.getWebBrowser("SaveGeometry")) + self.webSuggestionsCheckBox.setChecked( + Preferences.getWebBrowser("WebSearchSuggestions")) + self.showTabPreviews.setChecked( + Preferences.getWebBrowser("ShowPreview")) + self.errorPageCheckBox.setChecked( + Preferences.getWebBrowser("ErrorPageEnabled")) + self.scrollingCheckBox.setChecked( + Preferences.getWebBrowser("ScrollAnimatorEnabled")) + self.fullscreenCheckBox.setChecked( + Preferences.getWebBrowser("FullScreenSupportEnabled")) + + self.javaScriptGroup.setChecked( + Preferences.getWebBrowser("JavaScriptEnabled")) + self.jsOpenWindowsCheckBox.setChecked( + Preferences.getWebBrowser("JavaScriptCanOpenWindows")) + # TODO: Qt 5.7? +## self.jsCloseWindowsCheckBox.setChecked( +## Preferences.getWebBrowser("JavaScriptCanCloseWindows")) + self.jsClipboardCheckBox.setChecked( + Preferences.getWebBrowser("JavaScriptCanAccessClipboard")) + self.pluginsCheckBox.setChecked( + Preferences.getWebBrowser("PluginsEnabled")) + self.doNotTrackCheckBox.setChecked( + Preferences.getWebBrowser("DoNotTrack")) + self.sendRefererCheckBox.setChecked( + Preferences.getWebBrowser("SendReferer")) + + self.diskCacheCheckBox.setChecked( + Preferences.getWebBrowser("DiskCacheEnabled")) + self.cacheSizeSpinBox.setValue( + Preferences.getWebBrowser("DiskCacheSize")) + + self.startupCombo.setCurrentIndex( + Preferences.getWebBrowser("StartupBehavior")) + self.homePageEdit.setText( + Preferences.getWebBrowser("HomePage")) + + self.defaultSchemeCombo.setCurrentIndex( + self.defaultSchemeCombo.findText( + Preferences.getWebBrowser("DefaultScheme"))) + + historyLimit = Preferences.getWebBrowser("HistoryLimit") + idx = 0 + if historyLimit == 1: + idx = 0 + elif historyLimit == 7: + idx = 1 + elif historyLimit == 14: + idx = 2 + elif historyLimit == 30: + idx = 3 + elif historyLimit == 365: + idx = 4 + elif historyLimit == -1: + idx = 5 + elif historyLimit == -2: + idx = 6 + else: + idx = 5 + self.expireHistory.setCurrentIndex(idx) + + for language in range(2, QLocale.LastLanguage + 1): + countries = [l.country() for l in QLocale.matchingLocales( + language, QLocale.AnyScript, QLocale.AnyCountry)] + if len(countries) > 0: + self.languageCombo.addItem( + QLocale.languageToString(language), language) + self.languageCombo.model().sort(0) + self.languageCombo.insertSeparator(0) + self.languageCombo.insertItem(0, QLocale.languageToString(0), 0) + index = self.languageCombo.findData( + Preferences.getWebBrowser("SearchLanguage")) + if index > -1: + self.languageCombo.setCurrentIndex(index) + + self.spatialCheckBox.setChecked( + Preferences.getWebBrowser("SpatialNavigationEnabled")) + self.linksInFocusChainCheckBox.setChecked( + Preferences.getWebBrowser("LinksIncludedInFocusChain")) + self.xssAuditingCheckBox.setChecked( + Preferences.getWebBrowser("XSSAuditingEnabled")) + + self.webInspectorGroup.setChecked( + Preferences.getWebBrowser("WebInspectorEnabled")) + self.webInspectorPortSpinBox.setValue( + Preferences.getWebBrowser("WebInspectorPort")) + + # TODO: Qt 5.7? + # Hide entries not yet supported + self.jsCloseWindowsCheckBox.hide() + + def save(self): + """ + Public slot to save the Help Viewers configuration. + """ + Preferences.setWebBrowser( + "SingleWebBrowserWindow", + self.singleHelpWindowCheckBox.isChecked()) + Preferences.setWebBrowser( + "SaveGeometry", + self.saveGeometryCheckBox.isChecked()) + Preferences.setWebBrowser( + "WebSearchSuggestions", + self.webSuggestionsCheckBox.isChecked()) + Preferences.setWebBrowser( + "ShowPreview", + self.showTabPreviews.isChecked()) + Preferences.setWebBrowser( + "ErrorPageEnabled", + self.errorPageCheckBox.isChecked()) + Preferences.setWebBrowser( + "ScrollAnimatorEnabled", + self.scrollingCheckBox.isChecked()) + Preferences.setWebBrowser( + "FullScreenSupportEnabled", + self.fullscreenCheckBox.isChecked()) + + Preferences.setWebBrowser( + "JavaScriptEnabled", + self.javaScriptGroup.isChecked()) + Preferences.setWebBrowser( + "JavaScriptCanOpenWindows", + self.jsOpenWindowsCheckBox.isChecked()) + # TODO: Qt 5.7? +## Preferences.setWebBrowser( +## "JavaScriptCanCloseWindows", +## self.jsCloseWindowsCheckBox.isChecked()) + Preferences.setWebBrowser( + "JavaScriptCanAccessClipboard", + self.jsClipboardCheckBox.isChecked()) + Preferences.setWebBrowser( + "PluginsEnabled", + self.pluginsCheckBox.isChecked()) + Preferences.setWebBrowser( + "DoNotTrack", + self.doNotTrackCheckBox.isChecked()) + Preferences.setWebBrowser( + "SendReferer", + self.sendRefererCheckBox.isChecked()) + + Preferences.setWebBrowser( + "DiskCacheEnabled", + self.diskCacheCheckBox.isChecked()) + Preferences.setWebBrowser( + "DiskCacheSize", + self.cacheSizeSpinBox.value()) + + Preferences.setWebBrowser( + "StartupBehavior", + self.startupCombo.currentIndex()) + Preferences.setWebBrowser( + "HomePage", + self.homePageEdit.text()) + + Preferences.setWebBrowser( + "DefaultScheme", + self.defaultSchemeCombo.currentText()) + + idx = self.expireHistory.currentIndex() + if idx == 0: + historyLimit = 1 + elif idx == 1: + historyLimit = 7 + elif idx == 2: + historyLimit = 14 + elif idx == 3: + historyLimit = 30 + elif idx == 4: + historyLimit = 365 + elif idx == 5: + historyLimit = -1 + elif idx == 6: + historyLimit = -2 + Preferences.setWebBrowser("HistoryLimit", historyLimit) + + languageIndex = self.languageCombo.currentIndex() + if languageIndex > -1: + language = self.languageCombo.itemData(languageIndex) + else: + # fall back to system default + language = QLocale.system().language() + Preferences.setWebBrowser("SearchLanguage", language) + + Preferences.setWebBrowser( + "SpatialNavigationEnabled", + self.spatialCheckBox.isChecked()) + Preferences.setWebBrowser( + "LinksIncludedInFocusChain", + self.linksInFocusChainCheckBox.isChecked()) + Preferences.setWebBrowser( + "XSSAuditingEnabled", + self.xssAuditingCheckBox.isChecked()) + + Preferences.setWebBrowser( + "WebInspectorEnabled", + self.webInspectorGroup.isChecked()) + Preferences.setWebBrowser( + "WebInspectorPort", + self.webInspectorPortSpinBox.value()) + + @pyqtSlot() + def on_setCurrentPageButton_clicked(self): + """ + Private slot to set the current page as the home page. + """ + url = self.__browserWindow.currentBrowser().url() + self.homePageEdit.setText(bytes(url.toEncoded()).decode()) + + @pyqtSlot() + def on_defaultHomeButton_clicked(self): + """ + Private slot to set the default home page. + """ + self.homePageEdit.setText(Preferences.Prefs.helpDefaults["HomePage"]) + + @pyqtSlot(int) + def on_startupCombo_currentIndexChanged(self, index): + """ + Private slot to enable elements depending on the selected startup + entry. + + @param index index of the selected entry (integer) + """ + enable = index == 0 + self.homePageLabel.setEnabled(enable) + self.homePageEdit.setEnabled(enable) + self.defaultHomeButton.setEnabled(enable) + self.setCurrentPageButton.setEnabled(enable) + + @pyqtSlot() + def on_refererWhitelistButton_clicked(self): + """ + Private slot to edit the referer whitelist. + """ + from WebBrowser.Network.SendRefererWhitelistDialog import \ + SendRefererWhitelistDialog + SendRefererWhitelistDialog(self).exec_() + + +def create(dlg): + """ + Module function to create the configuration page. + + @param dlg reference to the configuration dialog + @return reference to the instantiated page (ConfigurationPageBase) + """ + page = WebBrowserPage(dlg) + return page
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Preferences/ConfigurationPages/WebBrowserPage.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,681 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>WebBrowserPage</class> + <widget class="QWidget" name="WebBrowserPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>616</width> + <height>1329</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="headerLabel"> + <property name="text"> + <string><b>Configure Web Browser</b></string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line17"> + <property name="frameShape"> + <enum>QFrame::HLine</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Sunken</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_4"> + <property name="title"> + <string>General</string> + </property> + <layout class="QGridLayout" name="gridLayout_5"> + <item row="0" column="0"> + <widget class="QCheckBox" name="singleHelpWindowCheckBox"> + <property name="toolTip"> + <string>Select to use a single web browser window only</string> + </property> + <property name="text"> + <string>Use single web browser window</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QCheckBox" name="webSuggestionsCheckBox"> + <property name="toolTip"> + <string>Select to enable suggestions for web searches</string> + </property> + <property name="text"> + <string>Show suggestions for web searches</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QCheckBox" name="saveGeometryCheckBox"> + <property name="toolTip"> + <string>Select to save the window size and position</string> + </property> + <property name="text"> + <string>Save size and position upon exit</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QCheckBox" name="showTabPreviews"> + <property name="toolTip"> + <string>Select to show a page preview when the mouse hovers over the tab</string> + </property> + <property name="text"> + <string>Show preview when hovering tab</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="errorPageCheckBox"> + <property name="toolTip"> + <string>Select to enable displaying the built-in Chromium error pages.</string> + </property> + <property name="text"> + <string>Use built-in Chromium error page</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QCheckBox" name="scrollingCheckBox"> + <property name="toolTip"> + <string>Select to activate animated scrolling</string> + </property> + <property name="text"> + <string>Enable animated scrolling</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="fullscreenCheckBox"> + <property name="toolTip"> + <string>Select to enable fullscreen support</string> + </property> + <property name="text"> + <string>Enable Fullscreen Support</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="startupGroupBox"> + <property name="title"> + <string>Startup</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>On startup:</string> + </property> + </widget> + </item> + <item row="0" column="1" colspan="3"> + <widget class="QComboBox" name="startupCombo"> + <property name="toolTip"> + <string>Select the startup behavior</string> + </property> + <item> + <property name="text"> + <string>Show Home Page</string> + </property> + </item> + <item> + <property name="text"> + <string>Show Speed Dial</string> + </property> + </item> + <item> + <property name="text"> + <string>Show Empty Page</string> + </property> + </item> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="homePageLabel"> + <property name="text"> + <string>Home Page:</string> + </property> + </widget> + </item> + <item row="1" column="1" colspan="3"> + <widget class="QLineEdit" name="homePageEdit"> + <property name="toolTip"> + <string>Enter the desired home page</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QPushButton" name="setCurrentPageButton"> + <property name="toolTip"> + <string>Press to set the current page as the home page</string> + </property> + <property name="text"> + <string>Set to current page</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QPushButton" name="defaultHomeButton"> + <property name="toolTip"> + <string>Press to set the default home page</string> + </property> + <property name="text"> + <string>Set to default home page</string> + </property> + </widget> + </item> + <item row="2" column="3"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>160</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Scheme</string> + </property> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Default Scheme:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="defaultSchemeCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the default scheme</string> + </property> + <property name="whatsThis"> + <string><b>Default Scheme</b><p>Select the default scheme. This scheme is prepended to URLs, that don't contain one.</p></string> + </property> + <property name="editable"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="privacyGroup"> + <property name="title"> + <string>Privacy</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QGroupBox" name="javaScriptGroup"> + <property name="toolTip"> + <string>Select to enable JavaScript</string> + </property> + <property name="title"> + <string>Enable JavaScript</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QCheckBox" name="jsOpenWindowsCheckBox"> + <property name="toolTip"> + <string>Select to allow JavaScript to open windows</string> + </property> + <property name="text"> + <string>JavaScript can open windows</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QCheckBox" name="jsCloseWindowsCheckBox"> + <property name="toolTip"> + <string>Select to allow JavaScript to close windows</string> + </property> + <property name="text"> + <string>JavaScript can close windows</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QCheckBox" name="jsClipboardCheckBox"> + <property name="toolTip"> + <string>Select to allow JavaScript to access the clipboard</string> + </property> + <property name="text"> + <string>JavaScript can access clipboard</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QCheckBox" name="pluginsCheckBox"> + <property name="toolTip"> + <string>Select to enable plugins in web pages</string> + </property> + <property name="text"> + <string>Enable Plug-ins</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="doNotTrackCheckBox"> + <property name="toolTip"> + <string>Select to enabled the "Do Not Track" feature</string> + </property> + <property name="text"> + <string>Tell web sites I do not want to be tracked</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QCheckBox" name="sendRefererCheckBox"> + <property name="toolTip"> + <string>Select to send referer headers to the server</string> + </property> + <property name="text"> + <string>Send Referer header to servers</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_6"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="refererWhitelistButton"> + <property name="toolTip"> + <string>Press to edit the list of whitelisted hosts</string> + </property> + <property name="text"> + <string>Edit Referer Whitelist ...</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_6"> + <property name="title"> + <string>Security</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QCheckBox" name="xssAuditingCheckBox"> + <property name="toolTip"> + <string>Select to enable XSS auditing</string> + </property> + <property name="whatsThis"> + <string><b>Enable XSS Auditing</b> +<p>This selects whether load requests should be monitored for cross-site scripting attempts. Suspicious scripts will be blocked. These will be reported in the JavaScript console. Enabling this feature might have an impact on performance.</p></string> + </property> + <property name="text"> + <string>Enable XSS Auditing</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>History</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Remove history items:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="expireHistory"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the period for expiration of history entries</string> + </property> + <item> + <property name="text"> + <string>After one day</string> + </property> + </item> + <item> + <property name="text"> + <string>After one week</string> + </property> + </item> + <item> + <property name="text"> + <string>After two weeks</string> + </property> + </item> + <item> + <property name="text"> + <string>After one month</string> + </property> + </item> + <item> + <property name="text"> + <string>After one year</string> + </property> + </item> + <item> + <property name="text"> + <string>Manually</string> + </property> + </item> + <item> + <property name="text"> + <string>On application exit</string> + </property> + </item> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="cacheGroup"> + <property name="title"> + <string>Browser Cache</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0" colspan="3"> + <widget class="QCheckBox" name="diskCacheCheckBox"> + <property name="text"> + <string>Enable disk cache</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Cache size:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="cacheSizeSpinBox"> + <property name="toolTip"> + <string>Enter the maximum size of the disk cache</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string> MB</string> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>999</number> + </property> + </widget> + </item> + <item row="1" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>410</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_5"> + <property name="title"> + <string>Web Search</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Language:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="languageCombo"> + <property name="toolTip"> + <string>Select the language to be used for web searches</string> + </property> + <property name="sizeAdjustPolicy"> + <enum>QComboBox::AdjustToContents</enum> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>450</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Navigation</string> + </property> + <layout class="QGridLayout" name="gridLayout_6"> + <item row="0" column="0"> + <widget class="QCheckBox" name="spatialCheckBox"> + <property name="toolTip"> + <string>Select to enable the spatial navigation feature</string> + </property> + <property name="whatsThis"> + <string><b>Enable Spatial Navigation</b> +<p>This enables or disables the Spatial Navigation feature, which consists in the ability to navigate between focusable elements in a Web page, such as hyperlinks and form controls, by using Left, Right, Up and Down arrow keys. For example, if a user presses the Right key, heuristics determine whether there is an element he might be trying to reach towards the right and which element he probably wants.</p></string> + </property> + <property name="text"> + <string>Enable Spatial Navigation</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QCheckBox" name="linksInFocusChainCheckBox"> + <property name="toolTip"> + <string>Select to include links in focus chain</string> + </property> + <property name="whatsThis"> + <string><b>Include Links in Focus Chain</b> +<p>This selects whether hyperlinks should be included in the keyboard focus chain.</p></string> + </property> + <property name="text"> + <string>Include Links in Focus Chain</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="webInspectorGroup"> + <property name="toolTip"> + <string>Select to enable the Web Inspector tool</string> + </property> + <property name="title"> + <string>Enable Web Development (Web Inspector)</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QGridLayout" name="gridLayout_8"> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Web Inspector Port:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="webInspectorPortSpinBox"> + <property name="toolTip"> + <string>Enter the port to be used by the web inspector</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="minimum"> + <number>1025</number> + </property> + <property name="maximum"> + <number>65535</number> + </property> + </widget> + </item> + <item row="0" column="2"> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>372</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="0" colspan="3"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string><font color="red"><b>Note:</b> Web Inspector settings are activated after a restart of the application.</font></string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>479</width> + <height>121</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <tabstops> + <tabstop>singleHelpWindowCheckBox</tabstop> + <tabstop>webSuggestionsCheckBox</tabstop> + <tabstop>saveGeometryCheckBox</tabstop> + <tabstop>showTabPreviews</tabstop> + <tabstop>errorPageCheckBox</tabstop> + <tabstop>scrollingCheckBox</tabstop> + <tabstop>fullscreenCheckBox</tabstop> + <tabstop>startupCombo</tabstop> + <tabstop>homePageEdit</tabstop> + <tabstop>setCurrentPageButton</tabstop> + <tabstop>defaultHomeButton</tabstop> + <tabstop>defaultSchemeCombo</tabstop> + <tabstop>javaScriptGroup</tabstop> + <tabstop>jsOpenWindowsCheckBox</tabstop> + <tabstop>jsCloseWindowsCheckBox</tabstop> + <tabstop>jsClipboardCheckBox</tabstop> + <tabstop>pluginsCheckBox</tabstop> + <tabstop>doNotTrackCheckBox</tabstop> + <tabstop>sendRefererCheckBox</tabstop> + <tabstop>refererWhitelistButton</tabstop> + <tabstop>xssAuditingCheckBox</tabstop> + <tabstop>expireHistory</tabstop> + <tabstop>diskCacheCheckBox</tabstop> + <tabstop>cacheSizeSpinBox</tabstop> + <tabstop>languageCombo</tabstop> + <tabstop>spatialCheckBox</tabstop> + <tabstop>linksInFocusChainCheckBox</tabstop> + <tabstop>webInspectorGroup</tabstop> + <tabstop>webInspectorPortSpinBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- a/Preferences/Shortcuts.py Sun Apr 03 12:29:37 2016 +0200 +++ b/Preferences/Shortcuts.py Sun Apr 03 16:33:37 2016 +0200 @@ -37,7 +37,8 @@ act.setAlternateShortcut(QKeySequence(accel), removeEmpty=True) -def readShortcuts(prefClass=Prefs, helpViewer=None, pluginName=None): +def readShortcuts(prefClass=Prefs, helpViewer=None, pluginName=None, + helpViewerCategory=""): """ Module function to read the keyboard shortcuts for the defined QActions. @@ -45,6 +46,7 @@ @keyparam helpViewer reference to the help window object @keyparam pluginName name of the plugin for which to load shortcuts (string) + @keyparam helpViewerCategory name of the help viewer category (string) """ if helpViewer is None and pluginName is None: for act in e5App().getObject("Project").getActions(): @@ -92,8 +94,10 @@ __readShortcut(act, category, prefClass) if helpViewer is not None: + if not helpViewerCategory: + helpViewerCategory = "HelpViewer" for act in helpViewer.getActions(): - __readShortcut(act, "HelpViewer", prefClass) + __readShortcut(act, helpViewerCategory, prefClass) if pluginName is not None: try:
--- a/Preferences/__init__.py Sun Apr 03 12:29:37 2016 +0200 +++ b/Preferences/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -37,6 +37,10 @@ from PyQt5.QtWebKit import QWebSettings except ImportError: QWebSettings = None +try: + from PyQt5.QtWebEngineWidgets import QWebEngineSettings +except ImportError: + QWebEngineSettings = None from PyQt5.Qsci import QsciScintilla, QsciLexerPython from E5Gui import E5FileDialog @@ -51,9 +55,12 @@ ResourcesBrowserFlag, TranslationsBrowserFlag, InterfacesBrowserFlag, \ OthersBrowserFlag, AllBrowsersFlag -from Helpviewer.FlashCookieManager.FlashCookieUtilities import \ - flashDataPathForOS - +try: + from Helpviewer.FlashCookieManager.FlashCookieUtilities import \ + flashDataPathForOS +except ImportError: + from WebBrowser.FlashCookieManager.FlashCookieUtilities import \ + flashDataPathForOS class Prefs(object): """ @@ -705,6 +712,7 @@ "RightMargin": 1.0, "TopMargin": 1.0, "BottomMargin": 1.0, + "Resolution": 150, # printer resolution in DPI } # defaults for the project settings @@ -835,7 +843,7 @@ "StartupBehavior": 1, # show speed dial "HomePage": "eric:home", "HistoryLimit": 30, - "DefaultScheme": "file://", + "DefaultScheme": "https://", "OfflineStorageDatabaseQuota": 50, # 50 MB "UserAgent": "", "ShowPreview": True, @@ -998,6 +1006,148 @@ cls.webSettingsIntitialized = True webSettingsIntitialized = False + + # defaults for the web browser settings + webBrowserDefaults = { + "SingleWebBrowserWindow": True, + "SaveGeometry": True, + "WebBrowserState": QByteArray(), + "StartupBehavior": 1, # show speed dial + "HomePage": "eric:home", + "WarnOnMultipleClose": True, + "DefaultScheme": "https://", + "UserStyleSheet": "", + "ZoomValuesDB": "{}", # empty JSON dictionary + "HistoryLimit": 30, + "WebSearchSuggestions": True, + "WebSearchEngine": "DuckDuckGo", + "WebSearchKeywords": [], # array of two tuples (keyword, + # search engine name) + "SearchLanguage": QLocale().language(), + "RssFeeds": [], + "ShowPreview": True, + "WebInspectorPort": 42024, + "WebInspectorEnabled": False, + "DiskCacheEnabled": True, + "DiskCacheSize": 50, # 50 MB + "SslExceptionsDB": "{}", # empty JSON dictionary + "DoNotTrack": False, + "SendReferer": True, + "SendRefererWhitelist": ["qt-apps.org", "kde-apps.org"], + "AcceptCookies": 2, # CookieJar.AcceptOnlyFromSitesNavigatedTo + "KeepCookiesUntil": 0, # CookieJar.KeepUntilExpire + "FilterTrackingCookies": True, + "SaveUrlColor": QColor(184, 248, 169), + "UserAgent": "", + # Grease Monkey + "GreaseMonkeyDisabledScripts": [], + # Downloads + "DownloadManagerRemovePolicy": 0, # never delete downloads + "DownloadManagerSize": QSize(400, 300), + "DownloadManagerPosition": QPoint(), + "DownloadManagerDownloads": [], + # Sync + "SyncEnabled": False, + "SyncBookmarks": True, + "SyncHistory": True, + "SyncPasswords": False, + "SyncUserAgents": True, + "SyncSpeedDial": True, + "SyncEncryptData": False, + "SyncEncryptionKey": "", + "SyncEncryptionKeyLength": 32, # 16, 24 or 32 + "SyncEncryptPasswordsOnly": False, + "SyncType": 0, + "SyncFtpServer": "", + "SyncFtpUser": "", + "SyncFtpPassword": "", + "SyncFtpPath": "", + "SyncFtpPort": 21, + "SyncFtpIdleTimeout": 30, + "SyncDirectoryPath": "", + # AdBlock + "AdBlockEnabled": False, + "AdBlockSubscriptions": [], + "AdBlockUpdatePeriod": 1, + "AdBlockExceptions": [], + "AdBlockUseLimitedEasyList": True, + # Flash Cookie Manager: identical to helpDefaults + # PIM: identical to helpDefaults + # VirusTotal: identical to helpDefaults + } + if QWebEngineSettings: + webBrowserDefaults["HelpViewerType"] = 1 # eric browser + else: + webBrowserDefaults["HelpViewerType"] = 2 # Qt Assistant + + @classmethod + def initWebEngineSettingsDefaults(cls): + """ + Class method to initialize the web engine settings related defaults. + """ + if QWebEngineSettings is None: + return + + webEngineSettings = QWebEngineSettings.globalSettings() + cls.webBrowserDefaults.update({ + "StandardFontFamily": webEngineSettings.fontFamily( + QWebEngineSettings.StandardFont), + "FixedFontFamily": webEngineSettings.fontFamily( + QWebEngineSettings.FixedFont), + "SerifFontFamily": webEngineSettings.fontFamily( + QWebEngineSettings.StandardFont), + "SansSerifFontFamily": webEngineSettings.fontFamily( + QWebEngineSettings.SansSerifFont), + "CursiveFontFamily": webEngineSettings.fontFamily( + QWebEngineSettings.CursiveFont), + "FantasyFontFamily": webEngineSettings.fontFamily( + QWebEngineSettings.FantasyFont), + "DefaultFontSize": webEngineSettings.fontSize( + QWebEngineSettings.DefaultFontSize), + "DefaultFixedFontSize": webEngineSettings.fontSize( + QWebEngineSettings.DefaultFixedFontSize), + "MinimumFontSize": webEngineSettings.fontSize( + QWebEngineSettings.MinimumFontSize), + "MinimumLogicalFontSize": webEngineSettings.fontSize( + QWebEngineSettings.MinimumLogicalFontSize), + + "AutoLoadImages": webEngineSettings.testAttribute( + QWebEngineSettings.AutoLoadImages), + "JavaScriptEnabled": webEngineSettings.testAttribute( + QWebEngineSettings.JavascriptEnabled), + "JavaScriptCanOpenWindows": webEngineSettings.testAttribute( + QWebEngineSettings.JavascriptCanOpenWindows), + # TODO: Qt 5.7? +## "JavaScriptCanCloseWindows": webEngineSettings.testAttribute( +## QWebEngineSettings.JavascriptCanCloseWindows), + "JavaScriptCanAccessClipboard": webEngineSettings.testAttribute( + QWebEngineSettings.JavascriptCanAccessClipboard), + "PluginsEnabled": webEngineSettings.testAttribute( + QWebEngineSettings.PluginsEnabled), + "LocalStorageEnabled": webEngineSettings.testAttribute( + QWebEngineSettings.LocalStorageEnabled), + "DefaultTextEncoding": webEngineSettings.defaultTextEncoding(), + "SpatialNavigationEnabled": webEngineSettings.testAttribute( + QWebEngineSettings.SpatialNavigationEnabled), + "LinksIncludedInFocusChain": webEngineSettings.testAttribute( + QWebEngineSettings.LinksIncludedInFocusChain), + "LocalContentCanAccessRemoteUrls": webEngineSettings.testAttribute( + QWebEngineSettings.LocalContentCanAccessRemoteUrls), + "LocalContentCanAccessFileUrls": webEngineSettings.testAttribute( + QWebEngineSettings.LocalContentCanAccessFileUrls), + "XSSAuditingEnabled": webEngineSettings.testAttribute( + QWebEngineSettings.XSSAuditingEnabled), + "ScrollAnimatorEnabled": webEngineSettings.testAttribute( + QWebEngineSettings.ScrollAnimatorEnabled), + "ErrorPageEnabled": webEngineSettings.testAttribute( + QWebEngineSettings.ErrorPageEnabled), + "FullScreenSupportEnabled": webEngineSettings.testAttribute( + QWebEngineSettings.FullScreenSupportEnabled), + }) + + cls.webEngineSettingsIntitialized = True + + webEngineSettingsIntitialized = False # defaults for system settings sysDefaults = { @@ -1128,10 +1278,12 @@ geometryDefaults = { "HelpViewerGeometry": QByteArray(), "HelpInspectorGeometry": QByteArray(), + "WebBrowserGeometry": QByteArray(), "IconEditorGeometry": QByteArray(), "HexEditorGeometry": QByteArray(), "MainGeometry": QByteArray(), "MainMaximized": False, + "WebInspectorGeometry": QByteArray(), } # if true, revert layouts to factory defaults @@ -2141,7 +2293,7 @@ if key in ["ColorMode", "FirstPageFirst"]: return toBool(prefClass.settings.value( "Printer/" + key, prefClass.printerDefaults[key])) - elif key in ["Magnification", "Orientation", "PageSize"]: + elif key in ["Magnification", "Orientation", "PageSize", "Resolution"]: return int(prefClass.settings.value( "Printer/" + key, prefClass.printerDefaults[key])) elif key in ["LeftMargin", "RightMargin", "TopMargin", "BottomMargin"]: @@ -2543,6 +2695,191 @@ "Help/" + key, pwConvert(value, encode=True)) else: prefClass.settings.setValue("Help/" + key, value) + + +def getWebBrowser(key, prefClass=Prefs): + """ + Module function to retrieve the various web browser settings. + + @param key the key of the value to get + @param prefClass preferences class used as the storage area + @return the requested help setting + """ + # the following entries are identical to the ones of the QtWebKit based + # help viewer and are being redirected there + if key.startswith(("FlashCookie", "Pim", "VirusTotal")): + return getHelp(key, prefClass) + + # Web inspector stuff must come before initializing web engine settings + # because that starts the chromium web process + if key == "WebInspectorPort": + return int(prefClass.settings.value( + "WebBrowser/" + key, prefClass.webBrowserDefaults[key])) + elif key == "WebInspectorEnabled": + return toBool(prefClass.settings.value( + "WebBrowser/" + key, prefClass.webBrowserDefaults[key])) + + if not prefClass.webEngineSettingsIntitialized: + prefClass.initWebEngineSettingsDefaults() + + if key in ["StandardFont", "FixedFont"]: + f = QFont() + f.fromString(prefClass.settings.value( + "WebBrowser/" + key, prefClass.webBrowserDefaults[key])) + return f + elif key in ["SaveUrlColor"]: + col = prefClass.settings.value("WebBrowser/" + key) + if col is not None: + return QColor(col) + else: + return prefClass.webBrowserDefaults[key] + elif key in ["WebSearchKeywords"]: + # return a list of tuples of (keyword, engine name) + keywords = [] + size = prefClass.settings.beginReadArray("WebBrowser/" + key) + for index in range(size): + prefClass.settings.setArrayIndex(index) + keyword = prefClass.settings.value("Keyword") + engineName = prefClass.settings.value("Engine") + keywords.append((keyword, engineName)) + prefClass.settings.endArray() + return keywords + elif key in ["DownloadManagerDownloads"]: + # return a list of tuples of (URL, save location, done flag, page url) + downloads = [] + length = prefClass.settings.beginReadArray("WebBrowser/" + key) + for index in range(length): + prefClass.settings.setArrayIndex(index) + url = prefClass.settings.value("URL") + location = prefClass.settings.value("Location") + done = toBool(prefClass.settings.value("Done")) + pageUrl = prefClass.settings.value("PageURL") + if pageUrl is None: + pageUrl = QUrl() + downloads.append((url, location, done, pageUrl)) + prefClass.settings.endArray() + return downloads + elif key == "RssFeeds": + # return a list of tuples of (URL, title, icon) + feeds = [] + length = prefClass.settings.beginReadArray("WebBrowser/" + key) + for index in range(length): + prefClass.settings.setArrayIndex(index) + url = prefClass.settings.value("URL") + title = prefClass.settings.value("Title") + icon = prefClass.settings.value("Icon") + feeds.append((url, title, icon)) + prefClass.settings.endArray() + return feeds + elif key in ["SyncFtpPassword", "SyncEncryptionKey"]: + from Utilities.crypto import pwConvert + return pwConvert(prefClass.settings.value( + "WebBrowser/" + key, prefClass.helpDefaults[key]), encode=False) + elif key == "HelpViewerType": + # special treatment to adjust for missing QtWebEngine + value = int(prefClass.settings.value( + "WebBrowser/" + key, prefClass.helpDefaults[key])) + if QWebEngineSettings is None: + value = prefClass.helpDefaults[key] + return value + elif key in ["StartupBehavior", "HistoryLimit", + "DownloadManagerRemovePolicy","SyncType", "SyncFtpPort", + "SyncFtpIdleTimeout", "SyncEncryptionKeyLength", + "SearchLanguage", "WebInspectorPort", + "DefaultFontSize", "DefaultFixedFontSize", + "MinimumFontSize", "MinimumLogicalFontSize", + "DiskCacheSize", "AcceptCookies", "KeepCookiesUntil", + "AdBlockUpdatePeriod", + ]: + return int(prefClass.settings.value( + "WebBrowser/" + key, prefClass.webBrowserDefaults[key])) + elif key in ["SingleWebBrowserWindow", "SaveGeometry", "JavaScriptEnabled", + "JavaScriptCanOpenWindows", "JavaScriptCanAccessClipboard", + "AutoLoadImages", "LocalStorageEnabled", + "SpatialNavigationEnabled", "LinksIncludedInFocusChain", + "LocalContentCanAccessRemoteUrls", + "LocalContentCanAccessFileUrls", "XSSAuditingEnabled", + "ScrollAnimatorEnabled", "ErrorPageEnabled", + "WarnOnMultipleClose", "WebSearchSuggestions", + "SyncEnabled", "SyncBookmarks", "SyncHistory", + "SyncPasswords", "SyncUserAgents", "SyncSpeedDial", + "SyncEncryptData", "SyncEncryptPasswordsOnly", + "ShowPreview", "WebInspectorEnabled", "DiskCacheEnabled", + "DoNotTrack", "SendReferer", "FilterTrackingCookies", + "AdBlockEnabled", "AdBlockUseLimitedEasyList", + "PluginsEnabled", "FullScreenSupportEnabled", + ]: + return toBool(prefClass.settings.value( + "WebBrowser/" + key, prefClass.webBrowserDefaults[key])) + elif key in ["GreaseMonkeyDisabledScripts", "SendRefererWhitelist", + "AdBlockSubscriptions", "AdBlockExceptions", + ]: + return toList(prefClass.settings.value( + "WebBrowser/" + key, prefClass.helpDefaults[key])) + else: + return prefClass.settings.value("WebBrowser/" + key, + prefClass.webBrowserDefaults[key]) + + +def setWebBrowser(key, value, prefClass=Prefs): + """ + Module function to store the various web browser settings. + + @param key the key of the setting to be set + @param value the value to be set + @param prefClass preferences class used as the storage area + """ + # the following entries are identical to the ones of the QtWebKit based + # help viewer and are being redirected there + if key.startswith(("FlashCookie", "Pim", "VirusTotal")): + setHelp(key, value, prefClass) + + if key in ["StandardFont", "FixedFont"]: + prefClass.settings.setValue("WebBrowser/" + key, value.toString()) + elif key == "SaveUrlColor": + prefClass.settings.setValue("WebBrowser/" + key, value.name()) + elif key == "WebSearchKeywords": + # value is list of tuples of (keyword, engine name) + prefClass.settings.remove("WebBrowser/" + key) + prefClass.settings.beginWriteArray("WebBrowser/" + key, len(value)) + index = 0 + for v in value: + prefClass.settings.setArrayIndex(index) + prefClass.settings.setValue("Keyword", v[0]) + prefClass.settings.setValue("Engine", v[1]) + index += 1 + prefClass.settings.endArray() + elif key == "DownloadManagerDownloads": + # value is list of tuples of (URL, save location, done flag, page url) + prefClass.settings.remove("Help/" + key) + prefClass.settings.beginWriteArray("WebBrowser/" + key, len(value)) + index = 0 + for v in value: + prefClass.settings.setArrayIndex(index) + prefClass.settings.setValue("URL", v[0]) + prefClass.settings.setValue("Location", v[1]) + prefClass.settings.setValue("Done", v[2]) + prefClass.settings.setValue("PageURL", v[3]) + index += 1 + prefClass.settings.endArray() + elif key == "RssFeeds": + # value is list of tuples of (URL, title, icon) + prefClass.settings.remove("WebBrowser/" + key) + prefClass.settings.beginWriteArray("WebBrowser/" + key, len(value)) + index = 0 + for v in value: + prefClass.settings.setArrayIndex(index) + prefClass.settings.setValue("URL", v[0]) + prefClass.settings.setValue("Title", v[1]) + prefClass.settings.setValue("Icon", v[2]) + index += 1 + prefClass.settings.endArray() + elif key in ["SyncFtpPassword", "SyncEncryptionKey"]: + from Utilities.crypto import pwConvert + prefClass.settings.setValue( + "WebBrowser/" + key, pwConvert(value, encode=True)) + else: + prefClass.settings.setValue("WebBrowser/" + key, value) def getSystem(key, prefClass=Prefs): @@ -3198,6 +3535,16 @@ newPassword ) ) + for key in ["SyncFtpPassword", "SyncEncryptionKey"]: + prefClass.settings.setValue( + "WebBrowser/" + key, + pwRecode( + prefClass.settings.value("WebBrowser/" + key, + prefClass.webBrowserDefaults[key]), + oldPassword, + newPassword + ) + ) initPreferences()
--- a/UI/UserInterface.py Sun Apr 03 12:29:37 2016 +0200 +++ b/UI/UserInterface.py Sun Apr 03 16:33:37 2016 +0200 @@ -9,7 +9,7 @@ from __future__ import unicode_literals try: - str = unicode + str = unicode # __IGNORE_EXCEPTION__ except NameError: pass @@ -32,6 +32,14 @@ WEBKIT_AVAILABLE = True except ImportError: WEBKIT_AVAILABLE = False +if qVersion() < "5.6.0": + WEBENGINE_AVAILABLE = False +else: + try: + from PyQt5 import QtWebEngineWidgets # __IGNORE_WARNING__ + WEBENGINE_AVAILABLE = True + except ImportError: + WEBENGINE_AVAILABLE = False from .Info import Version, BugAddress, Program, FeatureAddress from . import Config @@ -468,10 +476,16 @@ self.__initExternalToolsActions() # create a dummy help window for shortcuts handling - if WEBKIT_AVAILABLE: + if WEBENGINE_AVAILABLE: + from WebBrowser.WebBrowserWindow import WebBrowserWindow + self.dummyHelpViewer = \ + WebBrowserWindow(None, '.', None, 'web_browser', True, True) + elif WEBKIT_AVAILABLE: from Helpviewer.HelpWindow import HelpWindow self.dummyHelpViewer = \ HelpWindow(None, '.', None, 'help viewer', True, True) + else: + self.dummyHelpViewer = None # register all relevant objects splash.showMessage(self.tr("Registering Objects...")) @@ -486,7 +500,7 @@ e5App().registerObject("TaskViewer", self.taskViewer) e5App().registerObject("TemplateViewer", self.templateViewer) e5App().registerObject("Shell", self.shell) - if WEBKIT_AVAILABLE: + if self.dummyHelpViewer is not None: e5App().registerObject("DummyHelpViewer", self.dummyHelpViewer) e5App().registerObject("PluginManager", self.pluginManager) e5App().registerObject("ToolbarManager", self.toolbarManager) @@ -1596,7 +1610,7 @@ self.whatsThisAct.triggered.connect(self.__whatsThis) self.actions.append(self.whatsThisAct) - if WEBKIT_AVAILABLE: + if WEBENGINE_AVAILABLE or WEBKIT_AVAILABLE: self.helpviewerAct = E5Action( self.tr('Helpviewer'), UI.PixmapCache.getIcon("help.png"), @@ -1925,7 +1939,7 @@ self.hexEditorAct.triggered.connect(self.__openHexEditor) self.actions.append(self.hexEditorAct) - if WEBKIT_AVAILABLE: + if WEBENGINE_AVAILABLE or WEBKIT_AVAILABLE: self.webBrowserAct = E5Action( self.tr('eric6 Web Browser'), UI.PixmapCache.getIcon("ericWeb.png"), @@ -3002,12 +3016,16 @@ .format(sip_version_str) versionText += """<tr><td><b>QScintilla</b></td><td>{0}</td></tr>"""\ .format(QSCINTILLA_VERSION_STR) - try: + if WEBENGINE_AVAILABLE: + from WebBrowser.Tools import WebBrowserTools + chromeVersion = WebBrowserTools.getWebEngineVersions()[0] + versionText += \ + """<tr><td><b>WebEngine</b></td><td>{0}</td></tr>"""\ + .format(chromeVersion) + if WEBKIT_AVAILABLE: from PyQt5.QtWebKit import qWebKitVersion versionText += """<tr><td><b>WebKit</b></td><td>{0}</td></tr>"""\ .format(qWebKitVersion()) - except ImportError: - pass versionText += """<tr><td><b>{0}</b></td><td>{1}</td></tr>"""\ .format(Program, Version) versionText += self.tr("""</table>""") @@ -4846,7 +4864,10 @@ if home.endswith(".chm"): self.__chmViewer(home) else: - hvType = Preferences.getHelp("HelpViewerType") + if WEBENGINE_AVAILABLE: + hvType = Preferences.getWebBrowser("HelpViewerType") + else: + hvType = Preferences.getHelp("HelpViewerType") if hvType == 1: self.launchHelpViewer(home) elif hvType == 2: @@ -4905,7 +4926,10 @@ if home.endswith(".chm"): self.__chmViewer(home) else: - hvType = Preferences.getHelp("HelpViewerType") + if WEBENGINE_AVAILABLE: + hvType = Preferences.getWebBrowser("HelpViewerType") + else: + hvType = Preferences.getHelp("HelpViewerType") if hvType == 1: self.launchHelpViewer(home) elif hvType == 2: @@ -4971,7 +4995,10 @@ else: home = "file://" + home - hvType = Preferences.getHelp("HelpViewerType") + if WEBENGINE_AVAILABLE: + hvType = Preferences.getWebBrowser("HelpViewerType") + else: + hvType = Preferences.getHelp("HelpViewerType") if hvType == 1: self.launchHelpViewer(home) elif hvType == 2: @@ -5033,7 +5060,10 @@ else: home = pyqt4DocDir - hvType = Preferences.getHelp("HelpViewerType") + if WEBENGINE_AVAILABLE: + hvType = Preferences.getWebBrowser("HelpViewerType") + else: + hvType = Preferences.getHelp("HelpViewerType") if hvType == 1: self.launchHelpViewer(home) elif hvType == 2: @@ -5097,7 +5127,10 @@ else: home = pyqt5DocDir - hvType = Preferences.getHelp("HelpViewerType") + if WEBENGINE_AVAILABLE: + hvType = Preferences.getWebBrowser("HelpViewerType") + else: + hvType = Preferences.getHelp("HelpViewerType") if hvType == 1: self.launchHelpViewer(home) elif hvType == 2: @@ -5134,7 +5167,10 @@ else: home = "file://" + home - hvType = Preferences.getHelp("HelpViewerType") + if WEBENGINE_AVAILABLE: + hvType = Preferences.getWebBrowser("HelpViewerType") + else: + hvType = Preferences.getHelp("HelpViewerType") if hvType == 1: self.launchHelpViewer(home) elif hvType == 2: @@ -5186,7 +5222,10 @@ else: home = pysideDocDir - hvType = Preferences.getHelp("HelpViewerType") + if WEBENGINE_AVAILABLE: + hvType = Preferences.getWebBrowser("HelpViewerType") + else: + hvType = Preferences.getHelp("HelpViewerType") if hvType == 1: self.launchHelpViewer(home) elif hvType == 2: @@ -5213,12 +5252,22 @@ if not homeUrl.scheme(): home = QUrl.fromLocalFile(home).toString() - if WEBKIT_AVAILABLE: - if not (useSingle or Preferences.getHelp("SingleHelpWindow")) or \ - self.helpWindow is None: - from Helpviewer.HelpWindow import HelpWindow - help = HelpWindow(home, '.', None, 'help viewer', True, - searchWord=searchWord) + if WEBENGINE_AVAILABLE or WEBKIT_AVAILABLE: + single = useSingle + if WEBENGINE_AVAILABLE: + single = single or \ + Preferences.getWebBrowser("SingleWebBrowserWindow") + elif WEBKIT_AVAILABLE: + single = single or Preferences.getHelp("SingleHelpWindow") + if not single or self.helpWindow is None: + if WEBENGINE_AVAILABLE: + from WebBrowser.WebBrowserWindow import WebBrowserWindow + help = WebBrowserWindow(home, '.', None, 'web_browser', + True, searchWord=searchWord) + elif WEBKIT_AVAILABLE: + from Helpviewer.HelpWindow import HelpWindow + help = HelpWindow(home, '.', None, 'help viewer', True, + searchWord=searchWord) if QApplication.desktop().width() > 400 and \ QApplication.desktop().height() > 500: @@ -5226,9 +5275,13 @@ else: help.showMaximized() - if useSingle or Preferences.getHelp("SingleHelpWindow"): + if single: self.helpWindow = help - self.helpWindow.helpClosed.connect(self.__helpClosed) + try: + self.helpWindow.webBrowserClosed.connect( + self.__helpClosed) + except AttributeError: + self.helpWindow.helpClosed.connect(self.__helpClosed) self.preferencesChanged.connect( self.helpWindow.preferencesChanged) self.masterPasswordChanged.connect( @@ -5246,7 +5299,11 @@ """ Private slot to handle the helpClosed signal of the help window. """ - if Preferences.getHelp("SingleHelpWindow"): + if WEBENGINE_AVAILABLE: + single = Preferences.getWebBrowser("SingleWebBrowserWindow") + elif WEBKIT_AVAILABLE: + single = Preferences.getHelp("SingleHelpWindow") + if single: self.preferencesChanged.disconnect( self.helpWindow.preferencesChanged) self.masterPasswordChanged.disconnect( @@ -5284,7 +5341,7 @@ (boolean) @return reference to the help window instance (HelpWindow) """ - if WEBKIT_AVAILABLE: + if WEBENGINE_AVAILABLE or WEBKIT_AVAILABLE: if self.helpWindow is None: self.launchHelpViewer("", useSingle=True) self.helpWindow.raise_() @@ -5304,6 +5361,7 @@ dlg = ConfigurationDialog( self, 'Configuration', expandedEntries=self.__expandedConfigurationEntries, + webEngine=WEBENGINE_AVAILABLE, ) dlg.preferencesChanged.connect(self.__preferencesChanged) dlg.masterPasswordChanged.connect(self.__masterPasswordChanged)
--- a/Utilities/__init__.py Sun Apr 03 12:29:37 2016 +0200 +++ b/Utilities/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -1682,6 +1682,14 @@ qVersion(), linesep, PYQT_VERSION_STR, linesep) info += " sip {0}{1} QScintilla {2}{3}".format( sip_version_str, linesep, QSCINTILLA_VERSION_STR, linesep) + if qVersion() >= "5.6.0": + try: + from PyQt5 import QtWebEngineWidgets # __IGNORE_WARNING__ + from WebBrowser.Tools import WebBrowserTools + chromeVersion = WebBrowserTools.getWebEngineVersions()[0] + info += " WebEngine {0}{1}".format(chromeVersion, linesep) + except ImportError: + pass try: from PyQt5.QtWebKit import qWebKitVersion info += " WebKit {0}{1}".format(qWebKitVersion(), linesep)
--- a/ViewManager/ViewManager.py Sun Apr 03 12:29:37 2016 +0200 +++ b/ViewManager/ViewManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -11,7 +11,7 @@ import os -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSignalMapper, QTimer, \ +from PyQt5.QtCore import pyqtSignal, QSignalMapper, QTimer, \ QFileInfo, QRegExp, Qt, QCoreApplication from PyQt5.QtGui import QColor, QKeySequence, QPalette, QPixmap from PyQt5.QtWidgets import QLineEdit, QToolBar, QWidgetAction, QDialog, \ @@ -6574,7 +6574,6 @@ if editor: self.editorRenamedEd.emit(editor) -## @pyqtSlot(str, int, int) def __cursorChanged(self, fn, line, pos): """ Private slot to handle the cursorChanged signal.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,346 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the AdBlock configuration dialog. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt, QTimer, QCoreApplication +from PyQt5.QtWidgets import QDialog, QMenu, QToolButton + +from E5Gui import E5MessageBox + +from .Ui_AdBlockDialog import Ui_AdBlockDialog + +import UI.PixmapCache +import Preferences + + +class AdBlockDialog(QDialog, Ui_AdBlockDialog): + """ + Class implementing the AdBlock configuration dialog. + """ + def __init__(self, manager, parent=None): + """ + Constructor + + @param manager reference to the AdBlock manager (AdBlockManager) + @param parent reference to the parent object (QWidget) + """ + super(AdBlockDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.__manager = manager + + self.iconLabel.setPixmap(UI.PixmapCache.getPixmap("adBlockPlus48.png")) + + self.updateSpinBox.setValue( + Preferences.getWebBrowser("AdBlockUpdatePeriod")) + + self.useLimitedEasyListCheckBox.setChecked(Preferences.getWebBrowser( + "AdBlockUseLimitedEasyList")) + + self.searchEdit.setInactiveText(self.tr("Search...")) + + self.adBlockGroup.setChecked(self.__manager.isEnabled()) + self.__manager.requiredSubscriptionLoaded.connect(self.addSubscription) + + self.__currentTreeWidget = None + self.__currentSubscription = None + self.__loaded = False + + menu = QMenu(self) + menu.aboutToShow.connect(self.__aboutToShowActionMenu) + self.actionButton.setMenu(menu) + self.actionButton.setIcon(UI.PixmapCache.getIcon("adBlockAction.png")) + self.actionButton.setPopupMode(QToolButton.InstantPopup) + + self.__load() + + self.buttonBox.setFocus() + + def __loadSubscriptions(self): + """ + Private slot to load the AdBlock subscription rules. + """ + for index in range(self.subscriptionsTabWidget.count()): + tree = self.subscriptionsTabWidget.widget(index) + tree.refresh() + + def __load(self): + """ + Private slot to populate the tab widget with subscriptions. + """ + if self.__loaded or not self.adBlockGroup.isChecked(): + return + + from .AdBlockTreeWidget import AdBlockTreeWidget + for subscription in self.__manager.subscriptions(): + tree = AdBlockTreeWidget(subscription, self.subscriptionsTabWidget) + if subscription.isEnabled(): + icon = UI.PixmapCache.getIcon("adBlockPlus.png") + else: + icon = UI.PixmapCache.getIcon("adBlockPlusDisabled.png") + self.subscriptionsTabWidget.addTab( + tree, icon, subscription.title()) + + self.__loaded = True + QCoreApplication.processEvents() + + QTimer.singleShot(50, self.__loadSubscriptions) + + def addSubscription(self, subscription, refresh=True): + """ + Public slot adding a subscription to the list. + + @param subscription reference to the subscription to be + added (AdBlockSubscription) + @param refresh flag indicating to refresh the tree (boolean) + """ + from .AdBlockTreeWidget import AdBlockTreeWidget + tree = AdBlockTreeWidget(subscription, self.subscriptionsTabWidget) + index = self.subscriptionsTabWidget.insertTab( + self.subscriptionsTabWidget.count() - 1, tree, + subscription.title()) + self.subscriptionsTabWidget.setCurrentIndex(index) + QCoreApplication.processEvents() + if refresh: + tree.refresh() + self.__setSubscriptionEnabled(subscription, True) + + def __aboutToShowActionMenu(self): + """ + Private slot to show the actions menu. + """ + subscriptionEditable = self.__currentSubscription and \ + self.__currentSubscription.canEditRules() + subscriptionRemovable = self.__currentSubscription and \ + self.__currentSubscription.canBeRemoved() + subscriptionEnabled = self.__currentSubscription and \ + self.__currentSubscription.isEnabled() + + menu = self.actionButton.menu() + menu.clear() + + menu.addAction(self.tr("Add Rule"), self.__addCustomRule)\ + .setEnabled(subscriptionEditable) + menu.addAction(self.tr("Remove Rule"), self.__removeCustomRule)\ + .setEnabled(subscriptionEditable) + menu.addSeparator() + menu.addAction( + self.tr("Browse Subscriptions..."), self.__browseSubscriptions) + menu.addAction( + self.tr("Remove Subscription"), self.__removeSubscription)\ + .setEnabled(subscriptionRemovable) + if self.__currentSubscription: + menu.addSeparator() + if subscriptionEnabled: + txt = self.tr("Disable Subscription") + else: + txt = self.tr("Enable Subscription") + menu.addAction(txt, self.__switchSubscriptionEnabled) + menu.addSeparator() + menu.addAction( + self.tr("Update Subscription"), self.__updateSubscription)\ + .setEnabled(not subscriptionEditable) + menu.addAction( + self.tr("Update All Subscriptions"), + self.__updateAllSubscriptions) + menu.addSeparator() + menu.addAction(self.tr("Learn more about writing rules..."), + self.__learnAboutWritingFilters) + + def addCustomRule(self, filter): + """ + Public slot to add a custom AdBlock rule. + + @param filter filter to be added (string) + """ + self.subscriptionsTabWidget.setCurrentIndex( + self.subscriptionsTabWidget.count() - 1) + self.__currentTreeWidget.addRule(filter) + + def __addCustomRule(self): + """ + Private slot to add a custom AdBlock rule. + """ + self.__currentTreeWidget.addRule() + + def __removeCustomRule(self): + """ + Private slot to remove a custom AdBlock rule. + """ + self.__currentTreeWidget.removeRule() + + def __updateSubscription(self): + """ + Private slot to update the selected subscription. + """ + self.__currentSubscription.updateNow() + + def __updateAllSubscriptions(self): + """ + Private slot to update all subscriptions. + """ + self.__manager.updateAllSubscriptions() + + def __browseSubscriptions(self): + """ + Private slot to browse the list of available AdBlock subscriptions. + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + mw = WebBrowserWindow.mainWindow() + mw.newTab("http://adblockplus.org/en/subscriptions") + mw.raise_() + + def __learnAboutWritingFilters(self): + """ + Private slot to show the web page about how to write filters. + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + mw = WebBrowserWindow.mainWindow() + mw.newTab("http://adblockplus.org/en/filters") + mw.raise_() + + def __removeSubscription(self): + """ + Private slot to remove the selected subscription. + """ + requiresTitles = [] + requiresSubscriptions = \ + self.__manager.getRequiresSubscriptions(self.__currentSubscription) + for subscription in requiresSubscriptions: + requiresTitles.append(subscription.title()) + if requiresTitles: + message = self.tr( + "<p>Do you really want to remove subscription" + " <b>{0}</b> and all subscriptions requiring it?</p>" + "<ul><li>{1}</li></ul>").format( + self.__currentSubscription.title(), + "</li><li>".join(requiresTitles)) + else: + message = self.tr( + "<p>Do you really want to remove subscription" + " <b>{0}</b>?</p>").format(self.__currentSubscription.title()) + res = E5MessageBox.yesNo( + self, + self.tr("Remove Subscription"), + message) + + if res: + removeSubscription = self.__currentSubscription + removeTrees = [self.__currentTreeWidget] + for index in range(self.subscriptionsTabWidget.count()): + tree = self.subscriptionsTabWidget.widget(index) + if tree.subscription() in requiresSubscriptions: + removeTrees.append(tree) + for tree in removeTrees: + self.subscriptionsTabWidget.removeTab( + self.subscriptionsTabWidget.indexOf(tree)) + self.__manager.removeSubscription(removeSubscription) + + def __switchSubscriptionEnabled(self): + """ + Private slot to switch the enabled state of the selected subscription. + """ + newState = not self.__currentSubscription.isEnabled() + self.__setSubscriptionEnabled(self.__currentSubscription, newState) + + def __setSubscriptionEnabled(self, subscription, enable): + """ + Private slot to set the enabled state of a subscription. + + @param subscription subscription to set the state for + (AdBlockSubscription) + @param enable state to set to (boolean) + """ + if enable: + # enable required one as well + sub = self.__manager.subscription(subscription.requiresLocation()) + requiresSubscriptions = [] if sub is None else [sub] + icon = UI.PixmapCache.getIcon("adBlockPlus.png") + else: + # disable dependent ones as well + requiresSubscriptions = \ + self.__manager.getRequiresSubscriptions(subscription) + icon = UI.PixmapCache.getIcon("adBlockPlusDisabled.png") + requiresSubscriptions.append(subscription) + for sub in requiresSubscriptions: + sub.setEnabled(enable) + + for index in range(self.subscriptionsTabWidget.count()): + tree = self.subscriptionsTabWidget.widget(index) + if tree.subscription() in requiresSubscriptions: + self.subscriptionsTabWidget.setTabIcon( + self.subscriptionsTabWidget.indexOf(tree), icon) + + @pyqtSlot(int) + def on_updateSpinBox_valueChanged(self, value): + """ + Private slot to handle changes of the update period. + + @param value update period (integer) + """ + if value != Preferences.getWebBrowser("AdBlockUpdatePeriod"): + Preferences.setWebBrowser("AdBlockUpdatePeriod", value) + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + manager = WebBrowserWindow.adBlockManager() + for subscription in manager.subscriptions(): + subscription.checkForUpdate() + + @pyqtSlot(int) + def on_subscriptionsTabWidget_currentChanged(self, index): + """ + Private slot handling the selection of another tab. + + @param index index of the new current tab (integer) + """ + if index != -1: + self.__currentTreeWidget = \ + self.subscriptionsTabWidget.widget(index) + self.__currentSubscription = \ + self.__currentTreeWidget.subscription() + + isEasyList = \ + self.__currentSubscription.url().toString().startswith( + self.__manager.getDefaultSubscriptionUrl()) + self.useLimitedEasyListCheckBox.setVisible(isEasyList) + + @pyqtSlot(str) + def on_searchEdit_textChanged(self, filter): + """ + Private slot to set a new filter on the current widget. + + @param filter filter to be set (string) + """ + if self.__currentTreeWidget and self.adBlockGroup.isChecked(): + self.__currentTreeWidget.filterString(filter) + + @pyqtSlot(bool) + def on_adBlockGroup_toggled(self, state): + """ + Private slot handling the enabling/disabling of AdBlock. + + @param state state of the toggle (boolean) + """ + self.__manager.setEnabled(state) + + if state: + self.__load() + + @pyqtSlot(bool) + def on_useLimitedEasyListCheckBox_clicked(self, checked): + """ + Private slot handling the selection of the limited EasyList. + + @param checked flag indicating the state of the check box + @type bool + """ + self.__manager.setUseLimitedEasyList( + self.useLimitedEasyListCheckBox.isChecked())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,204 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AdBlockDialog</class> + <widget class="QDialog" name="AdBlockDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>AdBlock Configuration</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="adBlockGroup"> + <property name="title"> + <string>Enable AdBlock</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <property name="horizontalSpacing"> + <number>20</number> + </property> + <item row="0" column="0" rowspan="2"> + <widget class="QLabel" name="iconLabel"> + <property name="minimumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="text"> + <string notr="true">Icon</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="1"> + <widget class="E5ClearableLineEdit" name="searchEdit"> + <property name="toolTip"> + <string>Enter search term for subscriptions and rules</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTabWidget" name="subscriptionsTabWidget"> + <property name="documentMode"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QToolButton" name="actionButton"> + <property name="text"> + <string>Actions</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Default Update Period (days):</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="updateSpinBox"> + <property name="toolTip"> + <string>Enter the update period (1 to 14 days)</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string/> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>14</number> + </property> + <property name="value"> + <number>7</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QCheckBox" name="useLimitedEasyListCheckBox"> + <property name="toolTip"> + <string/> + </property> + <property name="text"> + <string>Use only essential part of EasyList (for performance reasons)</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>adBlockGroup</tabstop> + <tabstop>searchEdit</tabstop> + <tabstop>subscriptionsTabWidget</tabstop> + <tabstop>actionButton</tabstop> + <tabstop>updateSpinBox</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>AdBlockDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>252</x> + <y>445</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>AdBlockDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>320</x> + <y>445</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockExceptionsDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to configure the AdBlock exceptions. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt +from PyQt5.QtWidgets import QDialog + +from .Ui_AdBlockExceptionsDialog import Ui_AdBlockExceptionsDialog + +import UI.PixmapCache + + +class AdBlockExceptionsDialog(QDialog, Ui_AdBlockExceptionsDialog): + """ + Class implementing a dialog to configure the AdBlock exceptions. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(AdBlockExceptionsDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.iconLabel.setPixmap( + UI.PixmapCache.getPixmap("adBlockPlusGreen48.png")) + + self.hostEdit.setInactiveText(self.tr("Enter host to be added...")) + + self.buttonBox.setFocus() + + def load(self, hosts): + """ + Public slot to load the list of excepted hosts. + + @param hosts list of excepted hosts + """ + self.hostList.clear() + self.hostList.addItems(hosts) + + @pyqtSlot(str) + def on_hostEdit_textChanged(self, txt): + """ + Private slot to handle changes of the host edit. + + @param txt text of the edit (string) + """ + self.addButton.setEnabled(bool(txt)) + + @pyqtSlot() + def on_addButton_clicked(self): + """ + Private slot to handle a click of the add button. + """ + self.hostList.addItem(self.hostEdit.text()) + self.hostEdit.clear() + + @pyqtSlot() + def on_hostList_itemSelectionChanged(self): + """ + Private slot handling a change of the number of selected items. + """ + self.deleteButton.setEnabled(len(self.hostList.selectedItems()) > 0) + + @pyqtSlot() + def on_deleteButton_clicked(self): + """ + Private slot handling a click of the delete button. + """ + for itm in self.hostList.selectedItems(): + row = self.hostList.row(itm) + removedItem = self.hostList.takeItem(row) + del removedItem + + def accept(self): + """ + Public slot handling the acceptance of the dialog. + """ + hosts = [] + for row in range(self.hostList.count()): + hosts.append(self.hostList.item(row).text()) + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + WebBrowserWindow.adBlockManager().setExceptions(hosts) + + super(AdBlockExceptionsDialog, self).accept()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockExceptionsDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AdBlockExceptionsDialog</class> + <widget class="QDialog" name="AdBlockExceptionsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>550</width> + <height>450</height> + </rect> + </property> + <property name="windowTitle"> + <string>AdBlock Exceptions</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" rowspan="2"> + <widget class="QLabel" name="iconLabel"> + <property name="minimumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="text"> + <string notr="true">Icon</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>188</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="1"> + <widget class="E5ClearableLineEdit" name="hostEdit"> + <property name="toolTip"> + <string>Enter a host to block AdBlock for</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QPushButton" name="addButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Press to add the host</string> + </property> + <property name="text"> + <string>&Add</string> + </property> + </widget> + </item> + <item row="2" column="0" rowspan="2" colspan="2"> + <widget class="QListWidget" name="hostList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QPushButton" name="deleteButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Press to delete the selected hosts</string> + </property> + <property name="text"> + <string>&Delete</string> + </property> + </widget> + </item> + <item row="3" column="2"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>148</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>hostEdit</tabstop> + <tabstop>addButton</tabstop> + <tabstop>hostList</tabstop> + <tabstop>deleteButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>AdBlockExceptionsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>AdBlockExceptionsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockIcon.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the AdBlock icon for the main window status bar. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QAction, QMenu + +from E5Gui.E5ClickableLabel import E5ClickableLabel + +import UI.PixmapCache + + +class AdBlockIcon(E5ClickableLabel): + """ + Class implementing the AdBlock icon for the main window status bar. + """ + def __init__(self, parent): + """ + Constructor + + @param parent reference to the parent widget (HelpWindow) + """ + super(AdBlockIcon, self).__init__(parent) + + self.__mw = parent + self.__menuAction = None + self.__enabled = False + + self.setMaximumHeight(16) + self.setCursor(Qt.PointingHandCursor) + self.setToolTip(self.tr( + "AdBlock lets you block unwanted content on web pages.")) + + self.clicked.connect(self.__showMenu) + + def setEnabled(self, enabled): + """ + Public slot to set the enabled state. + + @param enabled enabled state (boolean) + """ + self.__enabled = enabled + if enabled: + self.currentChanged() + else: + self.setPixmap( + UI.PixmapCache.getPixmap("adBlockPlusDisabled16.png")) + + def __createMenu(self, menu=None): + """ + Private slot to create the context menu. + + @param menu parent menu (QMenu) + """ + if menu is None: + menu = self.sender() + if menu is None: + return + + menu.clear() + + manager = self.__mw.adBlockManager() + + if manager.isEnabled(): + menu.addAction( + UI.PixmapCache.getIcon("adBlockPlusDisabled.png"), + self.tr("Disable AdBlock"), + self.__enableAdBlock).setData(False) + else: + menu.addAction( + UI.PixmapCache.getIcon("adBlockPlus.png"), + self.tr("Enable AdBlock"), + self.__enableAdBlock).setData(True) + menu.addSeparator() + if manager.isEnabled() and self.__mw.currentBrowser().url().host(): + if self.__isCurrentHostExcepted(): + menu.addAction( + UI.PixmapCache.getIcon("adBlockPlus.png"), + self.tr("Remove AdBlock Exception"), + self.__setException).setData(False) + else: + menu.addAction( + UI.PixmapCache.getIcon("adBlockPlusGreen.png"), + self.tr("Add AdBlock Exception"), + self.__setException).setData(True) + menu.addAction( + UI.PixmapCache.getIcon("adBlockPlusGreen.png"), + self.tr("AdBlock Exceptions..."), manager.showExceptionsDialog) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("adBlockPlus.png"), + self.tr("AdBlock Configuration..."), manager.showDialog) + + def menuAction(self): + """ + Public method to get a reference to the menu action. + + @return reference to the menu action (QAction) + """ + if not self.__menuAction: + self.__menuAction = QAction(self.tr("AdBlock"), self) + self.__menuAction.setMenu(QMenu()) + self.__menuAction.menu().aboutToShow.connect(self.__createMenu) + + if self.__enabled: + self.__menuAction.setIcon( + UI.PixmapCache.getIcon("adBlockPlus.png")) + else: + self.__menuAction.setIcon( + UI.PixmapCache.getIcon("adBlockPlusDisabled.png")) + + return self.__menuAction + + def __showMenu(self, pos): + """ + Private slot to show the context menu. + + @param pos position the context menu should be shown (QPoint) + """ + menu = QMenu() + self.__createMenu(menu) + menu.exec_(pos) + + def __enableAdBlock(self): + """ + Private slot to enable or disable AdBlock. + """ + act = self.sender() + if act is not None: + self.__mw.adBlockManager().setEnabled(act.data()) + + def __isCurrentHostExcepted(self): + """ + Private method to check, if the host of the current browser is + excepted. + + @return flag indicating an exception (boolean) + """ + browser = self.__mw.currentBrowser() + if browser is None: + return False + + urlHost = browser.page().url().host() + + return urlHost and \ + self.__mw.adBlockManager().isHostExcepted(urlHost) + + def currentChanged(self): + """ + Public slot to handle a change of the current browser tab. + """ + if self.__enabled: + if self.__isCurrentHostExcepted(): + self.setPixmap( + UI.PixmapCache.getPixmap("adBlockPlusGreen16.png")) + else: + self.setPixmap(UI.PixmapCache.getPixmap("adBlockPlus16.png")) + + def __setException(self): + """ + Private slot to add or remove the current host from the list of + exceptions. + """ + act = self.sender() + if act is not None: + urlHost = self.__mw.currentBrowser().url().host() + if act.data(): + self.__mw.adBlockManager().addException(urlHost) + else: + self.__mw.adBlockManager().removeException(urlHost) + self.currentChanged() + + def sourceChanged(self, browser, url): + """ + Public slot to handle URL changes. + + @param browser reference to the browser (HelpBrowser) + @param url new URL (QUrl) + """ + if browser == self.__mw.currentBrowser(): + self.currentChanged()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,609 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the AdBlock manager. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QUrlQuery, QFile, \ + QByteArray +from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInfo + +from E5Gui import E5MessageBox + +from .AdBlockSubscription import AdBlockSubscription +from .AdBlockUrlInterceptor import AdBlockUrlInterceptor + +from Utilities.AutoSaver import AutoSaver +import Utilities +import Preferences + + +class AdBlockManager(QObject): + """ + Class implementing the AdBlock manager. + + @signal rulesChanged() emitted after some rule has changed + """ + rulesChanged = pyqtSignal() + requiredSubscriptionLoaded = pyqtSignal(AdBlockSubscription) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(AdBlockManager, self).__init__(parent) + + self.__loaded = False + self.__subscriptionsLoaded = False + self.__enabled = False + self.__adBlockDialog = None + self.__adBlockExceptionsDialog = None + self.__adBlockNetwork = None + self.__adBlockPage = None + self.__subscriptions = [] + self.__exceptedHosts = Preferences.getWebBrowser("AdBlockExceptions") + self.__saveTimer = AutoSaver(self, self.save) + self.__limitedEasyList = Preferences.getWebBrowser( + "AdBlockUseLimitedEasyList") + + self.__defaultSubscriptionUrlString = \ + "abp:subscribe?location=" \ + "https://easylist-downloads.adblockplus.org/easylist.txt&"\ + "title=EasyList" + self.__customSubscriptionUrlString = \ + bytes(self.__customSubscriptionUrl().toEncoded()).decode() + + self.rulesChanged.connect(self.__saveTimer.changeOccurred) + self.rulesChanged.connect(self.__rulesChanged) + + self.__interceptor = AdBlockUrlInterceptor(self) + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + WebBrowserWindow.networkManager().installUrlInterceptor( + self.__interceptor) + + def __rulesChanged(self): + """ + Private slot handling a change of the AdBlock rules. + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + WebBrowserWindow.mainWindow().reloadUserStyleSheet() + + def close(self): + """ + Public method to close the open search engines manager. + """ + self.__adBlockDialog and self.__adBlockDialog.close() + self.__adBlockExceptionsDialog and \ + self.__adBlockExceptionsDialog.close() + + self.__saveTimer.saveIfNeccessary() + + def isEnabled(self): + """ + Public method to check, if blocking ads is enabled. + + @return flag indicating the enabled state (boolean) + """ + if not self.__loaded: + self.load() + + return self.__enabled + + def setEnabled(self, enabled): + """ + Public slot to set the enabled state. + + @param enabled flag indicating the enabled state (boolean) + """ + if self.isEnabled() == enabled: + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + self.__enabled = enabled + for mainWindow in WebBrowserWindow.mainWindows(): + mainWindow.adBlockIcon().setEnabled(enabled) + if enabled: + self.__loadSubscriptions() + self.rulesChanged.emit() + + def block(self, info): + """ + Public method to check, if a request should be blocked. + + @param info request info aobject + @type QWebEngineUrlRequestInfo + @return flag indicating to block the request + @rtype bool + """ + urlString = bytes(info.requestUrl().toEncoded()).decode().lower() + urlDomain = info.requestUrl().host().lower() + urlScheme = info.requestUrl().scheme().lower() + refererHost = info.firstPartyUrl().host().lower() + + if not self.isEnabled() or not self.__canRunOnScheme(urlScheme): + return False + + if self.isHostExcepted(urlDomain) or self.isHostExcepted(refererHost): + return False + + res = False + + for subscription in self.subscriptions(): + if subscription.isEnabled(): + if subscription.adBlockDisabledForUrl(info.requestUrl()): + continue + + blockedRule = subscription.match(info, urlDomain, urlString) + if blockedRule: + res = True + if info.resourceType() == \ + QWebEngineUrlRequestInfo.ResourceTypeMainFrame: + url = QUrl("eric:adblock") + query = QUrlQuery() + query.addQueryItem("rule", blockedRule.filter()) + query.addQueryItem( + "subscription", blockedRule.subscription().title()) + url.setQuery(query) + info.redirect(url) + res = False + else: + info.block(True) + break + + return res + + def __canRunOnScheme(self, scheme): + """ + Private method to check, if AdBlock can be performed on the scheme. + + @param scheme scheme to check (string) + @return flag indicating, that AdBlock can be performed (boolean) + """ + return scheme not in ["data", "eric", "qthelp", "qrc", "file", "abp"] + + def page(self): + """ + Public method to get a reference to the page block object. + + @return reference to the page block object (AdBlockPage) + """ + if self.__adBlockPage is None: + from .AdBlockPage import AdBlockPage + self.__adBlockPage = AdBlockPage(self) + return self.__adBlockPage + + def __customSubscriptionLocation(self): + """ + Private method to generate the path for custom subscriptions. + + @return URL for custom subscriptions (QUrl) + """ + dataDir = os.path.join(Utilities.getConfigDir(), "web_browser", + "subscriptions") + if not os.path.exists(dataDir): + os.makedirs(dataDir) + fileName = os.path.join(dataDir, "adblock_subscription_custom") + return QUrl.fromLocalFile(fileName) + + def __customSubscriptionUrl(self): + """ + Private method to generate the URL for custom subscriptions. + + @return URL for custom subscriptions (QUrl) + """ + location = self.__customSubscriptionLocation() + encodedUrl = bytes(location.toEncoded()).decode() + url = QUrl("abp:subscribe?location={0}&title={1}".format( + encodedUrl, self.tr("Custom Rules"))) + return url + + def customRules(self): + """ + Public method to get a subscription for custom rules. + + @return subscription object for custom rules (AdBlockSubscription) + """ + location = self.__customSubscriptionLocation() + for subscription in self.__subscriptions: + if subscription.location() == location: + return subscription + + url = self.__customSubscriptionUrl() + customAdBlockSubscription = AdBlockSubscription(url, True, self) + self.addSubscription(customAdBlockSubscription) + return customAdBlockSubscription + + def subscriptions(self): + """ + Public method to get all subscriptions. + + @return list of subscriptions (list of AdBlockSubscription) + """ + if not self.__loaded: + self.load() + + return self.__subscriptions[:] + + def subscription(self, location): + """ + Public method to get a subscription based on its location. + + @param location location of the subscription to search for (string) + @return subscription or None (AdBlockSubscription) + """ + if location != "": + for subscription in self.__subscriptions: + if subscription.location().toString() == location: + return subscription + + return None + + def updateAllSubscriptions(self): + """ + Public method to update all subscriptions. + """ + for subscription in self.__subscriptions: + subscription.updateNow() + + def removeSubscription(self, subscription, emitSignal=True): + """ + Public method to remove an AdBlock subscription. + + @param subscription AdBlock subscription to be removed + (AdBlockSubscription) + @param emitSignal flag indicating to send a signal (boolean) + """ + if subscription is None: + return + + if subscription.url().toString().startswith( + (self.__defaultSubscriptionUrlString, + self.__customSubscriptionUrlString)): + return + + try: + self.__subscriptions.remove(subscription) + rulesFileName = subscription.rulesFileName() + QFile.remove(rulesFileName) + requiresSubscriptions = self.getRequiresSubscriptions(subscription) + for requiresSubscription in requiresSubscriptions: + self.removeSubscription(requiresSubscription, False) + if emitSignal: + self.rulesChanged.emit() + except ValueError: + pass + + def addSubscriptionFromUrl(self, url): + """ + Public method to ad an AdBlock subscription given the abp URL: + + @param url URL to subscribe an AdBlock subscription + @type QUrl + @return flag indicating success + @rtype bool + """ + if url.path() != "subscribe": + return False + + title = QUrl.fromPercentEncoding( + QByteArray(QUrlQuery(url).queryItemValue("title").encode())) + if not title: + return False + + res = E5MessageBox.yesNo( + None, + self.tr("Subscribe?"), + self.tr( + """<p>Subscribe to this AdBlock subscription?</p>""" + """<p>{0}</p>""").format(title)) + if res: + from .AdBlockSubscription import AdBlockSubscription + from WebBrowser.WebBrowserWindow import WebBrowserWindow + + dlg = WebBrowserWindow.adBlockManager().showDialog() + subscription = AdBlockSubscription( + url, False, + WebBrowserWindow.adBlockManager()) + WebBrowserWindow.adBlockManager().addSubscription(subscription) + dlg.addSubscription(subscription, False) + dlg.setFocus() + dlg.raise_() + + def addSubscription(self, subscription): + """ + Public method to add an AdBlock subscription. + + @param subscription AdBlock subscription to be added + (AdBlockSubscription) + """ + if subscription is None: + return + + self.__subscriptions.insert(-1, subscription) + + subscription.rulesChanged.connect(self.rulesChanged) + subscription.changed.connect(self.rulesChanged) + subscription.enabledChanged.connect(self.rulesChanged) + + self.rulesChanged.emit() + + def save(self): + """ + Public method to save the AdBlock subscriptions. + """ + if not self.__loaded: + return + + Preferences.setWebBrowser("AdBlockEnabled", self.__enabled) + if self.__subscriptionsLoaded: + subscriptions = [] + requiresSubscriptions = [] + # intermediate store for subscription requiring others + for subscription in self.__subscriptions: + if subscription is None: + continue + urlString = bytes(subscription.url().toEncoded()).decode() + if "requiresLocation" in urlString: + requiresSubscriptions.append(urlString) + else: + subscriptions.append(urlString) + subscription.saveRules() + for subscription in requiresSubscriptions: + subscriptions.insert(-1, subscription) # custom should be last + Preferences.setWebBrowser("AdBlockSubscriptions", subscriptions) + + def load(self): + """ + Public method to load the AdBlock subscriptions. + """ + if self.__loaded: + return + + self.__loaded = True + + self.__enabled = Preferences.getWebBrowser("AdBlockEnabled") + if self.__enabled: + self.__loadSubscriptions() + + def __loadSubscriptions(self): + """ + Private method to load the set of subscriptions. + """ + if self.__subscriptionsLoaded: + return + + subscriptions = Preferences.getWebBrowser("AdBlockSubscriptions") + if subscriptions: + for subscription in subscriptions: + if subscription.startswith( + self.__defaultSubscriptionUrlString): + break + else: + subscriptions.insert(0, self.__defaultSubscriptionUrlString) + for subscription in subscriptions: + if subscription.startswith(self.__customSubscriptionUrlString): + break + else: + subscriptions.append(self.__customSubscriptionUrlString) + else: + subscriptions = [self.__defaultSubscriptionUrlString, + self.__customSubscriptionUrlString] + for subscription in subscriptions: + url = QUrl.fromEncoded(subscription.encode("utf-8")) + adBlockSubscription = AdBlockSubscription( + url, + subscription.startswith(self.__customSubscriptionUrlString), + self, + subscription.startswith(self.__defaultSubscriptionUrlString)) + adBlockSubscription.rulesChanged.connect(self.rulesChanged) + adBlockSubscription.changed.connect(self.rulesChanged) + adBlockSubscription.enabledChanged.connect(self.rulesChanged) + self.__subscriptions.append(adBlockSubscription) + + self.__subscriptionsLoaded = True + + def loadRequiredSubscription(self, location, title): + """ + Public method to load a subscription required by another one. + + @param location location of the required subscription (string) + @param title title of the required subscription (string) + """ + # Step 1: check, if the subscription is in the list of subscriptions + urlString = "abp:subscribe?location={0}&title={1}".format( + location, title) + for subscription in self.__subscriptions: + if subscription.url().toString().startswith(urlString): + # We found it! + return + + # Step 2: if it is not, get it + url = QUrl.fromEncoded(urlString.encode("utf-8")) + adBlockSubscription = AdBlockSubscription(url, False, self) + self.addSubscription(adBlockSubscription) + self.requiredSubscriptionLoaded.emit(adBlockSubscription) + + def getRequiresSubscriptions(self, subscription): + """ + Public method to get a list of subscriptions, that require the given + one. + + @param subscription subscription to check for (AdBlockSubscription) + @return list of subscription requiring the given one (list of + AdBlockSubscription) + """ + subscriptions = [] + location = subscription.location().toString() + for subscription in self.__subscriptions: + if subscription.requiresLocation() == location: + subscriptions.append(subscription) + + return subscriptions + + def showDialog(self): + """ + Public slot to show the AdBlock subscription management dialog. + + @return reference to the dialog (AdBlockDialog) + """ + if self.__adBlockDialog is None: + from .AdBlockDialog import AdBlockDialog + self.__adBlockDialog = AdBlockDialog(self) + + self.__adBlockDialog.show() + return self.__adBlockDialog + + def elementHidingRules(self): + """ + Public method to get the element hiding rules. + + @return element hiding rules (string) + """ + if not self.isEnabled(): + return "" + + rules = "" + + for subscription in self.__subscriptions: + rules += subscription.elementHidingRules() + + if rules: + # remove last ", + rules = rules[:-1] + + return rules + + def elementHidingRulesForDomain(self, url): + """ + Public method to get the element hiding rules for a domain. + + @param url URL to get hiding rules for (QUrl) + @return element hiding rules (string) + """ + if not self.isEnabled(): + return "" + + rules = "" + + for subscription in self.__subscriptions: + if subscription.elemHideDisabledForUrl(url): + continue + + rules += subscription.elementHidingRulesForDomain(url.host()) + + if rules: + # remove last "," + rules = rules[:-1] + + rules += "{display:none !important;}\n" + + return rules + + def exceptions(self): + """ + Public method to get a list of excepted hosts. + + @return list of excepted hosts (list of string) + """ + return self.__exceptedHosts + + def setExceptions(self, hosts): + """ + Public method to set the list of excepted hosts. + + @param hosts list of excepted hosts (list of string) + """ + self.__exceptedHosts = [host.lower() for host in hosts] + Preferences.setWebBrowser("AdBlockExceptions", self.__exceptedHosts) + + def addException(self, host): + """ + Public method to add an exception. + + @param host to be excepted (string) + """ + host = host.lower() + if host and host not in self.__exceptedHosts: + self.__exceptedHosts.append(host) + Preferences.setWebBrowser( + "AdBlockExceptions", self.__exceptedHosts) + + def removeException(self, host): + """ + Public method to remove an exception. + + @param host to be removed from the list of exceptions (string) + """ + host = host.lower() + if host in self.__exceptedHosts: + self.__exceptedHosts.remove(host) + Preferences.setWebBrowser( + "AdBlockExceptions", self.__exceptedHosts) + + def isHostExcepted(self, host): + """ + Public slot to check, if a host is excepted. + + @param host host to check (string) + @return flag indicating an exception (boolean) + """ + host = host.lower() + return host in self.__exceptedHosts + + def showExceptionsDialog(self): + """ + Public method to show the AdBlock Exceptions dialog. + + @return reference to the exceptions dialog (AdBlockExceptionsDialog) + """ + if self.__adBlockExceptionsDialog is None: + from .AdBlockExceptionsDialog import AdBlockExceptionsDialog + self.__adBlockExceptionsDialog = AdBlockExceptionsDialog() + + self.__adBlockExceptionsDialog.load(self.__exceptedHosts) + self.__adBlockExceptionsDialog.show() + return self.__adBlockExceptionsDialog + + def useLimitedEasyList(self): + """ + Public method to test, if limited EasyList rules shall be used. + + @return flag indicating limited EasyList rules + @rtype bool + """ + return self.__limitedEasyList + + def setUseLimitedEasyList(self, limited): + """ + Public method to set the limited EasyList flag. + + @param limited flag indicating to use limited EasyList + @type bool + """ + self.__limitedEasyList = limited + + for subscription in self.__subscriptions: + if subscription.url().toString().startswith( + self.__defaultSubscriptionUrlString): + subscription.updateNow() + + Preferences.setWebBrowser("AdBlockUseLimitedEasyList", limited) + + def getDefaultSubscriptionUrl(self): + """ + Public method to get the default subscription URL. + + @return default subscription URL + @rtype str + """ + return self.__defaultSubscriptionUrlString
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to apply AdBlock rules to a web page. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QObject + +from ..Tools import Scripts + + +class AdBlockPage(QObject): + """ + Class to apply AdBlock rules to a web page. + """ + def hideBlockedPageEntries(self, page): + """ + Public method to apply AdBlock rules to a web page. + + @param page reference to the web page (HelpWebPage) + """ + if page is None: + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + manager = WebBrowserWindow.adBlockManager() + if not manager.isEnabled(): + return + + # apply domain specific element hiding rules + elementHiding = manager.elementHidingRulesForDomain(page.url()) + if elementHiding: + script = Scripts.setCss(elementHiding) + page.runJavaScript(script)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockRule.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,681 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the AdBlock rule class. +""" + +from __future__ import unicode_literals + +import re + +from PyQt5.QtCore import Qt, QRegExp +from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInfo + + +def toSecondLevelDomain(url): + """ + Module function to get a second level domain from the given URL. + + @param url URL to extract domain from (QUrl) + @return name of second level domain (string) + """ + topLevelDomain = url.topLevelDomain() + urlHost = url.host() + + if not topLevelDomain or not urlHost: + return "" + + domain = urlHost[:len(urlHost) - len(topLevelDomain)] + if domain.count(".") == 0: + return urlHost + + while domain.count(".") != 0: + domain = domain[domain.find(".") + 1:] + + return domain + topLevelDomain + + +class AdBlockRule(object): + """ + Class implementing the AdBlock rule. + """ + def __init__(self, filter="", subscription=None): + """ + Constructor + + @param filter filter string of the rule (string) + @param subscription reference to the subscription object + (AdBlockSubscription) + """ + self.__subscription = subscription + + self.__regExp = QRegExp() + self.__options = [] + self.__blockedDomains = [] + self.__allowedDomains = [] + + self.__enabled = True + self.__cssRule = False + self.__exception = False + self.__internalDisabled = False + self.__domainRestricted = False + self.__useRegExp = False + self.__useDomainMatch = False + self.__useEndsMatch = False + self.__thirdParty = False + self.__thirdPartyException = False + self.__object = False + self.__objectException = False + self.__subdocument = False + self.__subdocumentException = False + self.__xmlhttprequest = False + self.__xmlhttprequestException = False + self.__document = False + self.__elemhide = False + self.__caseSensitivity = Qt.CaseInsensitive + self.__image = False + self.__imageException = False + self.__script = False + self.__scriptException = False + self.__stylesheet = False + self.__stylesheetException = False + self.__objectSubrequest = False + self.__objectSubrequestException = False + self.__stringMatchRule = False + + self.setFilter(filter) + + def subscription(self): + """ + Public method to get the subscription this rule belongs to. + + @return subscription of the rule (AdBlockSubscription) + """ + return self.__subscription + + def filter(self): + """ + Public method to get the rule filter string. + + @return rule filter string (string) + """ + return self.__filter + + def setFilter(self, filter): + """ + Public method to set the rule filter string. + + @param filter rule filter string (string) + """ + self.__filter = filter + self.__parseFilter() + + def __parseFilter(self): + """ + Private method to parse the filter pattern. + """ + parsedLine = self.__filter + + # empty rule or just a comment + if not parsedLine.strip() or parsedLine.startswith(("!", "[Adblock")): + self.__enabled = False + return + + # CSS element hiding rule + if "##" in parsedLine or "#@#" in parsedLine: + self.__cssRule = True + pos = parsedLine.find("#") + + # domain restricted rule + if not parsedLine.startswith("##"): + domains = parsedLine[:pos] + self.__parseDomains(domains, ",") + + self.__exception = parsedLine[pos + 1] == "@" + + if self.__exception: + self.__cssSelector = parsedLine[pos + 3:] + else: + self.__cssSelector = parsedLine[pos + 2:] + # CSS rule cannot have more options -> stop parsing + return + + # Exception always starts with @@ + if parsedLine.startswith("@@"): + self.__exception = True + parsedLine = parsedLine[2:] + + # Parse all options following '$' character + optionsIndex = parsedLine.find("$") + if optionsIndex >= 0: + options = parsedLine[optionsIndex + 1:].split(",") + + handledOptions = 0 + for option in options: + if option.startswith("domain="): + self.__parseDomains(option[7:], "|") + handledOptions += 1 + elif option == "match-case": + self.__caseSensitivity = Qt.CaseSensitive + handledOptions += 1 + elif option.endswith("third-party"): + self.__thirdParty = True + self.__thirdPartyException = option.startswith("~") + handledOptions += 1 + elif option.endswith("object"): + self.__object = True + self.__objectException = option.startswith("~") + handledOptions += 1 + elif option.endswith("subdocument"): + self.__subdocument = True + self.__subdocumentException = option.startswith("~") + handledOptions += 1 + elif option.endswith("xmlhttprequest"): + self.__xmlhttprequest = True + self.__xmlhttprequestException = option.startswith("~") + handledOptions += 1 + elif option.endswith("image"): + self.__image = True + self.__imageException = option.startswith("~") + elif option.endswith("script"): + self.__script = True + self.__scriptException = option.startswith("~") + elif option.endswith("stylesheet"): + self.__stylesheet = True + self.__stylesheetException = option.startswith("~") + elif option.endswith("object-subrequest"): + self.__objectSubrequest = True + self.__objectSubrequestException = option.startswith("~") + elif option == "document" and self.__exception: + self.__document = True + handledOptions += 1 + elif option == "elemhide" and self.__exception: + self.__elemhide = True + handledOptions += 1 + elif option == "collapse": + # Hiding placeholders of blocked elements + handledOptions += 1 + + # If we don't handle all options, it's safer to just disable + # this rule + if handledOptions != len(options): + self.__internalDisabled = True + return + + parsedLine = parsedLine[:optionsIndex] + + # Rule is classic regexp + if parsedLine.startswith("/") and parsedLine.endswith("/"): + parsedLine = parsedLine[1:-1] + self.__useRegExp = True + self.__regExp = QRegExp(parsedLine, self.__caseSensitivity, + QRegExp.RegExp) + return + + # Remove starting / ending wildcards + if parsedLine.startswith("*"): + parsedLine = parsedLine[1:] + if parsedLine.endswith("*"): + parsedLine = parsedLine[:-1] + + # Fast string matching for domain can be used + if parsedLine.startswith("||") and \ + parsedLine.endswith("^") and \ + QRegExp("[/:?=&\\*]").indexIn(parsedLine) == -1: + parsedLine = parsedLine[2:-1] + self.__useDomainMatch = True + self.__matchString = parsedLine + return + + # If rule contains '|' only at the end, string matching can be used + if parsedLine.endswith("|") and \ + QRegExp("[\\^\\*]").indexIn(parsedLine) == -1 and \ + parsedLine.count("|") == 1: + parsedLine = parsedLine[:-1] + self.__useEndsMatch = True + self.__matchString = parsedLine + return + + # If there is still a wildcard (*) or separator (^) or (|), + # the rule must be modified to comply with QRegExp. + if "*" in parsedLine or "^" in parsedLine or "|" in parsedLine: + pattern = self.__convertPatternToRegExp(parsedLine) + self.__useRegExp = True + self.__regExp = QRegExp(pattern, self.__caseSensitivity, + QRegExp.RegExp) + return + + # no regexp required + self.__useRegExp = False + self.__matchString = parsedLine + self.__stringMatchRule = True + + def __parseDomains(self, domains, separator): + """ + Private method to parse a string with a domain list. + + @param domains list of domains (string) + @param separator separator character used by the list (string) + """ + domainsList = domains.split(separator) + + for domain in domainsList: + if not domain: + continue + if domain.startswith("~"): + self.__blockedDomains.append(domain[1:]) + else: + self.__allowedDomains.append(domain) + + self.__domainRestricted = \ + bool(self.__blockedDomains) or bool(self.__allowedDomains) + + def networkMatch(self, request, domain, encodedUrl): + """ + Public method to check the rule for a match. + + @param request reference to the network request + @type QWebEngineUrlRequestInfo + @param domain domain name + @type str + @param encodedUrl string encoded URL to be checked + @type str + @return flag indicating a match + @rtype bool + """ + if self.__cssRule or not self.__enabled or self.__internalDisabled: + return False + + matched = self.__stringMatch(domain, encodedUrl) + + if matched: + # check domain restrictions + if self.__domainRestricted and \ + not self.matchDomain(request.firstPartyUrl().host()): + return False + + # check third-party restrictions + if self.__thirdParty and not self.matchThirdParty(request): + return False + + # check object restrictions + if self.__object and not self.matchObject(request): + return False + + # check subdocument restrictions + if self.__subdocument and not self.matchSubdocument(request): + return False + + # check xmlhttprequest restriction + if self.__xmlhttprequest and not self.matchXmlHttpRequest(request): + return False + + # check image restriction + if self.__image and not self.matchImage(request): + return False + + # check script restriction + if self.__script and not self.matchScript(request): + return False + + # check stylesheet restriction + if self.__stylesheet and not self.matchStyleSheet(request): + return False + + # check object-subrequest restriction + if self.__objectSubrequest and \ + not self.matchObjectSubrequest(request): + return False + + return matched + + def urlMatch(self, url): + """ + Public method to check an URL against the rule. + + @param url URL to check (QUrl) + @return flag indicating a match (boolean) + """ + if not self.__document and not self.__elemhide: + return False + + encodedUrl = bytes(url.toEncoded()).decode() + domain = url.host() + return self.__stringMatch(domain, encodedUrl) + + def __stringMatch(self, domain, encodedUrl): + """ + Private method to match a domain string. + + @param domain domain to match + @type str + @param encodedUrl URL in encoded form + @type str + @return flag indicating a match + @rtype bool + """ + if self.__cssRule or not self.__enabled or self.__internalDisabled: + return False + + matched = False + + if self.__useRegExp: + matched = self.__regExp.indexIn(encodedUrl) != -1 + elif self.__useDomainMatch: + matched = domain.endswith(self.__matchString) + elif self.__useEndsMatch: + if self.__caseSensitivity == Qt.CaseInsensitive: + matched = encodedUrl.lower().endswith( + self.__matchString.lower()) + else: + matched = encodedUrl.endswith(self.__matchString) + else: + if self.__caseSensitivity == Qt.CaseInsensitive: + matched = self.__matchString.lower() in encodedUrl.lower() + else: + matched = self.__matchString in encodedUrl + + return matched + + def matchDomain(self, domain): + """ + Public method to match a domain. + + @param domain domain name to check (string) + @return flag indicating a match (boolean) + """ + if not self.__enabled: + return False + + if not self.__domainRestricted: + return True + + if len(self.__blockedDomains) == 0: + for dom in self.__allowedDomains: + if domain.endswith(dom): + return True + elif len(self.__allowedDomains) == 0: + for dom in self.__blockedDomains: + if domain.endswith(dom): + return False + return True + else: + for dom in self.__blockedDomains: + if domain.endswith(dom): + return False + for dom in self.__allowedDomains: + if domain.endswith(dom): + return True + + return False + + def matchThirdParty(self, req): + """ + Public slot to match a third-party rule. + + @param req request object to check (QWebEngineUrlRequestInfo) + @return flag indicating a match (boolean) + """ + # Third-party matching should be performed on second-level domains + firstPartyHost = toSecondLevelDomain(req.firstPartyUrl()) + host = toSecondLevelDomain(req.requestUrl()) + + match = firstPartyHost != host + + if self.__thirdPartyException: + return not match + else: + return match + + def matchObject(self, req): + """ + Public slot to match an object rule. + + @param req request object to check (QWebEngineUrlRequestInfo) + @return flag indicating a match (boolean) + """ + match = ( + req.resourceType() == QWebEngineUrlRequestInfo.ResourceTypeObject) + + if self.__objectException: + return not match + else: + return match + + def matchSubdocument(self, req): + """ + Public slot to match a sub-document rule. + + @param req request object to check (QWebEngineUrlRequestInfo) + @return flag indicating a match (boolean) + """ + match = ( + req.resourceType() == + QWebEngineUrlRequestInfo.ResourceTypeSubFrame) + + if self.__subdocumentException: + return not match + else: + return match + + def matchXmlHttpRequest(self, req): + """ + Public slot to match a XmlHttpRequest rule. + + @param req request object to check (QWebEngineUrlRequestInfo) + @return flag indicating a match (boolean) + """ + match = ( + req.resourceType() == QWebEngineUrlRequestInfo.ResourceTypeXhr) + + if self.__xmlhttprequestException: + return not match + else: + return match + + def matchImage(self, req): + """ + Public slot to match an Image rule. + + @param req request object to check (QWebEngineUrlRequestInfo) + @return flag indicating a match (boolean) + """ + match = ( + req.resourceType() == QWebEngineUrlRequestInfo.ResourceTypeImage) + + if self.__imageException: + return not match + else: + return match + + def matchScript(self, req): + """ + Public slot to match a Script rule. + + @param req request object to check (QWebEngineUrlRequestInfo) + @return flag indicating a match (boolean) + """ + match = ( + req.resourceType() == QWebEngineUrlRequestInfo.ResourceTypeScript) + + if self.__scriptException: + return not match + else: + return match + + def matchStyleSheet(self, req): + """ + Public slot to match a StyleSheet rule. + + @param req request object to check (QWebEngineUrlRequestInfo) + @return flag indicating a match (boolean) + """ + match = ( + req.resourceType() == + QWebEngineUrlRequestInfo.ResourceTypeStylesheet) + + if self.__stylesheetException: + return not match + else: + return match + + def matchObjectSubrequest(self, req): + """ + Public slot to match an Object Subrequest rule. + + @param req request object to check (QWebEngineUrlRequestInfo) + @return flag indicating a match (boolean) + """ + match = ( + req.resourceType() == + QWebEngineUrlRequestInfo.ResourceTypeSubResource) + + if self.__objectSubrequestException: + return not match + else: + return match + + def isException(self): + """ + Public method to check, if the rule defines an exception. + + @return flag indicating an exception (boolean) + """ + return self.__exception + + def setException(self, exception): + """ + Public method to set the rule's exception flag. + + @param exception flag indicating an exception rule (boolean) + """ + self.__exception = exception + + def isEnabled(self): + """ + Public method to check, if the rule is enabled. + + @return flag indicating enabled state (boolean) + """ + return self.__enabled + + def setEnabled(self, enabled): + """ + Public method to set the rule's enabled state. + + @param enabled flag indicating the new enabled state (boolean) + """ + self.__enabled = enabled + if not enabled: + self.__filter = "!" + self.__filter + else: + self.__filter = self.__filter[1:] + + def isCSSRule(self): + """ + Public method to check, if the rule is a CSS rule. + + @return flag indicating a CSS rule (boolean) + """ + return self.__cssRule + + def cssSelector(self): + """ + Public method to get the CSS selector of the rule. + + @return CSS selector (string) + """ + return self.__cssSelector + + def isDocument(self): + """ + Public method to check, if this is a document rule. + + @return flag indicating a document rule (boolean) + """ + return self.__document + + def isElementHiding(self): + """ + Public method to check, if this is an element hiding rule. + + @return flag indicating an element hiding rule (boolean) + """ + return self.__elemhide + + def isDomainRestricted(self): + """ + Public method to check, if this rule is restricted by domain. + + @return flag indicating a domain restriction (boolean) + """ + return self.__domainRestricted + + def isComment(self): + """ + Public method to check, if this is a comment. + + @return flag indicating a comment (boolean) + """ + return self.__filter.startswith("!") + + def isHeader(self): + """ + Public method to check, if this is a header. + + @return flag indicating a header (boolean) + """ + return self.__filter.startswith("[Adblock") + + def isSlow(self): + """ + Public method to check, if this is a slow rule. + + @return flag indicating a slow rule (boolean) + """ + return self.__useRegExp + + def isInternalDisabled(self): + """ + Public method to check, if this rule was disabled internally. + + @return flag indicating an internally disabled rule (boolean) + """ + return self.__internalDisabled + + def __convertPatternToRegExp(self, wildcardPattern): + """ + Private method to convert a wildcard pattern to a regular expression. + + @param wildcardPattern string containing the wildcard pattern (string) + @return string containing a regular expression (string) + """ + pattern = wildcardPattern + + # remove multiple wildcards + pattern = re.sub(r"\*+", "*", pattern) + # remove anchors following separator placeholder + pattern = re.sub(r"\^\|$", "^", pattern) + # remove leading wildcards + pattern = re.sub(r"^(\*)", "", pattern) + # remove trailing wildcards + pattern = re.sub(r"(\*)$", "", pattern) + # escape special symbols + pattern = re.sub(r"(\W)", r"\\\1", pattern) + # process extended anchor at expression start + pattern = re.sub( + r"^\\\|\\\|", + r"^[\w\-]+:\/+(?!\/)(?:[^\/]+\.)?", pattern) + # process separator placeholders + pattern = re.sub(r"\\\^", r"(?:[^\w\d\-.%]|$)", pattern) + # process anchor at expression start + pattern = re.sub(r"^\\\|", "^", pattern) + # process anchor at expression end + pattern = re.sub(r"\\\|$", "$", pattern) + # replace wildcards by .* + pattern = re.sub(r"\\\*", ".*", pattern) + + return pattern
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockSubscription.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,716 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the AdBlock subscription class. +""" + +from __future__ import unicode_literals + +import os +import re +import hashlib +import base64 + +from PyQt5.QtCore import pyqtSignal, Qt, QObject, QByteArray, QDateTime, \ + QUrl, QUrlQuery, QCryptographicHash, QFile, QIODevice, QTextStream, \ + QDate, QTime +from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest + +from E5Gui import E5MessageBox + +import Utilities +import Preferences + + +class AdBlockSubscription(QObject): + """ + Class implementing the AdBlock subscription. + + @signal changed() emitted after the subscription has changed + @signal rulesChanged() emitted after the subscription's rules have changed + @signal enabledChanged(bool) emitted after the enabled state was changed + """ + changed = pyqtSignal() + rulesChanged = pyqtSignal() + enabledChanged = pyqtSignal(bool) + + def __init__(self, url, custom, parent=None, default=False): + """ + Constructor + + @param url AdBlock URL for the subscription (QUrl) + @param custom flag indicating a custom subscription (boolean) + @param parent reference to the parent object (QObject) + @param default flag indicating a default subscription (boolean) + """ + super(AdBlockSubscription, self).__init__(parent) + + self.__custom = custom + self.__url = url.toEncoded() + self.__enabled = False + self.__downloading = None + self.__defaultSubscription = default + + self.__title = "" + self.__location = QByteArray() + self.__lastUpdate = QDateTime() + self.__requiresLocation = "" + self.__requiresTitle = "" + + self.__updatePeriod = 0 # update period in hours, 0 = use default + self.__remoteModified = QDateTime() + + self.__rules = [] # list containing all AdBlock rules + + self.__networkExceptionRules = [] + self.__networkBlockRules = [] + self.__domainRestrictedCssRules = [] + self.__elementHidingRules = "" + self.__documentRules = [] + self.__elemhideRules = [] + + self.__checksumRe = re.compile( + r"""^\s*!\s*checksum[\s\-:]+([\w\+\/=]+).*\n""", + re.IGNORECASE | re.MULTILINE) + self.__expiresRe = re.compile( + r"""(?:expires:|expires after)\s*(\d+)\s*(hour|h)?""", + re.IGNORECASE) + self.__remoteModifiedRe = re.compile( + r"""!\s*(?:Last modified|Updated):\s*(\d{1,2})\s*""" + r"""(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s*""" + r"""(\d{2,4})\s*((\d{1,2}):(\d{2}))?""", + re.IGNORECASE) + + self.__monthNameToNumber = { + "Jan": 1, + "Feb": 2, + "Mar": 3, + "Apr": 4, + "May": 5, + "Jun": 6, + "Jul": 7, + "Aug": 8, + "Sep": 9, + "Oct": 10, + "Nov": 11, + "Dec": 12 + } + + self.__parseUrl(url) + + def __parseUrl(self, url): + """ + Private method to parse the AdBlock URL for the subscription. + + @param url AdBlock URL for the subscription (QUrl) + """ + if url.scheme() != "abp": + return + + if url.path() != "subscribe": + return + + urlQuery = QUrlQuery(url) + self.__title = QUrl.fromPercentEncoding( + QByteArray(urlQuery.queryItemValue("title").encode())) + self.__enabled = urlQuery.queryItemValue("enabled") != "false" + self.__location = QByteArray(QUrl.fromPercentEncoding( + QByteArray(urlQuery.queryItemValue("location").encode())) + .encode("utf-8")) + + # Check for required subscription + self.__requiresLocation = QUrl.fromPercentEncoding( + QByteArray(urlQuery.queryItemValue( + "requiresLocation").encode())) + self.__requiresTitle = QUrl.fromPercentEncoding( + QByteArray(urlQuery.queryItemValue("requiresTitle").encode())) + if self.__requiresLocation and self.__requiresTitle: + from WebBrowser.WebBrowserWindow import WebBrowserWindow + WebBrowserWindow.adBlockManager().loadRequiredSubscription( + self.__requiresLocation, self.__requiresTitle) + + lastUpdateString = urlQuery.queryItemValue("lastUpdate") + self.__lastUpdate = QDateTime.fromString(lastUpdateString, + Qt.ISODate) + + self.__loadRules() + + def url(self): + """ + Public method to generate the URL for this subscription. + + @return AdBlock URL for the subscription (QUrl) + """ + url = QUrl() + url.setScheme("abp") + url.setPath("subscribe") + + queryItems = [] + queryItems.append(("location", bytes(self.__location).decode())) + queryItems.append(("title", self.__title)) + if self.__requiresLocation and self.__requiresTitle: + queryItems.append(("requiresLocation", self.__requiresLocation)) + queryItems.append(("requiresTitle", self.__requiresTitle)) + if not self.__enabled: + queryItems.append(("enabled", "false")) + if self.__lastUpdate.isValid(): + queryItems.append(("lastUpdate", + self.__lastUpdate.toString(Qt.ISODate))) + + query = QUrlQuery() + query.setQueryItems(queryItems) + url.setQuery(query) + return url + + def isEnabled(self): + """ + Public method to check, if the subscription is enabled. + + @return flag indicating the enabled status (boolean) + """ + return self.__enabled + + def setEnabled(self, enabled): + """ + Public method to set the enabled status. + + @param enabled flag indicating the enabled status (boolean) + """ + if self.__enabled == enabled: + return + + self.__enabled = enabled + self.enabledChanged.emit(enabled) + + def title(self): + """ + Public method to get the subscription title. + + @return subscription title (string) + """ + return self.__title + + def setTitle(self, title): + """ + Public method to set the subscription title. + + @param title subscription title (string) + """ + if self.__title == title: + return + + self.__title = title + self.changed.emit() + + def location(self): + """ + Public method to get the subscription location. + + @return URL of the subscription location (QUrl) + """ + return QUrl.fromEncoded(self.__location) + + def setLocation(self, url): + """ + Public method to set the subscription location. + + @param url URL of the subscription location (QUrl) + """ + if url == self.location(): + return + + self.__location = url.toEncoded() + self.__lastUpdate = QDateTime() + self.changed.emit() + + def requiresLocation(self): + """ + Public method to get the location of a required subscription. + + @return location of a required subscription (string) + """ + return self.__requiresLocation + + def lastUpdate(self): + """ + Public method to get the date and time of the last update. + + @return date and time of the last update (QDateTime) + """ + return self.__lastUpdate + + def rulesFileName(self): + """ + Public method to get the name of the rules file. + + @return name of the rules file (string) + """ + if self.location().scheme() == "file": + return self.location().toLocalFile() + + if self.__location.isEmpty(): + return "" + + sha1 = bytes(QCryptographicHash.hash( + self.__location, QCryptographicHash.Sha1).toHex()).decode() + dataDir = os.path.join( + Utilities.getConfigDir(), "web_browser", "subscriptions") + if not os.path.exists(dataDir): + os.makedirs(dataDir) + fileName = os.path.join( + dataDir, "adblock_subscription_{0}".format(sha1)) + return fileName + + def __loadRules(self): + """ + Private method to load the rules of the subscription. + """ + fileName = self.rulesFileName() + f = QFile(fileName) + if f.exists(): + if not f.open(QIODevice.ReadOnly): + E5MessageBox.warning( + None, + self.tr("Load subscription rules"), + self.tr( + """Unable to open AdBlock file '{0}' for reading.""") + .format(fileName)) + else: + textStream = QTextStream(f) + header = textStream.readLine(1024) + if not header.startswith("[Adblock"): + E5MessageBox.warning( + None, + self.tr("Load subscription rules"), + self.tr("""AdBlock file '{0}' does not start""" + """ with [Adblock.""") + .format(fileName)) + f.close() + f.remove() + self.__lastUpdate = QDateTime() + else: + from .AdBlockRule import AdBlockRule + + self.__updatePeriod = 0 + self.__remoteModified = QDateTime() + self.__rules = [] + self.__rules.append(AdBlockRule(header, self)) + while not textStream.atEnd(): + line = textStream.readLine() + self.__rules.append(AdBlockRule(line, self)) + expires = self.__expiresRe.search(line) + if expires: + period, kind = expires.groups() + if kind: + # hours + self.__updatePeriod = int(period) + else: + # days + self.__updatePeriod = int(period) * 24 + remoteModified = self.__remoteModifiedRe.search(line) + if remoteModified: + day, month, year, time, hour, minute = \ + remoteModified.groups() + self.__remoteModified.setDate( + QDate(int(year), + self.__monthNameToNumber[month], + int(day)) + ) + if time: + self.__remoteModified.setTime( + QTime(int(hour), int(minute))) + self.__populateCache() + self.changed.emit() + elif not fileName.endswith("_custom"): + self.__lastUpdate = QDateTime() + + self.checkForUpdate() + + def checkForUpdate(self): + """ + Public method to check for an update. + """ + if self.__updatePeriod: + updatePeriod = self.__updatePeriod + else: + updatePeriod = \ + Preferences.getWebBrowser("AdBlockUpdatePeriod") * 24 + if not self.__lastUpdate.isValid() or \ + (self.__remoteModified.isValid() and + self.__remoteModified.addSecs(updatePeriod * 3600) < + QDateTime.currentDateTime()) or \ + self.__lastUpdate.addSecs(updatePeriod * 3600) < \ + QDateTime.currentDateTime(): + self.updateNow() + + def updateNow(self): + """ + Public method to update the subscription immediately. + """ + if self.__downloading is not None: + return + + if not self.location().isValid(): + return + + if self.location().scheme() == "file": + self.__lastUpdate = QDateTime.currentDateTime() + self.__loadRules() + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + self.__downloading = WebBrowserWindow.networkManager().get( + QNetworkRequest(self.location())) + self.__downloading.finished.connect(self.__rulesDownloaded) + + def __rulesDownloaded(self): + """ + Private slot to deal with the downloaded rules. + """ + reply = self.sender() + + response = reply.readAll() + reply.close() + self.__downloading = None + + if reply.error() != QNetworkReply.NoError: + if not self.__defaultSubscription: + # don't show error if we try to load the default + E5MessageBox.warning( + None, + self.tr("Downloading subscription rules"), + self.tr( + """<p>Subscription rules could not be""" + """ downloaded.</p><p>Error: {0}</p>""") + .format(reply.errorString())) + else: + # reset after first download attempt + self.__defaultSubscription = False + return + + if response.isEmpty(): + E5MessageBox.warning( + None, + self.tr("Downloading subscription rules"), + self.tr("""Got empty subscription rules.""")) + return + + fileName = self.rulesFileName() + QFile.remove(fileName) + f = QFile(fileName) + if not f.open(QIODevice.ReadWrite): + E5MessageBox.warning( + None, + self.tr("Downloading subscription rules"), + self.tr( + """Unable to open AdBlock file '{0}' for writing.""") + .file(fileName)) + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if WebBrowserWindow.adBlockManager().useLimitedEasyList() and \ + self.url().toString().startswith( + WebBrowserWindow.adBlockManager().getDefaultSubscriptionUrl()): + limited = True + # ignore Third-party advertisers rules for performance + # whitelist rules at the end will be used + index = response.indexOf( + "!---------------------------" + "Third-party advertisers" + "---------------------------!") + part1 = response.left(index) + index = response.indexOf( + "!-----------------------" + "Whitelists to fix broken sites" + "------------------------!") + part2 = response.mid(index) + f.write(part1) + f.write(part2) + else: + limited = False + f.write(response) + f.close() + self.__lastUpdate = QDateTime.currentDateTime() + if limited or self.__validateCheckSum(fileName): + self.__loadRules() + else: + QFile.remove(fileName) + self.__downloading = None + reply.deleteLater() + + def __validateCheckSum(self, fileName): + """ + Private method to check the subscription file's checksum. + + @param fileName name of the file containing the subscription (string) + @return flag indicating a valid file (boolean). A file is considered + valid, if the checksum is OK, the file does not contain a + checksum (i.e. cannot be checked) or we are using the limited + EasyList (because we fiddled with the original). + """ + try: + f = open(fileName, "r", encoding="utf-8") + data = f.read() + f.close() + except (IOError, OSError): + return False + + match = re.search(self.__checksumRe, data) + if match: + expectedChecksum = match.group(1) + else: + # consider it as valid + return True + + # normalize the data + data = re.sub(r"\r", "", data) # normalize eol + data = re.sub(r"\n+", "\n", data) # remove empty lines + data = re.sub(self.__checksumRe, "", data) # remove checksum line + + # calculate checksum + md5 = hashlib.md5() + md5.update(data.encode("utf-8")) + calculatedChecksum = base64.b64encode(md5.digest()).decode()\ + .rstrip("=") + if calculatedChecksum == expectedChecksum: + return True + else: + res = E5MessageBox.yesNo( + None, + self.tr("Downloading subscription rules"), + self.tr( + """<p>AdBlock subscription <b>{0}</b> has a wrong""" + """ checksum.<br/>""" + """Found: {1}<br/>""" + """Calculated: {2}<br/>""" + """Use it anyway?</p>""") + .format(self.__title, expectedChecksum, + calculatedChecksum)) + return res + + def saveRules(self): + """ + Public method to save the subscription rules. + """ + fileName = self.rulesFileName() + if not fileName: + return + + f = QFile(fileName) + if not f.open(QIODevice.ReadWrite | QIODevice.Truncate): + E5MessageBox.warning( + None, + self.tr("Saving subscription rules"), + self.tr( + """Unable to open AdBlock file '{0}' for writing.""") + .format(fileName)) + return + + textStream = QTextStream(f) + if not self.__rules or not self.__rules[0].isHeader(): + textStream << "[Adblock Plus 1.1.1]\n" + for rule in self.__rules: + textStream << rule.filter() << "\n" + + def match(self, req, urlDomain, urlString): + """ + Public method to check the subscription for a matching rule. + + @param req reference to the network request (QWebEngineUrlRequestInfo) + @param urlDomain domain of the URL (string) + @param urlString URL (string) + @return reference to the rule object or None (AdBlockRule) + """ + for rule in self.__networkExceptionRules: + if rule.networkMatch(req, urlDomain, urlString): + return None + + for rule in self.__networkBlockRules: + if rule.networkMatch(req, urlDomain, urlString): + return rule + + return None + + def adBlockDisabledForUrl(self, url): + """ + Public method to check, if AdBlock is disabled for the given URL. + + @param url URL to check (QUrl) + @return flag indicating disabled state (boolean) + """ + for rule in self.__documentRules: + if rule.urlMatch(url): + return True + + return False + + def elemHideDisabledForUrl(self, url): + """ + Public method to check, if element hiding is disabled for the given + URL. + + @param url URL to check (QUrl) + @return flag indicating disabled state (boolean) + """ + if self.adBlockDisabledForUrl(url): + return True + + for rule in self.__elemhideRules: + if rule.urlMatch(url): + return True + + return False + + def elementHidingRules(self): + """ + Public method to get the element hiding rules. + + @return element hiding rules (string) + """ + return self.__elementHidingRules + + def elementHidingRulesForDomain(self, domain): + """ + Public method to get the element hiding rules for the given domain. + + @param domain domain name (string) + @return element hiding rules (string) + """ + rules = "" + + for rule in self.__domainRestrictedCssRules: + if rule.matchDomain(domain): + rules += rule.cssSelector() + "," + + return rules + + def rule(self, offset): + """ + Public method to get a specific rule. + + @param offset offset of the rule (integer) + @return requested rule (AdBlockRule) + """ + if offset >= len(self.__rules): + return None + + return self.__rules[offset] + + def allRules(self): + """ + Public method to get the list of rules. + + @return list of rules (list of AdBlockRule) + """ + return self.__rules[:] + + def addRule(self, rule): + """ + Public method to add a rule. + + @param rule reference to the rule to add (AdBlockRule) + @return offset of the rule (integer) + """ + self.__rules.append(rule) + self.__populateCache() + self.rulesChanged.emit() + + return len(self.__rules) - 1 + + def removeRule(self, offset): + """ + Public method to remove a rule given the offset. + + @param offset offset of the rule to remove (integer) + """ + if offset < 0 or offset > len(self.__rules): + return + + del self.__rules[offset] + self.__populateCache() + self.rulesChanged.emit() + + def replaceRule(self, rule, offset): + """ + Public method to replace a rule given the offset. + + @param rule reference to the rule to set (AdBlockRule) + @param offset offset of the rule to remove (integer) + @return requested rule (AdBlockRule) + """ + if offset >= len(self.__rules): + return None + + self.__rules[offset] = rule + self.__populateCache() + self.rulesChanged.emit() + + return self.__rules[offset] + + def __populateCache(self): + """ + Private method to populate the various rule caches. + """ + self.__networkExceptionRules = [] + self.__networkBlockRules = [] + self.__domainRestrictedCssRules = [] + self.__elementHidingRules = "" + self.__documentRules = [] + self.__elemhideRules = [] + + for rule in self.__rules: + if not rule.isEnabled(): + continue + + if rule.isCSSRule(): + if rule.isDomainRestricted(): + self.__domainRestrictedCssRules.append(rule) + else: + self.__elementHidingRules += rule.cssSelector() + "," + elif rule.isDocument(): + self.__documentRules.append(rule) + elif rule.isElementHiding(): + self.__elemhideRules.append(rule) + elif rule.isException(): + self.__networkExceptionRules.append(rule) + else: + self.__networkBlockRules.append(rule) + + def canEditRules(self): + """ + Public method to check, if rules can be edited. + + @return flag indicating rules may be edited (boolean) + """ + return self.__custom + + def canBeRemoved(self): + """ + Public method to check, if the subscription can be removed. + + @return flag indicating removal is allowed (boolean) + """ + return not self.__custom and not self.__defaultSubscription + + def setRuleEnabled(self, offset, enabled): + """ + Public method to enable a specific rule. + + @param offset offset of the rule (integer) + @param enabled new enabled state (boolean) + @return reference to the changed rule (AdBlockRule) + """ + if offset >= len(self.__rules): + return None + + rule = self.__rules[offset] + rule.setEnabled(enabled) + if rule.isCSSRule(): + from WebBrowser.WebBrowserWindow import WebBrowserWindow + self.__populateCache() + WebBrowserWindow.mainWindow().reloadUserStyleSheet() + + return rule
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockTreeWidget.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a tree widget for the AdBlock configuration dialog. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont, QColor +from PyQt5.QtWidgets import QAbstractItemView, QTreeWidgetItem, QInputDialog, \ + QLineEdit, QMenu, QApplication + +from E5Gui.E5TreeWidget import E5TreeWidget + + +class AdBlockTreeWidget(E5TreeWidget): + """ + Class implementing a tree widget for the AdBlock configuration dialog. + """ + def __init__(self, subscription, parent=None): + """ + Constructor + + @param subscription reference to the subscription (AdBlockSubscription) + @param parent reference to the parent widget (QWidget) + """ + super(AdBlockTreeWidget, self).__init__(parent) + + self.__subscription = subscription + self.__topItem = None + self.__ruleToBeSelected = "" + self.__itemChangingBlock = False + + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.setDefaultItemShowMode(E5TreeWidget.ItemsExpanded) + self.setHeaderHidden(True) + self.setAlternatingRowColors(True) + + self.customContextMenuRequested.connect(self.__contextMenuRequested) + self.itemChanged.connect(self.__itemChanged) + self.__subscription.changed.connect(self.__subscriptionChanged) + self.__subscription.rulesChanged.connect(self.__subscriptionChanged) + + def subscription(self): + """ + Public method to get a reference to the subscription. + + @return reference to the subscription (AdBlockSubscription) + """ + return self.__subscription + + def showRule(self, rule): + """ + Public method to highlight the given rule. + + @param rule AdBlock rule to be shown (AdBlockRule) + """ + if rule: + self.__ruleToBeSelected = rule.filter() + if not self.__topItem: + return + if self.__ruleToBeSelected: + items = self.findItems(self.__ruleToBeSelected, Qt.MatchRecursive) + if items: + item = items[0] + self.setCurrentItem(item) + self.scrollToItem(item, QAbstractItemView.PositionAtCenter) + + self.__ruleToBeSelected = "" + + def refresh(self): + """ + Public method to refresh the tree. + """ + QApplication.setOverrideCursor(Qt.WaitCursor) + self.__itemChangingBlock = True + self.clear() + + boldFont = QFont() + boldFont.setBold(True) + + self.__topItem = QTreeWidgetItem(self) + self.__topItem.setText(0, self.__subscription.title()) + self.__topItem.setFont(0, boldFont) + self.addTopLevelItem(self.__topItem) + + allRules = self.__subscription.allRules() + + index = 0 + for rule in allRules: + item = QTreeWidgetItem(self.__topItem) + item.setText(0, rule.filter()) + item.setData(0, Qt.UserRole, index) + if self.__subscription.canEditRules(): + item.setFlags(item.flags() | Qt.ItemIsEditable) + self.__adjustItemFeatures(item, rule) + index += 1 + + self.expandAll() + self.showRule(None) + self.__itemChangingBlock = False + QApplication.restoreOverrideCursor() + QApplication.processEvents() + + def addRule(self, filter=""): + """ + Public slot to add a new rule. + + @param filter filter to be added (string) + """ + if not self.__subscription.canEditRules(): + return + + if not filter: + filter, ok = QInputDialog.getText( + self, + self.tr("Add Custom Rule"), + self.tr("Write your rule here:"), + QLineEdit.Normal) + if not ok or filter == "": + return + + from .AdBlockRule import AdBlockRule + rule = AdBlockRule(filter, self.__subscription) + self.__subscription.addRule(rule) + + def removeRule(self): + """ + Public slot to remove the current rule. + """ + item = self.currentItem() + if item is None or \ + not self.__subscription.canEditRules() or \ + item == self.__topItem: + return + + offset = item.data(0, Qt.UserRole) + self.__subscription.removeRule(offset) + self.deleteItem(item) + + def __contextMenuRequested(self, pos): + """ + Private slot to show the context menu. + + @param pos position for the menu (QPoint) + """ + if not self.__subscription.canEditRules(): + return + + item = self.itemAt(pos) + if item is None: + return + + menu = QMenu() + menu.addAction(self.tr("Add Rule"), self.addRule) + menu.addSeparator() + act = menu.addAction(self.tr("Remove Rule"), self.removeRule) + if item.parent() is None: + act.setDisabled(True) + + menu.exec_(self.viewport().mapToGlobal(pos)) + + def __itemChanged(self, itm): + """ + Private slot to handle the change of an item. + + @param itm changed item (QTreeWidgetItem) + """ + if itm is None or self.__itemChangingBlock: + return + + self.__itemChangingBlock = True + + offset = itm.data(0, Qt.UserRole) + oldRule = self.__subscription.rule(offset) + + if itm.checkState(0) == Qt.Unchecked and oldRule.isEnabled(): + # Disable rule + rule = self.__subscription.setRuleEnabled(offset, False) + self.__adjustItemFeatures(itm, rule) + elif itm.checkState(0) == Qt.Checked and not oldRule.isEnabled(): + # Enable rule + rule = self.__subscription.setRuleEnabled(offset, True) + self.__adjustItemFeatures(itm, rule) + elif self.__subscription.canEditRules(): + from .AdBlockRule import AdBlockRule + # Custom rule has been changed + rule = self.__subscription.replaceRule( + AdBlockRule(itm.text(0), self.__subscription), offset) + self.__adjustItemFeatures(itm, rule) + + self.__itemChangingBlock = False + + def __copyFilter(self): + """ + Private slot to copy the current filter to the clipboard. + """ + item = self.currentItem() + if item is not None: + QApplication.clipboard().setText(item.text(0)) + + def __subscriptionChanged(self): + """ + Private slot handling a subscription change. + """ + self.refresh() + + self.__itemChangingBlock = True + self.__topItem.setText( + 0, self.tr("{0} (recently updated)").format( + self.__subscription.title())) + self.__itemChangingBlock = False + + def __adjustItemFeatures(self, itm, rule): + """ + Private method to adjust an item. + + @param itm item to be adjusted (QTreeWidgetItem) + @param rule rule for the adjustment (AdBlockRule) + """ + if not rule.isEnabled(): + font = QFont() + font.setItalic(True) + itm.setForeground(0, QColor(Qt.gray)) + + if not rule.isComment() and not rule.isHeader(): + itm.setFlags(itm.flags() | Qt.ItemIsUserCheckable) + itm.setCheckState(0, Qt.Unchecked) + itm.setFont(0, font) + + return + + itm.setFlags(itm.flags() | Qt.ItemIsUserCheckable) + itm.setCheckState(0, Qt.Checked) + + if rule.isCSSRule(): + itm.setForeground(0, QColor(Qt.darkBlue)) + itm.setFont(0, QFont()) + elif rule.isException(): + itm.setForeground(0, QColor(Qt.darkGreen)) + itm.setFont(0, QFont()) + else: + itm.setForeground(0, QColor()) + itm.setFont(0, QFont()) + + def keyPressEvent(self, evt): + """ + Protected method handling key presses. + + @param evt key press event (QKeyEvent) + """ + if evt.key() == Qt.Key_C and \ + evt.modifiers() & Qt.ControlModifier: + self.__copyFilter() + elif evt.key() == Qt.Key_Delete: + self.removeRule() + else: + super(AdBlockTreeWidget, self).keyPressEvent(evt)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/AdBlockUrlInterceptor.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an URL interceptor base class. +""" + +from __future__ import unicode_literals + +from ..Network.UrlInterceptor import UrlInterceptor + +class AdBlockUrlInterceptor(UrlInterceptor): + """ + Class implementing an URL interceptor for AdBlock. + """ + def __init__(self, manager, parent=None): + """ + Constructor + + @param manager reference to the AdBlock manager + @type AdBlockManager + @param parent referemce to the parent object + @type QObject + """ + super(AdBlockUrlInterceptor, self).__init__(parent) + + self.__manager = manager + + def interceptRequest(self, info): + """ + Public method to intercept a request. + + @param info request info object + @type QWebEngineUrlRequestInfo + """ + if self.__manager.block(info): + info.block(True)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/AdBlock/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the advertisements blocker functionality. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/AddBookmarkDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to add a bookmark or a bookmark folder. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QModelIndex, QSortFilterProxyModel +from PyQt5.QtWidgets import QDialog, QTreeView + +from .Ui_AddBookmarkDialog import Ui_AddBookmarkDialog + + +class AddBookmarkProxyModel(QSortFilterProxyModel): + """ + Class implementing a proxy model used by the AddBookmarkDialog dialog. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(AddBookmarkProxyModel, self).__init__(parent) + + def columnCount(self, parent): + """ + Public method to return the number of columns. + + @param parent index of the parent (QModelIndex) + @return number of columns (integer) + """ + return min(1, QSortFilterProxyModel.columnCount(self, parent)) + + def filterAcceptsRow(self, sourceRow, sourceParent): + """ + Public method to determine, if the row is acceptable. + + @param sourceRow row number in the source model (integer) + @param sourceParent index of the source item (QModelIndex) + @return flag indicating acceptance (boolean) + """ + idx = self.sourceModel().index(sourceRow, 0, sourceParent) + return self.sourceModel().hasChildren(idx) + + def filterAcceptsColumn(self, sourceColumn, sourceParent): + """ + Public method to determine, if the column is acceptable. + + @param sourceColumn column number in the source model (integer) + @param sourceParent index of the source item (QModelIndex) + @return flag indicating acceptance (boolean) + """ + return sourceColumn == 0 + + def hasChildren(self, parent=QModelIndex()): + """ + Public method to check, if a parent node has some children. + + @param parent index of the parent node (QModelIndex) + @return flag indicating the presence of children (boolean) + """ + sindex = self.mapToSource(parent) + return self.sourceModel().hasChildren(sindex) + + +class AddBookmarkDialog(QDialog, Ui_AddBookmarkDialog): + """ + Class implementing a dialog to add a bookmark or a bookmark folder. + """ + def __init__(self, parent=None, bookmarksManager=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + @param bookmarksManager reference to the bookmarks manager + object (BookmarksManager) + """ + super(AddBookmarkDialog, self).__init__(parent) + self.setupUi(self) + + self.__bookmarksManager = bookmarksManager + self.__addedNode = None + self.__addFolder = False + + if self.__bookmarksManager is None: + import WebBrowser.WebBrowserWindow + self.__bookmarksManager = \ + WebBrowser.WebBrowserWindow.WebBrowserWindow.bookmarksManager() + + self.__proxyModel = AddBookmarkProxyModel(self) + model = self.__bookmarksManager.bookmarksModel() + self.__proxyModel.setSourceModel(model) + + self.__treeView = QTreeView(self) + self.__treeView.setModel(self.__proxyModel) + self.__treeView.expandAll() + self.__treeView.header().setStretchLastSection(True) + self.__treeView.header().hide() + self.__treeView.setItemsExpandable(False) + self.__treeView.setRootIsDecorated(False) + self.__treeView.setIndentation(10) + self.__treeView.show() + + self.locationCombo.setModel(self.__proxyModel) + self.locationCombo.setView(self.__treeView) + + self.addressEdit.setInactiveText(self.tr("Url")) + self.nameEdit.setInactiveText(self.tr("Title")) + + self.resize(self.sizeHint()) + + def setUrl(self, url): + """ + Public slot to set the URL of the new bookmark. + + @param url URL of the bookmark (string) + """ + self.addressEdit.setText(url) + self.resize(self.sizeHint()) + + def url(self): + """ + Public method to get the URL of the bookmark. + + @return URL of the bookmark (string) + """ + return self.addressEdit.text() + + def setTitle(self, title): + """ + Public method to set the title of the new bookmark. + + @param title title of the bookmark (string) + """ + self.nameEdit.setText(title) + + def title(self): + """ + Public method to get the title of the bookmark. + + @return title of the bookmark (string) + """ + return self.nameEdit.text() + + def setDescription(self, description): + """ + Public method to set the description of the new bookmark. + + @param description description of the bookamrk (string) + """ + self.descriptionEdit.setPlainText(description) + + def description(self): + """ + Public method to get the description of the bookmark. + + @return description of the bookamrk (string) + """ + return self.descriptionEdit.toPlainText() + + def setCurrentIndex(self, idx): + """ + Public method to set the current index. + + @param idx current index to be set (QModelIndex) + """ + proxyIndex = self.__proxyModel.mapFromSource(idx) + self.__treeView.setCurrentIndex(proxyIndex) + self.locationCombo.setCurrentIndex(proxyIndex.row()) + + def currentIndex(self): + """ + Public method to get the current index. + + @return current index (QModelIndex) + """ + idx = self.locationCombo.view().currentIndex() + idx = self.__proxyModel.mapToSource(idx) + return idx + + def setFolder(self, folder): + """ + Public method to set the dialog to "Add Folder" mode. + + @param folder flag indicating "Add Folder" mode (boolean) + """ + self.__addFolder = folder + + if folder: + self.setWindowTitle(self.tr("Add Folder")) + self.addressEdit.setVisible(False) + else: + self.setWindowTitle(self.tr("Add Bookmark")) + self.addressEdit.setVisible(True) + + self.resize(self.sizeHint()) + + def isFolder(self): + """ + Public method to test, if the dialog is in "Add Folder" mode. + + @return flag indicating "Add Folder" mode (boolean) + """ + return self.__addFolder + + def addedNode(self): + """ + Public method to get a reference to the added bookmark node. + + @return reference to the added bookmark node (BookmarkNode) + """ + return self.__addedNode + + def accept(self): + """ + Public slot handling the acceptance of the dialog. + """ + if (not self.__addFolder and not self.addressEdit.text()) or \ + not self.nameEdit.text(): + super(AddBookmarkDialog, self).accept() + return + + from .BookmarkNode import BookmarkNode + + idx = self.currentIndex() + if not idx.isValid(): + idx = self.__bookmarksManager.bookmarksModel().index(0, 0) + parent = self.__bookmarksManager.bookmarksModel().node(idx) + + if self.__addFolder: + type_ = BookmarkNode.Folder + else: + type_ = BookmarkNode.Bookmark + bookmark = BookmarkNode(type_) + bookmark.title = self.nameEdit.text() + if not self.__addFolder: + bookmark.url = self.addressEdit.text() + bookmark.desc = self.descriptionEdit.toPlainText() + + self.__bookmarksManager.addBookmark(parent, bookmark) + self.__addedNode = bookmark + + super(AddBookmarkDialog, self).accept()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/AddBookmarkDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,148 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AddBookmarkDialog</class> + <widget class="QDialog" name="AddBookmarkDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>230</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>500</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>500</width> + <height>250</height> + </size> + </property> + <property name="windowTitle"> + <string>Add Bookmark</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Name:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="E5LineEdit" name="nameEdit"> + <property name="toolTip"> + <string>Enter the name</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Address:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="E5LineEdit" name="addressEdit"> + <property name="toolTip"> + <string>Enter the address</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Description:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QPlainTextEdit" name="descriptionEdit"> + <property name="toolTip"> + <string>Enter a description</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Folder:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QComboBox" name="locationCombo"/> + </item> + <item row="4" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5LineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>nameEdit</tabstop> + <tabstop>addressEdit</tabstop> + <tabstop>descriptionEdit</tabstop> + <tabstop>locationCombo</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>AddBookmarkDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>AddBookmarkDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarkNode.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the bookmark node. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QDateTime + + +class BookmarkNode(object): + """ + Class implementing the bookmark node type. + """ + # possible bookmark node types + Root = 0 + Folder = 1 + Bookmark = 2 + Separator = 3 + + # possible timestamp types + TsAdded = 0 + TsModified = 1 + TsVisited = 2 + + def __init__(self, type_=Root, parent=None): + """ + Constructor + + @param type_ type of the bookmark node (BookmarkNode.Type) + @param parent reference to the parent node (BookmarkNode) + """ + self.url = "" + self.title = "" + self.desc = "" + self.expanded = False + self.added = QDateTime() + self.modified = QDateTime() + self.visited = QDateTime() + + self._children = [] + self._parent = parent + self._type = type_ + + if parent is not None: + parent.add(self) + + def type(self): + """ + Public method to get the bookmark's type. + + @return bookmark type (BookmarkNode.Type) + """ + return self._type + + def setType(self, type_): + """ + Public method to set the bookmark's type. + + @param type_ type of the bookmark node (BookmarkNode.Type) + """ + self._type = type_ + + def children(self): + """ + Public method to get the list of child nodes. + + @return list of all child nodes (list of BookmarkNode) + """ + return self._children[:] + + def parent(self): + """ + Public method to get a reference to the parent node. + + @return reference to the parent node (BookmarkNode) + """ + return self._parent + + def add(self, child, offset=-1): + """ + Public method to add/insert a child node. + + @param child reference to the node to add (BookmarkNode) + @param offset position where to insert child (integer, -1 = append) + """ + if child._type == BookmarkNode.Root: + return + + if child._parent is not None: + child._parent.remove(child) + + child._parent = self + if offset == -1: + self._children.append(child) + else: + self._children.insert(offset, child) + + def remove(self, child): + """ + Public method to remove a child node. + + @param child reference to the child node (BookmarkNode) + """ + child._parent = None + if child in self._children: + self._children.remove(child)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarkPropertiesDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show and edit bookmark properties. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QDialog + +from .Ui_BookmarkPropertiesDialog import Ui_BookmarkPropertiesDialog + + +class BookmarkPropertiesDialog(QDialog, Ui_BookmarkPropertiesDialog): + """ + Class implementing a dialog to show and edit bookmark properties. + """ + def __init__(self, node, parent=None): + """ + Constructor + + @param node reference to the bookmark (BookmarkNode) + @param parent reference to the parent widget (QWidget) + """ + super(BookmarkPropertiesDialog, self).__init__(parent) + self.setupUi(self) + + from .BookmarkNode import BookmarkNode + self.__node = node + if self.__node.type() == BookmarkNode.Folder: + self.addressLabel.hide() + self.addressEdit.hide() + + self.nameEdit.setText(self.__node.title) + self.descriptionEdit.setPlainText(self.__node.desc) + self.addressEdit.setText(self.__node.url) + + def accept(self): + """ + Public slot handling the acceptance of the dialog. + """ + from .BookmarkNode import BookmarkNode + + if (self.__node.type() == BookmarkNode.Bookmark and + not self.addressEdit.text()) or \ + not self.nameEdit.text(): + super(BookmarkPropertiesDialog, self).accept() + return + + import WebBrowser.WebBrowserWindow + bookmarksManager = WebBrowser.WebBrowserWindow.WebBrowserWindow\ + .bookmarksManager() + title = self.nameEdit.text() + if title != self.__node.title: + bookmarksManager.setTitle(self.__node, title) + if self.__node.type() == BookmarkNode.Bookmark: + url = self.addressEdit.text() + if url != self.__node.url: + bookmarksManager.setUrl(self.__node, url) + description = self.descriptionEdit.toPlainText() + if description != self.__node.desc: + self.__node.desc = description + bookmarksManager.setNodeChanged(self.__node) + + super(BookmarkPropertiesDialog, self).accept()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarkPropertiesDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,137 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BookmarkPropertiesDialog</class> + <widget class="QDialog" name="BookmarkPropertiesDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>221</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>500</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>500</width> + <height>250</height> + </size> + </property> + <property name="windowTitle"> + <string>Bookmark Properties</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Name:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="E5LineEdit" name="nameEdit"> + <property name="toolTip"> + <string>Enter the name</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="addressLabel"> + <property name="text"> + <string>Address:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="E5LineEdit" name="addressEdit"> + <property name="toolTip"> + <string>Enter the address</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Description:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QPlainTextEdit" name="descriptionEdit"> + <property name="toolTip"> + <string>Enter a description</string> + </property> + </widget> + </item> + <item row="3" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5LineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>nameEdit</tabstop> + <tabstop>addressEdit</tabstop> + <tabstop>descriptionEdit</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>BookmarkPropertiesDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>BookmarkPropertiesDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to manage bookmarks. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, Qt, QUrl, QModelIndex +from PyQt5.QtGui import QFontMetrics, QCursor +from PyQt5.QtWidgets import QDialog, QMenu, QApplication + +from E5Gui.E5TreeSortFilterProxyModel import E5TreeSortFilterProxyModel + +from .Ui_BookmarksDialog import Ui_BookmarksDialog + + +class BookmarksDialog(QDialog, Ui_BookmarksDialog): + """ + Class implementing a dialog to manage bookmarks. + + @signal openUrl(QUrl, str) emitted to open a URL in the current tab + @signal newUrl(QUrl, str) emitted to open a URL in a new tab + """ + openUrl = pyqtSignal(QUrl, str) + newUrl = pyqtSignal(QUrl, str) + + def __init__(self, parent=None, manager=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget + @param manager reference to the bookmarks manager object + (BookmarksManager) + """ + super(BookmarksDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.__bookmarksManager = manager + if self.__bookmarksManager is None: + import WebBrowser.WebBrowserWindow + self.__bookmarksManager = WebBrowser.WebBrowserWindow\ + .WebBrowserWindow.bookmarksManager() + + self.__bookmarksModel = self.__bookmarksManager.bookmarksModel() + self.__proxyModel = E5TreeSortFilterProxyModel(self) + self.__proxyModel.setFilterKeyColumn(-1) + self.__proxyModel.setSourceModel(self.__bookmarksModel) + + self.searchEdit.textChanged.connect( + self.__proxyModel.setFilterFixedString) + + self.bookmarksTree.setModel(self.__proxyModel) + self.bookmarksTree.setExpanded(self.__proxyModel.index(0, 0), True) + fm = QFontMetrics(self.font()) + header = fm.width("m") * 40 + self.bookmarksTree.header().resizeSection(0, header) + self.bookmarksTree.header().setStretchLastSection(True) + self.bookmarksTree.setContextMenuPolicy(Qt.CustomContextMenu) + + self.bookmarksTree.activated.connect(self.__activated) + self.bookmarksTree.customContextMenuRequested.connect( + self.__customContextMenuRequested) + + self.removeButton.clicked.connect( + self.bookmarksTree.removeSelected) + self.addFolderButton.clicked.connect(self.__newFolder) + + self.__expandNodes(self.__bookmarksManager.bookmarks()) + + def closeEvent(self, evt): + """ + Protected method to handle the closing of the dialog. + + @param evt reference to the event object (QCloseEvent) (ignored) + """ + self.__shutdown() + + def reject(self): + """ + Public method called when the dialog is rejected. + """ + self.__shutdown() + super(BookmarksDialog, self).reject() + + def __shutdown(self): + """ + Private method to perform shutdown actions for the dialog. + """ + if self.__saveExpandedNodes(self.bookmarksTree.rootIndex()): + self.__bookmarksManager.changeExpanded() + + def __saveExpandedNodes(self, parent): + """ + Private method to save the child nodes of an expanded node. + + @param parent index of the parent node (QModelIndex) + @return flag indicating a change (boolean) + """ + changed = False + for row in range(self.__proxyModel.rowCount(parent)): + child = self.__proxyModel.index(row, 0, parent) + sourceIndex = self.__proxyModel.mapToSource(child) + childNode = self.__bookmarksModel.node(sourceIndex) + wasExpanded = childNode.expanded + if self.bookmarksTree.isExpanded(child): + childNode.expanded = True + changed |= self.__saveExpandedNodes(child) + else: + childNode.expanded = False + changed |= (wasExpanded != childNode.expanded) + + return changed + + def __expandNodes(self, node): + """ + Private method to expand all child nodes of a node. + + @param node reference to the bookmark node to expand (BookmarkNode) + """ + for childNode in node.children(): + if childNode.expanded: + idx = self.__bookmarksModel.nodeIndex(childNode) + idx = self.__proxyModel.mapFromSource(idx) + self.bookmarksTree.setExpanded(idx, True) + self.__expandNodes(childNode) + + def __customContextMenuRequested(self, pos): + """ + Private slot to handle the context menu request for the bookmarks tree. + + @param pos position the context menu was requested (QPoint) + """ + from .BookmarkNode import BookmarkNode + + menu = QMenu() + idx = self.bookmarksTree.indexAt(pos) + idx = idx.sibling(idx.row(), 0) + sourceIndex = self.__proxyModel.mapToSource(idx) + node = self.__bookmarksModel.node(sourceIndex) + if idx.isValid() and node.type() != BookmarkNode.Folder: + menu.addAction( + self.tr("&Open"), self.__openBookmarkInCurrentTab) + menu.addAction( + self.tr("Open in New &Tab"), self.__openBookmarkInNewTab) + menu.addSeparator() + act = menu.addAction(self.tr("Edit &Name"), self.__editName) + act.setEnabled(idx.flags() & Qt.ItemIsEditable) + if idx.isValid() and node.type() != BookmarkNode.Folder: + menu.addAction(self.tr("Edit &Address"), self.__editAddress) + menu.addSeparator() + act = menu.addAction( + self.tr("&Delete"), self.bookmarksTree.removeSelected) + act.setEnabled(idx.flags() & Qt.ItemIsDragEnabled) + menu.addSeparator() + act = menu.addAction(self.tr("&Properties..."), self.__edit) + act.setEnabled(idx.flags() & Qt.ItemIsEditable) + menu.exec_(QCursor.pos()) + + def __activated(self, idx): + """ + Private slot to handle the activation of an entry. + + @param idx reference to the entry index (QModelIndex) + """ + self.__openBookmark( + QApplication.keyboardModifiers() & Qt.ControlModifier) + + def __openBookmarkInCurrentTab(self): + """ + Private slot to open a bookmark in the current browser tab. + """ + self.__openBookmark(False) + + def __openBookmarkInNewTab(self): + """ + Private slot to open a bookmark in a new browser tab. + """ + self.__openBookmark(True) + + def __openBookmark(self, newTab): + """ + Private method to open a bookmark. + + @param newTab flag indicating to open the bookmark in a new tab + (boolean) + """ + from .BookmarkNode import BookmarkNode + from .BookmarksModel import BookmarksModel + + idx = self.bookmarksTree.currentIndex() + sourceIndex = self.__proxyModel.mapToSource(idx) + node = self.__bookmarksModel.node(sourceIndex) + if not idx.parent().isValid() or \ + node is None or \ + node.type() == BookmarkNode.Folder: + return + if newTab: + self.newUrl.emit( + idx.sibling(idx.row(), 1).data(BookmarksModel.UrlRole), + idx.sibling(idx.row(), 0).data(Qt.DisplayRole)) + else: + self.openUrl.emit( + idx.sibling(idx.row(), 1).data(BookmarksModel.UrlRole), + idx.sibling(idx.row(), 0).data(Qt.DisplayRole)) + + def __editName(self): + """ + Private slot to edit the name part of a bookmark. + """ + idx = self.bookmarksTree.currentIndex() + idx = idx.sibling(idx.row(), 0) + self.bookmarksTree.edit(idx) + + def __editAddress(self): + """ + Private slot to edit the address part of a bookmark. + """ + idx = self.bookmarksTree.currentIndex() + idx = idx.sibling(idx.row(), 1) + self.bookmarksTree.edit(idx) + + def __edit(self): + """ + Private slot to edit a bookmarks properties. + """ + from .BookmarkPropertiesDialog import BookmarkPropertiesDialog + + idx = self.bookmarksTree.currentIndex() + sourceIndex = self.__proxyModel.mapToSource(idx) + node = self.__bookmarksModel.node(sourceIndex) + dlg = BookmarkPropertiesDialog(node) + dlg.exec_() + + def __newFolder(self): + """ + Private slot to add a new bookmarks folder. + """ + from .BookmarkNode import BookmarkNode + + currentIndex = self.bookmarksTree.currentIndex() + idx = QModelIndex(currentIndex) + sourceIndex = self.__proxyModel.mapToSource(idx) + sourceNode = self.__bookmarksModel.node(sourceIndex) + row = -1 # append new folder as the last item per default + + if sourceNode is not None and \ + sourceNode.type() != BookmarkNode.Folder: + # If the selected item is not a folder, add a new folder to the + # parent folder, but directly below the selected item. + idx = idx.parent() + row = currentIndex.row() + 1 + + if not idx.isValid(): + # Select bookmarks menu as default. + idx = self.__proxyModel.index(1, 0) + + idx = self.__proxyModel.mapToSource(idx) + parent = self.__bookmarksModel.node(idx) + node = BookmarkNode(BookmarkNode.Folder) + node.title = self.tr("New Folder") + self.__bookmarksManager.addBookmark(parent, node, row)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,179 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BookmarksDialog</class> + <widget class="QDialog" name="BookmarksDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>750</width> + <height>450</height> + </rect> + </property> + <property name="windowTitle"> + <string>Manage Bookmarks</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="E5ClearableLineEdit" name="searchEdit"> + <property name="toolTip"> + <string>Enter search term for bookmarks</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <widget class="E5TreeView" name="bookmarksTree"> + <property name="dragDropMode"> + <enum>QAbstractItemView::InternalMove</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="textElideMode"> + <enum>Qt::ElideMiddle</enum> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to delete the selected entries</string> + </property> + <property name="text"> + <string>&Delete</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="addFolderButton"> + <property name="toolTip"> + <string>Press to add a new bookmarks folder</string> + </property> + <property name="text"> + <string>Add &Folder</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="spacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + <customwidget> + <class>E5TreeView</class> + <extends>QTreeView</extends> + <header>E5Gui/E5TreeView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>bookmarksTree</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>addFolderButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>BookmarksDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>252</x> + <y>445</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>BookmarksDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>320</x> + <y>445</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksImportDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog for importing bookmarks from other sources. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSlot, Qt, QSize +from PyQt5.QtWidgets import QDialog, QListWidgetItem + +from E5Gui import E5MessageBox +from E5Gui.E5PathPicker import E5PathPickerModes + +from .Ui_BookmarksImportDialog import Ui_BookmarksImportDialog + +from . import BookmarksImporters + +import Globals + + +class BookmarksImportDialog(QDialog, Ui_BookmarksImportDialog): + """ + Class implementing a dialog for importing bookmarks from other sources. + """ + SourcesListIdRole = Qt.UserRole + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(BookmarksImportDialog, self).__init__(parent) + self.setupUi(self) + + self.filePicker.setMode(E5PathPickerModes.OpenFileMode) + + self.sourcesList.setIconSize(QSize(48, 48)) + for icon, displayText, idText in BookmarksImporters.getImporters(): + itm = QListWidgetItem(icon, displayText, self.sourcesList) + itm.setData(self.SourcesListIdRole, idText) + + self.__currentPage = 0 + self.__selectedSource = "" + self.__topLevelBookmarkNode = None + self.__sourceFile = "" + self.__sourceDir = "" + + self.pagesWidget.setCurrentIndex(self.__currentPage) + self.__enableNextButton() + + def __enableNextButton(self): + """ + Private slot to set the enabled state of the next button. + """ + if self.__currentPage == 0: + self.nextButton.setEnabled( + len(self.sourcesList.selectedItems()) == 1) + elif self.__currentPage == 1: + self.nextButton.setEnabled(self.filePicker.text() != "") + + @pyqtSlot() + def on_sourcesList_itemSelectionChanged(self): + """ + Private slot to handle changes of the selection of the import source. + """ + self.__enableNextButton() + + @pyqtSlot(str) + def on_filePicker_textChanged(self, txt): + """ + Private slot handling changes of the file to import bookmarks form. + + @param txt text of the line edit (string) + """ + self.__enableNextButton() + + @pyqtSlot() + def on_nextButton_clicked(self): + """ + Private slot to switch to the next page. + """ + if self.sourcesList.currentItem() is None: + return + + if self.__currentPage == 0: + self.__selectedSource = self.sourcesList.currentItem().data( + self.SourcesListIdRole) + (pixmap, sourceName, self.__sourceFile, info, prompt, + self.__sourceDir) = BookmarksImporters.getImporterInfo( + self.__selectedSource) + + self.iconLabel.setPixmap(pixmap) + self.importingFromLabel.setText( + self.tr("<b>Importing from {0}</b>").format(sourceName)) + self.fileLabel1.setText(info) + self.fileLabel2.setText(prompt) + self.standardDirLabel.setText( + "<i>{0}</i>".format(self.__sourceDir)) + + self.nextButton.setText(self.tr("Finish")) + + self.__currentPage += 1 + self.pagesWidget.setCurrentIndex(self.__currentPage) + self.__enableNextButton() + + if self.__selectedSource == "ie": + self.filePicker.setMode(E5PathPickerModes.DirectoryMode) + else: + self.filePicker.setMode(E5PathPickerModes.OpenFileMode) + if Globals.isMacPlatform(): + filter = "*{0}".format( + os.path.splitext(self.__sourceFile)[1]) + else: + filter = self.__sourceFile + self.filePicker.setFilters(filter) + self.filePicker.setDefaultDirectory(self.__sourceDir) + + elif self.__currentPage == 1: + if self.filePicker.text() == "": + return + + importer = BookmarksImporters.getImporter(self.__selectedSource) + importer.setPath(self.filePicker.text()) + if importer.open(): + self.__topLevelBookmarkNode = importer.importedBookmarks() + if importer.error(): + E5MessageBox.critical( + self, + self.tr("Error importing bookmarks"), + importer.errorString()) + return + + self.accept() + + @pyqtSlot() + def on_cancelButton_clicked(self): + """ + Private slot documentation goes here. + """ + self.reject() + + def getImportedBookmarks(self): + """ + Public method to get the imported bookmarks. + + @return top level bookmark (BookmarkNode) + """ + return self.__topLevelBookmarkNode
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksImportDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,211 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BookmarksImportDialog</class> + <widget class="QDialog" name="BookmarksImportDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>550</width> + <height>354</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>550</width> + <height>350</height> + </size> + </property> + <property name="windowTitle"> + <string>Import Bookmarks</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QStackedWidget" name="pagesWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="sourcePage"> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Choose source from which you want to import bookmarks:</string> + </property> + </widget> + </item> + <item> + <widget class="QListWidget" name="sourcesList"> + <property name="toolTip"> + <string>Choose the source to import from</string> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="filePage"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="iconLabel"> + <property name="minimumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="importingFromLabel"/> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>44</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="fileLabel1"> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="standardDirLabel"> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="fileLabel2"/> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>44</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="E5PathPicker" name="filePicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the name of the bookmarks file or directory</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>44</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="nextButton"> + <property name="text"> + <string>Next ></string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="cancelButton"> + <property name="text"> + <string>Cancel</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5PathPicker</class> + <extends>QWidget</extends> + <header>E5Gui/E5PathPicker.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>sourcesList</tabstop> + <tabstop>nextButton</tabstop> + <tabstop>cancelButton</tabstop> + <tabstop>filePicker</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksImporters/BookmarksImporter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a base class for the bookmarks importers. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QObject + + +class BookmarksImporter(QObject): + """ + Class implementing the base class for the bookmarks importers. + """ + def __init__(self, id="", parent=None): + """ + Constructor + + @param id source ID (string) + @param parent reference to the parent object (QObject) + """ + super(BookmarksImporter, self).__init__(parent) + + self._path = "" + self._file = "" + self._error = False + self._errorString = "" + self._id = id + + def setPath(self, path): + """ + Public method to set the path of the bookmarks file or directory. + + @param path bookmarks file or directory (string) + @exception NotImplementedError raised to indicate this method must + be implemented by a subclass + """ + raise NotImplementedError + + def open(self): + """ + Public method to open the bookmarks file. + + It must return a flag indicating success (boolean). + + @exception NotImplementedError raised to indicate this method must + be implemented by a subclass + """ + raise NotImplementedError + + def importedBookmarks(self): + """ + Public method to get the imported bookmarks. + + It must return the imported bookmarks (BookmarkNode). + + @exception NotImplementedError raised to indicate this method must + be implemented by a subclass + """ + raise NotImplementedError + + def error(self): + """ + Public method to check for an error. + + @return flag indicating an error (boolean) + """ + return self._error + + def errorString(self): + """ + Public method to get the error description. + + @return error description (string) + """ + return self._errorString
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksImporters/ChromeImporter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an importer for Chrome bookmarks. +""" + +from __future__ import unicode_literals + +import os +import json + +from PyQt5.QtCore import QCoreApplication, QDate, Qt + +from .BookmarksImporter import BookmarksImporter + +import UI.PixmapCache +import Globals + + +def getImporterInfo(id): + """ + Module function to get information for the given source id. + + @param id id of the browser ("chrome" or "chromium") + @return tuple with an icon (QPixmap), readable name (string), name of + the default bookmarks file (string), an info text (string), + a prompt (string) and the default directory of the bookmarks file + (string) + @exception ValueError raised to indicate an invalid browser ID + """ + if id == "chrome": + if Globals.isWindowsPlatform(): + standardDir = os.path.expandvars( + "%USERPROFILE%\\AppData\\Local\\Google\\Chrome\\" + "User Data\\Default") + elif Globals.isMacPlatform(): + standardDir = os.path.expanduser( + "~/Library/Application Support/Google/Chrome/Default") + else: + standardDir = os.path.expanduser("~/.config/google-chrome/Default") + return ( + UI.PixmapCache.getPixmap("chrome.png"), + "Google Chrome", + "Bookmarks", + QCoreApplication.translate( + "ChromeImporter", + """Google Chrome stores its bookmarks in the""" + """ <b>Bookmarks</b> text file. This file is usually""" + """ located in"""), + QCoreApplication.translate( + "ChromeImporter", + """Please choose the file to begin importing bookmarks."""), + standardDir, + ) + elif id == "chromium": + if Globals.isWindowsPlatform(): + standardDir = os.path.expandvars( + "%USERPROFILE%\\AppData\\Local\\Google\\Chrome\\" + "User Data\\Default") + else: + standardDir = os.path.expanduser("~/.config/chromium/Default") + return ( + UI.PixmapCache.getPixmap("chromium.png"), + "Chromium", + "Bookmarks", + QCoreApplication.translate( + "ChromeImporter", + """Chromium stores its bookmarks in the <b>Bookmarks</b>""" + """ text file. This file is usually located in"""), + QCoreApplication.translate( + "ChromeImporter", + """Please choose the file to begin importing bookmarks."""), + standardDir, + ) + else: + raise ValueError("Unsupported browser ID given ({0}).".format(id)) + + +class ChromeImporter(BookmarksImporter): + """ + Class implementing the Chrome bookmarks importer. + """ + def __init__(self, id="", parent=None): + """ + Constructor + + @param id source ID (string) + @param parent reference to the parent object (QObject) + """ + super(ChromeImporter, self).__init__(id, parent) + + self.__fileName = "" + + def setPath(self, path): + """ + Public method to set the path of the bookmarks file or directory. + + @param path bookmarks file or directory (string) + """ + self.__fileName = path + + def open(self): + """ + Public method to open the bookmarks file. + + @return flag indicating success (boolean) + """ + if not os.path.exists(self.__fileName): + self._error = True + self._errorString = self.tr( + "File '{0}' does not exist.").format(self.__fileName) + return False + return True + + def importedBookmarks(self): + """ + Public method to get the imported bookmarks. + + @return imported bookmarks (BookmarkNode) + """ + try: + f = open(self.__fileName, "r", encoding="utf-8") + contents = json.load(f) + f.close() + except IOError as err: + self._error = True + self._errorString = self.tr( + "File '{0}' cannot be read.\nReason: {1}")\ + .format(self.__fileName, str(err)) + return None + + from ..BookmarkNode import BookmarkNode + importRootNode = BookmarkNode(BookmarkNode.Folder) + if contents["version"] == 1: + self.__processRoots(contents["roots"], importRootNode) + + if self._id == "chrome": + importRootNode.title = self.tr("Google Chrome Import") + elif self._id == "chromium": + importRootNode.title = self.tr("Chromium Import") + else: + importRootNode.title = self.tr("Imported {0}")\ + .format(QDate.currentDate().toString(Qt.SystemLocaleShortDate)) + return importRootNode + + def __processRoots(self, data, rootNode): + """ + Private method to process the bookmark roots. + + @param data dictionary with the bookmarks data (dict) + @param rootNode node to add the bookmarks to (BookmarkNode) + """ + for node in data.values(): + if node["type"] == "folder": + self.__generateFolderNode(node, rootNode) + elif node["type"] == "url": + self.__generateUrlNode(node, rootNode) + + def __generateFolderNode(self, data, rootNode): + """ + Private method to process a bookmarks folder. + + @param data dictionary with the bookmarks data (dict) + @param rootNode node to add the bookmarks to (BookmarkNode) + """ + from ..BookmarkNode import BookmarkNode + folder = BookmarkNode(BookmarkNode.Folder, rootNode) + folder.title = data["name"].replace("&", "&&") + for node in data["children"]: + if node["type"] == "folder": + self.__generateFolderNode(node, folder) + elif node["type"] == "url": + self.__generateUrlNode(node, folder) + + def __generateUrlNode(self, data, rootNode): + """ + Private method to process a bookmarks node. + + @param data dictionary with the bookmarks data (dict) + @param rootNode node to add the bookmarks to (BookmarkNode) + """ + from ..BookmarkNode import BookmarkNode + bookmark = BookmarkNode(BookmarkNode.Bookmark, rootNode) + bookmark.url = data["url"] + bookmark.title = data["name"].replace("&", "&&")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksImporters/FirefoxImporter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an importer for Firefox bookmarks. +""" + +from __future__ import unicode_literals + +import os +import sqlite3 + +from PyQt5.QtCore import QCoreApplication, QDate, Qt, QUrl + +from .BookmarksImporter import BookmarksImporter + +import UI.PixmapCache +import Globals + + +def getImporterInfo(id): + """ + Module function to get information for the given source id. + + @param id id of the browser ("chrome" or "chromium") + @return tuple with an icon (QPixmap), readable name (string), name of + the default bookmarks file (string), an info text (string), + a prompt (string) and the default directory of the bookmarks file + (string) + @exception ValueError raised to indicate an invalid browser ID + """ + if id == "firefox": + if Globals.isWindowsPlatform(): + standardDir = os.path.expandvars( + "%APPDATA%\\Mozilla\\Firefox\\Profiles") + elif Globals.isMacPlatform(): + standardDir = os.path.expanduser( + "~/Library/Application Support/Firefox/Profiles") + else: + standardDir = os.path.expanduser("~/.mozilla/firefox") + return ( + UI.PixmapCache.getPixmap("chrome.png"), + "Mozilla Firefox", + "places.sqlite", + QCoreApplication.translate( + "FirefoxImporter", + """Mozilla Firefox stores its bookmarks in the""" + """ <b>places.sqlite</b> SQLite database. This file is""" + """ usually located in"""), + QCoreApplication.translate( + "FirefoxImporter", + """Please choose the file to begin importing bookmarks."""), + standardDir, + ) + else: + raise ValueError("Unsupported browser ID given ({0}).".format(id)) + + +class FirefoxImporter(BookmarksImporter): + """ + Class implementing the Chrome bookmarks importer. + """ + def __init__(self, id="", parent=None): + """ + Constructor + + @param id source ID (string) + @param parent reference to the parent object (QObject) + """ + super(FirefoxImporter, self).__init__(id, parent) + + self.__fileName = "" + self.__db = None + + def setPath(self, path): + """ + Public method to set the path of the bookmarks file or directory. + + @param path bookmarks file or directory (string) + """ + self.__fileName = path + + def open(self): + """ + Public method to open the bookmarks file. + + @return flag indicating success (boolean) + """ + if not os.path.exists(self.__fileName): + self._error = True + self._errorString = self.tr("File '{0}' does not exist.")\ + .format(self.__fileName) + return False + + try: + self.__db = sqlite3.connect(self.__fileName) + except sqlite3.DatabaseError as err: + self._error = True + self._errorString = self.tr( + "Unable to open database.\nReason: {0}").format(str(err)) + return False + + return True + + def importedBookmarks(self): + """ + Public method to get the imported bookmarks. + + @return imported bookmarks (BookmarkNode) + """ + from ..BookmarkNode import BookmarkNode + importRootNode = BookmarkNode(BookmarkNode.Root) + + # step 1: build the hierarchy of bookmark folders + folders = {} + + try: + cursor = self.__db.cursor() + cursor.execute( + "SELECT id, parent, title FROM moz_bookmarks " + "WHERE type = 2 and title !=''") + for row in cursor: + id_ = row[0] + parent = row[1] + title = row[2] + if parent in folders: + folder = BookmarkNode(BookmarkNode.Folder, folders[parent]) + else: + folder = BookmarkNode(BookmarkNode.Folder, importRootNode) + folder.title = title.replace("&", "&&") + folders[id_] = folder + except sqlite3.DatabaseError as err: + self._error = True + self._errorString = self.tr( + "Unable to open database.\nReason: {0}").format(str(err)) + return None + + try: + cursor = self.__db.cursor() + cursor.execute( + "SELECT parent, title, fk, position FROM moz_bookmarks" + " WHERE type = 1 and title != '' ORDER BY position") + for row in cursor: + parent = row[0] + title = row[1] + placesId = row[2] + + cursor2 = self.__db.cursor() + cursor2.execute( + "SELECT url FROM moz_places WHERE id = {0}" + .format(placesId)) + row2 = cursor2.fetchone() + if row2: + url = QUrl(row2[0]) + if not title or url.isEmpty() or \ + url.scheme() in ["place", "about"]: + continue + + if parent in folders: + bookmark = BookmarkNode(BookmarkNode.Bookmark, + folders[parent]) + else: + bookmark = BookmarkNode(BookmarkNode.Bookmark, + importRootNode) + bookmark.url = url.toString() + bookmark.title = title.replace("&", "&&") + except sqlite3.DatabaseError as err: + self._error = True + self._errorString = self.tr( + "Unable to open database.\nReason: {0}").format(str(err)) + return None + + importRootNode.setType(BookmarkNode.Folder) + if self._id == "firefox": + importRootNode.title = self.tr("Mozilla Firefox Import") + else: + importRootNode.title = self.tr("Imported {0}")\ + .format(QDate.currentDate().toString(Qt.SystemLocaleShortDate)) + return importRootNode
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksImporters/HtmlImporter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an importer for HTML bookmark files. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import QCoreApplication, QDate, Qt + +from .BookmarksImporter import BookmarksImporter + +import UI.PixmapCache + + +def getImporterInfo(id): + """ + Module function to get information for the given HTML source id. + + @param id id of the browser ("chrome" or "chromium") + @return tuple with an icon (QPixmap), readable name (string), name of + the default bookmarks file (string), an info text (string), + a prompt (string) and the default directory of the bookmarks file + (string) + @exception ValueError raised to indicate an invalid browser ID + """ + if id == "html": + return ( + UI.PixmapCache.getPixmap("html.png"), + "HTML Netscape Bookmarks", + QCoreApplication.translate( + "HtmlImporter", + "HTML Netscape Bookmarks") + " (*.htm *.html)", + QCoreApplication.translate( + "HtmlImporter", + """You can import bookmarks from any browser that supports""" + """ HTML exporting. This file has usually the extension""" + """ .htm or .html."""), + QCoreApplication.translate( + "HtmlImporter", + """Please choose the file to begin importing bookmarks."""), + "", + ) + else: + raise ValueError("Unsupported browser ID given ({0}).".format(id)) + + +class HtmlImporter(BookmarksImporter): + """ + Class implementing the HTML bookmarks importer. + """ + def __init__(self, id="", parent=None): + """ + Constructor + + @param id source ID (string) + @param parent reference to the parent object (QObject) + """ + super(HtmlImporter, self).__init__(id, parent) + + self.__fileName = "" + self.__inFile = None + + def setPath(self, path): + """ + Public method to set the path of the bookmarks file or directory. + + @param path bookmarks file or directory (string) + """ + self.__fileName = path + + def open(self): + """ + Public method to open the bookmarks file. + + @return flag indicating success (boolean) + """ + if not os.path.exists(self.__fileName): + self._error = True + self._errorString = self.tr("File '{0}' does not exist.")\ + .format(self.__fileName) + return False + return True + + def importedBookmarks(self): + """ + Public method to get the imported bookmarks. + + @return imported bookmarks (BookmarkNode) + """ + from ..BookmarkNode import BookmarkNode + from ..NsHtmlReader import NsHtmlReader + + reader = NsHtmlReader() + importRootNode = reader.read(self.__fileName) + + importRootNode.setType(BookmarkNode.Folder) + if self._id == "html": + importRootNode.title = self.tr("HTML Import") + else: + importRootNode.title = self.tr("Imported {0}")\ + .format(QDate.currentDate().toString(Qt.SystemLocaleShortDate)) + return importRootNode
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksImporters/IExplorerImporter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an importer for Internet Explorer bookmarks. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import QCoreApplication, QDate, Qt + +from .BookmarksImporter import BookmarksImporter + +import UI.PixmapCache +import Globals + + +def getImporterInfo(id): + """ + Module function to get information for the given source id. + + @param id id of the browser ("chrome" or "chromium") + @return tuple with an icon (QPixmap), readable name (string), name of + the default bookmarks file (string), an info text (string), + a prompt (string) and the default directory of the bookmarks file + (string) + @exception ValueError raised to indicate an invalid browser ID + """ + if id == "ie": + if Globals.isWindowsPlatform(): + standardDir = os.path.expandvars( + "%USERPROFILE%\\Favorites") + else: + standardDir = "" + return ( + UI.PixmapCache.getPixmap("internet_explorer.png"), + "Internet Explorer", + "", + QCoreApplication.translate( + "IExplorerImporter", + """Internet Explorer stores its bookmarks in the""" + """ <b>Favorites</b> folder This folder is usually""" + """ located in"""), + QCoreApplication.translate( + "IExplorerImporter", + """Please choose the folder to begin importing bookmarks."""), + standardDir, + ) + else: + raise ValueError("Unsupported browser ID given ({0}).".format(id)) + + +class IExplorerImporter(BookmarksImporter): + """ + Class implementing the Chrome bookmarks importer. + """ + def __init__(self, id="", parent=None): + """ + Constructor + + @param id source ID (string) + @param parent reference to the parent object (QObject) + """ + super(IExplorerImporter, self).__init__(id, parent) + + self.__fileName = "" + + def setPath(self, path): + """ + Public method to set the path of the bookmarks file or directory. + + @param path bookmarks file or directory (string) + """ + self.__fileName = path + + def open(self): + """ + Public method to open the bookmarks file. + + @return flag indicating success (boolean) + """ + if not os.path.exists(self.__fileName): + self._error = True + self._errorString = self.tr("Folder '{0}' does not exist.")\ + .format(self.__fileName) + return False + if not os.path.isdir(self.__fileName): + self._error = True + self._errorString = self.tr("'{0}' is not a folder.")\ + .format(self.__fileName) + return True + + def importedBookmarks(self): + """ + Public method to get the imported bookmarks. + + @return imported bookmarks (BookmarkNode) + """ + from ..BookmarkNode import BookmarkNode + + folders = {} + + importRootNode = BookmarkNode(BookmarkNode.Folder) + folders[self.__fileName] = importRootNode + + for dir, subdirs, files in os.walk(self.__fileName): + for subdir in subdirs: + path = os.path.join(dir, subdir) + if dir in folders: + folder = BookmarkNode(BookmarkNode.Folder, folders[dir]) + else: + folder = BookmarkNode(BookmarkNode.Folder, importRootNode) + folder.title = subdir.replace("&", "&&") + folders[path] = folder + + for file in files: + name, ext = os.path.splitext(file) + if ext.lower() == ".url": + path = os.path.join(dir, file) + try: + f = open(path, "r") + contents = f.read() + f.close() + except IOError: + continue + url = "" + for line in contents.splitlines(): + if line.startswith("URL="): + url = line.replace("URL=", "") + break + if url: + if dir in folders: + bookmark = BookmarkNode(BookmarkNode.Bookmark, + folders[dir]) + else: + bookmark = BookmarkNode(BookmarkNode.Bookmark, + importRootNode) + bookmark.url = url + bookmark.title = name.replace("&", "&&") + + if self._id == "ie": + importRootNode.title = self.tr("Internet Explorer Import") + else: + importRootNode.title = self.tr("Imported {0}")\ + .format(QDate.currentDate().toString(Qt.SystemLocaleShortDate)) + return importRootNode
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksImporters/OperaImporter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an importer for Opera bookmarks. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import QCoreApplication, QDate, Qt + +from .BookmarksImporter import BookmarksImporter + +import UI.PixmapCache +import Globals + + +def getImporterInfo(id): + """ + Module function to get information for the given source id. + + @param id id of the browser ("chrome" or "chromium") + @return tuple with an icon (QPixmap), readable name (string), name of + the default bookmarks file (string), an info text (string), + a prompt (string) and the default directory of the bookmarks file + (string) + @exception ValueError raised to indicate an invalid browser ID + """ + if id == "opera": + if Globals.isWindowsPlatform(): + standardDir = os.path.expandvars("%APPDATA%\\Opera\\Opera") + elif Globals.isMacPlatform(): + standardDir = os.path.expanduser( + "~/Library/Opera") + else: + standardDir = os.path.expanduser("~/.opera") + return ( + UI.PixmapCache.getPixmap("opera.png"), + "Opera", + "bookmarks.adr", + QCoreApplication.translate( + "OperaImporter", + """Opera stores its bookmarks in the <b>bookmarks.adr</b> """ + """text file. This file is usually located in"""), + QCoreApplication.translate( + "OperaImporter", + """Please choose the file to begin importing bookmarks."""), + standardDir, + ) + else: + raise ValueError("Unsupported browser ID given ({0}).".format(id)) + + +class OperaImporter(BookmarksImporter): + """ + Class implementing the Opera bookmarks importer. + """ + def __init__(self, id="", parent=None): + """ + Constructor + + @param id source ID (string) + @param parent reference to the parent object (QObject) + """ + super(OperaImporter, self).__init__(id, parent) + + self.__fileName = "" + + def setPath(self, path): + """ + Public method to set the path of the bookmarks file or directory. + + @param path bookmarks file or directory (string) + """ + self.__fileName = path + + def open(self): + """ + Public method to open the bookmarks file. + + @return flag indicating success (boolean) + """ + if not os.path.exists(self.__fileName): + self._error = True + self._errorString = self.tr("File '{0}' does not exist.")\ + .format(self.__fileName) + return False + return True + + def importedBookmarks(self): + """ + Public method to get the imported bookmarks. + + @return imported bookmarks (BookmarkNode) + """ + try: + f = open(self.__fileName, "r", encoding="utf-8") + contents = f.read() + f.close() + except IOError as err: + self._error = True + self._errorString = self.tr( + "File '{0}' cannot be read.\nReason: {1}")\ + .format(self.__fileName, str(err)) + return None + + folderStack = [] + + from ..BookmarkNode import BookmarkNode + importRootNode = BookmarkNode(BookmarkNode.Folder) + folderStack.append(importRootNode) + + for line in contents.splitlines(): + line = line.strip() + if line == "#FOLDER": + node = BookmarkNode(BookmarkNode.Folder, folderStack[-1]) + folderStack.append(node) + elif line == "#URL": + node = BookmarkNode(BookmarkNode.Bookmark, folderStack[-1]) + elif line == "-": + folderStack.pop() + elif line.startswith("NAME="): + node.title = line.replace("NAME=", "").replace("&", "&&") + elif line.startswith("URL="): + node.url = line.replace("URL=", "") + + if self._id == "opera": + importRootNode.title = self.tr("Opera Import") + else: + importRootNode.title = self.tr("Imported {0}")\ + .format(QDate.currentDate().toString(Qt.SystemLocaleShortDate)) + return importRootNode
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksImporters/SafariImporter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an importer for Apple Safari bookmarks. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import QCoreApplication, QDate, Qt + +from .BookmarksImporter import BookmarksImporter + +import UI.PixmapCache +import Globals + +from Utilities import binplistlib + + +def getImporterInfo(id): + """ + Module function to get information for the given source id. + + @param id id of the browser ("chrome" or "chromium") + @return tuple with an icon (QPixmap), readable name (string), name of + the default bookmarks file (string), an info text (string), + a prompt (string) and the default directory of the bookmarks file + (string) + @exception ValueError raised to indicate an invalid browser ID + """ + if id == "safari": + if Globals.isWindowsPlatform(): + standardDir = os.path.expandvars( + "%APPDATA%\\Apple Computer\\Safari") + elif Globals.isMacPlatform(): + standardDir = os.path.expanduser("~/Library/Safari") + else: + standardDir = "" + return ( + UI.PixmapCache.getPixmap("safari.png"), + "Apple Safari", + "Bookmarks.plist", + QCoreApplication.translate( + "SafariImporter", + """Apple Safari stores its bookmarks in the""" + """ <b>Bookmarks.plist</b> file. This file is usually""" + """ located in"""), + QCoreApplication.translate( + "SafariImporter", + """Please choose the file to begin importing bookmarks."""), + standardDir, + ) + else: + raise ValueError("Unsupported browser ID given ({0}).".format(id)) + + +class SafariImporter(BookmarksImporter): + """ + Class implementing the Apple Safari bookmarks importer. + """ + def __init__(self, id="", parent=None): + """ + Constructor + + @param id source ID (string) + @param parent reference to the parent object (QObject) + """ + super(SafariImporter, self).__init__(id, parent) + + self.__fileName = "" + + def setPath(self, path): + """ + Public method to set the path of the bookmarks file or directory. + + @param path bookmarks file or directory (string) + """ + self.__fileName = path + + def open(self): + """ + Public method to open the bookmarks file. + + @return flag indicating success (boolean) + """ + if not os.path.exists(self.__fileName): + self._error = True + self._errorString = self.tr("File '{0}' does not exist.")\ + .format(self.__fileName) + return False + return True + + def importedBookmarks(self): + """ + Public method to get the imported bookmarks. + + @return imported bookmarks (BookmarkNode) + """ + try: + bookmarksDict = binplistlib.readPlist(self.__fileName) + except binplistlib.InvalidPlistException as err: + self._error = True + self._errorString = self.tr( + "Bookmarks file cannot be read.\nReason: {0}".format(str(err))) + return None + + from ..BookmarkNode import BookmarkNode + importRootNode = BookmarkNode(BookmarkNode.Folder) + if bookmarksDict["WebBookmarkFileVersion"] == 1 and \ + bookmarksDict["WebBookmarkType"] == "WebBookmarkTypeList": + self.__processChildren(bookmarksDict["Children"], importRootNode) + + if self._id == "safari": + importRootNode.title = self.tr("Apple Safari Import") + else: + importRootNode.title = self.tr("Imported {0}")\ + .format(QDate.currentDate().toString(Qt.SystemLocaleShortDate)) + return importRootNode + + def __processChildren(self, children, rootNode): + """ + Private method to process the list of children. + + @param children list of child nodes to be processed (list of dict) + @param rootNode node to add the bookmarks to (BookmarkNode) + """ + from ..BookmarkNode import BookmarkNode + for child in children: + if child["WebBookmarkType"] == "WebBookmarkTypeList": + folder = BookmarkNode(BookmarkNode.Folder, rootNode) + folder.title = child["Title"].replace("&", "&&") + if "Children" in child: + self.__processChildren(child["Children"], folder) + elif child["WebBookmarkType"] == "WebBookmarkTypeLeaf": + url = child["URLString"] + if url.startswith(("place:", "about:")): + continue + + bookmark = BookmarkNode(BookmarkNode.Bookmark, rootNode) + bookmark.url = url + bookmark.title = child["URIDictionary"]["title"]\ + .replace("&", "&&")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksImporters/XbelImporter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an importer for XBEL files. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import QCoreApplication, QXmlStreamReader, QDate, Qt + +from .BookmarksImporter import BookmarksImporter + +import UI.PixmapCache + + +def getImporterInfo(id): + """ + Module function to get information for the given XBEL source id. + + @param id id of the browser ("chrome" or "chromium") + @return tuple with an icon (QPixmap), readable name (string), name of + the default bookmarks file (string), an info text (string), + a prompt (string) and the default directory of the bookmarks file + (string) + @exception ValueError raised to indicate an invalid browser ID + """ + if id == "e5browser": + from ..BookmarksManager import BookmarksManager + bookmarksFile = BookmarksManager.getFileName() + return ( + UI.PixmapCache.getPixmap("ericWeb48.png"), + "eric6 Web Browser", + os.path.basename(bookmarksFile), + QCoreApplication.translate( + "XbelImporter", + """eric6 Web Browser stores its bookmarks in the""" + """ <b>{0}</b> XML file. This file is usually located in""" + ).format(os.path.basename(bookmarksFile)), + QCoreApplication.translate( + "XbelImporter", + """Please choose the file to begin importing bookmarks."""), + os.path.dirname(bookmarksFile), + ) + elif id == "konqueror": + if os.path.exists(os.path.expanduser("~/.kde4")): + standardDir = os.path.expanduser("~/.kde4/share/apps/konqueror") + elif os.path.exists(os.path.expanduser("~/.kde")): + standardDir = os.path.expanduser("~/.kde/share/apps/konqueror") + else: + standardDir = "" + return ( + UI.PixmapCache.getPixmap("konqueror.png"), + "Konqueror", + "bookmarks.xml", + QCoreApplication.translate( + "XbelImporter", + """Konqueror stores its bookmarks in the""" + """ <b>bookmarks.xml</b> XML file. This file is usually""" + """ located in"""), + QCoreApplication.translate( + "XbelImporter", + """Please choose the file to begin importing bookmarks."""), + standardDir, + ) + elif id == "xbel": + return ( + UI.PixmapCache.getPixmap("xbel.png"), + "XBEL Bookmarks", + QCoreApplication.translate( + "XbelImporter", "XBEL Bookmarks") + " (*.xbel *.xml)", + QCoreApplication.translate( + "XbelImporter", + """You can import bookmarks from any browser that supports""" + """ XBEL exporting. This file has usually the extension""" + """ .xbel or .xml."""), + QCoreApplication.translate( + "XbelImporter", + """Please choose the file to begin importing bookmarks."""), + "", + ) + else: + raise ValueError("Unsupported browser ID given ({0}).".format(id)) + + +class XbelImporter(BookmarksImporter): + """ + Class implementing the XBEL bookmarks importer. + """ + def __init__(self, id="", parent=None): + """ + Constructor + + @param id source ID (string) + @param parent reference to the parent object (QObject) + """ + super(XbelImporter, self).__init__(id, parent) + + self.__fileName = "" + + def setPath(self, path): + """ + Public method to set the path of the bookmarks file or directory. + + @param path bookmarks file or directory (string) + """ + self.__fileName = path + + def open(self): + """ + Public method to open the bookmarks file. + + @return flag indicating success (boolean) + """ + if not os.path.exists(self.__fileName): + self._error = True + self._errorString = self.tr("File '{0}' does not exist.")\ + .format(self.__fileName) + return False + return True + + def importedBookmarks(self): + """ + Public method to get the imported bookmarks. + + @return imported bookmarks (BookmarkNode) + """ + from ..XbelReader import XbelReader + + reader = XbelReader() + importRootNode = reader.read(self.__fileName) + + if reader.error() != QXmlStreamReader.NoError: + self._error = True + self._errorString = self.tr( + """Error when importing bookmarks on line {0},""" + """ column {1}:\n{2}""")\ + .format(reader.lineNumber(), + reader.columnNumber(), + reader.errorString()) + return None + + from ..BookmarkNode import BookmarkNode + importRootNode.setType(BookmarkNode.Folder) + if self._id == "e5browser": + importRootNode.title = self.tr("eric6 Web Browser Import") + elif self._id == "konqueror": + importRootNode.title = self.tr("Konqueror Import") + elif self._id == "xbel": + importRootNode.title = self.tr("XBEL Import") + else: + importRootNode.title = self.tr("Imported {0}")\ + .format(QDate.currentDate().toString(Qt.SystemLocaleShortDate)) + return importRootNode
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksImporters/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing bookmarks importers for various sources. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QCoreApplication + +import UI.PixmapCache +import Globals + + +def getImporters(): + """ + Module function to get a list of supported importers. + + @return list of tuples with an icon (QIcon), readable name (string) and + internal name (string) + """ + importers = [] + importers.append( + (UI.PixmapCache.getIcon("ericWeb48.png"), "eric6 Web Browser", + "e5browser")) + importers.append( + (UI.PixmapCache.getIcon("firefox.png"), "Mozilla Firefox", "firefox")) + importers.append( + (UI.PixmapCache.getIcon("chrome.png"), "Google Chrome", "chrome")) + if Globals.isLinuxPlatform(): + importers.append( + (UI.PixmapCache.getIcon("chromium.png"), "Chromium", "chromium")) + importers.append( + (UI.PixmapCache.getIcon("konqueror.png"), "Konqueror", + "konqueror")) + importers.append( + (UI.PixmapCache.getIcon("opera.png"), "Opera", "opera")) + importers.append( + (UI.PixmapCache.getIcon("safari.png"), "Apple Safari", "safari")) + if Globals.isWindowsPlatform(): + importers.append( + (UI.PixmapCache.getIcon("internet_explorer.png"), + "Internet Explorer", "ie")) + importers.append( + (UI.PixmapCache.getIcon("xbel.png"), + QCoreApplication.translate("BookmarksImporters", "XBEL File"), + "xbel")) + importers.append( + (UI.PixmapCache.getIcon("html.png"), + QCoreApplication.translate("BookmarksImporters", "HTML File"), + "html")) + return importers + + +def getImporterInfo(id): + """ + Module function to get information for the given source id. + + @param id source id to get info for (string) + @return tuple with an icon (QPixmap), readable name (string), name of + the default bookmarks file (string), an info text (string), + a prompt (string) and the default directory of the bookmarks + file (string) + @exception ValueError raised to indicate an unsupported importer + """ + if id in ["e5browser", "xbel", "konqueror"]: + from . import XbelImporter + return XbelImporter.getImporterInfo(id) + elif id == "html": + from . import HtmlImporter + return HtmlImporter.getImporterInfo(id) + elif id in ["chrome", "chromium"]: + from . import ChromeImporter + return ChromeImporter.getImporterInfo(id) + elif id == "opera": + from . import OperaImporter + return OperaImporter.getImporterInfo(id) + elif id == "firefox": + from . import FirefoxImporter + return FirefoxImporter.getImporterInfo(id) + elif id == "ie": + from . import IExplorerImporter + return IExplorerImporter.getImporterInfo(id) + elif id == "safari": + from . import SafariImporter + return SafariImporter.getImporterInfo(id) + else: + raise ValueError("Invalid importer ID given ({0}).".format(id)) + + +def getImporter(id, parent=None): + """ + Module function to get an importer for the given source id. + + @param id source id to get an importer for (string) + @param parent reference to the parent object (QObject) + @return bookmarks importer (BookmarksImporter) + @exception ValueError raised to indicate an unsupported importer + """ + if id in ["e5browser", "xbel", "konqueror"]: + from . import XbelImporter + return XbelImporter.XbelImporter(id, parent) + elif id == "html": + from . import HtmlImporter + return HtmlImporter.HtmlImporter(id, parent) + elif id in ["chrome", "chromium"]: + from . import ChromeImporter + return ChromeImporter.ChromeImporter(id, parent) + elif id == "opera": + from . import OperaImporter + return OperaImporter.OperaImporter(id, parent) + elif id == "firefox": + from . import FirefoxImporter + return FirefoxImporter.FirefoxImporter(id, parent) + elif id == "ie": + from . import IExplorerImporter + return IExplorerImporter.IExplorerImporter(id, parent) + elif id == "safari": + from . import SafariImporter + return SafariImporter.SafariImporter(id, parent) + else: + raise ValueError("No importer for ID {0}.".format(id))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,614 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the bookmarks manager. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, QT_TRANSLATE_NOOP, QObject, QFile, \ + QIODevice, QXmlStreamReader, QDateTime, QFileInfo, QUrl, \ + QCoreApplication +from PyQt5.QtWidgets import QUndoStack, QUndoCommand, QDialog + +from E5Gui import E5MessageBox, E5FileDialog + +from .BookmarkNode import BookmarkNode + +from Utilities.AutoSaver import AutoSaver +import Utilities + +BOOKMARKBAR = QT_TRANSLATE_NOOP("BookmarksManager", "Bookmarks Bar") +BOOKMARKMENU = QT_TRANSLATE_NOOP("BookmarksManager", "Bookmarks Menu") + +StartRoot = 0 +StartMenu = 1 +StartToolBar = 2 + + +class BookmarksManager(QObject): + """ + Class implementing the bookmarks manager. + + @signal entryAdded(BookmarkNode) emitted after a bookmark node has been + added + @signal entryRemoved(BookmarkNode, int, BookmarkNode) emitted after a + bookmark node has been removed + @signal entryChanged(BookmarkNode) emitted after a bookmark node has been + changed + @signal bookmarksSaved() emitted after the bookmarks were saved + @signal bookmarksReloaded() emitted after the bookmarks were reloaded + """ + entryAdded = pyqtSignal(BookmarkNode) + entryRemoved = pyqtSignal(BookmarkNode, int, BookmarkNode) + entryChanged = pyqtSignal(BookmarkNode) + bookmarksSaved = pyqtSignal() + bookmarksReloaded = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(BookmarksManager, self).__init__(parent) + + self.__saveTimer = AutoSaver(self, self.save) + self.entryAdded.connect(self.__saveTimer.changeOccurred) + self.entryRemoved.connect(self.__saveTimer.changeOccurred) + self.entryChanged.connect(self.__saveTimer.changeOccurred) + + self.__initialize() + + def __initialize(self): + """ + Private method to initialize some data. + """ + self.__loaded = False + self.__bookmarkRootNode = None + self.__toolbar = None + self.__menu = None + self.__bookmarksModel = None + self.__commands = QUndoStack() + + @classmethod + def getFileName(cls): + """ + Class method to get the file name of the bookmark file. + + @return name of the bookmark file (string) + """ + return os.path.join(Utilities.getConfigDir(), "web_browser", + "bookmarks.xbel") + + def close(self): + """ + Public method to close the bookmark manager. + """ + self.__saveTimer.saveIfNeccessary() + + def undoRedoStack(self): + """ + Public method to get a reference to the undo stack. + + @return reference to the undo stack (QUndoStack) + """ + return self.__commands + + def changeExpanded(self): + """ + Public method to handle a change of the expanded state. + """ + self.__saveTimer.changeOccurred() + + def reload(self): + """ + Public method used to initiate a reloading of the bookmarks. + """ + self.__initialize() + self.load() + self.bookmarksReloaded.emit() + + def load(self): + """ + Public method to load the bookmarks. + + @exception RuntimeError raised to indicate an error loading the + bookmarks + """ + if self.__loaded: + return + + self.__loaded = True + + bookmarkFile = self.getFileName() + if not QFile.exists(bookmarkFile): + from . import DefaultBookmarks_rc # __IGNORE_WARNING__ + bookmarkFile = QFile(":/DefaultBookmarks.xbel") + bookmarkFile.open(QIODevice.ReadOnly) + + from .XbelReader import XbelReader + reader = XbelReader() + self.__bookmarkRootNode = reader.read(bookmarkFile) + if reader.error() != QXmlStreamReader.NoError: + E5MessageBox.warning( + None, + self.tr("Loading Bookmarks"), + self.tr( + """Error when loading bookmarks on line {0},""" + """ column {1}:\n {2}""") + .format(reader.lineNumber(), + reader.columnNumber(), + reader.errorString())) + + others = [] + for index in range( + len(self.__bookmarkRootNode.children()) - 1, -1, -1): + node = self.__bookmarkRootNode.children()[index] + if node.type() == BookmarkNode.Folder: + if (node.title == self.tr("Toolbar Bookmarks") or + node.title == BOOKMARKBAR) and \ + self.__toolbar is None: + node.title = self.tr(BOOKMARKBAR) + self.__toolbar = node + + if (node.title == self.tr("Menu") or + node.title == BOOKMARKMENU) and \ + self.__menu is None: + node.title = self.tr(BOOKMARKMENU) + self.__menu = node + else: + others.append(node) + self.__bookmarkRootNode.remove(node) + + if len(self.__bookmarkRootNode.children()) > 0: + raise RuntimeError("Error loading bookmarks.") + + if self.__toolbar is None: + self.__toolbar = BookmarkNode(BookmarkNode.Folder, + self.__bookmarkRootNode) + self.__toolbar.title = self.tr(BOOKMARKBAR) + else: + self.__bookmarkRootNode.add(self.__toolbar) + + if self.__menu is None: + self.__menu = BookmarkNode(BookmarkNode.Folder, + self.__bookmarkRootNode) + self.__menu.title = self.tr(BOOKMARKMENU) + else: + self.__bookmarkRootNode.add(self.__menu) + + for node in others: + self.__menu.add(node) + + def save(self): + """ + Public method to save the bookmarks. + """ + if not self.__loaded: + return + + from .XbelWriter import XbelWriter + writer = XbelWriter() + bookmarkFile = self.getFileName() + + # save root folder titles in English (i.e. not localized) + self.__menu.title = BOOKMARKMENU + self.__toolbar.title = BOOKMARKBAR + if not writer.write(bookmarkFile, self.__bookmarkRootNode): + E5MessageBox.warning( + None, + self.tr("Saving Bookmarks"), + self.tr("""Error saving bookmarks to <b>{0}</b>.""") + .format(bookmarkFile)) + + # restore localized titles + self.__menu.title = self.tr(BOOKMARKMENU) + self.__toolbar.title = self.tr(BOOKMARKBAR) + + self.bookmarksSaved.emit() + + def addBookmark(self, parent, node, row=-1): + """ + Public method to add a bookmark. + + @param parent reference to the node to add to (BookmarkNode) + @param node reference to the node to add (BookmarkNode) + @param row row number (integer) + """ + if not self.__loaded: + return + + self.setTimestamp(node, BookmarkNode.TsAdded, + QDateTime.currentDateTime()) + + command = InsertBookmarksCommand(self, parent, node, row) + self.__commands.push(command) + + def removeBookmark(self, node): + """ + Public method to remove a bookmark. + + @param node reference to the node to be removed (BookmarkNode) + """ + if not self.__loaded: + return + + parent = node.parent() + row = parent.children().index(node) + command = RemoveBookmarksCommand(self, parent, row) + self.__commands.push(command) + + def setTitle(self, node, newTitle): + """ + Public method to set the title of a bookmark. + + @param node reference to the node to be changed (BookmarkNode) + @param newTitle title to be set (string) + """ + if not self.__loaded: + return + + command = ChangeBookmarkCommand(self, node, newTitle, True) + self.__commands.push(command) + + def setUrl(self, node, newUrl): + """ + Public method to set the URL of a bookmark. + + @param node reference to the node to be changed (BookmarkNode) + @param newUrl URL to be set (string) + """ + if not self.__loaded: + return + + command = ChangeBookmarkCommand(self, node, newUrl, False) + self.__commands.push(command) + + def setNodeChanged(self, node): + """ + Public method to signal changes of bookmarks other than title, URL + or timestamp. + + @param node reference to the bookmark (BookmarkNode) + """ + self.__saveTimer.changeOccurred() + + def setTimestamp(self, node, timestampType, timestamp): + """ + Public method to set the URL of a bookmark. + + @param node reference to the node to be changed (BookmarkNode) + @param timestampType type of the timestamp to set + (BookmarkNode.TsAdded, BookmarkNode.TsModified, + BookmarkNode.TsVisited) + @param timestamp timestamp to set (QDateTime) + """ + if not self.__loaded: + return + + assert timestampType in [BookmarkNode.TsAdded, + BookmarkNode.TsModified, + BookmarkNode.TsVisited] + + if timestampType == BookmarkNode.TsAdded: + node.added = timestamp + elif timestampType == BookmarkNode.TsModified: + node.modified = timestamp + elif timestampType == BookmarkNode.TsVisited: + node.visited = timestamp + self.__saveTimer.changeOccurred() + + def bookmarks(self): + """ + Public method to get a reference to the root bookmark node. + + @return reference to the root bookmark node (BookmarkNode) + """ + if not self.__loaded: + self.load() + + return self.__bookmarkRootNode + + def menu(self): + """ + Public method to get a reference to the bookmarks menu node. + + @return reference to the bookmarks menu node (BookmarkNode) + """ + if not self.__loaded: + self.load() + + return self.__menu + + def toolbar(self): + """ + Public method to get a reference to the bookmarks toolbar node. + + @return reference to the bookmarks toolbar node (BookmarkNode) + """ + if not self.__loaded: + self.load() + + return self.__toolbar + + def bookmarksModel(self): + """ + Public method to get a reference to the bookmarks model. + + @return reference to the bookmarks model (BookmarksModel) + """ + if self.__bookmarksModel is None: + from .BookmarksModel import BookmarksModel + self.__bookmarksModel = BookmarksModel(self, self) + return self.__bookmarksModel + + def importBookmarks(self): + """ + Public method to import bookmarks. + """ + from .BookmarksImportDialog import BookmarksImportDialog + dlg = BookmarksImportDialog() + if dlg.exec_() == QDialog.Accepted: + importRootNode = dlg.getImportedBookmarks() + if importRootNode is not None: + self.addBookmark(self.menu(), importRootNode) + + def exportBookmarks(self): + """ + Public method to export the bookmarks. + """ + fileName, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( + None, + self.tr("Export Bookmarks"), + "eric6_bookmarks.xbel", + self.tr("XBEL bookmarks (*.xbel);;" + "XBEL bookmarks (*.xml);;" + "HTML Bookmarks (*.html)")) + if not fileName: + return + + ext = QFileInfo(fileName).suffix() + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fileName += ex + + ext = QFileInfo(fileName).suffix() + if ext == "html": + from .NsHtmlWriter import NsHtmlWriter + writer = NsHtmlWriter() + else: + from .XbelWriter import XbelWriter + writer = XbelWriter() + if not writer.write(fileName, self.__bookmarkRootNode): + E5MessageBox.critical( + None, + self.tr("Exporting Bookmarks"), + self.tr("""Error exporting bookmarks to <b>{0}</b>.""") + .format(fileName)) + + def iconChanged(self, url): + """ + Public slot to update the icon image for an URL. + + @param url URL of the icon to update (QUrl or string) + """ + if isinstance(url, QUrl): + url = url.toString() + nodes = self.bookmarksForUrl(url) + for node in nodes: + self.bookmarksModel().entryChanged(node) + + def bookmarkForUrl(self, url, start=StartRoot): + """ + Public method to get a bookmark node for a given URL. + + @param url URL of the bookmark to search for (QUrl or string) + @keyparam start indicator for the start of the search + (StartRoot, StartMenu, StartToolBar) + @return bookmark node for the given url (BookmarkNode) + """ + if start == StartMenu: + startNode = self.__menu + elif start == StartToolBar: + startNode = self.__toolbar + else: + startNode = self.__bookmarkRootNode + if startNode is None: + return None + + if isinstance(url, QUrl): + url = url.toString() + + return self.__searchBookmark(url, startNode) + + def __searchBookmark(self, url, startNode): + """ + Private method get a bookmark node for a given URL. + + @param url URL of the bookmark to search for (string) + @param startNode reference to the node to start searching + (BookmarkNode) + @return bookmark node for the given url (BookmarkNode) + """ + bm = None + for node in startNode.children(): + if node.type() == BookmarkNode.Folder: + bm = self.__searchBookmark(url, node) + elif node.type() == BookmarkNode.Bookmark: + if node.url == url: + bm = node + if bm is not None: + return bm + return None + + def bookmarksForUrl(self, url, start=StartRoot): + """ + Public method to get a list of bookmark nodes for a given URL. + + @param url URL of the bookmarks to search for (QUrl or string) + @keyparam start indicator for the start of the search + (StartRoot, StartMenu, StartToolBar) + @return list of bookmark nodes for the given url (list of BookmarkNode) + """ + if start == StartMenu: + startNode = self.__menu + elif start == StartToolBar: + startNode = self.__toolbar + else: + startNode = self.__bookmarkRootNode + if startNode is None: + return [] + + if isinstance(url, QUrl): + url = url.toString() + + return self.__searchBookmarks(url, startNode) + + def __searchBookmarks(self, url, startNode): + """ + Private method get a list of bookmark nodes for a given URL. + + @param url URL of the bookmarks to search for (string) + @param startNode reference to the node to start searching + (BookmarkNode) + @return list of bookmark nodes for the given url (list of BookmarkNode) + """ + bm = [] + for node in startNode.children(): + if node.type() == BookmarkNode.Folder: + bm.extend(self.__searchBookmarks(url, node)) + elif node.type() == BookmarkNode.Bookmark: + if node.url == url: + bm.append(node) + return bm + + +class RemoveBookmarksCommand(QUndoCommand): + """ + Class implementing the Remove undo command. + """ + def __init__(self, bookmarksManager, parent, row): + """ + Constructor + + @param bookmarksManager reference to the bookmarks manager + (BookmarksManager) + @param parent reference to the parent node (BookmarkNode) + @param row row number of bookmark (integer) + """ + super(RemoveBookmarksCommand, self).__init__( + QCoreApplication.translate("BookmarksManager", "Remove Bookmark")) + + self._row = row + self._bookmarksManager = bookmarksManager + try: + self._node = parent.children()[row] + except IndexError: + self._node = BookmarkNode() + self._parent = parent + + def undo(self): + """ + Public slot to perform the undo action. + """ + self._parent.add(self._node, self._row) + self._bookmarksManager.entryAdded.emit(self._node) + + def redo(self): + """ + Public slot to perform the redo action. + """ + self._parent.remove(self._node) + self._bookmarksManager.entryRemoved.emit( + self._parent, self._row, self._node) + + +class InsertBookmarksCommand(RemoveBookmarksCommand): + """ + Class implementing the Insert undo command. + """ + def __init__(self, bookmarksManager, parent, node, row): + """ + Constructor + + @param bookmarksManager reference to the bookmarks manager + (BookmarksManager) + @param parent reference to the parent node (BookmarkNode) + @param node reference to the node to be inserted (BookmarkNode) + @param row row number of bookmark (integer) + """ + RemoveBookmarksCommand.__init__(self, bookmarksManager, parent, row) + self.setText(QCoreApplication.translate( + "BookmarksManager", "Insert Bookmark")) + self._node = node + + def undo(self): + """ + Public slot to perform the undo action. + """ + RemoveBookmarksCommand.redo(self) + + def redo(self): + """ + Public slot to perform the redo action. + """ + RemoveBookmarksCommand.undo(self) + + +class ChangeBookmarkCommand(QUndoCommand): + """ + Class implementing the Insert undo command. + """ + def __init__(self, bookmarksManager, node, newValue, title): + """ + Constructor + + @param bookmarksManager reference to the bookmarks manager + (BookmarksManager) + @param node reference to the node to be changed (BookmarkNode) + @param newValue new value to be set (string) + @param title flag indicating a change of the title (True) or + the URL (False) (boolean) + """ + super(ChangeBookmarkCommand, self).__init__() + + self._bookmarksManager = bookmarksManager + self._title = title + self._newValue = newValue + self._node = node + + if self._title: + self._oldValue = self._node.title + self.setText(QCoreApplication.translate( + "BookmarksManager", "Name Change")) + else: + self._oldValue = self._node.url + self.setText(QCoreApplication.translate( + "BookmarksManager", "Address Change")) + + def undo(self): + """ + Public slot to perform the undo action. + """ + if self._title: + self._node.title = self._oldValue + else: + self._node.url = self._oldValue + self._bookmarksManager.entryChanged.emit(self._node) + + def redo(self): + """ + Public slot to perform the redo action. + """ + if self._title: + self._node.title = self._newValue + else: + self._node.url = self._newValue + self._bookmarksManager.entryChanged.emit(self._node)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksMenu.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,333 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the bookmarks menu. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, Qt, QUrl +from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QMenu + +from E5Gui.E5ModelMenu import E5ModelMenu + +from .BookmarksModel import BookmarksModel +from .BookmarkNode import BookmarkNode + + +class BookmarksMenu(E5ModelMenu): + """ + Class implementing the bookmarks menu base class. + + @signal openUrl(QUrl, str) emitted to open a URL with the given title in + the current tab + @signal newUrl(QUrl, str) emitted to open a URL with the given title in a + new tab + """ + openUrl = pyqtSignal(QUrl, str) + newUrl = pyqtSignal(QUrl, str) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + E5ModelMenu.__init__(self, parent) + + self.activated.connect(self.__activated) + self.setStatusBarTextRole(BookmarksModel.UrlStringRole) + self.setSeparatorRole(BookmarksModel.SeparatorRole) + + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.__contextMenuRequested) + + def createBaseMenu(self): + """ + Public method to get the menu that is used to populate sub menu's. + + @return reference to the menu (BookmarksMenu) + """ + menu = BookmarksMenu(self) + menu.openUrl.connect(self.openUrl) + menu.newUrl.connect(self.newUrl) + return menu + + def __activated(self, idx): + """ + Private slot handling the activated signal. + + @param idx index of the activated item (QModelIndex) + """ + if self._keyboardModifiers & Qt.ControlModifier: + self.newUrl.emit( + idx.data(BookmarksModel.UrlRole), + idx.data(Qt.DisplayRole)) + else: + self.openUrl.emit( + idx.data(BookmarksModel.UrlRole), + idx.data(Qt.DisplayRole)) + self.resetFlags() + + def postPopulated(self): + """ + Public method to add any actions after the tree. + """ + if self.isEmpty(): + return + + parent = self.rootIndex() + + hasBookmarks = False + + for i in range(parent.model().rowCount(parent)): + child = parent.model().index(i, 0, parent) + + if child.data(BookmarksModel.TypeRole) == BookmarkNode.Bookmark: + hasBookmarks = True + break + + if not hasBookmarks: + return + + self.addSeparator() + act = self.addAction(self.tr("Open all in Tabs")) + act.triggered.connect(self.openAll) + + def openAll(self): + """ + Public slot to open all the menu's items. + """ + menu = self.sender().parent() + if menu is None: + return + + parent = menu.rootIndex() + if not parent.isValid(): + return + + for i in range(parent.model().rowCount(parent)): + child = parent.model().index(i, 0, parent) + + if child.data(BookmarksModel.TypeRole) != BookmarkNode.Bookmark: + continue + + if i == 0: + self.openUrl.emit( + child.data(BookmarksModel.UrlRole), + child.data(Qt.DisplayRole)) + else: + self.newUrl.emit( + child.data(BookmarksModel.UrlRole), + child.data(Qt.DisplayRole)) + + def __contextMenuRequested(self, pos): + """ + Private slot to handle the context menu request. + + @param pos position the context menu shall be shown (QPoint) + """ + act = self.actionAt(pos) + + if act is not None and \ + act.menu() is None and \ + self.index(act).isValid(): + menu = QMenu() + v = act.data() + + menu.addAction( + self.tr("Open"), + self.__openBookmark).setData(v) + menu.addAction( + self.tr("Open in New Tab\tCtrl+LMB"), + self.__openBookmarkInNewTab).setData(v) + menu.addAction( + self.tr("Open in New Window"), + self.__openBookmarkInNewWindow).setData(v) + menu.addAction( + self.tr("Open in New Private Window"), + self.__openBookmarkInPrivateWindow).setData(v) + menu.addSeparator() + + menu.addAction( + self.tr("Remove"), + self.__removeBookmark).setData(v) + menu.addSeparator() + + menu.addAction( + self.tr("Properties..."), + self.__edit).setData(v) + + execAct = menu.exec_(QCursor.pos()) + if execAct is not None: + self.close() + parent = self.parent() + while parent is not None and isinstance(parent, QMenu): + parent.close() + parent = parent.parent() + + def __openBookmark(self): + """ + Private slot to open a bookmark in the current browser tab. + """ + idx = self.index(self.sender()) + + self.openUrl.emit( + idx.data(BookmarksModel.UrlRole), + idx.data(Qt.DisplayRole)) + + def __openBookmarkInNewTab(self): + """ + Private slot to open a bookmark in a new browser tab. + """ + idx = self.index(self.sender()) + + self.newUrl.emit( + idx.data(BookmarksModel.UrlRole), + idx.data(Qt.DisplayRole)) + + def __openBookmarkInNewWindow(self): + """ + Private slot to open a bookmark in a new window. + """ + idx = self.index(self.sender()) + url = idx.data(BookmarksModel.UrlRole) + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + WebBrowserWindow.mainWindow().newWindow(url) + + def __openBookmarkInPrivateWindow(self): + """ + Private slot to open a bookmark in a new private window. + """ + idx = self.index(self.sender()) + url = idx.data(BookmarksModel.UrlRole) + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + WebBrowserWindow.mainWindow().newPrivateWindow(url) + + def __removeBookmark(self): + """ + Private slot to remove a bookmark. + """ + idx = self.index(self.sender()) + self.removeEntry(idx) + + def __edit(self): + """ + Private slot to edit a bookmarks properties. + """ + from .BookmarkPropertiesDialog import BookmarkPropertiesDialog + + idx = self.index(self.sender()) + node = self.model().node(idx) + dlg = BookmarkPropertiesDialog(node) + dlg.exec_() + +############################################################################## + + +class BookmarksMenuBarMenu(BookmarksMenu): + """ + Class implementing a dynamically populated menu for bookmarks. + + @signal openUrl(QUrl, str) emitted to open a URL with the given title in + the current tab + """ + openUrl = pyqtSignal(QUrl, str) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + BookmarksMenu.__init__(self, parent) + + self.__bookmarksManager = None + self.__initialActions = [] + + def prePopulated(self): + """ + Public method to add any actions before the tree. + + @return flag indicating if any actions were added (boolean) + """ + import WebBrowser.WebBrowserWindow + + self.__bookmarksManager = WebBrowser.WebBrowserWindow.WebBrowserWindow\ + .bookmarksManager() + self.setModel(self.__bookmarksManager.bookmarksModel()) + self.setRootIndex(self.__bookmarksManager.bookmarksModel() + .nodeIndex(self.__bookmarksManager.menu())) + + # initial actions + for act in self.__initialActions: + if act == "--SEPARATOR--": + self.addSeparator() + else: + self.addAction(act) + if len(self.__initialActions) != 0: + self.addSeparator() + + self.createMenu( + self.__bookmarksManager.bookmarksModel() + .nodeIndex(self.__bookmarksManager.toolbar()), + 1, self) + return True + + def postPopulated(self): + """ + Public method to add any actions after the tree. + """ + if self.isEmpty(): + return + + parent = self.rootIndex() + + hasBookmarks = False + + for i in range(parent.model().rowCount(parent)): + child = parent.model().index(i, 0, parent) + + if child.data(BookmarksModel.TypeRole) == BookmarkNode.Bookmark: + hasBookmarks = True + break + + if not hasBookmarks: + return + + self.addSeparator() + act = self.addAction(self.tr("Default Home Page")) + act.setData("eric:home") + act.triggered.connect(self.__defaultBookmarkTriggered) + act = self.addAction(self.tr("Speed Dial")) + act.setData("eric:speeddial") + act.triggered.connect(self.__defaultBookmarkTriggered) + self.addSeparator() + act = self.addAction(self.tr("Open all in Tabs")) + act.triggered.connect(self.openAll) + + def setInitialActions(self, actions): + """ + Public method to set the list of actions that should appear first in + the menu. + + @param actions list of initial actions (list of QAction) + """ + self.__initialActions = actions[:] + for act in self.__initialActions: + self.addAction(act) + + def __defaultBookmarkTriggered(self): + """ + Private slot handling the default bookmark menu entries. + """ + act = self.sender() + urlStr = act.data() + if urlStr.startswith("eric:"): + self.openUrl.emit(QUrl(urlStr), "")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksModel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,466 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the bookmark model class. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QAbstractItemModel, QModelIndex, QUrl, \ + QByteArray, QDataStream, QIODevice, QBuffer, QMimeData + +import UI.PixmapCache + + +class BookmarksModel(QAbstractItemModel): + """ + Class implementing the bookmark model. + """ + TypeRole = Qt.UserRole + 1 + UrlRole = Qt.UserRole + 2 + UrlStringRole = Qt.UserRole + 3 + SeparatorRole = Qt.UserRole + 4 + + MIMETYPE = "application/bookmarks.xbel" + + def __init__(self, manager, parent=None): + """ + Constructor + + @param manager reference to the bookmark manager object + (BookmarksManager) + @param parent reference to the parent object (QObject) + """ + super(BookmarksModel, self).__init__(parent) + + self.__endMacro = False + self.__bookmarksManager = manager + + manager.entryAdded.connect(self.entryAdded) + manager.entryRemoved.connect(self.entryRemoved) + manager.entryChanged.connect(self.entryChanged) + + self.__headers = [ + self.tr("Title"), + self.tr("Address"), + ] + + def bookmarksManager(self): + """ + Public method to get a reference to the bookmarks manager. + + @return reference to the bookmarks manager object (BookmarksManager) + """ + return self.__bookmarksManager + + def nodeIndex(self, node): + """ + Public method to get a model index. + + @param node reference to the node to get the index for (BookmarkNode) + @return model index (QModelIndex) + """ + parent = node.parent() + if parent is None: + return QModelIndex() + return self.createIndex(parent.children().index(node), 0, node) + + def entryAdded(self, node): + """ + Public slot to add a bookmark node. + + @param node reference to the bookmark node to add (BookmarkNode) + """ + if node is None or node.parent() is None: + return + + parent = node.parent() + row = parent.children().index(node) + # node was already added so remove before beginInsertRows is called + parent.remove(node) + self.beginInsertRows(self.nodeIndex(parent), row, row) + parent.add(node, row) + self.endInsertRows() + + def entryRemoved(self, parent, row, node): + """ + Public slot to remove a bookmark node. + + @param parent reference to the parent bookmark node (BookmarkNode) + @param row row number of the node (integer) + @param node reference to the bookmark node to remove (BookmarkNode) + """ + # node was already removed, re-add so beginRemoveRows works + parent.add(node, row) + self.beginRemoveRows(self.nodeIndex(parent), row, row) + parent.remove(node) + self.endRemoveRows() + + def entryChanged(self, node): + """ + Public method to change a node. + + @param node reference to the bookmark node to change (BookmarkNode) + """ + idx = self.nodeIndex(node) + self.dataChanged.emit(idx, idx) + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove bookmarks from the model. + + @param row row of the first bookmark to remove (integer) + @param count number of bookmarks to remove (integer) + @param parent index of the parent bookmark node (QModelIndex) + @return flag indicating successful removal (boolean) + """ + if row < 0 or count <= 0 or row + count > self.rowCount(parent): + return False + + bookmarkNode = self.node(parent) + children = bookmarkNode.children()[row:(row + count)] + for node in children: + if node == self.__bookmarksManager.menu() or \ + node == self.__bookmarksManager.toolbar(): + continue + self.__bookmarksManager.removeBookmark(node) + + if self.__endMacro: + self.__bookmarksManager.undoRedoStack().endMacro() + self.__endMacro = False + + return True + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Public method to get the header data. + + @param section section number (integer) + @param orientation header orientation (Qt.Orientation) + @param role data role (integer) + @return header data + """ + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + try: + return self.__headers[section] + except IndexError: + pass + return QAbstractItemModel.headerData(self, section, orientation, role) + + def data(self, index, role=Qt.DisplayRole): + """ + Public method to get data from the model. + + @param index index of bookmark to get data for (QModelIndex) + @param role data role (integer) + @return bookmark data + """ + if not index.isValid() or index.model() != self: + return None + + from .BookmarkNode import BookmarkNode + + bookmarkNode = self.node(index) + if role in [Qt.EditRole, Qt.DisplayRole]: + if bookmarkNode.type() == BookmarkNode.Separator: + if index.column() == 0: + return 50 * '\xB7' + elif index.column() == 1: + return "" + + if index.column() == 0: + return bookmarkNode.title + elif index.column() == 1: + return bookmarkNode.url + + elif role == self.UrlRole: + return QUrl(bookmarkNode.url) + + elif role == self.UrlStringRole: + return bookmarkNode.url + + elif role == self.TypeRole: + return bookmarkNode.type() + + elif role == self.SeparatorRole: + return bookmarkNode.type() == BookmarkNode.Separator + + elif role == Qt.DecorationRole: + if index.column() == 0: + if bookmarkNode.type() == BookmarkNode.Folder: + return UI.PixmapCache.getIcon("dirOpen.png") + import WebBrowser.WebBrowserWindow + return WebBrowser.WebBrowserWindow.WebBrowserWindow.icon( + QUrl(bookmarkNode.url)) + + return None + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns. + + @param parent index of parent (QModelIndex) + @return number of columns (integer) + """ + if parent.column() > 0: + return 0 + else: + return len(self.__headers) + + def rowCount(self, parent=QModelIndex()): + """ + Public method to determine the number of rows. + + @param parent index of parent (QModelIndex) + @return number of rows (integer) + """ + if parent.column() > 0: + return 0 + + if not parent.isValid(): + return len(self.__bookmarksManager.bookmarks().children()) + + itm = parent.internalPointer() + return len(itm.children()) + + def index(self, row, column, parent=QModelIndex()): + """ + Public method to get a model index for a node cell. + + @param row row number (integer) + @param column column number (integer) + @param parent index of the parent (QModelIndex) + @return index (QModelIndex) + """ + if row < 0 or column < 0 or \ + row >= self.rowCount(parent) or column >= self.columnCount(parent): + return QModelIndex() + + parentNode = self.node(parent) + return self.createIndex(row, column, parentNode.children()[row]) + + def parent(self, index=QModelIndex()): + """ + Public method to get the index of the parent node. + + @param index index of the child node (QModelIndex) + @return index of the parent node (QModelIndex) + """ + if not index.isValid(): + return QModelIndex() + + itemNode = self.node(index) + if itemNode is None: + parentNode = None + else: + parentNode = itemNode.parent() + + if parentNode is None or \ + parentNode == self.__bookmarksManager.bookmarks(): + return QModelIndex() + + # get the parent's row + grandParentNode = parentNode.parent() + parentRow = grandParentNode.children().index(parentNode) + return self.createIndex(parentRow, 0, parentNode) + + def hasChildren(self, parent=QModelIndex()): + """ + Public method to check, if a parent node has some children. + + @param parent index of the parent node (QModelIndex) + @return flag indicating the presence of children (boolean) + """ + if not parent.isValid(): + return True + + from .BookmarkNode import BookmarkNode + parentNode = self.node(parent) + return parentNode.type() == BookmarkNode.Folder + + def flags(self, index): + """ + Public method to get flags for a node cell. + + @param index index of the node cell (QModelIndex) + @return flags (Qt.ItemFlags) + """ + if not index.isValid(): + return Qt.NoItemFlags + + node = self.node(index) + type_ = node.type() + flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled + + if self.hasChildren(index): + flags |= Qt.ItemIsDropEnabled + + if node == self.__bookmarksManager.menu() or \ + node == self.__bookmarksManager.toolbar(): + return flags + + flags |= Qt.ItemIsDragEnabled + + from .BookmarkNode import BookmarkNode + if (index.column() == 0 and type_ != BookmarkNode.Separator) or \ + (index.column() == 1 and type_ == BookmarkNode.Bookmark): + flags |= Qt.ItemIsEditable + + return flags + + def supportedDropActions(self): + """ + Public method to report the supported drop actions. + + @return supported drop actions (Qt.DropAction) + """ + return Qt.CopyAction | Qt.MoveAction + + def mimeTypes(self): + """ + Public method to report the supported mime types. + + @return supported mime types (list of strings) + """ + return [self.MIMETYPE, "text/uri-list"] + + def mimeData(self, indexes): + """ + Public method to return the mime data. + + @param indexes list of indexes (QModelIndexList) + @return mime data (QMimeData) + """ + from .XbelWriter import XbelWriter + + data = QByteArray() + stream = QDataStream(data, QIODevice.WriteOnly) + urls = [] + + for index in indexes: + if index.column() != 0 or not index.isValid(): + continue + + encodedData = QByteArray() + buffer = QBuffer(encodedData) + buffer.open(QIODevice.ReadWrite) + writer = XbelWriter() + parentNode = self.node(index) + writer.write(buffer, parentNode) + stream << encodedData + urls.append(index.data(self.UrlRole)) + + mdata = QMimeData() + mdata.setData(self.MIMETYPE, data) + mdata.setUrls(urls) + return mdata + + def dropMimeData(self, data, action, row, column, parent): + """ + Public method to accept the mime data of a drop action. + + @param data reference to the mime data (QMimeData) + @param action drop action requested (Qt.DropAction) + @param row row number (integer) + @param column column number (integer) + @param parent index of the parent node (QModelIndex) + @return flag indicating successful acceptance of the data (boolean) + """ + if action == Qt.IgnoreAction: + return True + + if column > 0: + return False + + parentNode = self.node(parent) + + if not data.hasFormat(self.MIMETYPE): + if not data.hasUrls(): + return False + + from .BookmarkNode import BookmarkNode + node = BookmarkNode(BookmarkNode.Bookmark, parentNode) + node.url = bytes(data.urls()[0].toEncoded()).decode() + + if data.hasText(): + node.title = data.text() + else: + node.title = node.url + + self.__bookmarksManager.addBookmark(parentNode, node, row) + return True + + ba = data.data(self.MIMETYPE) + stream = QDataStream(ba, QIODevice.ReadOnly) + if stream.atEnd(): + return False + + undoStack = self.__bookmarksManager.undoRedoStack() + undoStack.beginMacro("Move Bookmarks") + + from .XbelReader import XbelReader + while not stream.atEnd(): + encodedData = QByteArray() + stream >> encodedData + buffer = QBuffer(encodedData) + buffer.open(QIODevice.ReadOnly) + + reader = XbelReader() + rootNode = reader.read(buffer) + for bookmarkNode in rootNode.children(): + rootNode.remove(bookmarkNode) + row = max(0, row) + self.__bookmarksManager.addBookmark( + parentNode, bookmarkNode, row) + self.__endMacro = True + + return True + + def setData(self, index, value, role=Qt.EditRole): + """ + Public method to set the data of a node cell. + + @param index index of the node cell (QModelIndex) + @param value value to be set + @param role role of the data (integer) + @return flag indicating success (boolean) + """ + if not index.isValid() or (self.flags(index) & Qt.ItemIsEditable) == 0: + return False + + item = self.node(index) + + if role in (Qt.EditRole, Qt.DisplayRole): + if index.column() == 0: + self.__bookmarksManager.setTitle(item, value) + elif index.column() == 1: + self.__bookmarksManager.setUrl(item, value) + else: + return False + + elif role == BookmarksModel.UrlRole: + self.__bookmarksManager.setUrl(item, value.toString()) + + elif role == BookmarksModel.UrlStringRole: + self.__bookmarksManager.setUrl(item, value) + + else: + return False + + return True + + def node(self, index): + """ + Public method to get a bookmark node given its index. + + @param index index of the node (QModelIndex) + @return bookmark node (BookmarkNode) + """ + itemNode = index.internalPointer() + if itemNode is None: + return self.__bookmarksManager.bookmarks() + else: + return itemNode
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/BookmarksToolBar.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a tool bar showing bookmarks. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, Qt, QUrl, QCoreApplication +from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QMenu +from PyQt5.QtWebEngineWidgets import QWebEnginePage + +from E5Gui.E5ModelToolBar import E5ModelToolBar + +from .BookmarksModel import BookmarksModel + + +class BookmarksToolBar(E5ModelToolBar): + """ + Class implementing a tool bar showing bookmarks. + + @signal openUrl(QUrl, str) emitted to open a URL in the current tab + @signal newUrl(QUrl, str) emitted to open a URL in a new tab + """ + openUrl = pyqtSignal(QUrl, str) + newUrl = pyqtSignal(QUrl, str) + + def __init__(self, mainWindow, model, parent=None): + """ + Constructor + + @param mainWindow reference to the main window (HelpWindow) + @param model reference to the bookmarks model (BookmarksModel) + @param parent reference to the parent widget (QWidget) + """ + E5ModelToolBar.__init__( + self, QCoreApplication.translate("BookmarksToolBar", "Bookmarks"), + parent) + + self.__mw = mainWindow + self.__bookmarksModel = model + + self.__mw.bookmarksManager().bookmarksReloaded.connect(self.__rebuild) + + self.setModel(model) + self.setRootIndex(model.nodeIndex( + self.__mw.bookmarksManager().toolbar())) + + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.__contextMenuRequested) + self.activated.connect(self.__bookmarkActivated) + + self.setHidden(True) + self.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + + self._build() + + def __rebuild(self): + """ + Private slot to rebuild the toolbar. + """ + self.__bookmarksModel = \ + self.__mw.bookmarksManager().bookmarksModel() + self.setModel(self.__bookmarksModel) + self.setRootIndex(self.__bookmarksModel.nodeIndex( + self.__mw.bookmarksManager().toolbar())) + self._build() + + def __contextMenuRequested(self, pos): + """ + Private slot to handle the context menu request. + + @param pos position the context menu shall be shown (QPoint) + """ + act = self.actionAt(pos) + menu = QMenu() + + if act is not None: + v = act.data() + + if act.menu() is None: + menu.addAction( + self.tr("Open"), + self.__openBookmark).setData(v) + menu.addAction( + self.tr("Open in New Tab\tCtrl+LMB"), + self.__openBookmarkInNewTab).setData(v) + menu.addAction( + self.tr("Open in New Window"), + self.__openBookmarkInNewWindow).setData(v) + menu.addAction( + self.tr("Open in New Private Window"), + self.__openBookmarkInPrivateWindow).setData(v) + menu.addSeparator() + + menu.addAction( + self.tr("Remove"), + self.__removeBookmark).setData(v) + menu.addSeparator() + + menu.addAction( + self.tr("Properties..."), + self.__edit).setData(v) + menu.addSeparator() + + menu.addAction(self.tr("Add Bookmark..."), self.__newBookmark) + menu.addAction(self.tr("Add Folder..."), self.__newFolder) + + menu.exec_(QCursor.pos()) + + def __bookmarkActivated(self, idx): + """ + Private slot handling the activation of a bookmark. + + @param idx index of the activated bookmark (QModelIndex) + """ + assert idx.isValid() + + if self._mouseButton == Qt.XButton1: + self.__mw.currentBrowser().triggerPageAction(QWebEnginePage.Back) + elif self._mouseButton == Qt.XButton2: + self.__mw.currentBrowser().triggerPageAction( + QWebEnginePage.Forward) + elif self._mouseButton == Qt.LeftButton: + if self._keyboardModifiers & Qt.ControlModifier: + self.newUrl.emit( + idx.data(BookmarksModel.UrlRole), + idx.data(Qt.DisplayRole)) + else: + self.openUrl.emit( + idx.data(BookmarksModel.UrlRole), + idx.data(Qt.DisplayRole)) + + def __openToolBarBookmark(self): + """ + Private slot to open a bookmark in the current browser tab. + """ + idx = self.index(self.sender()) + + if self._keyboardModifiers & Qt.ControlModifier: + self.newUrl.emit( + idx.data(BookmarksModel.UrlRole), + idx.data(Qt.DisplayRole)) + else: + self.openUrl.emit( + idx.data(BookmarksModel.UrlRole), + idx.data(Qt.DisplayRole)) + self.resetFlags() + + def __openBookmark(self): + """ + Private slot to open a bookmark in the current browser tab. + """ + idx = self.index(self.sender()) + + self.openUrl.emit( + idx.data(BookmarksModel.UrlRole), + idx.data(Qt.DisplayRole)) + + def __openBookmarkInNewTab(self): + """ + Private slot to open a bookmark in a new browser tab. + """ + idx = self.index(self.sender()) + + self.newUrl.emit( + idx.data(BookmarksModel.UrlRole), + idx.data(Qt.DisplayRole)) + + def __openBookmarkInNewWindow(self): + """ + Private slot to open a bookmark in a new window. + """ + idx = self.index(self.sender()) + url = idx.data(BookmarksModel.UrlRole) + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + WebBrowserWindow.mainWindow().newWindow(url) + + def __openBookmarkInPrivateWindow(self): + """ + Private slot to open a bookmark in a new private window. + """ + idx = self.index(self.sender()) + url = idx.data(BookmarksModel.UrlRole) + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + WebBrowserWindow.mainWindow().newPrivateWindow(url) + + def __removeBookmark(self): + """ + Private slot to remove a bookmark. + """ + idx = self.index(self.sender()) + + self.__bookmarksModel.removeRow(idx.row(), self.rootIndex()) + + def __newBookmark(self): + """ + Private slot to add a new bookmark. + """ + from .AddBookmarkDialog import AddBookmarkDialog + dlg = AddBookmarkDialog() + dlg.setCurrentIndex(self.rootIndex()) + dlg.exec_() + + def __newFolder(self): + """ + Private slot to add a new bookmarks folder. + """ + from .AddBookmarkDialog import AddBookmarkDialog + dlg = AddBookmarkDialog() + dlg.setCurrentIndex(self.rootIndex()) + dlg.setFolder(True) + dlg.exec_() + + def _createMenu(self): + """ + Protected method to create the menu for a tool bar action. + + @return menu for a tool bar action (E5ModelMenu) + """ + from .BookmarksMenu import BookmarksMenu + menu = BookmarksMenu(self) + menu.openUrl.connect(self.openUrl) + menu.newUrl.connect(self.newUrl) + return menu + + def __edit(self): + """ + Private slot to edit a bookmarks properties. + """ + from .BookmarkPropertiesDialog import BookmarkPropertiesDialog + idx = self.index(self.sender()) + node = self.__bookmarksModel.node(idx) + dlg = BookmarkPropertiesDialog(node) + dlg.exec_()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/DefaultBookmarks.qrc Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,6 @@ +<!DOCTYPE RCC> +<RCC version="1.0"> +<qresource> + <file>DefaultBookmarks.xbel</file> +</qresource> +</RCC>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/DefaultBookmarks.xbel Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE xbel> +<xbel version="1.0"> + <folder folded="no"> + <title>Bookmarks Bar</title> + <bookmark href="http://eric-ide.python-projects.org/"> + <title>Eric Web Site</title> + </bookmark> + <bookmark href="http://www.riverbankcomputing.com/"> + <title>PyQt Web Site</title> + </bookmark> + <folder folded="no"> + <title>Qt Web Sites</title> + <bookmark href="http://www.qt.io//"> + <title>Qt Web Site</title> + </bookmark> + <bookmark href="http://www.qt.io/developers/"> + <title>Qt Developers</title> + </bookmark> + <bookmark href="http://doc.qt.io/"> + <title>Qt Documentation</title> + </bookmark> + <bookmark href="http://blog.qt.io/"> + <title>Qt Blog</title> + </bookmark> + <bookmark href="http://forum.qt.io/"> + <title>Qt Forum</title> + </bookmark> + <bookmark href="http://planet.qt.io/"> + <title>Planet Qt</title> + </bookmark> + <bookmark href="http://qtcentre.org/"> + <title>Qt Centre</title> + </bookmark> + <bookmark href="http://qt-apps.org/"> + <title>Qt-Apps.org</title> + </bookmark> + </folder> + <folder folded="no"> + <title>Python Web Sites</title> + <bookmark href="http://www.python.org/"> + <title>Python Language Website</title> + </bookmark> + <bookmark href="http://pypi.python.org/pypi"> + <title>Python Package Index: PyPI</title> + </bookmark> + </folder> + </folder> + <folder folded="yes"> + <title>Bookmarks Menu</title> + <bookmark href="http://eric-ide.python-projects.org/"> + <title>Eric Web Site</title> + </bookmark> + <bookmark href="http://www.riverbankcomputing.com/"> + <title>PyQt4 Web Site</title> + </bookmark> + <bookmark href="javascript:location.href='mailto:?SUBJECT=' + document.title + '&BODY=' + escape(location.href);"> + <title>Send Link</title> + </bookmark> + </folder> +</xbel>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/DefaultBookmarks_rc.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.4.1) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x01\xf1\ +\x00\ +\x00\x09\x00\x78\x9c\xdd\x96\x51\x6f\x9b\x30\x10\xc7\xdf\xfb\x29\ +\x3c\x1e\x9a\x4d\x15\xd0\x49\x7b\x98\x52\x48\x34\x92\x4c\xea\xd4\ +\xaa\x54\x69\x55\xf5\xd1\x98\x0b\x71\x01\xdb\x35\x26\x09\xdf\x7e\ +\x86\xb0\x96\xa4\x2c\xa4\x1d\x4f\xe3\xc5\xd8\x77\xbe\xdf\x9d\x8d\ +\xff\xc6\x19\x6f\xd2\x04\xad\x40\x66\x94\x33\xd7\xf8\x6a\x9d\x1b\ +\x08\x18\xe1\x21\x65\x91\x6b\xe4\x6a\x61\x7e\x37\xc6\xa3\x13\xe7\ +\xd3\xf4\x66\x72\xf7\xe8\xcf\xd0\x26\x80\x44\xf7\xcb\x66\x77\xda\ +\xe8\x04\xe9\xc7\x59\xf0\x24\x04\x89\xaa\x26\x74\x0d\xc6\x6b\x43\ +\x65\x54\x54\x25\x30\xf2\x38\x8f\x53\x2c\xe3\x0c\x79\x58\x3a\xf6\ +\x76\xf0\xd5\x29\xa8\xcd\x68\x29\x61\xe1\x1a\x4b\xa5\xc4\xd0\xb6\ +\x41\x52\x62\xd2\x10\x2c\x51\xa8\x25\x67\xa6\x90\xfc\x09\x88\xca\ +\x2c\x2e\x23\xbb\xc1\x68\x70\x66\x7a\x0a\x7a\x80\x00\xcd\xa9\x82\ +\xb7\x1c\xfb\x0f\xa8\x93\xbd\x5e\xaf\x2d\x49\x75\xb5\x01\x66\x31\ +\xe1\xa9\xc8\x95\x5e\x1e\x4b\xbf\xfd\x85\xec\x17\xb7\xea\x9d\xe4\ +\x43\xeb\xd6\x88\xdc\x88\x9b\xbd\x09\xdc\x51\xc2\xb3\xb2\x28\xb7\ +\xf7\x53\x6e\x0f\xde\x1e\xbb\x25\xf1\xa3\x98\x21\xac\x20\xe1\x42\ +\x7f\x2e\x87\xe9\xd3\x17\xbf\x3e\xf8\x21\x27\x35\xff\x30\x94\x93\ +\x3c\x05\xa6\xb0\xd2\xdf\x72\x1f\xdc\x20\xe1\xd1\x31\x60\x4f\xfb\ +\xf5\xc1\x5b\x70\x99\xa7\xc7\x00\x7f\x96\x8e\x7d\x10\x45\x82\x19\ +\xa8\x4e\xa4\x5f\xb9\xa1\x5b\xd5\x07\xf3\x59\x11\xbd\x49\x12\xda\ +\x0e\xfc\x6e\x99\x93\xca\xaf\x1f\xa6\x89\x85\x68\xd5\x98\x1d\xa4\ +\xf9\xa3\xf6\x3a\x1a\xea\xd8\xdb\x03\xff\x7e\x05\xf0\x2b\xfd\xfb\ +\xb8\x0a\x6c\xf5\xb3\xa3\xa4\x1a\x72\x85\x59\x94\xe3\x08\x4a\x5a\ +\xd6\x93\x2a\x88\x42\xd0\x66\x12\x65\xbf\x33\x11\x1f\x93\xb8\xcc\ +\xe3\x92\x85\xb0\x19\x22\xbf\xf0\x2f\x3f\xb8\xd4\x7b\xbd\xbd\x45\ +\x2f\x20\x3b\x74\x5f\x5d\x03\xcb\xff\xdb\x0b\xeb\xdb\xbf\xa1\x9f\ +\xf0\x0a\x67\x44\x52\xa1\x86\x09\x27\x95\x98\x5a\x95\x65\x90\x62\ +\x9a\x28\x3e\x1c\xcf\xef\xbd\x5f\xb3\xc9\x9d\x3b\x40\x67\x28\xac\ +\x45\xd7\xaa\x48\x7a\x60\x70\x8a\x53\x71\xe1\xdd\x4c\x1f\x2b\x3b\ +\x64\x04\x0b\xf8\xbc\x13\xe9\xcb\x45\x7b\xf2\x73\x60\x21\xba\xa2\ +\x2c\xee\xcc\xfb\x75\xf3\x1d\x7b\xfb\x23\xf3\x1b\xc5\xa5\x8d\x58\ +\ +" + +qt_resource_name = b"\ +\x00\x15\ +\x0c\xd3\x2e\x3c\ +\x00\x44\ +\x00\x65\x00\x66\x00\x61\x00\x75\x00\x6c\x00\x74\x00\x42\x00\x6f\x00\x6f\x00\x6b\x00\x6d\x00\x61\x00\x72\x00\x6b\x00\x73\x00\x2e\ +\x00\x78\x00\x62\x00\x65\x00\x6c\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/NsHtmlReader.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to read Netscape HTML bookmark files. +""" + +from __future__ import unicode_literals +try: + str = unicode +except NameError: + pass + +from PyQt5.QtCore import QObject, QIODevice, QFile, QRegExp, Qt, QDateTime + +from .BookmarkNode import BookmarkNode + +import Utilities + + +class NsHtmlReader(QObject): + """ + Class implementing a reader object for Netscape HTML bookmark files. + """ + indentSize = 4 + + def __init__(self): + """ + Constructor + """ + super(NsHtmlReader, self).__init__() + + self.__folderRx = QRegExp("<DT><H3(.*)>(.*)</H3>", Qt.CaseInsensitive) + self.__folderRx.setMinimal(True) + + self.__endFolderRx = QRegExp("</DL>", Qt.CaseInsensitive) + + self.__bookmarkRx = QRegExp("<DT><A(.*)>(.*)</A>", Qt.CaseInsensitive) + self.__bookmarkRx.setMinimal(True) + + self.__descRx = QRegExp("<DD>(.*)", Qt.CaseInsensitive) + + self.__separatorRx = QRegExp("<HR>", Qt.CaseInsensitive) + + self.__urlRx = QRegExp('HREF="(.*)"', Qt.CaseInsensitive) + self.__urlRx.setMinimal(True) + + self.__addedRx = QRegExp('ADD_DATE="(\d*)"', Qt.CaseInsensitive) + self.__addedRx.setMinimal(True) + + self.__modifiedRx = QRegExp( + 'LAST_MODIFIED="(\d*)"', Qt.CaseInsensitive) + self.__modifiedRx.setMinimal(True) + + self.__visitedRx = QRegExp('LAST_VISIT="(\d*)"', Qt.CaseInsensitive) + self.__visitedRx.setMinimal(True) + + self.__foldedRx = QRegExp("FOLDED", Qt.CaseInsensitive) + + def read(self, fileNameOrDevice): + """ + Public method to read a Netscape HTML bookmark file. + + @param fileNameOrDevice name of the file to read (string) + or reference to the device to read (QIODevice) + @return reference to the root node (BookmarkNode) + """ + if isinstance(fileNameOrDevice, QIODevice): + dev = fileNameOrDevice + else: + f = QFile(fileNameOrDevice) + if not f.exists(): + return BookmarkNode(BookmarkNode.Root) + f.open(QFile.ReadOnly) + dev = f + + folders = [] + lastNode = None + + root = BookmarkNode(BookmarkNode.Root) + folders.append(root) + + while not dev.atEnd(): + line = str(dev.readLine(), encoding="utf-8").rstrip() + if self.__folderRx.indexIn(line) != -1: + # folder definition + arguments = self.__folderRx.cap(1) + name = self.__folderRx.cap(2) + node = BookmarkNode(BookmarkNode.Folder, folders[-1]) + node.title = Utilities.html_udecode(name) + node.expanded = self.__foldedRx.indexIn(arguments) == -1 + if self.__addedRx.indexIn(arguments) != -1: + node.added = QDateTime.fromTime_t( + int(self.__addedRx.cap(1))) + folders.append(node) + lastNode = node + + elif self.__endFolderRx.indexIn(line) != -1: + # end of folder definition + folders.pop() + + elif self.__bookmarkRx.indexIn(line) != -1: + # bookmark definition + arguments = self.__bookmarkRx.cap(1) + name = self.__bookmarkRx.cap(2) + node = BookmarkNode(BookmarkNode.Bookmark, folders[-1]) + node.title = Utilities.html_udecode(name) + if self.__urlRx.indexIn(arguments) != -1: + node.url = self.__urlRx.cap(1) + if self.__addedRx.indexIn(arguments) != -1: + node.added = QDateTime.fromTime_t( + int(self.__addedRx.cap(1))) + if self.__modifiedRx.indexIn(arguments) != -1: + node.modified = QDateTime.fromTime_t( + int(self.__modifiedRx.cap(1))) + if self.__visitedRx.indexIn(arguments) != -1: + node.visited = QDateTime.fromTime_t( + int(self.__visitedRx.cap(1))) + lastNode = node + + elif self.__descRx.indexIn(line) != -1: + # description + if lastNode: + lastNode.desc = Utilities.html_udecode( + self.__descRx.cap(1)) + + elif self.__separatorRx.indexIn(line) != -1: + # separator definition + BookmarkNode(BookmarkNode.Separator, folders[-1]) + + return root
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/NsHtmlWriter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to write Netscape HTML bookmark files. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QObject, QIODevice, QFile + +from .BookmarkNode import BookmarkNode + +import Utilities + + +class NsHtmlWriter(QObject): + """ + Class implementing a writer object to generate Netscape HTML bookmark + files. + """ + indentSize = 4 + + def __init__(self): + """ + Constructor + """ + super(NsHtmlWriter, self).__init__() + + def write(self, fileNameOrDevice, root): + """ + Public method to write an Netscape HTML bookmark file. + + @param fileNameOrDevice name of the file to write (string) + or device to write to (QIODevice) + @param root root node of the bookmark tree (BookmarkNode) + @return flag indicating success (boolean) + """ + if isinstance(fileNameOrDevice, QIODevice): + f = fileNameOrDevice + else: + f = QFile(fileNameOrDevice) + if root is None or not f.open(QFile.WriteOnly): + return False + + self.__dev = f + return self.__write(root) + + def __write(self, root): + """ + Private method to write an Netscape HTML bookmark file. + + @param root root node of the bookmark tree (BookmarkNode) + @return flag indicating success (boolean) + """ + self.__dev.write( + "<!DOCTYPE NETSCAPE-Bookmark-file-1>\n" + "<!-- This is an automatically generated file.\n" + " It will be read and overwritten.\n" + " DO NOT EDIT! -->\n" + "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html;" + " charset=UTF-8\">\n" + "<TITLE>Bookmarks</TITLE>\n" + "<H1>Bookmarks</H1>\n" + "\n" + "<DL><p>\n") + if root.type() == BookmarkNode.Root: + for child in root.children(): + self.__writeItem(child, self.indentSize) + else: + self.__writeItem(root, self.indentSize) + self.__dev.write("</DL><p>\n") + return True + + def __writeItem(self, node, indent): + """ + Private method to write an entry for a node. + + @param node reference to the node to be written (BookmarkNode) + @param indent size of the indentation (integer) + """ + if node.type() == BookmarkNode.Folder: + self.__writeFolder(node, indent) + elif node.type() == BookmarkNode.Bookmark: + self.__writeBookmark(node, indent) + elif node.type() == BookmarkNode.Separator: + self.__writeSeparator(indent) + + def __writeSeparator(self, indent): + """ + Private method to write a separator. + + @param indent size of the indentation (integer) + """ + self.__dev.write(" " * indent) + self.__dev.write("<HR>\n") + + def __writeBookmark(self, node, indent): + """ + Private method to write a bookmark node. + + @param node reference to the node to be written (BookmarkNode) + @param indent size of the indentation (integer) + """ + if node.added.isValid(): + added = " ADD_DATE=\"{0}\"".format(node.added.toTime_t()) + else: + added = "" + if node.modified.isValid(): + modified = " LAST_MODIFIED=\"{0}\"".format( + node.modified.toTime_t()) + else: + modified = "" + if node.visited.isValid(): + visited = " LAST_VISIT=\"{0}\"".format(node.visited.toTime_t()) + else: + visited = "" + + self.__dev.write(" " * indent) + self.__dev.write("<DT><A HREF=\"{0}\"{1}{2}{3}>{4}</A>\n".format( + node.url, added, modified, visited, + Utilities.html_uencode(node.title) + )) + + if node.desc: + self.__dev.write(" " * indent) + self.__dev.write("<DD>{0}\n".format( + Utilities.html_uencode("".join(node.desc.splitlines())))) + + def __writeFolder(self, node, indent): + """ + Private method to write a bookmark node. + + @param node reference to the node to be written (BookmarkNode) + @param indent size of the indentation (integer) + """ + if node.expanded: + folded = "" + else: + folded = " FOLDED" + + if node.added.isValid(): + added = " ADD_DATE=\"{0}\"".format(node.added.toTime_t()) + else: + added = "" + + self.__dev.write(" " * indent) + self.__dev.write("<DT><H3{0}{1}>{2}</H3>\n".format( + folded, added, Utilities.html_uencode(node.title) + )) + + if node.desc: + self.__dev.write(" " * indent) + self.__dev.write("<DD>{0}\n".format( + "".join(node.desc.splitlines()))) + + self.__dev.write(" " * indent) + self.__dev.write("<DL><p>\n") + + for child in node.children(): + self.__writeItem(child, indent + self.indentSize) + + self.__dev.write(" " * indent) + self.__dev.write("</DL><p>\n")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/XbelReader.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to read XBEL bookmark files. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamReader, QXmlStreamEntityResolver, \ + QIODevice, QFile, QCoreApplication, QXmlStreamNamespaceDeclaration, \ + QDateTime, Qt + +from .BookmarkNode import BookmarkNode + + +class XmlEntityResolver(QXmlStreamEntityResolver): + """ + Class implementing an XML entity resolver for bookmark files. + """ + def resolveUndeclaredEntity(self, entity): + """ + Public method to resolve undeclared entities. + + @param entity entity to be resolved (string) + @return resolved entity (string) + """ + if entity == "nbsp": + return " " + return "" + + +class XbelReader(QXmlStreamReader): + """ + Class implementing a reader object for XBEL bookmark files. + """ + def __init__(self): + """ + Constructor + """ + super(XbelReader, self).__init__() + + self.__resolver = XmlEntityResolver() + self.setEntityResolver(self.__resolver) + + def read(self, fileNameOrDevice): + """ + Public method to read an XBEL bookmark file. + + @param fileNameOrDevice name of the file to read (string) + or reference to the device to read (QIODevice) + @return reference to the root node (BookmarkNode) + """ + if isinstance(fileNameOrDevice, QIODevice): + self.setDevice(fileNameOrDevice) + else: + f = QFile(fileNameOrDevice) + if not f.exists(): + return BookmarkNode(BookmarkNode.Root) + f.open(QFile.ReadOnly) + self.setDevice(f) + + root = BookmarkNode(BookmarkNode.Root) + while not self.atEnd(): + self.readNext() + if self.isStartElement(): + version = self.attributes().value("version") + if self.name() == "xbel" and \ + (not version or version == "1.0"): + self.__readXBEL(root) + else: + self.raiseError(QCoreApplication.translate( + "XbelReader", + "The file is not an XBEL version 1.0 file.")) + + return root + + def __readXBEL(self, node): + """ + Private method to read and parse the XBEL file. + + @param node reference to the node to attach to (BookmarkNode) + """ + if not self.isStartElement() and self.name() != "xbel": + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + break + + if self.isStartElement(): + if self.name() == "folder": + self.__readFolder(node) + elif self.name() == "bookmark": + self.__readBookmarkNode(node) + elif self.name() == "separator": + self.__readSeparator(node) + else: + self.__skipUnknownElement() + + def __readFolder(self, node): + """ + Private method to read and parse a folder subtree. + + @param node reference to the node to attach to (BookmarkNode) + """ + if not self.isStartElement() and self.name() != "folder": + return + + folder = BookmarkNode(BookmarkNode.Folder, node) + folder.expanded = self.attributes().value("folded") == "no" + folder.added = QDateTime.fromString( + self.attributes().value("added"), Qt.ISODate) + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + break + + if self.isStartElement(): + if self.name() == "title": + self.__readTitle(folder) + elif self.name() == "desc": + self.__readDescription(folder) + elif self.name() == "folder": + self.__readFolder(folder) + elif self.name() == "bookmark": + self.__readBookmarkNode(folder) + elif self.name() == "separator": + self.__readSeparator(folder) + elif self.name() == "info": + self.__readInfo() + else: + self.__skipUnknownElement() + + def __readTitle(self, node): + """ + Private method to read the title element. + + @param node reference to the bookmark node title belongs to + (BookmarkNode) + """ + if not self.isStartElement() and self.name() != "title": + return + + node.title = self.readElementText() + + def __readDescription(self, node): + """ + Private method to read the desc element. + + @param node reference to the bookmark node desc belongs to + (BookmarkNode) + """ + if not self.isStartElement() and self.name() != "desc": + return + + node.desc = self.readElementText() + + def __readSeparator(self, node): + """ + Private method to read a separator element. + + @param node reference to the bookmark node the separator belongs to + (BookmarkNode) + """ + sep = BookmarkNode(BookmarkNode.Separator, node) + sep.added = QDateTime.fromString( + self.attributes().value("added"), Qt.ISODate) + + # empty elements have a start and end element + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + break + + if self.isStartElement(): + if self.name() == "info": + self.__readInfo() + else: + self.__skipUnknownElement() + + def __readBookmarkNode(self, node): + """ + Private method to read and parse a bookmark subtree. + + @param node reference to the node to attach to (BookmarkNode) + """ + if not self.isStartElement() and self.name() != "bookmark": + return + + bookmark = BookmarkNode(BookmarkNode.Bookmark, node) + bookmark.url = self.attributes().value("href") + bookmark.added = QDateTime.fromString( + self.attributes().value("added"), Qt.ISODate) + bookmark.modified = QDateTime.fromString( + self.attributes().value("modified"), Qt.ISODate) + bookmark.visited = QDateTime.fromString( + self.attributes().value("visited"), Qt.ISODate) + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + break + + if self.isStartElement(): + if self.name() == "title": + self.__readTitle(bookmark) + elif self.name() == "desc": + self.__readDescription(bookmark) + elif self.name() == "info": + self.__readInfo() + else: + self.__skipUnknownElement() + + if not bookmark.title: + bookmark.title = QCoreApplication.translate( + "XbelReader", "Unknown title") + + def __readInfo(self): + """ + Private method to read and parse an info subtree. + """ + self.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration( + "bookmark", "http://www.python.org")) + self.skipCurrentElement() + + def __skipUnknownElement(self): + """ + Private method to skip over all unknown elements. + """ + self.skipCurrentElement()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/XbelWriter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to write XBEL bookmark files. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamWriter, QIODevice, QFile, Qt + +from .BookmarkNode import BookmarkNode + + +class XbelWriter(QXmlStreamWriter): + """ + Class implementing a writer object to generate XBEL bookmark files. + """ + def __init__(self): + """ + Constructor + """ + super(XbelWriter, self).__init__() + + self.setAutoFormatting(True) + + def write(self, fileNameOrDevice, root): + """ + Public method to write an XBEL bookmark file. + + @param fileNameOrDevice name of the file to write (string) + or device to write to (QIODevice) + @param root root node of the bookmark tree (BookmarkNode) + @return flag indicating success (boolean) + """ + if isinstance(fileNameOrDevice, QIODevice): + f = fileNameOrDevice + else: + f = QFile(fileNameOrDevice) + if root is None or not f.open(QFile.WriteOnly): + return False + + self.setDevice(f) + return self.__write(root) + + def __write(self, root): + """ + Private method to write an XBEL bookmark file. + + @param root root node of the bookmark tree (BookmarkNode) + @return flag indicating success (boolean) + """ + self.writeStartDocument() + self.writeDTD("<!DOCTYPE xbel>") + self.writeStartElement("xbel") + self.writeAttribute("version", "1.0") + if root.type() == BookmarkNode.Root: + for child in root.children(): + self.__writeItem(child) + else: + self.__writeItem(root) + + self.writeEndDocument() + return True + + def __writeItem(self, node): + """ + Private method to write an entry for a node. + + @param node reference to the node to be written (BookmarkNode) + """ + if node.type() == BookmarkNode.Folder: + self.writeStartElement("folder") + if node.added.isValid(): + self.writeAttribute("added", node.added.toString(Qt.ISODate)) + self.writeAttribute("folded", node.expanded and "no" or "yes") + self.writeTextElement("title", node.title) + for child in node.children(): + self.__writeItem(child) + self.writeEndElement() + elif node.type() == BookmarkNode.Bookmark: + self.writeStartElement("bookmark") + if node.url: + self.writeAttribute("href", node.url) + if node.added.isValid(): + self.writeAttribute("added", node.added.toString(Qt.ISODate)) + if node.modified.isValid(): + self.writeAttribute( + "modified", node.modified.toString(Qt.ISODate)) + if node.visited.isValid(): + self.writeAttribute( + "visited", node.visited.toString(Qt.ISODate)) + self.writeTextElement("title", node.title) + if node.desc: + self.writeTextElement("desc", node.desc) + self.writeEndElement() + elif node.type() == BookmarkNode.Separator: + self.writeEmptyElement("separator") + if node.added.isValid(): + self.writeAttribute("added", node.added.toString(Qt.ISODate))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Bookmarks/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the bookmarks system. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/ClosedTabsManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to manage closed tabs. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, QUrl, QObject + + +class ClosedTab(object): + """ + Class implementing a structure to store data about a closed tab. + """ + def __init__(self, url=QUrl(), title="", position=-1): + """ + Constructor + + @param url URL of the closed tab (QUrl) + @param title title of the closed tab (string) + @param position index of the closed tab (integer) + """ + self.url = url + self.title = title + self.position = position + + def __eq__(self, other): + """ + Special method implementing the equality operator. + + @param other reference to the object to compare against (ClosedTab) + @return flag indicating equality of the tabs (boolean) + """ + return self.url == other.url and \ + self.title == other.title and \ + self.position == other.position + + +class ClosedTabsManager(QObject): + """ + Class implementing a manager for closed tabs. + + @signal closedTabAvailable(boolean) emitted to signal a change of + availability of closed tabs + """ + closedTabAvailable = pyqtSignal(bool) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(ClosedTabsManager, self).__init__() + + self.__closedTabs = [] + + def recordBrowser(self, browser, position): + """ + Public method to record the data of a browser about to be closed. + + @param browser reference to the browser to be closed (HelpBrowser) + @param position index of the tab to be closed (integer) + """ + import WebBrowser.WebBrowserWindow + if WebBrowser.WebBrowserWindow.WebBrowserWindow.isPrivate(): + return + + if browser.url().isEmpty(): + return + + tab = ClosedTab(browser.url(), browser.title(), position) + self.__closedTabs.insert(0, tab) + self.closedTabAvailable.emit(True) + + def getClosedTabAt(self, index): + """ + Public method to get the indexed closed tab. + + @param index index of the tab to return (integer) + @return requested tab (ClosedTab) + """ + if len(self.__closedTabs) > 0 and len(self.__closedTabs) > index: + tab = self.__closedTabs.pop(index) + else: + tab = ClosedTab() + self.closedTabAvailable.emit(len(self.__closedTabs) > 0) + return tab + + def isClosedTabAvailable(self): + """ + Public method to check for closed tabs. + + @return flag indicating the availability of closed tab data (boolean) + """ + return len(self.__closedTabs) > 0 + + def clearList(self): + """ + Public method to clear the list of closed tabs. + """ + self.__closedTabs = [] + self.closedTabAvailable.emit(False) + + def allClosedTabs(self): + """ + Public method to get a list of all closed tabs. + + @return list of closed tabs (list of ClosedTab) + """ + return self.__closedTabs
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/CookieDetailsDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog showing the cookie data. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QDialog + +from .Ui_CookieDetailsDialog import Ui_CookieDetailsDialog + + +class CookieDetailsDialog(QDialog, Ui_CookieDetailsDialog): + """ + Class implementing a dialog showing the cookie data. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QWidget) + """ + super(CookieDetailsDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + def setData(self, domain, name, path, secure, expires, value): + """ + Public method to set the data to be shown. + + @param domain domain of the cookie (string) + @param name name of the cookie (string) + @param path path of the cookie (string) + @param secure flag indicating a secure cookie (boolean) + @param expires expiration time of the cookie (string) + @param value value of the cookie (string) + """ + self.domainEdit.setText(domain) + self.nameEdit.setText(name) + self.pathEdit.setText(path) + self.secureCheckBox.setChecked(secure) + self.expirationEdit.setText(expires) + self.valueEdit.setPlainText(value)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/CookieDetailsDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CookieDetailsDialog</class> + <widget class="QDialog" name="CookieDetailsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Cookie Details</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Domain:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="domainEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Name:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="nameEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Path:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="pathEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Secure:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QCheckBox" name="secureCheckBox"> + <property name="text"> + <string/> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Expires:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLineEdit" name="expirationEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Contents:</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QPlainTextEdit" name="valueEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>domainEdit</tabstop> + <tabstop>nameEdit</tabstop> + <tabstop>pathEdit</tabstop> + <tabstop>secureCheckBox</tabstop> + <tabstop>expirationEdit</tabstop> + <tabstop>valueEdit</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CookieDetailsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>CookieDetailsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/CookieExceptionsModel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the cookie exceptions model. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QAbstractTableModel, QSize, QModelIndex +from PyQt5.QtGui import QFont, QFontMetrics + + +class CookieExceptionsModel(QAbstractTableModel): + """ + Class implementing the cookie exceptions model. + """ + def __init__(self, cookieJar, parent=None): + """ + Constructor + + @param cookieJar reference to the cookie jar (CookieJar) + @param parent reference to the parent object (QObject) + """ + super(CookieExceptionsModel, self).__init__(parent) + + self.__cookieJar = cookieJar + self.__allowedCookies = self.__cookieJar.allowedCookies() + self.__blockedCookies = self.__cookieJar.blockedCookies() + self.__sessionCookies = self.__cookieJar.allowForSessionCookies() + + self.__headers = [ + self.tr("Website"), + self.tr("Status"), + ] + + def headerData(self, section, orientation, role): + """ + Public method to get header data from the model. + + @param section section number (integer) + @param orientation orientation (Qt.Orientation) + @param role role of the data to retrieve (integer) + @return requested data + """ + if role == Qt.SizeHintRole: + fm = QFontMetrics(QFont()) + height = fm.height() + fm.height() // 3 + width = \ + fm.width(self.headerData(section, orientation, Qt.DisplayRole)) + return QSize(width, height) + + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + try: + return self.__headers[section] + except IndexError: + return None + + return QAbstractTableModel.headerData(self, section, orientation, role) + + def data(self, index, role): + """ + Public method to get data from the model. + + @param index index to get data for (QModelIndex) + @param role role of the data to retrieve (integer) + @return requested data + """ + if index.row() < 0 or index.row() >= self.rowCount(): + return None + + if role in (Qt.DisplayRole, Qt.EditRole): + row = index.row() + if row < len(self.__allowedCookies): + if index.column() == 0: + return self.__allowedCookies[row] + elif index.column() == 1: + return self.tr("Allow") + else: + return None + + row -= len(self.__allowedCookies) + if row < len(self.__blockedCookies): + if index.column() == 0: + return self.__blockedCookies[row] + elif index.column() == 1: + return self.tr("Block") + else: + return None + + row -= len(self.__blockedCookies) + if row < len(self.__sessionCookies): + if index.column() == 0: + return self.__sessionCookies[row] + elif index.column() == 1: + return self.tr("Allow For Session") + else: + return None + + return None + + return None + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns of the model. + + @param parent parent index (QModelIndex) + @return number of columns (integer) + """ + if parent.isValid(): + return 0 + else: + return len(self.__headers) + + def rowCount(self, parent=QModelIndex()): + """ + Public method to get the number of rows of the model. + + @param parent parent index (QModelIndex) + @return number of rows (integer) + """ + if parent.isValid() or self.__cookieJar is None: + return 0 + else: + return len(self.__allowedCookies) + \ + len(self.__blockedCookies) + \ + len(self.__sessionCookies) + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove entries from the model. + + @param row start row (integer) + @param count number of rows to remove (integer) + @param parent parent index (QModelIndex) + @return flag indicating success (boolean) + """ + if parent.isValid() or self.__cookieJar is None: + return False + + lastRow = row + count - 1 + self.beginRemoveRows(parent, row, lastRow) + for i in range(lastRow, row - 1, -1): + rowToRemove = i + + if rowToRemove < len(self.__allowedCookies): + del self.__allowedCookies[rowToRemove] + continue + + rowToRemove -= len(self.__allowedCookies) + if rowToRemove < len(self.__blockedCookies): + del self.__blockedCookies[rowToRemove] + continue + + rowToRemove -= len(self.__blockedCookies) + if rowToRemove < len(self.__sessionCookies): + del self.__sessionCookies[rowToRemove] + continue + + self.__cookieJar.setAllowedCookies(self.__allowedCookies) + self.__cookieJar.setBlockedCookies(self.__blockedCookies) + self.__cookieJar.setAllowForSessionCookies(self.__sessionCookies) + self.endRemoveRows() + + return True + + def addRule(self, host, rule): + """ + Public method to add an exception rule. + + @param host name of the host to add a rule for (string) + @param rule type of rule to add (CookieJar.Allow, CookieJar.Block or + CookieJar.AllowForSession) + """ + if not host: + return + + from .CookieJar import CookieJar + + if rule == CookieJar.Allow: + self.__addHost( + host, self.__allowedCookies, self.__blockedCookies, + self.__sessionCookies) + return + elif rule == CookieJar.Block: + self.__addHost( + host, self.__blockedCookies, self.__allowedCookies, + self.__sessionCookies) + return + elif rule == CookieJar.AllowForSession: + self.__addHost( + host, self.__sessionCookies, self.__allowedCookies, + self.__blockedCookies) + return + + def __addHost(self, host, addList, removeList1, removeList2): + """ + Private method to add a host to an exception list. + + @param host name of the host to add (string) + @param addList reference to the list to add it to (list of strings) + @param removeList1 reference to first list to remove it from + (list of strings) + @param removeList2 reference to second list to remove it from + (list of strings) + """ + if host not in addList: + addList.append(host) + if host in removeList1: + removeList1.remove(host) + if host in removeList2: + removeList2.remove(host) + + # Avoid to have similar rules (with or without leading dot) + # e.g. python-projects.org and .python-projects.org + if host.startswith("."): + otherRule = host[1:] + else: + otherRule = '.' + host + if otherRule in addList: + addList.remove(otherRule) + if otherRule in removeList1: + removeList1.remove(otherRule) + if otherRule in removeList2: + removeList2.remove(otherRule) + + self.beginResetModel() + self.endResetModel()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/CookieJar.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,433 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a QNetworkCookieJar subclass with various accept policies. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSettings +from PyQt5.QtNetwork import QNetworkCookieJar, QNetworkCookie + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + +from Utilities.AutoSaver import AutoSaver +import Utilities +import Preferences + + +class CookieJar(QNetworkCookieJar): + """ + Class implementing a QNetworkCookieJar subclass with various accept + policies. + + @signal cookiesChanged() emitted after the cookies have been changed + """ + cookiesChanged = pyqtSignal() + + AcceptAlways = 0 + AcceptNever = 1 + AcceptOnlyFromSitesNavigatedTo = 2 + AcceptMax = 2 + + KeepUntilExpire = 0 + KeepUntilExit = 1 + KeepMax = 1 + + Allow = 0 + Block = 1 + AllowForSession = 2 + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(CookieJar, self).__init__(parent) + + self.__loaded = False + self.__acceptCookies = self.AcceptOnlyFromSitesNavigatedTo + self.__saveTimer = AutoSaver(self, self.__save) + + self.__cookiesFile = os.path.join(Utilities.getConfigDir(), + "web_browser", "cookies.ini") + + self.__store = WebBrowserWindow.webProfile().cookieStore() + self.__store.cookieAdded.connect(self.__cookieAdded) + self.__store.cookieRemoved.connect(self.__cookieRemoved) + + self.__load() + self.__store.loadAllCookies() + + def close(self): + """ + Public slot to close the cookie jar. + """ + if not self.__loaded: + self.__load() + + if self.__keepCookies == self.KeepUntilExit: + self.clear() + self.__saveTimer.saveIfNeccessary() + + def clear(self): + """ + Public method to clear all cookies. + """ + if not self.__loaded: + self.__load() + + self.setAllCookies([]) + self.__store.deleteAllCookies() + self.cookiesChanged.emit() + + def removeCookies(self, cookies): + """ + Public method to remove a list of cookies. + + @param cookies list of cookies to be removed + @type list of QNetworkCookie + """ + wasBlocked = self.blockSignals(True) + for cookie in cookies: + self.__store.deleteCookie(cookie) + self.blockSignals(wasBlocked) + + self.cookiesChanged.emit() + + def __load(self): + """ + Private method to load the cookies settings. + """ + if self.__loaded: + return + + cookieSettings = QSettings(self.__cookiesFile, QSettings.IniFormat) + + # load exceptions + self.__exceptionsBlock = Preferences.toList( + cookieSettings.value("Exceptions/block")) + self.__exceptionsAllow = Preferences.toList( + cookieSettings.value("Exceptions/allow")) + self.__exceptionsAllowForSession = Preferences.toList( + cookieSettings.value("Exceptions/allowForSession")) + self.__exceptionsBlock.sort() + self.__exceptionsAllow.sort() + self.__exceptionsAllowForSession.sort() + + self.__acceptCookies = Preferences.getWebBrowser("AcceptCookies") + self.__keepCookies = Preferences.getWebBrowser("KeepCookiesUntil") + if self.__keepCookies == self.KeepUntilExit: + self.clear() + + self.__filterTrackingCookies = Preferences.toBool( + Preferences.getWebBrowser("FilterTrackingCookies")) + + self.__loaded = True + self.cookiesChanged.emit() + + def __save(self): + """ + Private method to save the cookies settings. + """ + if not self.__loaded: + return + + cookieSettings = QSettings(self.__cookiesFile, QSettings.IniFormat) + + cookieSettings.setValue("Exceptions/block", self.__exceptionsBlock) + cookieSettings.setValue("Exceptions/allow", self.__exceptionsAllow) + cookieSettings.setValue("Exceptions/allowForSession", + self.__exceptionsAllowForSession) + + Preferences.setWebBrowser("AcceptCookies", self.__acceptCookies) + Preferences.setWebBrowser("KeepCookiesUntil", self.__keepCookies) + Preferences.setWebBrowser("FilterTrackingCookies", + self.__filterTrackingCookies) + + @pyqtSlot(QNetworkCookie) + def __cookieAdded(self, cookie): + """ + Private slot handling the addition of a cookie. + + @param cookie cookie which was added + @type QNetworkCookie + """ + if self.__rejectCookie(cookie, cookie.domain()): + self.__store.deleteCookie(cookie) + return + + self.insertCookie(cookie) + self.cookiesChanged.emit() + + @pyqtSlot(QNetworkCookie) + def __cookieRemoved(self, cookie): + """ + Private slot handling the removal of a cookie. + + @param cookie cookie which was removed + @type QNetworkCookie + """ + if self.deleteCookie(cookie): + self.cookiesChanged.emit() + + def __rejectCookie(self, cookie, cookieDomain): + """ + Public method to test, if a cookie shall be rejected. + + @param cookie cookie to be tested + @type QNetworkCookie + @param cookieDomain domain of the cookie + @type str + @return flag indicating the cookie shall be rejected + @rtype bool + """ + if not self.__loaded: + self.__load() + + eBlock = self.__isOnDomainList(self.__exceptionsBlock, cookieDomain) + eAllow = not eBlock and \ + self.__isOnDomainList(self.__exceptionsAllow, cookieDomain) + eAllowSession = not eBlock and \ + not eAllow and \ + self.__isOnDomainList( + self.__exceptionsAllowForSession, cookieDomain) + + if self.__acceptCookies == self.AcceptNever: + if not eAllow and not eAllowSession: + return True + + if self.__acceptCookies == self.AcceptAlways: + if eBlock: + return True + + if self.__acceptCookies == self.AcceptOnlyFromSitesNavigatedTo: + url = WebBrowserWindow.mainWindow().getWindow().currentBrowser()\ + .url() + if url.isValid(): + host = url.host() + else: + host = "" + if not self.__matchDomain(cookieDomain, host): + return True + + if self.__filterTrackingCookies and cookie.name().startsWith(b"__utm"): + return True + + return False + + def acceptPolicy(self): + """ + Public method to get the accept policy. + + @return current accept policy + """ + if not self.__loaded: + self.__load() + + return self.__acceptCookies + + def setAcceptPolicy(self, policy): + """ + Public method to set the accept policy. + + @param policy accept policy to be set + """ + if not self.__loaded: + self.__load() + + if policy > self.AcceptMax: + return + if policy == self.__acceptCookies: + return + + self.__acceptCookies = policy + self.__saveTimer.changeOccurred() + + def keepPolicy(self): + """ + Public method to get the keep policy. + + @return keep policy + """ + if not self.__loaded: + self.__load() + + return self.__keepCookies + + def setKeepPolicy(self, policy): + """ + Public method to set the keep policy. + + @param policy keep policy to be set + """ + if not self.__loaded: + self.__load() + + if policy > self.KeepMax: + return + if policy == self.__keepCookies: + return + + self.__keepCookies = policy + self.__saveTimer.changeOccurred() + + def blockedCookies(self): + """ + Public method to return the list of blocked domains. + + @return list of blocked domains (list of strings) + """ + if not self.__loaded: + self.__load() + + return self.__exceptionsBlock + + def allowedCookies(self): + """ + Public method to return the list of allowed domains. + + @return list of allowed domains (list of strings) + """ + if not self.__loaded: + self.__load() + + return self.__exceptionsAllow + + def allowForSessionCookies(self): + """ + Public method to return the list of allowed session cookie domains. + + @return list of allowed session cookie domains (list of strings) + """ + if not self.__loaded: + self.__load() + + return self.__exceptionsAllowForSession + + def setBlockedCookies(self, list_): + """ + Public method to set the list of blocked domains. + + @param list_ list of blocked domains (list of strings) + """ + if not self.__loaded: + self.__load() + + self.__exceptionsBlock = list_[:] + self.__exceptionsBlock.sort() + self.__saveTimer.changeOccurred() + + def setAllowedCookies(self, list_): + """ + Public method to set the list of allowed domains. + + @param list_ list of allowed domains (list of strings) + """ + if not self.__loaded: + self.__load() + + self.__exceptionsAllow = list_[:] + self.__exceptionsAllow.sort() + self.__saveTimer.changeOccurred() + + def setAllowForSessionCookies(self, list_): + """ + Public method to set the list of allowed session cookie domains. + + @param list_ list of allowed session cookie domains (list of strings) + """ + if not self.__loaded: + self.__load() + + self.__exceptionsAllowForSession = list_[:] + self.__exceptionsAllowForSession.sort() + self.__saveTimer.changeOccurred() + + def filterTrackingCookies(self): + """ + Public method to get the filter tracking cookies flag. + + @return filter tracking cookies flag (boolean) + """ + return self.__filterTrackingCookies + + def setFilterTrackingCookies(self, filterTrackingCookies): + """ + Public method to set the filter tracking cookies flag. + + @param filterTrackingCookies filter tracking cookies flag (boolean) + """ + self.__filterTrackingCookies = filterTrackingCookies + + def __isOnDomainList(self, rules, domain): + """ + Private method to check, if either the rule matches the domain exactly + or the domain ends with ".rule". + + @param rules list of rules (list of strings) + @param domain domain name to check (string) + @return flag indicating a match (boolean) + """ + for rule in rules: + if rule.startswith("."): + if domain.endswith(rule): + return True + + withoutDot = rule[1:] + if domain == withoutDot: + return True + else: + domainEnding = domain[-(len(rule) + 1):] + if domainEnding and \ + domainEnding[0] == "." and \ + domain.endswith(rule): + return True + + if rule == domain: + return True + + return False + + def __matchDomain(self, cookieDomain, siteDomain): + """ + Private method to check, if a URLs host matches a cookie domain + according to RFC 6265. + + @param cookieDomain domain of the cookie + @type str + @param siteDomain domain or host of an URL + @type str + @return flag indicating a match + @rtype bool + """ + if cookieDomain.startswith("."): + cookieDomain = cookieDomain[1:] + if siteDomain.startswith("."): + siteDomain = siteDomain[1:] + + if cookieDomain == siteDomain: + return True + + if not siteDomain.endswith(cookieDomain): + return False + + index = siteDomain.find(cookieDomain) + return index > 0 and siteDomain[index - 1] == "." + + def cookies(self): + """ + Public method to get the cookies of the cookie jar. + + @return list of all cookies (list of QNetworkCookie) + """ + if not self.__loaded: + self.__load() + + return self.allCookies()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/CookieModel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the cookie model. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QAbstractTableModel, QSize, QModelIndex +from PyQt5.QtGui import QFont, QFontMetrics + + +class CookieModel(QAbstractTableModel): + """ + Class implementing the cookie model. + """ + def __init__(self, cookieJar, parent=None): + """ + Constructor + + @param cookieJar reference to the cookie jar (CookieJar) + @param parent reference to the parent object (QObject) + """ + super(CookieModel, self).__init__(parent) + + self.__headers = [ + self.tr("Website"), + self.tr("Name"), + self.tr("Path"), + self.tr("Secure"), + self.tr("Expires"), + ] + self.__cookieJar = cookieJar + self.__cookieJar.cookiesChanged.connect(self.__cookiesChanged) + + def headerData(self, section, orientation, role): + """ + Public method to get header data from the model. + + @param section section number (integer) + @param orientation orientation (Qt.Orientation) + @param role role of the data to retrieve (integer) + @return requested data + """ + if role == Qt.SizeHintRole: + fm = QFontMetrics(QFont()) + height = fm.height() + fm.height() // 3 + width = \ + fm.width(self.headerData(section, orientation, Qt.DisplayRole)) + return QSize(width, height) + + if orientation == Qt.Horizontal: + if role == Qt.DisplayRole: + try: + return self.__headers[section] + except IndexError: + return None + + return None + + return QAbstractTableModel.headerData(self, section, orientation, role) + + def data(self, index, role): + """ + Public method to get data from the model. + + @param index index to get data for (QModelIndex) + @param role role of the data to retrieve (integer) + @return requested data + """ + lst = [] + if self.__cookieJar is not None: + lst = self.__cookieJar.cookies() + if index.row() < 0 or index.row() >= len(lst): + return None + + if role in (Qt.DisplayRole, Qt.EditRole): + cookie = lst[index.row()] + col = index.column() + if col == 0: + return cookie.domain() + elif col == 1: + return bytes(cookie.name()).decode() + elif col == 2: + return cookie.path() + elif col == 3: + return cookie.isSecure() + elif col == 4: + return cookie.expirationDate() + elif col == 5: + return cookie.value() + else: + return None + + return None + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns of the model. + + @param parent parent index (QModelIndex) + @return number of columns (integer) + """ + if parent.isValid(): + return 0 + else: + return len(self.__headers) + + def rowCount(self, parent=QModelIndex()): + """ + Public method to get the number of rows of the model. + + @param parent parent index (QModelIndex) + @return number of columns (integer) + """ + if parent.isValid() or self.__cookieJar is None: + return 0 + else: + return len(self.__cookieJar.cookies()) + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove entries from the model. + + @param row start row (integer) + @param count number of rows to remove (integer) + @param parent parent index (QModelIndex) + @return flag indicating success (boolean) + """ + if parent.isValid() or self.__cookieJar is None: + return False + + lst = self.__cookieJar.cookies()[row:row + count] + self.__cookieJar.removeCookies(lst) + + return True + + def __cookiesChanged(self): + """ + Private slot handling changes of the cookies list in the cookie jar. + """ + self.beginResetModel() + self.endResetModel()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/CookiesConfigurationDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the cookies configuration dialog. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QDialog + +from .CookieJar import CookieJar + +from .Ui_CookiesConfigurationDialog import Ui_CookiesConfigurationDialog + + +class CookiesConfigurationDialog(QDialog, Ui_CookiesConfigurationDialog): + """ + Class implementing the cookies configuration dialog. + """ + def __init__(self, parent): + """ + Constructor + + @param parent reference to the parent object (QWidget) + """ + super(CookiesConfigurationDialog, self).__init__(parent) + self.setupUi(self) + + self.__mw = parent + + jar = self.__mw.cookieJar() + acceptPolicy = jar.acceptPolicy() + if acceptPolicy == CookieJar.AcceptAlways: + self.acceptCombo.setCurrentIndex(0) + elif acceptPolicy == CookieJar.AcceptNever: + self.acceptCombo.setCurrentIndex(1) + elif acceptPolicy == CookieJar.AcceptOnlyFromSitesNavigatedTo: + self.acceptCombo.setCurrentIndex(2) + + keepPolicy = jar.keepPolicy() + if keepPolicy == CookieJar.KeepUntilExpire: + self.keepUntilCombo.setCurrentIndex(0) + elif keepPolicy == CookieJar.KeepUntilExit: + self.keepUntilCombo.setCurrentIndex(1) + + self.filterTrackingCookiesCheckbox.setChecked( + jar.filterTrackingCookies()) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + def accept(self): + """ + Public slot to accept the dialog. + """ + acceptSelection = self.acceptCombo.currentIndex() + if acceptSelection == 0: + acceptPolicy = CookieJar.AcceptAlways + elif acceptSelection == 1: + acceptPolicy = CookieJar.AcceptNever + elif acceptSelection == 2: + acceptPolicy = CookieJar.AcceptOnlyFromSitesNavigatedTo + + keepSelection = self.keepUntilCombo.currentIndex() + if keepSelection == 0: + keepPolicy = CookieJar.KeepUntilExpire + elif keepSelection == 1: + keepPolicy = CookieJar.KeepUntilExit + + jar = self.__mw.cookieJar() + jar.setAcceptPolicy(acceptPolicy) + jar.setKeepPolicy(keepPolicy) + jar.setFilterTrackingCookies( + self.filterTrackingCookiesCheckbox.isChecked()) + + super(CookiesConfigurationDialog, self).accept() + + @pyqtSlot() + def on_exceptionsButton_clicked(self): + """ + Private slot to show the cookies exceptions dialog. + """ + from .CookiesExceptionsDialog import CookiesExceptionsDialog + dlg = CookiesExceptionsDialog(self.__mw.cookieJar()) + dlg.exec_() + + @pyqtSlot() + def on_cookiesButton_clicked(self): + """ + Private slot to show the cookies dialog. + """ + from .CookiesDialog import CookiesDialog + dlg = CookiesDialog(self.__mw.cookieJar()) + dlg.exec_()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/CookiesConfigurationDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,200 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CookiesConfigurationDialog</class> + <widget class="QDialog" name="CookiesConfigurationDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>160</height> + </rect> + </property> + <property name="windowTitle"> + <string>Configure cookies</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="headerLabel"> + <property name="text"> + <string><b>Configure cookies</b></string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line17"> + <property name="frameShape"> + <enum>QFrame::HLine</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Sunken</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>&Accept Cookies:</string> + </property> + <property name="buddy"> + <cstring>acceptCombo</cstring> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="acceptCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the accept policy</string> + </property> + <item> + <property name="text"> + <string>Always</string> + </property> + </item> + <item> + <property name="text"> + <string>Never</string> + </property> + </item> + <item> + <property name="text"> + <string>Only from sites you navigate to</string> + </property> + </item> + </widget> + </item> + <item row="0" column="2"> + <widget class="QPushButton" name="exceptionsButton"> + <property name="toolTip"> + <string>Show a dialog to configure exceptions</string> + </property> + <property name="text"> + <string>&Exceptions...</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>&Keep until:</string> + </property> + <property name="buddy"> + <cstring>keepUntilCombo</cstring> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="keepUntilCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the keep policy</string> + </property> + <item> + <property name="text"> + <string>They expire</string> + </property> + </item> + <item> + <property name="text"> + <string>I exit the application</string> + </property> + </item> + </widget> + </item> + <item row="1" column="2"> + <widget class="QPushButton" name="cookiesButton"> + <property name="toolTip"> + <string>Show a dialog listing all cookies</string> + </property> + <property name="text"> + <string>&Show Cookies...</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QCheckBox" name="filterTrackingCookiesCheckbox"> + <property name="toolTip"> + <string>Select to filter tracking cookies</string> + </property> + <property name="text"> + <string>&Filter Tracking Cookies</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>acceptCombo</tabstop> + <tabstop>exceptionsButton</tabstop> + <tabstop>keepUntilCombo</tabstop> + <tabstop>cookiesButton</tabstop> + <tabstop>filterTrackingCookiesCheckbox</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CookiesConfigurationDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>CookiesConfigurationDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/CookiesDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show all cookies. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt, QDateTime, QByteArray, \ + QSortFilterProxyModel +from PyQt5.QtGui import QFont, QFontMetrics +from PyQt5.QtWidgets import QDialog + +from .CookieModel import CookieModel + +from .Ui_CookiesDialog import Ui_CookiesDialog + + +class CookiesDialog(QDialog, Ui_CookiesDialog): + """ + Class implementing a dialog to show all cookies. + """ + def __init__(self, cookieJar, parent=None): + """ + Constructor + + @param cookieJar reference to the cookie jar (CookieJar) + @param parent reference to the parent widget (QWidget) + """ + super(CookiesDialog, self).__init__(parent) + self.setupUi(self) + + self.addButton.setEnabled(False) + + self.__cookieJar = cookieJar + + self.removeButton.clicked.connect(self.cookiesTable.removeSelected) + self.removeAllButton.clicked.connect(self.cookiesTable.removeAll) + + self.cookiesTable.verticalHeader().hide() + model = CookieModel(cookieJar, self) + self.__proxyModel = QSortFilterProxyModel(self) + self.__proxyModel.setSourceModel(model) + self.searchEdit.textChanged.connect( + self.__proxyModel.setFilterFixedString) + self.cookiesTable.setModel(self.__proxyModel) + self.cookiesTable.doubleClicked.connect(self.__showCookieDetails) + self.cookiesTable.selectionModel().selectionChanged.connect( + self.__tableSelectionChanged) + self.cookiesTable.model().modelReset.connect(self.__tableModelReset) + + fm = QFontMetrics(QFont()) + height = fm.height() + fm.height() // 3 + self.cookiesTable.verticalHeader().setDefaultSectionSize(height) + self.cookiesTable.verticalHeader().setMinimumSectionSize(-1) + for section in range(model.columnCount()): + header = self.cookiesTable.horizontalHeader()\ + .sectionSizeHint(section) + if section == 0: + header = fm.width("averagebiglonghost.averagedomain.info") + elif section == 1: + header = fm.width("_session_id") + elif section == 4: + header = fm.width( + QDateTime.currentDateTime().toString(Qt.LocalDate)) + buffer = fm.width("mm") + header += buffer + self.cookiesTable.horizontalHeader().resizeSection(section, header) + self.cookiesTable.horizontalHeader().setStretchLastSection(True) + self.cookiesTable.model().sort( + self.cookiesTable.horizontalHeader().sortIndicatorSection(), + Qt.AscendingOrder) + + self.__detailsDialog = None + + def __showCookieDetails(self, index): + """ + Private slot to show a dialog with the cookie details. + + @param index index of the entry to show (QModelIndex) + """ + if not index.isValid(): + return + + cookiesTable = self.sender() + if cookiesTable is None: + return + + model = cookiesTable.model() + row = index.row() + + domain = model.data(model.index(row, 0)) + name = model.data(model.index(row, 1)) + path = model.data(model.index(row, 2)) + secure = model.data(model.index(row, 3)) + expires = model.data(model.index(row, 4)).toString("yyyy-MM-dd hh:mm") + value = bytes( + QByteArray.fromPercentEncoding( + model.data(model.index(row, 5)))).decode() + + if self.__detailsDialog is None: + from .CookieDetailsDialog import CookieDetailsDialog + self.__detailsDialog = CookieDetailsDialog(self) + self.__detailsDialog.setData(domain, name, path, secure, expires, + value) + self.__detailsDialog.show() + + @pyqtSlot() + def on_addButton_clicked(self): + """ + Private slot to add a new exception. + """ + selection = self.cookiesTable.selectionModel().selectedRows() + if len(selection) == 0: + return + + from .CookiesExceptionsDialog import CookiesExceptionsDialog + + firstSelected = selection[0] + domainSelection = firstSelected.sibling(firstSelected.row(), 0) + domain = self.__proxyModel.data(domainSelection, Qt.DisplayRole) + dlg = CookiesExceptionsDialog(self.__cookieJar, self) + dlg.setDomainName(domain) + dlg.exec_() + + def __tableSelectionChanged(self, selected, deselected): + """ + Private slot to handle a change of selected items. + + @param selected selected indexes (QItemSelection) + @param deselected deselected indexes (QItemSelection) + """ + self.addButton.setEnabled(len(selected.indexes()) > 0) + + def __tableModelReset(self): + """ + Private slot to handle a reset of the cookies table. + """ + self.addButton.setEnabled(False)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/CookiesDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,195 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CookiesDialog</class> + <widget class="QDialog" name="CookiesDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>350</height> + </rect> + </property> + <property name="windowTitle"> + <string>Cookies</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="4"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="E5ClearableLineEdit" name="searchEdit"> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + <property name="toolTip"> + <string>Enter search term for cookies</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item row="1" column="0" colspan="4"> + <widget class="E5TableView" name="cookiesTable"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="textElideMode"> + <enum>Qt::ElideMiddle</enum> + </property> + <property name="showGrid"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected entries</string> + </property> + <property name="text"> + <string>&Remove</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all entries</string> + </property> + <property name="text"> + <string>Remove &All</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QPushButton" name="addButton"> + <property name="toolTip"> + <string>Press to open the cookies exceptions dialog to add a new rule</string> + </property> + <property name="text"> + <string>Add R&ule...</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="2" column="3"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>208</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="0" colspan="4"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + <customwidget> + <class>E5TableView</class> + <extends>QTableView</extends> + <header>E5Gui/E5TableView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>cookiesTable</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>removeAllButton</tabstop> + <tabstop>addButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CookiesDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>228</x> + <y>274</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>CookiesDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/CookiesExceptionsDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog for the configuration of cookie exceptions. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, QSortFilterProxyModel +from PyQt5.QtGui import QFont, QFontMetrics +from PyQt5.QtWidgets import QDialog, QCompleter + +from .CookieExceptionsModel import CookieExceptionsModel +from .CookieModel import CookieModel + +from .Ui_CookiesExceptionsDialog import Ui_CookiesExceptionsDialog + + +class CookiesExceptionsDialog(QDialog, Ui_CookiesExceptionsDialog): + """ + Class implementing a dialog for the configuration of cookie exceptions. + """ + def __init__(self, cookieJar, parent=None): + """ + Constructor + + @param cookieJar reference to the cookie jar (CookieJar) + @param parent reference to the parent widget (QWidget) + """ + super(CookiesExceptionsDialog, self).__init__(parent) + self.setupUi(self) + + self.__cookieJar = cookieJar + + self.removeButton.clicked.connect( + self.exceptionsTable.removeSelected) + self.removeAllButton.clicked.connect( + self.exceptionsTable.removeAll) + + self.exceptionsTable.verticalHeader().hide() + self.__exceptionsModel = CookieExceptionsModel(cookieJar) + self.__proxyModel = QSortFilterProxyModel(self) + self.__proxyModel.setSourceModel(self.__exceptionsModel) + self.searchEdit.textChanged.connect( + self.__proxyModel.setFilterFixedString) + self.exceptionsTable.setModel(self.__proxyModel) + + cookieModel = CookieModel(cookieJar, self) + self.domainEdit.setCompleter(QCompleter(cookieModel, self.domainEdit)) + + f = QFont() + f.setPointSize(10) + fm = QFontMetrics(f) + height = fm.height() + fm.height() // 3 + self.exceptionsTable.verticalHeader().setDefaultSectionSize(height) + self.exceptionsTable.verticalHeader().setMinimumSectionSize(-1) + for section in range(self.__exceptionsModel.columnCount()): + header = self.exceptionsTable.horizontalHeader()\ + .sectionSizeHint(section) + if section == 0: + header = fm.width("averagebiglonghost.averagedomain.info") + elif section == 1: + header = fm.width(self.tr("Allow For Session")) + buffer = fm.width("mm") + header += buffer + self.exceptionsTable.horizontalHeader()\ + .resizeSection(section, header) + + def setDomainName(self, domain): + """ + Public method to set the domain to be displayed. + + @param domain domain name to be displayed (string) + """ + self.domainEdit.setText(domain) + + @pyqtSlot(str) + def on_domainEdit_textChanged(self, txt): + """ + Private slot to handle a change of the domain edit text. + + @param txt current text of the edit (string) + """ + enabled = txt != "" + self.blockButton.setEnabled(enabled) + self.allowButton.setEnabled(enabled) + self.allowForSessionButton.setEnabled(enabled) + + @pyqtSlot() + def on_blockButton_clicked(self): + """ + Private slot to block cookies of a domain. + """ + from .CookieJar import CookieJar + self.__exceptionsModel.addRule(self.domainEdit.text(), CookieJar.Block) + self.domainEdit.clear() + + @pyqtSlot() + def on_allowForSessionButton_clicked(self): + """ + Private slot to allow cookies of a domain for the current session only. + """ + from .CookieJar import CookieJar + self.__exceptionsModel.addRule(self.domainEdit.text(), + CookieJar.AllowForSession) + self.domainEdit.clear() + + @pyqtSlot() + def on_allowButton_clicked(self): + """ + Private slot to allow cookies of a domain. + """ + from .CookieJar import CookieJar + self.__exceptionsModel.addRule(self.domainEdit.text(), CookieJar.Allow) + self.domainEdit.clear()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/CookiesExceptionsDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,292 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CookiesExceptionsDialog</class> + <widget class="QDialog" name="CookiesExceptionsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>450</height> + </rect> + </property> + <property name="windowTitle"> + <string>Cookie Exceptions</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="newExceptionGroupBox"> + <property name="title"> + <string>New Exception</string> + </property> + <layout class="QGridLayout"> + <item row="0" column="0"> + <layout class="QHBoxLayout" name="_2"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>&Domain:</string> + </property> + <property name="buddy"> + <cstring>domainEdit</cstring> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="domainEdit"> + <property name="toolTip"> + <string>Enter the domain name</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <layout class="QHBoxLayout" name="_3"> + <item> + <spacer> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>81</width> + <height>25</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="blockButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Press to always reject cookies for the domain</string> + </property> + <property name="text"> + <string>&Block</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="allowForSessionButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Press to accept cookies for the domain for the current session</string> + </property> + <property name="text"> + <string>Allow For &Session</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="allowButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Press to always accept cookies for the domain</string> + </property> + <property name="text"> + <string>Allo&w</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="exceptionsGroup"> + <property name="title"> + <string>Exceptions</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="3"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="E5ClearableLineEdit" name="searchEdit"> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + <property name="toolTip"> + <string>Enter search term for exceptions</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item row="1" column="0" colspan="3"> + <widget class="E5TableView" name="exceptionsTable"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="textElideMode"> + <enum>Qt::ElideMiddle</enum> + </property> + <property name="showGrid"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + </widget> + </item> + <item row="2" column="0"> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected entries</string> + </property> + <property name="text"> + <string>&Remove</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all entries</string> + </property> + <property name="text"> + <string>Remove &All</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="2" column="2"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>286</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + <customwidget> + <class>E5TableView</class> + <extends>QTableView</extends> + <header>E5Gui/E5TableView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>domainEdit</tabstop> + <tabstop>blockButton</tabstop> + <tabstop>allowForSessionButton</tabstop> + <tabstop>allowButton</tabstop> + <tabstop>searchEdit</tabstop> + <tabstop>exceptionsTable</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>removeAllButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CookiesExceptionsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>227</x> + <y>429</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>CookiesExceptionsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>295</x> + <y>435</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/CookieJar/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing a cookie jar and related dialogs with models. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Download/DownloadAskActionDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to ask for a download action. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QDialog + +from .Ui_DownloadAskActionDialog import Ui_DownloadAskActionDialog + +import Preferences + + +class DownloadAskActionDialog(QDialog, Ui_DownloadAskActionDialog): + """ + Class implementing a dialog to ask for a download action. + """ + def __init__(self, fileName, mimeType, baseUrl, parent=None): + """ + Constructor + + @param fileName file name (string) + @param mimeType mime type (string) + @param baseUrl URL (string) + @param parent reference to the parent widget (QWidget) + """ + super(DownloadAskActionDialog, self).__init__(parent) + self.setupUi(self) + + self.infoLabel.setText("<b>{0}</b>".format(fileName)) + self.typeLabel.setText(mimeType) + self.siteLabel.setText(baseUrl) + + if not Preferences.getWebBrowser("VirusTotalEnabled") or \ + Preferences.getWebBrowser("VirusTotalServiceKey") == "": + self.scanButton.setHidden(True) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + def getAction(self): + """ + Public method to get the selected action. + + @return selected action ("save", "open", "scan" or "cancel") + """ + if self.openButton.isChecked(): + return "open" + elif self.scanButton.isChecked(): + return "scan" + elif self.saveButton.isChecked(): + return "save" + else: + # should not happen, but keep it safe + return "cancel"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Download/DownloadAskActionDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,205 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DownloadAskActionDialog</class> + <widget class="QDialog" name="DownloadAskActionDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>209</height> + </rect> + </property> + <property name="windowTitle"> + <string>What to do?</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>You are about to download this file:</string> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string notr="true"/> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QLabel" name="infoLabel"> + <property name="text"> + <string/> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Type:</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QLabel" name="typeLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>From:</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QLabel" name="siteLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string notr="true"/> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QRadioButton" name="openButton"> + <property name="toolTip"> + <string>Select to open the downloaded file</string> + </property> + <property name="text"> + <string>&Open File</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QRadioButton" name="scanButton"> + <property name="statusTip"> + <string>Select to scan the file with VirusTotal</string> + </property> + <property name="text"> + <string>Scan with &VirusTotal</string> + </property> + </widget> + </item> + <item row="2" column="2" rowspan="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="1"> + <widget class="QRadioButton" name="saveButton"> + <property name="toolTip"> + <string>Select to save the file</string> + </property> + <property name="text"> + <string>&Save File</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label"> + <property name="text"> + <string><b>What do you want to do?</b></string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>openButton</tabstop> + <tabstop>scanButton</tabstop> + <tabstop>saveButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>DownloadAskActionDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>DownloadAskActionDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Download/DownloadItem.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,522 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a widget controlling a download. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QTime, QFileInfo, QUrl, \ + PYQT_VERSION_STR +from PyQt5.QtGui import QPalette, QDesktopServices +from PyQt5.QtWidgets import QWidget, QStyle, QDialog +from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem + +from E5Gui import E5FileDialog + +from .Ui_DownloadItem import Ui_DownloadItem + +from .DownloadUtilities import timeString, dataString +from WebBrowser.WebBrowserWindow import WebBrowserWindow + +import UI.PixmapCache +import Utilities.MimeTypes + + +class DownloadItem(QWidget, Ui_DownloadItem): + """ + Class implementing a widget controlling a download. + + @signal statusChanged() emitted upon a status change of a download + @signal downloadFinished() emitted when a download finished + @signal progress(int, int) emitted to signal the download progress + """ + statusChanged = pyqtSignal() + downloadFinished = pyqtSignal() + progress = pyqtSignal(int, int) + + Downloading = 0 + DownloadSuccessful = 1 + DownloadCancelled = 2 + + def __init__(self, downloadItem=None, parent=None): + """ + Constructor + + @param downloadItem reference to the download object containing the + download data. + @keyparam parent reference to the parent widget (QWidget) + @type QWebEngineDownloadItem + """ + super(DownloadItem, self).__init__(parent) + self.setupUi(self) + + p = self.infoLabel.palette() + p.setColor(QPalette.Text, Qt.darkGray) + self.infoLabel.setPalette(p) + + self.progressBar.setMaximum(0) + + self.stopButton.setIcon(UI.PixmapCache.getIcon("stopLoading.png")) + self.openButton.setIcon(UI.PixmapCache.getIcon("open.png")) + self.openButton.setEnabled(False) + self.openButton.setVisible(False) + + self.__state = DownloadItem.Downloading + + icon = self.style().standardIcon(QStyle.SP_FileIcon) + self.fileIcon.setPixmap(icon.pixmap(48, 48)) + + self.__downloadItem = downloadItem + self.__pageUrl = \ + WebBrowserWindow.mainWindow().getWindow().currentBrowser().url() + self.__bytesReceived = 0 + self.__bytesTotal = -1 + self.__downloadTime = QTime() + self.__fileName = "" + self.__originalFileName = "" + self.__finishedDownloading = False + self.__gettingFileName = False + self.__canceledFileSelect = False + self.__autoOpen = False + + self.__initialize() + + def __initialize(self): + """ + Private method to initialize the widget. + """ + if self.__downloadItem is None: + return + + self.__finishedDownloading = False + self.__bytesReceived = 0 + self.__bytesTotal = -1 + + # start timer for the download estimation + self.__downloadTime.start() + + # attach to the download item object + self.__url = self.__downloadItem.url() + self.__downloadItem.downloadProgress.connect(self.__downloadProgress) + self.__downloadItem.finished.connect(self.__finished) + + # reset info + self.infoLabel.clear() + self.progressBar.setValue(0) + self.__getFileName() + if not self.__fileName: + self.__downloadItem.cancel() + else: + self.__downloadItem.setPath(self.__fileName) + self.__downloadItem.accept() + + def __getFileName(self): + """ + Private method to get the file name to save to from the user. + """ + if self.__gettingFileName: + return + + downloadDirectory = WebBrowserWindow\ + .downloadManager().downloadDirectory() + + if self.__fileName: + fileName = self.__fileName + originalFileName = self.__originalFileName + self.__toDownload = True + ask = False + else: + defaultFileName, originalFileName = \ + self.__saveFileName(downloadDirectory) + fileName = defaultFileName + self.__originalFileName = originalFileName + ask = True + self.__autoOpen = False + from .DownloadAskActionDialog import DownloadAskActionDialog + url = self.__downloadItem.url() + mimetype = Utilities.MimeTypes.mimeType(originalFileName) + dlg = DownloadAskActionDialog( + QFileInfo(originalFileName).fileName(), + mimetype, + "{0}://{1}".format(url.scheme(), url.authority()), + self) + if dlg.exec_() == QDialog.Rejected or dlg.getAction() == "cancel": + self.progressBar.setVisible(False) + self.on_stopButton_clicked() + self.filenameLabel.setText( + self.tr("Download canceled: {0}").format( + QFileInfo(defaultFileName).fileName())) + self.__canceledFileSelect = True + return + + if dlg.getAction() == "scan": + self.__mainWindow.requestVirusTotalScan(url) + + self.progressBar.setVisible(False) + self.on_stopButton_clicked() + self.filenameLabel.setText( + self.tr("VirusTotal scan scheduled: {0}").format( + QFileInfo(defaultFileName).fileName())) + self.__canceledFileSelect = True + return + + self.__autoOpen = dlg.getAction() == "open" + if PYQT_VERSION_STR >= "5.0.0": + from PyQt5.QtCore import QStandardPaths + tempLocation = QStandardPaths.standardLocations( + QStandardPaths.TempLocation)[0] + else: + from PyQt5.QtGui import QDesktopServices + tempLocation = QDesktopServices.storageLocation( + QDesktopServices.TempLocation) + fileName = tempLocation + '/' + \ + QFileInfo(fileName).completeBaseName() + + if ask and not self.__autoOpen: + self.__gettingFileName = True + fileName = E5FileDialog.getSaveFileName( + None, + self.tr("Save File"), + defaultFileName, + "") + self.__gettingFileName = False + if not fileName: + self.progressBar.setVisible(False) + self.on_stopButton_clicked() + self.filenameLabel.setText( + self.tr("Download canceled: {0}") + .format(QFileInfo(defaultFileName).fileName())) + self.__canceledFileSelect = True + return + + fileInfo = QFileInfo(fileName) + WebBrowserWindow.downloadManager()\ + .setDownloadDirectory(fileInfo.absoluteDir().absolutePath()) + self.filenameLabel.setText(fileInfo.fileName()) + + self.__fileName = fileName + + # check file path for saving + saveDirPath = QFileInfo(self.__fileName).dir() + if not saveDirPath.exists(): + if not saveDirPath.mkpath(saveDirPath.absolutePath()): + self.progressBar.setVisible(False) + self.on_stopButton_clicked() + self.infoLabel.setText(self.tr( + "Download directory ({0}) couldn't be created.") + .format(saveDirPath.absolutePath())) + return + + self.filenameLabel.setText(QFileInfo(self.__fileName).fileName()) + + def __saveFileName(self, directory): + """ + Private method to calculate a name for the file to download. + + @param directory name of the directory to store the file into (string) + @return proposed filename and original filename (string, string) + """ + path = self.__downloadItem.path() + info = QFileInfo(path) + baseName = info.completeBaseName() + endName = info.suffix() + + origName = baseName + if endName: + origName += '.' + endName + + name = directory + baseName + if endName: + name += '.' + endName + return name, origName + + def __open(self): + """ + Private slot to open the downloaded file. + """ + info = QFileInfo(self.__fileName) + url = QUrl.fromLocalFile(info.absoluteFilePath()) + QDesktopServices.openUrl(url) + + @pyqtSlot() + def on_stopButton_clicked(self): + """ + Private slot to stop the download. + """ + self.cancelDownload() + + def cancelDownload(self): + """ + Public slot to stop the download. + """ + self.setUpdatesEnabled(False) + self.stopButton.setEnabled(False) + self.stopButton.setVisible(False) + self.openButton.setEnabled(False) + self.openButton.setVisible(False) + self.setUpdatesEnabled(True) + self.__state = DownloadItem.DownloadCancelled + self.__downloadItem.cancel() + self.downloadFinished.emit() + + @pyqtSlot() + def on_openButton_clicked(self): + """ + Private slot to open the downloaded file. + """ + self.openFile() + + def openFile(self): + """ + Public slot to open the downloaded file. + """ + info = QFileInfo(self.__fileName) + url = QUrl.fromLocalFile(info.absoluteFilePath()) + QDesktopServices.openUrl(url) + + def openFolder(self): + """ + Public slot to open the folder containing the downloaded file. + """ + info = QFileInfo(self.__fileName) + url = QUrl.fromLocalFile(info.absolutePath()) + QDesktopServices.openUrl(url) + + def __downloadProgress(self, bytesReceived, bytesTotal): + """ + Private method to show the download progress. + + @param bytesReceived number of bytes received (integer) + @param bytesTotal number of total bytes (integer) + """ + self.__bytesReceived = bytesReceived + self.__bytesTotal = bytesTotal + currentValue = 0 + totalValue = 0 + if bytesTotal > 0: + currentValue = bytesReceived * 100 / bytesTotal + totalValue = 100 + self.progressBar.setValue(currentValue) + self.progressBar.setMaximum(totalValue) + + self.progress.emit(currentValue, totalValue) + self.__updateInfoLabel() + + def bytesTotal(self): + """ + Public method to get the total number of bytes of the download. + + @return total number of bytes (integer) + """ + if self.__bytesTotal == -1: + self.__bytesTotal = self.__downloadItem.totalBytes() + return self.__bytesTotal + + def bytesReceived(self): + """ + Public method to get the number of bytes received. + + @return number of bytes received (integer) + """ + return self.__bytesReceived + + def remainingTime(self): + """ + Public method to get an estimation for the remaining time. + + @return estimation for the remaining time (float) + """ + if not self.downloading(): + return -1.0 + + if self.bytesTotal() == -1: + return -1.0 + + cSpeed = self.currentSpeed() + if cSpeed != 0: + timeRemaining = (self.bytesTotal() - self.bytesReceived()) / cSpeed + else: + timeRemaining = 1 + + # ETA should never be 0 + if timeRemaining == 0: + timeRemaining = 1 + + return timeRemaining + + def currentSpeed(self): + """ + Public method to get an estimation for the download speed. + + @return estimation for the download speed (float) + """ + if not self.downloading(): + return -1.0 + + return self.__bytesReceived * 1000.0 / self.__downloadTime.elapsed() + + def __updateInfoLabel(self): + """ + Private method to update the info label. + """ + bytesTotal = self.bytesTotal() + running = not self.downloadedSuccessfully() + + speed = self.currentSpeed() + timeRemaining = self.remainingTime() + + info = "" + if running: + remaining = "" + + if bytesTotal > 0: + remaining = timeString(timeRemaining) + + info = self.tr("{0} of {1} ({2}/sec)\n{3}")\ + .format( + dataString(self.__bytesReceived), + bytesTotal == -1 and self.tr("?") or + dataString(bytesTotal), + dataString(int(speed)), + remaining) + else: + if self.__bytesReceived == bytesTotal or bytesTotal == -1: + info = self.tr("{0} downloaded")\ + .format(dataString(self.__output.size())) + else: + info = self.tr("{0} of {1} - Stopped")\ + .format(dataString(self.__bytesReceived), + dataString(bytesTotal)) + self.infoLabel.setText(info) + + def downloading(self): + """ + Public method to determine, if a download is in progress. + + @return flag indicating a download is in progress (boolean) + """ + return self.__state == DownloadItem.Downloading + + def downloadedSuccessfully(self): + """ + Public method to check for a successful download. + + @return flag indicating a successful download (boolean) + """ + return self.__state == DownloadItem.DownloadSuccessful + + def downloadCanceled(self): + """ + Public method to check, if the download was cancelled. + + @return flag indicating a canceled download (boolean) + """ + return self.__state == DownloadItem.DownloadCancelled + + def __finished(self): + """ + Private slot to handle the download finished. + """ + self.__finishedDownloading = True + + noError = (self.__downloadItem.state() == + QWebEngineDownloadItem.DownloadCompleted) + + self.progressBar.setVisible(False) + self.stopButton.setEnabled(False) + self.stopButton.setVisible(False) + self.openButton.setEnabled(noError) + self.openButton.setVisible(noError) + self.__updateInfoLabel() + self.__state = DownloadItem.DownloadSuccessful + self.statusChanged.emit() + self.downloadFinished.emit() + + if self.__autoOpen: + self.__open() + + def canceledFileSelect(self): + """ + Public method to check, if the user canceled the file selection. + + @return flag indicating cancellation (boolean) + """ + return self.__canceledFileSelect + + def setIcon(self, icon): + """ + Public method to set the download icon. + + @param icon reference to the icon to be set (QIcon) + """ + self.fileIcon.setPixmap(icon.pixmap(48, 48)) + + def fileName(self): + """ + Public method to get the name of the output file. + + @return name of the output file (string) + """ + return self.__fileName + + def absoluteFilePath(self): + """ + Public method to get the absolute path of the output file. + + @return absolute path of the output file (string) + """ + return QFileInfo(self.__fileName).absoluteFilePath() + + def getData(self): + """ + Public method to get the relevant download data. + + @return tuple of URL, save location, flag and the + URL of the related web page (QUrl, string, boolean,QUrl) + """ + return (self.__url, QFileInfo(self.__fileName).filePath(), + self.downloadedSuccessfully(), self.__pageUrl) + + def setData(self, data): + """ + Public method to set the relevant download data. + + @param data tuple of URL, save location, flag and the + URL of the related web page (QUrl, string, boolean, QUrl) + """ + self.__url = data[0] + self.__fileName = data[1] + self.__pageUrl = data[3] + + self.filenameLabel.setText(QFileInfo(self.__fileName).fileName()) + self.infoLabel.setText(self.__fileName) + + self.stopButton.setEnabled(False) + self.stopButton.setVisible(False) + self.openButton.setEnabled(data[2]) + self.openButton.setVisible(data[2]) + if data[2]: + self.__state = DownloadItem.DownloadSuccessful + else: + self.__state = DownloadItem.DownloadCancelled + self.progressBar.setVisible(False) + + def getInfoData(self): + """ + Public method to get the text of the info label. + + @return text of the info label (string) + """ + return self.infoLabel.text() + + def getPageUrl(self): + """ + Public method to get the URL of the download page. + + @return URL of the download page (QUrl) + """ + return self.__pageUrl
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Download/DownloadItem.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DownloadItem</class> + <widget class="QWidget" name="DownloadItem"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>87</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string/> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="fileIcon"> + <property name="text"> + <string>Icon</string> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="filenameLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Filename</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QProgressBar" name="progressBar"/> + </item> + <item> + <widget class="QLabel" name="infoLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string notr="true">Info</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QToolButton" name="stopButton"> + <property name="toolTip"> + <string>Press to cancel the download</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="openButton"> + <property name="toolTip"> + <string>Press to open the downloaded file</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>stopButton</tabstop> + <tabstop>openButton</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Download/DownloadManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,482 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the download manager class. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt, QModelIndex, QFileInfo, QUrl +from PyQt5.QtGui import QCursor, QKeySequence +from PyQt5.QtWidgets import QDialog, QStyle, QFileIconProvider, QMenu, \ + QApplication, QShortcut + +from E5Gui import E5MessageBox + +from .Ui_DownloadManager import Ui_DownloadManager + +from .DownloadModel import DownloadModel + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + +from Utilities.AutoSaver import AutoSaver +import UI.PixmapCache +import Preferences + + +class DownloadManager(QDialog, Ui_DownloadManager): + """ + Class implementing the download manager. + """ + RemoveNever = 0 + RemoveExit = 1 + RemoveSuccessFullDownload = 2 + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(DownloadManager, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.__saveTimer = AutoSaver(self, self.save) + + self.__model = DownloadModel(self) + self.__manager = WebBrowserWindow.networkManager() + + self.__iconProvider = None + self.__downloads = [] + self.__downloadDirectory = "" + self.__loaded = False + + self.setDownloadDirectory(Preferences.getUI("DownloadPath")) + + self.downloadsView.setShowGrid(False) + self.downloadsView.verticalHeader().hide() + self.downloadsView.horizontalHeader().hide() + self.downloadsView.setAlternatingRowColors(True) + self.downloadsView.horizontalHeader().setStretchLastSection(True) + self.downloadsView.setModel(self.__model) + self.downloadsView.setContextMenuPolicy(Qt.CustomContextMenu) + self.downloadsView.customContextMenuRequested.connect( + self.__customContextMenuRequested) + + self.__clearShortcut = QShortcut(QKeySequence("Ctrl+L"), self) + self.__clearShortcut.activated.connect(self.on_cleanupButton_clicked) + + self.__load() + + def __customContextMenuRequested(self, pos): + """ + Private slot to handle the context menu request for the bookmarks tree. + + @param pos position the context menu was requested (QPoint) + """ + menu = QMenu() + + selectedRowsCount = len( + self.downloadsView.selectionModel().selectedRows()) + + if selectedRowsCount == 1: + row = self.downloadsView.selectionModel().selectedRows()[0].row() + itm = self.__downloads[row] + if itm.downloadedSuccessfully(): + menu.addAction( + UI.PixmapCache.getIcon("open.png"), + self.tr("Open"), self.__contextMenuOpen) + elif itm.downloading(): + menu.addAction( + UI.PixmapCache.getIcon("stopLoading.png"), + self.tr("Cancel"), self.__contextMenuCancel) + menu.addSeparator() + menu.addAction( + self.tr("Open Containing Folder"), + self.__contextMenuOpenFolder) + menu.addSeparator() + menu.addAction( + self.tr("Go to Download Page"), + self.__contextMenuGotoPage) + menu.addAction( + self.tr("Copy Download Link"), + self.__contextMenuCopyLink) + menu.addSeparator() + menu.addAction(self.tr("Select All"), self.__contextMenuSelectAll) + if selectedRowsCount > 1 or \ + (selectedRowsCount == 1 and + not self.__downloads[ + self.downloadsView.selectionModel().selectedRows()[0].row()] + .downloading()): + menu.addSeparator() + menu.addAction( + self.tr("Remove From List"), + self.__contextMenuRemoveSelected) + + menu.exec_(QCursor.pos()) + + def shutdown(self): + """ + Public method to stop the download manager. + """ + self.__saveTimer.changeOccurred() + self.__saveTimer.saveIfNeccessary() + self.close() + + def activeDownloads(self): + """ + Public method to get the number of active downloads. + + @return number of active downloads (integer) + """ + count = 0 + + for download in self.__downloads: + if download.downloading(): + count += 1 + return count + + def allowQuit(self): + """ + Public method to check, if it is ok to quit. + + @return flag indicating allowance to quit (boolean) + """ + if self.activeDownloads() > 0: + res = E5MessageBox.yesNo( + self, + self.tr(""), + self.tr("""There are %n downloads in progress.\n""" + """Do you want to quit anyway?""", "", + self.activeDownloads()), + icon=E5MessageBox.Warning) + if not res: + self.show() + return False + return True + + def download(self, downloadItem): + """ + Public method to download a file. + + @param downloadItem reference to the download object containing the + download data. + @type QWebEngineDownloadItem + """ + if downloadItem.url().isEmpty(): + return + + from .DownloadItem import DownloadItem + itm = DownloadItem(downloadItem, parent=self) + self.__addItem(itm) + + if itm.canceledFileSelect(): + return + + if not self.isVisible(): + self.show() + + self.activateWindow() + self.raise_() + + def __addItem(self, itm): + """ + Private method to add a download to the list of downloads. + + @param itm reference to the download item (DownloadItem) + """ + itm.statusChanged.connect(self.__updateRow) + itm.downloadFinished.connect(self.__finished) + + row = len(self.__downloads) + self.__model.beginInsertRows(QModelIndex(), row, row) + self.__downloads.append(itm) + self.__model.endInsertRows() + + self.downloadsView.setIndexWidget(self.__model.index(row, 0), itm) + icon = self.style().standardIcon(QStyle.SP_FileIcon) + itm.setIcon(icon) + self.downloadsView.setRowHeight(row, itm.sizeHint().height() * 1.5) + # just in case the download finished before the constructor returned + self.__updateRow(itm) + self.changeOccurred() + self.__updateActiveItemCount() + + def __updateRow(self, itm=None): + """ + Private slot to update a download item. + + @param itm reference to the download item (DownloadItem) + """ + if itm is None: + itm = self.sender() + + if itm not in self.__downloads: + return + + row = self.__downloads.index(itm) + + if self.__iconProvider is None: + self.__iconProvider = QFileIconProvider() + + icon = self.__iconProvider.icon(QFileInfo(itm.fileName())) + if icon.isNull(): + icon = self.style().standardIcon(QStyle.SP_FileIcon) + itm.setIcon(icon) + + oldHeight = self.downloadsView.rowHeight(row) + self.downloadsView.setRowHeight( + row, + max(oldHeight, itm.minimumSizeHint().height() * 1.5)) + + remove = False + + if itm.downloadedSuccessfully() and \ + self.removePolicy() == DownloadManager.RemoveSuccessFullDownload: + remove = True + + if remove: + self.__model.removeRow(row) + + self.cleanupButton.setEnabled( + (len(self.__downloads) - self.activeDownloads()) > 0) + + # record the change + self.changeOccurred() + + def removePolicy(self): + """ + Public method to get the remove policy. + + @return remove policy (integer) + """ + return Preferences.getWebBrowser("DownloadManagerRemovePolicy") + + def setRemovePolicy(self, policy): + """ + Public method to set the remove policy. + + @param policy policy to be set + (DownloadManager.RemoveExit, DownloadManager.RemoveNever, + DownloadManager.RemoveSuccessFullDownload) + """ + assert policy in (DownloadManager.RemoveExit, + DownloadManager.RemoveNever, + DownloadManager.RemoveSuccessFullDownload) + + if policy == self.removePolicy(): + return + + Preferences.setWebBrowser("DownloadManagerRemovePolicy", self.policy) + + def save(self): + """ + Public method to save the download settings. + """ + if not self.__loaded: + return + + Preferences.setWebBrowser("DownloadManagerSize", self.size()) + Preferences.setWebBrowser("DownloadManagerPosition", self.pos()) + if self.removePolicy() == DownloadManager.RemoveExit: + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if WebBrowserWindow.isPrivate(): + return + + downloads = [] + for download in self.__downloads: + downloads.append(download.getData()) + Preferences.setWebBrowser("DownloadManagerDownloads", downloads) + + def __load(self): + """ + Private method to load the download settings. + """ + if self.__loaded: + return + + size = Preferences.getWebBrowser("DownloadManagerSize") + if size.isValid(): + self.resize(size) + pos = Preferences.getWebBrowser("DownloadManagerPosition") + self.move(pos) + + downloads = Preferences.getWebBrowser("DownloadManagerDownloads") + for download in downloads: + if not download[0].isEmpty() and \ + download[1] != "": + from .DownloadItem import DownloadItem + itm = DownloadItem(parent=self) + itm.setData(download) + self.__addItem(itm) + self.cleanupButton.setEnabled( + (len(self.__downloads) - self.activeDownloads()) > 0) + + self.__loaded = True + self.__updateActiveItemCount() + + def cleanup(self): + """ + Public slot to cleanup the downloads. + """ + self.on_cleanupButton_clicked() + + @pyqtSlot() + def on_cleanupButton_clicked(self): + """ + Private slot cleanup the downloads. + """ + if len(self.__downloads) == 0: + return + + self.__model.removeRows(0, len(self.__downloads)) + if len(self.__downloads) == 0 and \ + self.__iconProvider is not None: + self.__iconProvider = None + + self.changeOccurred() + self.__updateActiveItemCount() + + def __updateItemCount(self): + """ + Private method to update the count label. + """ + count = len(self.__downloads) + self.countLabel.setText(self.tr("%n Download(s)", "", count)) + + def __updateActiveItemCount(self): + """ + Private method to update the window title. + """ + count = self.activeDownloads() + if count > 0: + self.setWindowTitle( + self.tr("Downloading %n file(s)", "", count)) + else: + self.setWindowTitle(self.tr("Downloads")) + + def __finished(self): + """ + Private slot to handle a finished download. + """ + self.__updateActiveItemCount() + if self.isVisible(): + QApplication.alert(self) + + def setDownloadDirectory(self, directory): + """ + Public method to set the current download directory. + + @param directory current download directory (string) + """ + self.__downloadDirectory = directory + if self.__downloadDirectory != "": + self.__downloadDirectory += "/" + + def downloadDirectory(self): + """ + Public method to get the current download directory. + + @return current download directory (string) + """ + return self.__downloadDirectory + + def count(self): + """ + Public method to get the number of downloads. + + @return number of downloads (integer) + """ + return len(self.__downloads) + + def downloads(self): + """ + Public method to get a reference to the downloads. + + @return reference to the downloads (list of DownloadItem) + """ + return self.__downloads + + def changeOccurred(self): + """ + Public method to signal a change. + """ + self.__saveTimer.changeOccurred() + self.__updateItemCount() + + ########################################################################### + ## Context menu related methods below + ########################################################################### + + def __currentItem(self): + """ + Private method to get a reference to the current item. + + @return reference to the current item (DownloadItem) + """ + index = self.downloadsView.currentIndex() + if index and index.isValid(): + row = index.row() + return self.__downloads[row] + + return None + + def __contextMenuOpen(self): + """ + Private method to open the downloaded file. + """ + itm = self.__currentItem() + if itm is not None: + itm.openFile() + + def __contextMenuOpenFolder(self): + """ + Private method to open the folder containing the downloaded file. + """ + itm = self.__currentItem() + if itm is not None: + itm.openFolder() + + def __contextMenuCancel(self): + """ + Private method to cancel the current download. + """ + itm = self.__currentItem() + if itm is not None: + itm.cancelDownload() + + def __contextMenuGotoPage(self): + """ + Private method to open the download page. + """ + itm = self.__currentItem() + if itm is not None: + url = itm.getPageUrl() + WebBrowserWindow.mainWindow().openUrl(url, "") + + def __contextMenuCopyLink(self): + """ + Private method to copy the download link to the clipboard. + """ + itm = self.__currentItem() + if itm is not None: + url = itm.getPageUrl().toDisplayString(QUrl.FullyDecoded) + QApplication.clipboard().setText(url) + + def __contextMenuSelectAll(self): + """ + Private method to select all downloads. + """ + self.downloadsView.selectAll() + + def __contextMenuRemoveSelected(self): + """ + Private method to remove the selected downloads from the list. + """ + self.downloadsView.removeSelected()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Download/DownloadManager.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DownloadManager</class> + <widget class="QDialog" name="DownloadManager"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Downloads</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="3"> + <widget class="E5TableView" name="downloadsView"/> + </item> + <item row="1" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="cleanupButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Press to clean up the list of downloads</string> + </property> + <property name="text"> + <string>Clear List</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="countLabel"> + <property name="text"> + <string>0 Items</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5TableView</class> + <extends>QTableView</extends> + <header>E5Gui/E5TableView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>downloadsView</tabstop> + <tabstop>cleanupButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>DownloadManager</receiver> + <slot>close()</slot> + <hints> + <hint type="sourcelabel"> + <x>330</x> + <y>274</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>294</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Download/DownloadModel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the download model. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex, QMimeData, QUrl + + +class DownloadModel(QAbstractListModel): + """ + Class implementing the download model. + """ + def __init__(self, manager, parent=None): + """ + Constructor + + @param manager reference to the download manager (DownloadManager) + @param parent reference to the parent object (QObject) + """ + super(DownloadModel, self).__init__(parent) + + self.__manager = manager + + def data(self, index, role): + """ + Public method to get data from the model. + + @param index index to get data for (QModelIndex) + @param role role of the data to retrieve (integer) + @return requested data + """ + if index.row() < 0 or index.row() >= self.rowCount(index.parent()): + return None + + if role == Qt.ToolTipRole: + if self.__manager.downloads()[index.row()]\ + .downloadedSuccessfully(): + return self.__manager.downloads()[index.row()].getInfoData() + + return None + + def rowCount(self, parent=QModelIndex()): + """ + Public method to get the number of rows of the model. + + @param parent parent index (QModelIndex) + @return number of rows (integer) + """ + if parent.isValid(): + return 0 + else: + return self.__manager.count() + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove bookmarks from the model. + + @param row row of the first bookmark to remove (integer) + @param count number of bookmarks to remove (integer) + @param parent index of the parent bookmark node (QModelIndex) + @return flag indicating successful removal (boolean) + """ + if parent.isValid(): + return False + + if row < 0 or count <= 0 or row + count > self.rowCount(parent): + return False + + lastRow = row + count - 1 + for i in range(lastRow, row - 1, -1): + if not self.__manager.downloads()[i].downloading(): + self.beginRemoveRows(parent, i, i) + del self.__manager.downloads()[i] + self.endRemoveRows() + self.__manager.changeOccurred() + return True + + def flags(self, index): + """ + Public method to get flags for an item. + + @param index index of the node cell (QModelIndex) + @return flags (Qt.ItemFlags) + """ + if index.row() < 0 or index.row() >= self.rowCount(index.parent()): + return Qt.NoItemFlags + + defaultFlags = QAbstractListModel.flags(self, index) + + itm = self.__manager.downloads()[index.row()] + if itm.downloadedSuccessfully(): + return defaultFlags | Qt.ItemIsDragEnabled + + return defaultFlags + + def mimeData(self, indexes): + """ + Public method to return the mime data. + + @param indexes list of indexes (QModelIndexList) + @return mime data (QMimeData) + """ + mimeData = QMimeData() + urls = [] + for index in indexes: + if index.isValid(): + itm = self.__manager.downloads()[index.row()] + urls.append(QUrl.fromLocalFile(itm.absoluteFilePath())) + mimeData.setUrls(urls) + return mimeData
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Download/DownloadUtilities.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing some utility functions for the Download package. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QCoreApplication + +from Globals import translate + + +def timeString(timeRemaining): + """ + Module function to format the given time. + + @param timeRemaining time to be formatted (float) + @return time string (string) + """ + if timeRemaining > 60: + minutes = int(timeRemaining / 60) + seconds = int(timeRemaining % 60) + remaining = translate( + "DownloadUtilities", + "%n:{0:02} minutes remaining", "", + minutes).format(seconds) + else: + seconds = int(timeRemaining) + remaining = translate( + "DownloadUtilities", + "%n seconds remaining", "", seconds) + + return remaining + + +def dataString(size): + """ + Module function to generate a formatted size string. + + @param size size to be formatted (integer) + @return formatted data string (string) + """ + unit = "" + if size < 1024: + unit = QCoreApplication.translate("DownloadUtilities", "Bytes") + elif size < 1024 * 1024: + size /= 1024 + unit = QCoreApplication.translate("DownloadUtilities", "KiB") + elif size < 1024 * 1024 * 1024: + size /= 1024 * 1024 + unit = QCoreApplication.translate("DownloadUtilities", "MiB") + else: + size /= 1024 * 1024 * 1024 + unit = QCoreApplication.translate("DownloadUtilities", "GiB") + return "{0:.1f} {1}".format(size, unit)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Download/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing all download related modules. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FeaturePermissions/FeaturePermissionBar.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the feature permission bar widget. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, QUrl +from PyQt5.QtWidgets import QLabel, QHBoxLayout, QPushButton +from PyQt5.QtWebEngineWidgets import QWebEnginePage + +from E5Gui.E5AnimatedWidget import E5AnimatedWidget + +import UI.PixmapCache + + +class FeaturePermissionBar(E5AnimatedWidget): + """ + Class implementing the feature permission bar widget. + """ + DefaultHeight = 30 + + def __init__(self, page, origin, feature, manager): + """ + Constructor + + @param page reference to the web page + @type QWebView + @param origin security origin requesting the feature + @type QUrl + @param feature requested feature + @type QWebPage.Feature + @param manager reference to the feature permissions manager + @type FeaturePermissionManager + """ + super(FeaturePermissionBar, self).__init__(parent=page.view()) + + self.__origin = QUrl(origin) + self.__feature = feature + self.__page = page + self.__manager = manager + + self.__permissionFeatureTexts = { + # TODO: Qt 5.7? +## QWebEnginePage.Notifications: +## self.tr("{0} wants to use desktop notifications."), + QWebEnginePage.Geolocation: + self.tr("{0} wants to use your position."), + QWebEnginePage.MediaAudioCapture: + self.tr("{0} wants to use your microphone."), + QWebEnginePage.MediaVideoCapture: + self.tr("{0} wants to use your camera."), + QWebEnginePage.MediaAudioVideoCapture: + self.tr("{0} wants to use your microphone and camera."), + QWebEnginePage.MouseLock: + self.tr("{0} wants to lock your mouse."), + } + self.__permissionFeatureIconNames = { + # TODO: Qt 5.7? +## QWebEnginePage.Notifications: "notification.png", + QWebEnginePage.Geolocation: "geolocation.png", + QWebEnginePage.MediaAudioCapture: "audiocapture.png", + QWebEnginePage.MediaVideoCapture: "camera.png", + QWebEnginePage.MediaAudioVideoCapture: "audio-video.png", + QWebEnginePage.MouseLock: "mouse.png", + } + + self.setAutoFillBackground(True) + self.__layout = QHBoxLayout() + self.setLayout(self.__layout) + self.__layout.setContentsMargins(9, 0, 0, 0) + self.__iconLabel = QLabel(self) + self.__layout.addWidget(self.__iconLabel) + self.__messageLabel = QLabel(self) + self.__layout.addWidget(self.__messageLabel) + self.__layout.addStretch() + self.__rememberButton = QPushButton(self.tr("Remember"), self) + self.__rememberButton.setCheckable(True) + self.__allowButton = QPushButton(self.tr("Allow"), self) + self.__denyButton = QPushButton(self.tr("Deny"), self) + self.__discardButton = QPushButton(UI.PixmapCache.getIcon("close.png"), + "", self) + self.__allowButton.clicked.connect(self.__permissionGranted) + self.__denyButton.clicked.connect(self.__permissionDenied) + self.__discardButton.clicked.connect(self.__permissionUnknown) + self.__layout.addWidget(self.__rememberButton) + self.__layout.addWidget(self.__allowButton) + self.__layout.addWidget(self.__denyButton) + self.__layout.addWidget(self.__discardButton) + + try: + self.__iconLabel.setPixmap(UI.PixmapCache.getPixmap( + self.__permissionFeatureIconNames[self.__feature])) + except KeyError: + pass + + try: + self.__messageLabel.setText( + self.__permissionFeatureTexts[self.__feature].format( + self.__origin.host())) + except KeyError: + self.__messageLabel.setText( + self.tr("{0} wants to use an unknown feature.").format( + self.__origin.host())) + + self.__page.loadStarted.connect(self.hide) + + self.resize(self.__page.view().width(), self.height()) + self.startAnimation() + + @pyqtSlot() + def hide(self): + """ + Public slot to hide the animated widget. + """ + self.__page.loadStarted.disconnect(self.hide) + super(FeaturePermissionBar, self).hide() + + def __permissionDenied(self): + """ + Private slot handling the user pressing the deny button. + """ + if self.__page is None or self.__manager is None: + return + + self.__page.setFeaturePermission( + self.__origin, self.__feature, + QWebEnginePage.PermissionDeniedByUser) + + if self.__rememberButton.isChecked(): + self.__manager.rememberFeaturePermission( + self.__page.url().host(), self.__feature, + QWebEnginePage.PermissionDeniedByUser) + + self.hide() + + def __permissionGranted(self): + """ + Private slot handling the user pressing the allow button. + """ + if self.__page is None or self.__manager is None: + return + + self.__page.setFeaturePermission( + self.__origin, self.__feature, + QWebEnginePage.PermissionGrantedByUser) + + if self.__rememberButton.isChecked(): + self.__manager.rememberFeaturePermission( + self.__page.url().host(), self.__feature, + QWebEnginePage.PermissionGrantedByUser) + + self.hide() + + def __permissionUnknown(self): + """ + Private slot handling the user closing the dialog without. + """ + if self.__page is None or self.__manager is None: + return + + self.__page.setFeaturePermission( + self.__origin, self.__feature, + QWebEnginePage.PermissionUnknown) + + self.hide()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FeaturePermissions/FeaturePermissionManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the feature permission manager object. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QObject +from PyQt5.QtWidgets import QDialog +from PyQt5.QtWebEngineWidgets import QWebEnginePage + +import Globals +import Preferences + + +class FeaturePermissionManager(QObject): + """ + Class implementing the feature permission manager object. + """ + SettingsKeyFormat = "WebBrowser/FeaturePermissions/{0}" + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object + @type QObject + """ + super(FeaturePermissionManager, self).__init__(parent) + + self.__featurePermissions = { + # TODO: Qt 5.7? +## QWebEnginePage.Notifications: { +## QWebEnginePage.PermissionGrantedByUser: [], +## QWebEnginePage.PermissionDeniedByUser: [], +## }, + QWebEnginePage.Geolocation: { + QWebEnginePage.PermissionGrantedByUser: [], + QWebEnginePage.PermissionDeniedByUser: [], + }, + QWebEnginePage.MediaAudioCapture: { + QWebEnginePage.PermissionGrantedByUser: [], + QWebEnginePage.PermissionDeniedByUser: [], + }, + QWebEnginePage.MediaVideoCapture: { + QWebEnginePage.PermissionGrantedByUser: [], + QWebEnginePage.PermissionDeniedByUser: [], + }, + QWebEnginePage.MediaAudioVideoCapture: { + QWebEnginePage.PermissionGrantedByUser: [], + QWebEnginePage.PermissionDeniedByUser: [], + }, + QWebEnginePage.MouseLock: { + QWebEnginePage.PermissionGrantedByUser: [], + QWebEnginePage.PermissionDeniedByUser: [], + }, + } + self.__featurePermissionsKeys = { + # TODO: Qt 5.7? +## (QWebEnginePage.Notifications, +## QWebEnginePage.PermissionGrantedByUser): +## "NotificationsGranted", +## (QWebEnginePage.Notifications, +## QWebEnginePage.PermissionDeniedByUser): +## "NotificationsDenied", + (QWebEnginePage.Geolocation, + QWebEnginePage.PermissionGrantedByUser): + "GeolocationGranted", + (QWebEnginePage.Geolocation, + QWebEnginePage.PermissionDeniedByUser): + "GeolocationDenied", + (QWebEnginePage.MediaAudioCapture, + QWebEnginePage.PermissionGrantedByUser): + "MediaAudioCaptureGranted", + (QWebEnginePage.MediaAudioCapture, + QWebEnginePage.PermissionDeniedByUser): + "MediaAudioCaptureDenied", + (QWebEnginePage.MediaVideoCapture, + QWebEnginePage.PermissionGrantedByUser): + "MediaVideoCaptureGranted", + (QWebEnginePage.MediaVideoCapture, + QWebEnginePage.PermissionDeniedByUser): + "MediaVideoCaptureDenied", + (QWebEnginePage.MediaAudioVideoCapture, + QWebEnginePage.PermissionGrantedByUser): + "MediaAudioVideoCaptureGranted", + (QWebEnginePage.MediaAudioVideoCapture, + QWebEnginePage.PermissionDeniedByUser): + "MediaAudioVideoCaptureDenied", + (QWebEnginePage.MouseLock, + QWebEnginePage.PermissionGrantedByUser): + "MouseLockGranted", + (QWebEnginePage.MouseLock, + QWebEnginePage.PermissionDeniedByUser): + "MouseLockDenied", + } + + self.__loaded = False + + def requestFeaturePermission(self, page, origin, feature): + """ + Public method to request a feature permission. + + @param page reference to the requesting web page + @type QWebEnginePage + @param origin security origin requesting the feature + @type QUrl + @param feature requested feature + @type QWebEnginePage.Feature + """ + if origin is None or origin.isEmpty(): + return + + if not self.__loaded: + self.__loadSettings() + + host = origin.host() + + if feature in self.__featurePermissions: + for permission in self.__featurePermissions[feature]: + if host in self.__featurePermissions[feature][permission]: + page.setFeaturePermission(origin, feature, permission) + return + + from .FeaturePermissionBar import FeaturePermissionBar + bar = FeaturePermissionBar(page, origin, feature, self) + bar.show() + + def rememberFeaturePermission(self, host, feature, permission): + """ + Public method to remember a user decision for a feature permission. + + @param host host name to remember the decision for + @type str + @param feature feature to be remembered + @type QWebEnginePage.Feature + @param permission feature permission to be remembered + @type QWebEnginePage.PermissionPolicy + """ + if feature in self.__featurePermissions: + if host not in self.__featurePermissions[feature][permission]: + self.__featurePermissions[feature][permission].append(host) + self.__saveSettings() + + def __loadSettings(self): + """ + Private method to load the remembered feature permissions. + """ + if self.__loaded: + # no reloading allowed + return + + for (feature, permission), key in \ + self.__featurePermissionsKeys.items(): + self.__featurePermissions[feature][permission] = \ + Globals.toList(Preferences.Prefs.settings.value( + FeaturePermissionManager.SettingsKeyFormat.format(key), + [] + )) + + self.__loaded = True + + def __saveSettings(self): + """ + Private method to save the remembered feature permissions. + """ + if not self.__loaded: + return + + import WebBrowser.WebBrowserWindow + if WebBrowser.WebBrowserWindow.WebBrowserWindow.isPrivate(): + return + + for (feature, permission), key in \ + self.__featurePermissionsKeys.items(): + Preferences.Prefs.settings.setValue( + FeaturePermissionManager.SettingsKeyFormat.format(key), + self.__featurePermissions[feature][permission]) + + def showFeaturePermissionsDialog(self): + """ + Public method to show a dialog to manage the remembered feature + permissions. + """ + if not self.__loaded: + self.__loadSettings() + + from .FeaturePermissionsDialog import FeaturePermissionsDialog + dlg = FeaturePermissionsDialog(self.__featurePermissions) + if dlg.exec_() == QDialog.Accepted: + newFeaturePermissions = dlg.getData() + self.__featurePermissions = newFeaturePermissions + self.__saveSettings()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FeaturePermissions/FeaturePermissionsDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the feature permission dialog. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem, QTreeWidget, \ + QAbstractItemView +from PyQt5.QtWebEngineWidgets import QWebEnginePage + +import UI.PixmapCache + +from .Ui_FeaturePermissionsDialog import Ui_FeaturePermissionsDialog + + +class FeaturePermissionsDialog(QDialog, Ui_FeaturePermissionsDialog): + """ + Class implementing the feature permission dialog. + """ + def __init__(self, featurePermissions, parent=None): + """ + Constructor + + @param featurePermissions dictionary with remembered feature + permissions + @type dict of dict of list + @param parent reference to the parent widget + @type QWidget + """ + super(FeaturePermissionsDialog, self).__init__(parent) + self.setupUi(self) + + # add the various lists + # TODO: Qt 5.7? +## self.notifList = QTreeWidget() +## self.notifList.setAlternatingRowColors(True) +## self.notifList.setSelectionMode(QAbstractItemView.ExtendedSelection) +## self.notifList.setRootIsDecorated(False) +## self.notifList.setItemsExpandable(False) +## self.notifList.setAllColumnsShowFocus(True) +## self.notifList.setObjectName("notifList") +## self.notifList.setSortingEnabled(True) +## self.notifList.headerItem().setText(0, self.tr("Host")) +## self.notifList.headerItem().setText(1, self.tr("Permission")) +## self.tabWidget.addTab( +## self.notifList, +## UI.PixmapCache.getIcon("notification.png"), +## self.tr("Notifications")) + + self.geoList = QTreeWidget() + self.geoList.setAlternatingRowColors(True) + self.geoList.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.geoList.setRootIsDecorated(False) + self.geoList.setItemsExpandable(False) + self.geoList.setAllColumnsShowFocus(True) + self.geoList.setObjectName("geoList") + self.geoList.setSortingEnabled(True) + self.geoList.headerItem().setText(0, self.tr("Host")) + self.geoList.headerItem().setText(1, self.tr("Permission")) + self.tabWidget.addTab( + self.geoList, + UI.PixmapCache.getIcon("geolocation.png"), + self.tr("Geolocation")) + + self.micList = QTreeWidget() + self.micList.setAlternatingRowColors(True) + self.micList.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.micList.setRootIsDecorated(False) + self.micList.setItemsExpandable(False) + self.micList.setAllColumnsShowFocus(True) + self.micList.setObjectName("micList") + self.micList.setSortingEnabled(True) + self.micList.headerItem().setText(0, self.tr("Host")) + self.micList.headerItem().setText(1, self.tr("Permission")) + self.tabWidget.addTab( + self.micList, + UI.PixmapCache.getIcon("audiocapture.png"), + self.tr("Microphone")) + + self.camList = QTreeWidget() + self.camList.setAlternatingRowColors(True) + self.camList.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.camList.setRootIsDecorated(False) + self.camList.setItemsExpandable(False) + self.camList.setAllColumnsShowFocus(True) + self.camList.setObjectName("camList") + self.camList.setSortingEnabled(True) + self.camList.headerItem().setText(0, self.tr("Host")) + self.camList.headerItem().setText(1, self.tr("Permission")) + self.tabWidget.addTab( + self.camList, + UI.PixmapCache.getIcon("camera.png"), + self.tr("Camera")) + + self.micCamList = QTreeWidget() + self.micCamList.setAlternatingRowColors(True) + self.micCamList.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.micCamList.setRootIsDecorated(False) + self.micCamList.setItemsExpandable(False) + self.micCamList.setAllColumnsShowFocus(True) + self.micCamList.setObjectName("micCamList") + self.micCamList.setSortingEnabled(True) + self.micCamList.headerItem().setText(0, self.tr("Host")) + self.micCamList.headerItem().setText(1, self.tr("Permission")) + self.tabWidget.addTab( + self.micCamList, + UI.PixmapCache.getIcon("audio-video.png"), + self.tr("Microphone && Camera")) + + self.mouseLockList = QTreeWidget() + self.mouseLockList.setAlternatingRowColors(True) + self.mouseLockList.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.mouseLockList.setRootIsDecorated(False) + self.mouseLockList.setItemsExpandable(False) + self.mouseLockList.setAllColumnsShowFocus(True) + self.mouseLockList.setObjectName("mouseLockList") + self.mouseLockList.setSortingEnabled(True) + self.mouseLockList.headerItem().setText(0, self.tr("Host")) + self.mouseLockList.headerItem().setText(1, self.tr("Permission")) + self.tabWidget.addTab( + self.mouseLockList, + UI.PixmapCache.getIcon("mouse.png"), + self.tr("Mouse Lock")) + + # TODO: Qt 5.7? +## self.setTabOrder(self.tabWidget, self.notifList) +## self.setTabOrder(self.notifList, self.geoList) + self.setTabOrder(self.tabWidget, self.geoList) + self.setTabOrder(self.geoList, self.micList) + self.setTabOrder(self.micList, self.camList) + self.setTabOrder(self.camList, self.micCamList) + self.setTabOrder(self.micCamList, self.mouseLockList) + self.setTabOrder(self.mouseLockList, self.removeButton) + self.setTabOrder(self.removeButton, self.removeAllButton) + + self.__permissionStrings = { + QWebEnginePage.PermissionGrantedByUser: self.tr("Allow"), + QWebEnginePage.PermissionDeniedByUser: self.tr("Deny"), + } + + self.__permissionsLists = { + # TODO: Qt 5.7? +## QWebEnginePage.Notifications: self.notifList, + QWebEnginePage.Geolocation: self.geoList, + QWebEnginePage.MediaAudioCapture: self.micList, + QWebEnginePage.MediaVideoCapture: self.camList, + QWebEnginePage.MediaAudioVideoCapture: self.micCamList, + QWebEnginePage.MouseLock: self.mouseLockList, + } + + for feature, permissionsList in self.__permissionsLists.items(): + for permission in featurePermissions[feature]: + for host in featurePermissions[feature][permission]: + itm = QTreeWidgetItem( + permissionsList, + [host, self.__permissionStrings[permission]]) + itm.setData(0, Qt.UserRole, permission) + + self.__previousCurrent = -1 + self.tabWidget.currentChanged.connect(self.__currentTabChanged) + self.tabWidget.setCurrentIndex(0) + + @pyqtSlot(int) + def __currentTabChanged(self, index): + """ + Private slot handling changes of the selected tab. + + @param index index of the current tab + @type int + """ + if self.__previousCurrent >= 0: + previousList = self.tabWidget.widget(self.__previousCurrent) + previousList.itemSelectionChanged.disconnect( + self.__itemSelectionChanged) + + self.__updateButtons() + + currentList = self.tabWidget.currentWidget() + currentList.itemSelectionChanged.connect(self.__itemSelectionChanged) + self.__previousCurrent = index + + def __updateButtons(self): + """ + Private method to update the buttons. + """ + currentList = self.tabWidget.currentWidget() + self.removeAllButton.setEnabled( + currentList.topLevelItemCount() > 0) + self.removeButton.setEnabled( + len(currentList.selectedItems()) > 0) + + @pyqtSlot() + def __itemSelectionChanged(self): + """ + Private slot handling changes in the current list of selected items. + """ + self.__updateButtons() + + @pyqtSlot() + def on_removeButton_clicked(self): + """ + Private slot to remove selected entries. + """ + currentList = self.tabWidget.currentWidget() + for itm in currentList.selectedItems(): + row = currentList.indexOfTopLevelItem(itm) + itm = currentList.takeTopLevelItem(row) + del itm + self.__updateButtons() + + @pyqtSlot() + def on_removeAllButton_clicked(self): + """ + Private slot to remove all entries. + """ + currentList = self.tabWidget.currentWidget() + while currentList.topLevelItemCount() > 0: + itm = currentList.takeTopLevelItem(0) # __IGNORE_WARNING__ + del itm + self.__updateGeoButtons() + + def getData(self): + """ + Public method to retrieve the dialog contents. + + @return new feature permission settings + @rtype dict of dict of list + """ + featurePermissions = {} + for feature, permissionsList in self.__permissionsLists.items(): + featurePermissions[feature] = { + QWebEnginePage.PermissionGrantedByUser: [], + QWebEnginePage.PermissionDeniedByUser: [], + } + for row in range(permissionsList.topLevelItemCount()): + itm = permissionsList.topLevelItem(row) + host = itm.text(0) + permission = itm.data(0, Qt.UserRole) + featurePermissions[feature][permission].append(host) + + return featurePermissions
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FeaturePermissions/FeaturePermissionsDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,110 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FeaturePermissionsDialog</class> + <widget class="QDialog" name="FeaturePermissionsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>400</height> + </rect> + </property> + <property name="windowTitle"> + <string>HTML5 Feature Permissions</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="removeButton"> + <property name="text"> + <string>&Remove</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeAllButton"> + <property name="text"> + <string>Remove &All</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="0" column="0"> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>-1</number> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>removeAllButton</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>FeaturePermissionsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>227</x> + <y>381</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>FeaturePermissionsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>295</x> + <y>387</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FeaturePermissions/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing feature permission related widgets for the eric6 +web browser. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Feeds/FeedEditDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to edit feed data. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, QUrl +from PyQt5.QtWidgets import QDialog, QDialogButtonBox + +from .Ui_FeedEditDialog import Ui_FeedEditDialog + + +class FeedEditDialog(QDialog, Ui_FeedEditDialog): + """ + Class implementing a dialog to edit feed data. + """ + def __init__(self, urlString, title, parent=None): + """ + Constructor + + @param urlString feed URL (string) + @param title feed title (string) + @param parent reference to the parent widget (QWidget) + """ + super(FeedEditDialog, self).__init__(parent) + self.setupUi(self) + + self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False) + + self.titleEdit.setText(title) + self.urlEdit.setText(urlString) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + def __setOkButton(self): + """ + Private slot to enable or disable the OK button. + """ + enable = True + + enable = enable and bool(self.titleEdit.text()) + + urlString = self.urlEdit.text() + enable = enable and bool(urlString) + if urlString: + url = QUrl(urlString) + enable = enable and bool(url.scheme()) + enable = enable and bool(url.host()) + + self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(enable) + + @pyqtSlot(str) + def on_titleEdit_textChanged(self, txt): + """ + Private slot to handle changes of the feed title. + + @param txt new feed title (string) + """ + self.__setOkButton() + + @pyqtSlot(str) + def on_urlEdit_textChanged(self, txt): + """ + Private slot to handle changes of the feed URL. + + @param txt new feed URL (string) + """ + self.__setOkButton() + + def getData(self): + """ + Public method to get the entered feed data. + + @return tuple of two strings giving the feed URL and feed title + (string, string) + """ + return (self.urlEdit.text(), self.titleEdit.text())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Feeds/FeedEditDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FeedEditDialog</class> + <widget class="QDialog" name="FeedEditDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>475</width> + <height>114</height> + </rect> + </property> + <property name="windowTitle"> + <string>Edit Feed Data</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Fill title and URL of a feed:</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Feed title:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="titleEdit"> + <property name="toolTip"> + <string>Enter the title of the feed</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Feed URL:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="urlEdit"> + <property name="toolTip"> + <string>Enter the URL of the feed</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>titleEdit</tabstop> + <tabstop>urlEdit</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>FeedEditDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>224</x> + <y>122</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>143</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>FeedEditDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>292</x> + <y>128</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>143</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Feeds/FeedsDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to add RSS feeds. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QUrl +from PyQt5.QtWidgets import QDialog, QPushButton, QLabel + +from E5Gui import E5MessageBox + +from .Ui_FeedsDialog import Ui_FeedsDialog + +import UI.PixmapCache + + +class FeedsDialog(QDialog, Ui_FeedsDialog): + """ + Class implementing a dialog to add RSS feeds. + """ + def __init__(self, availableFeeds, browser, parent=None): + """ + Constructor + + @param availableFeeds list of available RSS feeds (list of tuple of + two strings) + @param browser reference to the browser widget (WebBrowserView) + @param parent reference to the parent widget (QWidget) + """ + super(FeedsDialog, self).__init__(parent) + self.setupUi(self) + + self.iconLabel.setPixmap(UI.PixmapCache.getPixmap("rss48.png")) + + self.__browser = browser + + self.__availableFeeds = availableFeeds[:] + for row in range(len(self.__availableFeeds)): + feed = self.__availableFeeds[row] + button = QPushButton(self) + button.setText(self.tr("Add")) + button.feed = feed + label = QLabel(self) + label.setText(feed[0]) + self.feedsLayout.addWidget(label, row, 0) + self.feedsLayout.addWidget(button, row, 1) + button.clicked.connect(self.__addFeed) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + def __addFeed(self): + """ + Private slot to add a RSS feed. + """ + button = self.sender() + urlString = button.feed[1] + url = QUrl(urlString) + if url.isRelative(): + url = self.__browser.url().resolved(url) + urlString = url.toDisplayString(QUrl.FullyDecoded) + + if not url.isValid(): + return + + if button.feed[0]: + title = button.feed[0] + else: + title = self.__browser.url().host() + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + feedsManager = WebBrowserWindow.feedsManager() + if feedsManager.addFeed(urlString, title, self.__browser.icon()): + if WebBrowserWindow.notificationsEnabled(): + WebBrowserWindow.showNotification( + UI.PixmapCache.getPixmap("rss48.png"), + self.tr("Add RSS Feed"), + self.tr("""The feed was added successfully.""")) + else: + E5MessageBox.information( + self, + self.tr("Add RSS Feed"), + self.tr("""The feed was added successfully.""")) + else: + E5MessageBox.warning( + self, + self.tr("Add RSS Feed"), + self.tr("""The feed was already added before.""")) + + self.close()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Feeds/FeedsDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FeedsDialog</class> + <widget class="QDialog" name="FeedsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>352</width> + <height>94</height> + </rect> + </property> + <property name="windowTitle"> + <string>Add Feed</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="iconLabel"> + <property name="text"> + <string notr="true"/> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Add Feeds from this site</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QGridLayout" name="feedsLayout"/> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>FeedsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>FeedsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Feeds/FeedsManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,434 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a RSS feeds manager dialog. +""" + +from __future__ import unicode_literals +try: + str = unicode # __IGNORE_EXCEPTION__ +except NameError: + pass + +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QXmlStreamReader +from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem, QMenu, QApplication +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply + +from E5Gui import E5MessageBox + +from .Ui_FeedsManager import Ui_FeedsManager + +import Preferences +import UI.PixmapCache + + +class FeedsManager(QDialog, Ui_FeedsManager): + """ + Class implementing a RSS feeds manager dialog. + + @signal openUrl(QUrl, str) emitted to open a URL in the current tab + @signal newUrl(QUrl, str) emitted to open a URL in a new tab + """ + openUrl = pyqtSignal(QUrl, str) + newUrl = pyqtSignal(QUrl, str) + + UrlStringRole = Qt.UserRole + ErrorDataRole = Qt.UserRole + 1 + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(FeedsManager, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.__wasShown = False + self.__loaded = False + self.__feeds = [] + self.__replies = {} + # dict key is the id of the request object + # dict value is a tuple of request and tree item + + self.feedsTree.setContextMenuPolicy(Qt.CustomContextMenu) + self.feedsTree.customContextMenuRequested.connect( + self.__customContextMenuRequested) + self.feedsTree.itemActivated.connect(self.__itemActivated) + + def show(self): + """ + Public slot to show the feeds manager dialog. + """ + super(FeedsManager, self).show() + + if not self.__wasShown: + self.__enableButtons() + self.on_reloadAllButton_clicked() + self.__wasShown = True + + def addFeed(self, urlString, title, icon): + """ + Public method to add a feed. + + @param urlString URL of the feed (string) + @param title title of the feed (string) + @param icon icon for the feed (QIcon) + @return flag indicating a successful addition of the feed (boolean) + """ + if urlString == "": + return False + + if not self.__loaded: + self.__load() + + # step 1: check, if feed was already added + for feed in self.__feeds: + if feed[0] == urlString: + return False + + # step 2: add the feed + if icon.isNull(): + icon = UI.PixmapCache.getIcon("rss16.png") + feed = (urlString, title, icon) + self.__feeds.append(feed) + self.__addFeedItem(feed) + self.__save() + + return True + + def __addFeedItem(self, feed): + """ + Private slot to add a top level feed item. + + @param feed tuple containing feed info (URL, title, icon) + (string, string, QIcon) + """ + itm = QTreeWidgetItem(self.feedsTree, [feed[1]]) + itm.setIcon(0, feed[2]) + itm.setData(0, FeedsManager.UrlStringRole, feed[0]) + + def __load(self): + """ + Private method to load the feeds data. + """ + self.__feeds = Preferences.getWebBrowser("RssFeeds") + self.__loaded = True + + # populate the feeds tree top level with the feeds + self.feedsTree.clear() + for feed in self.__feeds: + self.__addFeedItem(feed) + + def __save(self): + """ + Private method to store the feeds data. + """ + if not self.__loaded: + self.__load() + + Preferences.setWebBrowser("RssFeeds", self.__feeds) + + @pyqtSlot() + def on_reloadAllButton_clicked(self): + """ + Private slot to reload all feeds. + """ + if not self.__loaded: + self.__load() + + for index in range(self.feedsTree.topLevelItemCount()): + itm = self.feedsTree.topLevelItem(index) + self.__reloadFeed(itm) + + @pyqtSlot() + def on_reloadButton_clicked(self): + """ + Private slot to reload the selected feed. + """ + itm = self.feedsTree.selectedItems()[0] + self.__reloadFeed(itm) + + @pyqtSlot() + def on_editButton_clicked(self): + """ + Private slot to edit the selected feed. + """ + itm = self.feedsTree.selectedItems()[0] + origTitle = itm.text(0) + origUrlString = itm.data(0, FeedsManager.UrlStringRole) + + feedToChange = None + for feed in self.__feeds: + if feed[0] == origUrlString: + feedToChange = feed + break + if feedToChange: + feedIndex = self.__feeds.index(feedToChange) + + from .FeedEditDialog import FeedEditDialog + dlg = FeedEditDialog(origUrlString, origTitle) + if dlg.exec_() == QDialog.Accepted: + urlString, title = dlg.getData() + for feed in self.__feeds: + if feed[0] == urlString: + E5MessageBox.critical( + self, + self.tr("Duplicate Feed URL"), + self.tr( + """A feed with the URL {0} exists already.""" + """ Aborting...""".format(urlString))) + return + + self.__feeds[feedIndex] = (urlString, title, feedToChange[2]) + self.__save() + + itm.setText(0, title) + itm.setData(0, FeedsManager.UrlStringRole, urlString) + self.__reloadFeed(itm) + + @pyqtSlot() + def on_deleteButton_clicked(self): + """ + Private slot to delete the selected feed. + """ + itm = self.feedsTree.selectedItems()[0] + title = itm.text(0) + res = E5MessageBox.yesNo( + self, + self.tr("Delete Feed"), + self.tr( + """<p>Do you really want to delete the feed""" + """ <b>{0}</b>?</p>""".format(title))) + if res: + urlString = itm.data(0, FeedsManager.UrlStringRole) + if urlString: + feedToDelete = None + for feed in self.__feeds: + if feed[0] == urlString: + feedToDelete = feed + break + if feedToDelete: + self.__feeds.remove(feedToDelete) + self.__save() + + index = self.feedsTree.indexOfTopLevelItem(itm) + if index != -1: + self.feedsTree.takeTopLevelItem(index) + del itm + + @pyqtSlot() + def on_feedsTree_itemSelectionChanged(self): + """ + Private slot to enable the various buttons depending on the selection. + """ + self.__enableButtons() + + def __enableButtons(self): + """ + Private slot to disable/enable various buttons. + """ + selItems = self.feedsTree.selectedItems() + if len(selItems) == 1 and \ + self.feedsTree.indexOfTopLevelItem(selItems[0]) != -1: + enable = True + else: + enable = False + + self.reloadButton.setEnabled(enable) + self.editButton.setEnabled(enable) + self.deleteButton.setEnabled(enable) + + def __reloadFeed(self, itm): + """ + Private method to reload the given feed. + + @param itm feed item to be reloaded (QTreeWidgetItem) + """ + urlString = itm.data(0, FeedsManager.UrlStringRole) + if urlString == "": + return + + for child in itm.takeChildren(): + del child + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + request = QNetworkRequest(QUrl(urlString)) + reply = WebBrowserWindow.networkManager().get(request) + reply.finished.connect(self.__feedLoaded) + self.__replies[id(reply)] = (reply, itm) + + def __feedLoaded(self): + """ + Private slot to extract the loaded feed data. + """ + reply = self.sender() + if id(reply) not in self.__replies: + return + + topItem = self.__replies[id(reply)][1] + del self.__replies[id(reply)] + + if reply.error() == QNetworkReply.NoError: + linkString = "" + titleString = "" + + xml = QXmlStreamReader() + xmlData = reply.readAll() + xml.addData(xmlData) + + while not xml.atEnd(): + xml.readNext() + if xml.isStartElement(): + if xml.name() == "item": + linkString = xml.attributes().value("rss:about") + elif xml.name() == "link": + linkString = xml.attributes().value("href") + currentTag = xml.name() + elif xml.isEndElement(): + if xml.name() in ["item", "entry"]: + itm = QTreeWidgetItem(topItem) + itm.setText(0, titleString) + itm.setData(0, FeedsManager.UrlStringRole, linkString) + itm.setIcon(0, UI.PixmapCache.getIcon("rss16.png")) + + linkString = "" + titleString = "" + elif xml.isCharacters() and not xml.isWhitespace(): + if currentTag == "title": + titleString = xml.text() + elif currentTag == "link": + linkString += xml.text() + + if topItem.childCount() == 0: + itm = QTreeWidgetItem(topItem) + itm.setText(0, self.tr("Error fetching feed")) + itm.setData(0, FeedsManager.UrlStringRole, "") + itm.setData(0, FeedsManager.ErrorDataRole, + str(xmlData, encoding="utf-8")) + + topItem.setExpanded(True) + else: + linkString = "" + titleString = reply.errorString() + itm = QTreeWidgetItem(topItem) + itm.setText(0, titleString) + itm.setData(0, FeedsManager.UrlStringRole, linkString) + topItem.setExpanded(True) + + def __customContextMenuRequested(self, pos): + """ + Private slot to handle the context menu request for the feeds tree. + + @param pos position the context menu was requested (QPoint) + """ + itm = self.feedsTree.currentItem() + if itm is None: + return + + if self.feedsTree.indexOfTopLevelItem(itm) != -1: + return + + urlString = itm.data(0, FeedsManager.UrlStringRole) + if urlString: + menu = QMenu() + menu.addAction( + self.tr("&Open"), self.__openMessageInCurrentTab) + menu.addAction( + self.tr("Open in New &Tab"), self.__openMessageInNewTab) + menu.addSeparator() + menu.addAction(self.tr("&Copy URL to Clipboard"), + self.__copyUrlToClipboard) + menu.exec_(QCursor.pos()) + else: + errorString = itm.data(0, FeedsManager.ErrorDataRole) + if errorString: + menu = QMenu() + menu.addAction( + self.tr("&Show error data"), self.__showError) + menu.exec_(QCursor.pos()) + + def __itemActivated(self, itm, column): + """ + Private slot to handle the activation of an item. + + @param itm reference to the activated item (QTreeWidgetItem) + @param column column of the activation (integer) + """ + if self.feedsTree.indexOfTopLevelItem(itm) != -1: + return + + self.__openMessage( + QApplication.keyboardModifiers() & + Qt.ControlModifier == Qt.ControlModifier) + + def __openMessageInCurrentTab(self): + """ + Private slot to open a feed message in the current browser tab. + """ + self.__openMessage(False) + + def __openMessageInNewTab(self): + """ + Private slot to open a feed message in a new browser tab. + """ + self.__openMessage(True) + + def __openMessage(self, newTab): + """ + Private method to open a feed message. + + @param newTab flag indicating to open the feed message in a new tab + (boolean) + """ + itm = self.feedsTree.currentItem() + if itm is None: + return + + urlString = itm.data(0, FeedsManager.UrlStringRole) + if urlString: + title = itm.text(0) + + if newTab: + self.newUrl.emit(QUrl(urlString), title) + else: + self.openUrl.emit(QUrl(urlString), title) + else: + errorString = itm.data(0, FeedsManager.ErrorDataRole) + if errorString: + self.__showError() + + def __copyUrlToClipboard(self): + """ + Private slot to copy the URL of the selected item to the clipboard. + """ + itm = self.feedsTree.currentItem() + if itm is None: + return + + if self.feedsTree.indexOfTopLevelItem(itm) != -1: + return + + urlString = itm.data(0, FeedsManager.UrlStringRole) + if urlString: + QApplication.clipboard().setText(urlString) + + def __showError(self): + """ + Private slot to show error info for a failed load operation. + """ + itm = self.feedsTree.currentItem() + if itm is None: + return + + errorStr = itm.data(0, FeedsManager.ErrorDataRole) + if errorStr: + E5MessageBox.critical( + self, + self.tr("Error loading feed"), + "{0}".format(errorStr))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Feeds/FeedsManager.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,165 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FeedsManager</class> + <widget class="QDialog" name="FeedsManager"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>750</width> + <height>500</height> + </rect> + </property> + <property name="windowTitle"> + <string>Feeds Manager</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTreeWidget" name="feedsTree"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="allColumnsShowFocus"> + <bool>true</bool> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <attribute name="headerVisible"> + <bool>false</bool> + </attribute> + <column> + <property name="text"> + <string>News</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="reloadAllButton"> + <property name="toolTip"> + <string>Press to reload all feeds</string> + </property> + <property name="text"> + <string>Reload &All</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="reloadButton"> + <property name="toolTip"> + <string>Press to reload the selected feed</string> + </property> + <property name="text"> + <string>&Reload</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="editButton"> + <property name="toolTip"> + <string>Press to edit the selected feed</string> + </property> + <property name="text"> + <string>&Edit Feed</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="deleteButton"> + <property name="toolTip"> + <string>Press to delete the selected feed</string> + </property> + <property name="text"> + <string>&Delete Feed</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>feedsTree</tabstop> + <tabstop>reloadAllButton</tabstop> + <tabstop>reloadButton</tabstop> + <tabstop>editButton</tabstop> + <tabstop>deleteButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>FeedsManager</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>FeedsManager</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Feeds/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing all RSS feed related modules. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FlashCookieManager/FlashCookie.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Flash cookie class. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QDateTime + + +class FlashCookie(object): + """ + Class implementing the Flash cookie. + """ + def __init__(self): + """ + Constructor + """ + self.name = "" + self.origin = "" + self.size = 0 + self.path = "" + self.contents = "" + self.lastModified = QDateTime() + + def __eq__(self, other): + """ + Special method to compare to another Flash cookie. + + @param other reference to the other Flash cookie + @type FlashCookie + @return flag indicating equality of the two cookies + @rtype bool + """ + return (self.name == other.name and + self.path == other.path)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FlashCookieManager/FlashCookieManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,363 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Flash cookie manager. +""" + +from __future__ import unicode_literals + +try: + str = unicode # __IGNORE_EXCEPTION__ +except NameError: + pass + +import shutil + +from PyQt5.QtCore import QObject, QTimer, QDir, QFileInfo, QFile + +from .FlashCookie import FlashCookie +from .FlashCookieReader import FlashCookieReader, FlashCookieReaderError + +import WebBrowser.WebBrowserWindow + +import Preferences + + +class FlashCookieManager(QObject): + """ + Class implementing the Flash cookie manager object. + """ + RefreshInterval = 60 * 1000 + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object + @type QObject + """ + super(FlashCookieManager, self).__init__(parent) + + self.__flashCookieManagerDialog = None + self.__flashCookies = [] # list of FlashCookie + self.__newCookiesList = [] # list of str + + self.__timer = QTimer(self) + self.__timer.setInterval(FlashCookieManager.RefreshInterval) + self.__timer.timeout.connect(self.__autoRefresh) + + # start the timer if needed + self.__startStopTimer() + + if Preferences.getWebBrowser("FlashCookiesDeleteOnStartExit"): + self.__loadFlashCookies() + self.__removeAllButWhitelisted() + + def shutdown(self): + """ + Public method to perform shutdown actions. + """ + if self.__flashCookieManagerDialog is not None: + self.__flashCookieManagerDialog.close() + + if Preferences.getWebBrowser("FlashCookiesDeleteOnStartExit"): + self.__removeAllButWhitelisted() + + def setFlashCookies(self, cookies): + """ + Public method to set the list of cached Flash cookies. + + @param cookies list of Flash cookies to store + @type list of FlashCookie + """ + self.__flashCookies = cookies[:] + + def flashCookies(self): + """ + Public method to get the list of cached Flash cookies. + + @return list of Flash cookies + @rtype list of FlashCookie + """ + if not self.__flashCookies: + self.__loadFlashCookies() + + return self.__flashCookies[:] + + def newCookiesList(self): + """ + Public method to get the list of newly detected Flash cookies. + + @return list of newly detected Flash cookies + @rtype list of str + """ + return self.__newCookiesList[:] + + def clearNewOrigins(self): + """ + Public method to clear the list of newly detected Flash cookies. + """ + self.__newCookiesList = [] + + def clearCache(self): + """ + Public method to clear the list of cached Flash cookies. + """ + self.__flashCookies = [] + + def __isBlacklisted(self, cookie): + """ + Private method to check for a blacklisted cookie. + + @param cookie Flash cookie to be tested + @type FlashCookie + @return flag indicating a blacklisted cookie + @rtype bool + """ + return cookie.origin in \ + Preferences.getWebBrowser("FlashCookiesBlacklist") + + def __isWhitelisted(self, cookie): + """ + Private method to check for a whitelisted cookie. + + @param cookie Flash cookie to be tested + @type FlashCookie + @return flag indicating a whitelisted cookie + @rtype bool + """ + return cookie.origin in \ + Preferences.getWebBrowser("FlashCookiesWhitelist") + + def __removeAllButWhitelisted(self): + """ + Private method to remove all non-whitelisted cookies. + """ + for cookie in self.__flashCookies[:]: + if not self.__isWhitelisted(cookie): + self.removeCookie(cookie) + + def removeAllCookies(self): + """ + Public method to remove all flash cookies. + """ + for cookie in self.__flashCookies[:]: + self.removeCookie(cookie) + self.clearNewOrigins() + self.clearCache() + + def __sharedObjectDirName(self): + """ + Private slot to determine the path of the shared data objects. + + @return path of the shared data objects + @rtype str + """ + if "macromedia" in self.flashPlayerDataPath().lower() or \ + "/.gnash" not in self.flashPlayerDataPath().lower(): + return "/#SharedObjects/" + else: + return "/SharedObjects/" + + def flashPlayerDataPath(self): + """ + Public method to get the Flash Player data path. + + @return Flash Player data path + @rtype str + """ + return Preferences.getWebBrowser("FlashCookiesDataPath") + + def preferencesChanged(self): + """ + Public slot to handle a change of preferences. + """ + self.__startStopTimer() + + def removeCookie(self, cookie): + """ + Public method to remove a cookie of the list of cached cookies. + + @param cookie Flash cookie to be removed + @type FlashCookie + """ + if cookie in self.__flashCookies: + self.__flashCookies.remove(cookie) + shutil.rmtree(cookie.path, True) + + def __autoRefresh(self): + """ + Private slot to refresh the list of cookies. + """ + if self.__flashCookieManagerDialog and \ + self.__flashCookieManagerDialog.isVisible(): + return + + oldFlashCookies = self.__flashCookies[:] + self.__loadFlashCookies() + newCookieList = [] + + for cookie in self.__flashCookies[:]: + if self.__isBlacklisted(cookie): + self.removeCookie(cookie) + continue + + if self.__isWhitelisted(cookie): + continue + + newCookie = True + for oldCookie in oldFlashCookies: + if (oldCookie.path + oldCookie.name == + cookie.path + cookie.name): + newCookie = False + break + + if newCookie: + newCookieList.append(cookie.path + "/" + cookie.name) + + if newCookieList and Preferences.getWebBrowser("FlashCookieNotify"): + self.__newCookiesList.extend(newCookieList) + win = WebBrowser.WebBrowserWindow.WebBrowserWindow.mainWindow() + if win is None: + return + + view = win.currentBrowser() + if view is None: + return + + from .FlashCookieNotification import FlashCookieNotification + notification = FlashCookieNotification( + view, self, len(newCookieList)) + notification.show() + + def showFlashCookieManagerDialog(self): + """ + Public method to show the Flash cookies management dialog. + """ + if self.__flashCookieManagerDialog is None: + from .FlashCookieManagerDialog import FlashCookieManagerDialog + self.__flashCookieManagerDialog = FlashCookieManagerDialog(self) + + self.__flashCookieManagerDialog.refreshView() + self.__flashCookieManagerDialog.showPage(0) + self.__flashCookieManagerDialog.show() + self.__flashCookieManagerDialog.raise_() + + def __startStopTimer(self): + """ + Private slot to start or stop the auto refresh timer. + """ + if Preferences.getWebBrowser("FlashCookieAutoRefresh"): + if not self.__timer.isActive(): + if not bool(self.__flashCookies): + self.__loadFlashCookies() + + self.__timer.start() + else: + self.__timer.stop() + + def __loadFlashCookies(self): + """ + Private slot to load the Flash cookies to be cached. + """ + self.__flashCookies = [] + self.__loadFlashCookiesFromPath(self.flashPlayerDataPath()) + + def __loadFlashCookiesFromPath(self, path): + """ + Private slot to load the Flash cookies from a path. + + @param path Flash cookies path + @type str + """ + if path.endswith("#AppContainer"): + # specific to IE and Windows + return + + path = path.replace("\\", "/") + solDir = QDir(path) + entryList = solDir.entryList() + for entry in entryList: + if entry == "." or entry == "..": + continue + entryInfo = QFileInfo(path + "/" + entry) + if entryInfo.isDir(): + self.__loadFlashCookiesFromPath(entryInfo.filePath()) + else: + self.__insertFlashCookie(entryInfo.filePath()) + + def __insertFlashCookie(self, path): + """ + Private method to insert a Flash cookie into the cache. + + @param path Flash cookies path + @type str + """ + solFile = QFile(path) + if not solFile.open(QFile.ReadOnly): + return + + dataStr = "" + data = bytes(solFile.readAll()) + if data: + try: + reader = FlashCookieReader() + reader.setBytes(data) + reader.parse() + dataStr = reader.toString() + except FlashCookieReaderError as err: + dataStr = err.msg + + solFileInfo = QFileInfo(solFile) + + cookie = FlashCookie() + cookie.contents = dataStr + cookie.name = solFileInfo.fileName() + cookie.path = solFileInfo.canonicalPath() + cookie.size = int(solFile.size()) + cookie.lastModified = solFileInfo.lastModified() + cookie.origin = self.__extractOriginFrom(path) + + self.__flashCookies.append(cookie) + + def __extractOriginFrom(self, path): + """ + Private method to extract the cookie origin given its file name. + + @param path file name of the cookie file + @type str + @return cookie origin + @rtype str + """ + origin = path + if path.startswith( + self.flashPlayerDataPath() + self.__sharedObjectDirName()): + origin = origin.replace( + self.flashPlayerDataPath() + self.__sharedObjectDirName(), "") + if "/" in origin: + origin = origin.split("/", 1)[1] + elif path.startswith( + self.flashPlayerDataPath() + + "/macromedia.com/support/flashplayer/sys/"): + origin = origin.replace( + self.flashPlayerDataPath() + + "/macromedia.com/support/flashplayer/sys/", "") + if origin == "settings.sol": + return self.tr("!default") + elif origin.startswith("#"): + origin = origin[1:] + else: + origin = "" + + index = origin.find("/") + if index == -1: + return self.tr("!other") + + origin = origin[:index] + if origin in ["localhost", "local"]: + origin = "!localhost" + + return origin
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FlashCookieManager/FlashCookieManagerDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,425 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to manage the flash cookies. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt, QPoint, QTimer +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem, QApplication, QMenu, \ + QInputDialog, QLineEdit + +from E5Gui import E5MessageBox + +from .Ui_FlashCookieManagerDialog import Ui_FlashCookieManagerDialog + +import Preferences +import UI.PixmapCache + + +class FlashCookieManagerDialog(QDialog, Ui_FlashCookieManagerDialog): + """ + Class implementing a dialog to manage the flash cookies. + """ + def __init__(self, manager, parent=None): + """ + Constructor + + @param manager reference to the Flash cookie manager object + @type FlashCookieManager + @param parent reference to the parent widget + @type QWidget + """ + super(FlashCookieManagerDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.cookiesList.setContextMenuPolicy(Qt.CustomContextMenu) + self.cookiesList.customContextMenuRequested.connect( + self.__cookiesListContextMenuRequested) + + self.__manager = manager + + @pyqtSlot() + def on_whiteList_itemSelectionChanged(self): + """ + Private slot handling the selection of items in the whitelist. + """ + enable = len(self.whiteList.selectedItems()) > 0 + self.removeWhiteButton.setEnabled(enable) + + @pyqtSlot() + def on_blackList_itemSelectionChanged(self): + """ + Private slot handling the selection of items in the blacklist. + """ + enable = len(self.blackList.selectedItems()) > 0 + self.removeBlackButton.setEnabled(enable) + + @pyqtSlot() + def on_removeWhiteButton_clicked(self): + """ + Private slot to remove a server from the whitelist. + """ + for itm in self.whiteList.selectedItems(): + row = self.whiteList.row(itm) + self.whiteList.takeItem(row) + del itm + + @pyqtSlot() + def on_addWhiteButton_clicked(self): + """ + Private slot to add a server to the whitelist. + """ + origin, ok = QInputDialog.getText( + self, + self.tr("Add to whitelist"), + self.tr("Origin:"), + QLineEdit.Normal) + if ok and bool(origin): + self.__addWhitelist(origin) + + def __addWhitelist(self, origin): + """ + Private method to add a cookie origin to the whitelist. + + @param origin origin to be added to the list + @type str + """ + if not origin: + return + + if len(self.blackList.findItems(origin, Qt.MatchFixedString)) > 0: + E5MessageBox.information( + self, + self.tr("Add to whitelist"), + self.tr("""The server '{0}' is already in the blacklist.""" + """ Please remove it first.""").format(origin)) + return + + if len(self.whiteList.findItems(origin, Qt.MatchFixedString)) == 0: + self.whiteList.addItem(origin) + + @pyqtSlot() + def on_removeBlackButton_clicked(self): + """ + Private slot to remove a server from the blacklist. + """ + for itm in self.blackList.selectedItems(): + row = self.blackList.row(itm) + self.blackList.takeItem(row) + del itm + + @pyqtSlot() + def on_addBlackButton_clicked(self): + """ + Private slot to add a server to the blacklist. + """ + origin, ok = QInputDialog.getText( + self, + self.tr("Add to blacklist"), + self.tr("Origin:"), + QLineEdit.Normal) + if ok and bool(origin): + self.__addBlacklist(origin) + + def __addBlacklist(self, origin): + """ + Private method to add a cookie origin to the blacklist. + + @param origin origin to be added to the list + @type str + """ + if not origin: + return + + if len(self.whiteList.findItems(origin, Qt.MatchFixedString)) > 0: + E5MessageBox.information( + self, + self.tr("Add to blacklist"), + self.tr("""The server '{0}' is already in the whitelist.""" + """ Please remove it first.""").format(origin)) + return + + if len(self.blackList.findItems(origin, Qt.MatchFixedString)) == 0: + self.blackList.addItem(origin) + + @pyqtSlot(str) + def on_filterEdit_textChanged(self, filter): + """ + Private slot to filter the cookies list. + + @param filter filter text + @type str + """ + if not filter: + # show all in collapsed state + for index in range(self.cookiesList.topLevelItemCount()): + self.cookiesList.topLevelItem(index).setHidden(False) + self.cookiesList.topLevelItem(index).setExpanded(False) + else: + # show matching in expanded state + filter = filter.lower() + for index in range(self.cookiesList.topLevelItemCount()): + txt = "." + self.cookiesList.topLevelItem(index)\ + .text(0).lower() + self.cookiesList.topLevelItem(index).setHidden( + filter not in txt) + self.cookiesList.topLevelItem(index).setExpanded(True) + + @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) + def on_cookiesList_currentItemChanged(self, current, previous): + """ + Private slot handling a change of the current cookie item. + + @param current reference to the current item + @type QTreeWidgetItem + @param previous reference to the previous item + @type QTreeWidgetItem + """ + if current is None: + self.removeButton.setEnabled(False) + return + + cookie = current.data(0, Qt.UserRole) + if cookie is None: + self.nameLabel.setText(self.tr("<no flash cookie selected>")) + self.sizeLabel.setText(self.tr("<no flash cookie selected>")) + self.originLabel.setText(self.tr("<no flash cookie selected>")) + self.modifiedLabel.setText(self.tr("<no flash cookie selected>")) + self.contentsEdit.clear() + self.pathEdit.clear() + self.removeButton.setText(self.tr("Remove Cookie Group")) + else: + suffix = "" + if cookie.path.startswith( + self.__manager.flashPlayerDataPath() + + "/macromedia.com/support/flashplayer/sys"): + suffix = self.tr(" (settings)") + self.nameLabel.setText( + self.tr("{0}{1}", "name and suffix") + .format(cookie.name, suffix)) + self.sizeLabel.setText(self.tr("{0} Byte").format(cookie.size)) + self.originLabel.setText(cookie.origin) + self.modifiedLabel.setText( + cookie.lastModified.toString("yyyy-MM-dd hh:mm:ss")) + self.contentsEdit.setPlainText(cookie.contents) + self.pathEdit.setText(cookie.path) + self.removeButton.setText(self.tr("Remove Cookie")) + self.removeButton.setEnabled(True) + + @pyqtSlot(QPoint) + def __cookiesListContextMenuRequested(self, pos): + """ + Private slot handling the cookies list context menu. + + @param pos position to show the menu at + @type QPoint + """ + itm = self.cookiesList.itemAt(pos) + if itm is None: + return + + menu = QMenu() + addBlacklistAct = menu.addAction(self.tr("Add to blacklist")) + addWhitelistAct = menu.addAction(self.tr("Add to whitelist")) + + self.cookiesList.setCurrentItem(itm) + + activatedAction = menu.exec_( + self.cookiesList.viewport().mapToGlobal(pos)) + if itm.childCount() == 0: + origin = itm.data(0, Qt.UserRole).origin + else: + origin = itm.text(0) + + if activatedAction == addBlacklistAct: + self.__addBlacklist(origin) + elif activatedAction == addWhitelistAct: + self.__addWhitelist(origin) + + @pyqtSlot() + def on_reloadButton_clicked(self): + """ + Private slot handling a press of the reload button. + """ + self.refreshView(True) + + @pyqtSlot() + def on_removeAllButton_clicked(self): + """ + Private slot to remove all cookies. + """ + ok = E5MessageBox.yesNo( + self, + self.tr("Remove All"), + self.tr("""Do you really want to delete all flash cookies on""" + """ your computer?""")) + if ok: + cookies = self.__manager.flashCookies() + for cookie in cookies: + self.__manager.removeCookie(cookie) + + self.cookiesList.clear() + self.__manager.clearNewOrigins() + self.__manager.clearCache() + + @pyqtSlot() + def on_removeButton_clicked(self): + """ + Private slot to remove one cookie or a cookie group. + """ + itm = self.cookiesList.currentItem() + if itm is None: + return + + cookie = itm.data(0, Qt.UserRole) + if cookie is None: + # remove a whole cookie group + origin = itm.text(0) + cookieList = self.__manager.flashCookies() + for fcookie in cookieList: + if fcookie.origin == origin: + self.__manager.removeCookie(fcookie) + + index = self.cookiesList.indexOfTopLevelItem(itm) + self.cookiesList.takeTopLevelItem(index) + else: + self.__manager.removeCookie(cookie) + parent = itm.parent() + index = parent.indexOfChild(itm) + parent.takeChild(index) + + if parent.childCount() == 0: + # remove origin item as well + index = self.cookiesList.indexOfTopLevelItem(parent) + self.cookiesList.takeTopLevelItem(index) + del parent + del itm + + def refreshView(self, forceReload=False): + """ + Public method to refresh the dialog view. + + @param forceReload flag indicating to reload the cookies + @type bool + """ + blocked = self.filterEdit.blockSignals(True) + self.filterEdit.clear() + self.contentsEdit.clear() + self.filterEdit.blockSignals(blocked) + + if forceReload: + self.__manager.clearCache() + self.__manager.clearNewOrigins() + + QTimer.singleShot(0, self.__refreshCookiesList) + QTimer.singleShot(0, self.__refreshFilterLists) + + def showPage(self, index): + """ + Public method to display a given page. + + @param index index of the page to be shown + @type int + """ + self.cookiesTabWidget.setCurrentIndex(index) + + @pyqtSlot() + def __refreshCookiesList(self): + """ + Private slot to refresh the cookies list. + """ + QApplication.setOverrideCursor(Qt.WaitCursor) + + cookies = self.__manager.flashCookies() + self.cookiesList.clear() + + counter = 0 + originDict = {} + for cookie in cookies: + cookieOrigin = cookie.origin + if cookieOrigin.startswith("."): + cookieOrigin = cookieOrigin[1:] + + if cookieOrigin in originDict: + itm = QTreeWidgetItem(originDict[cookieOrigin]) + else: + newParent = QTreeWidgetItem(self.cookiesList) + newParent.setText(0, cookieOrigin) + newParent.setIcon(0, UI.PixmapCache.getIcon("dirOpen.png")) + self.cookiesList.addTopLevelItem(newParent) + originDict[cookieOrigin] = newParent + + itm = QTreeWidgetItem(newParent) + + suffix = "" + if cookie.path.startswith( + self.__manager.flashPlayerDataPath() + + "/macromedia.com/support/flashplayer/sys"): + suffix = self.tr(" (settings)") + + if cookie.path + "/" + cookie.name in \ + self.__manager.newCookiesList(): + suffix += self.tr(" [new]") + font = itm.font(0) + font.setBold(True) + itm.setFont(font) + itm.parent().setExpanded(True) + + itm.setText(0, self.tr("{0}{1}", "name and suffix").format( + cookie.name, suffix)) + itm.setData(0, Qt.UserRole, cookie) + + counter += 1 + if counter > 100: + QApplication.processEvents() + counter = 0 + + self.removeAllButton.setEnabled( + self.cookiesList.topLevelItemCount() > 0) + self.removeButton.setEnabled(False) + + QApplication.restoreOverrideCursor() + + @pyqtSlot() + def __refreshFilterLists(self): + """ + Private slot to refresh the white and black lists. + """ + self.whiteList.clear() + self.blackList.clear() + + self.whiteList.addItems( + Preferences.getWebBrowser("FlashCookiesWhitelist")) + self.blackList.addItems( + Preferences.getWebBrowser("FlashCookiesBlacklist")) + + self.on_whiteList_itemSelectionChanged() + self.on_blackList_itemSelectionChanged() + + def closeEvent(self, evt): + """ + Protected method to handle the close event. + + @param evt reference to the close event + @type QCloseEvent + """ + self.__manager.clearNewOrigins() + + whiteList = [] + for row in range(self.whiteList.count()): + whiteList.append(self.whiteList.item(row).text()) + + blackList = [] + for row in range(self.blackList.count()): + blackList.append(self.blackList.item(row).text()) + + Preferences.setWebBrowser("FlashCookiesWhitelist", whiteList) + Preferences.setWebBrowser("FlashCookiesBlacklist", blackList) + + evt.accept()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FlashCookieManager/FlashCookieManagerDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,478 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FlashCookieManagerDialog</class> + <widget class="QDialog" name="FlashCookieManagerDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>550</width> + <height>500</height> + </rect> + </property> + <property name="windowTitle"> + <string>Flash Cookie Management</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTabWidget" name="cookiesTabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>Stored Flash Cookies</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="0" column="0"> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Filter:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="filterEdit"> + <property name="toolTip"> + <string>Enter cookie filter string</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QLabel" name="label_24"> + <property name="text"> + <string>Stored Flash Cookies:</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QTreeWidget" name="cookiesList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>Origin</string> + </property> + </column> + </widget> + </item> + </layout> + </item> + <item row="0" column="1"> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>158</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="reloadButton"> + <property name="toolTip"> + <string>Press to reload Flash cookies from disk</string> + </property> + <property name="text"> + <string>Reload</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="label_6"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Name:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string><no flash cookie selected></string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="labelSize_2"> + <property name="text"> + <string>Size:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="sizeLabel"> + <property name="text"> + <string><no flash cookie selected></string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Origin:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="originLabel"> + <property name="text"> + <string><no flash cookie selected></string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_22"> + <property name="text"> + <string>Last Modified:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLabel" name="modifiedLabel"> + <property name="text"> + <string><no flash cookie selected></string> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Contents:</string> + </property> + </widget> + </item> + <item row="3" column="0" colspan="2"> + <widget class="QPlainTextEdit" name="contentsEdit"> + <property name="tabChangesFocus"> + <bool>true</bool> + </property> + <property name="lineWrapMode"> + <enum>QPlainTextEdit::NoWrap</enum> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Path:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="pathEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all flash cookies</string> + </property> + <property name="text"> + <string>Remove All Cookies</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove selected flash cookies</string> + </property> + <property name="text"> + <string>Remove Cookie</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_2"> + <attribute name="title"> + <string>Flash Cookies Lists</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_5"> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string><b>Flash cookie whitelist</b></string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string><b>Flash cookie blacklist</b></string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_17"> + <property name="text"> + <string>Flash cookies from these origins will not be deleted automatically. (Also detection of them will not be notified to user.)</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="label_18"> + <property name="text"> + <string>Flash cookies from these origins will be deleted without any notification.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QListWidget" name="whiteList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QListWidget" name="blackList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="removeWhiteButton"> + <property name="toolTip"> + <string>Press to remove selected origins from the whitelist</string> + </property> + <property name="text"> + <string>Remove</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="addWhiteButton"> + <property name="toolTip"> + <string>Press to add an origin to the whitelist</string> + </property> + <property name="text"> + <string>Add...</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="3" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <spacer name="horizontalSpacer_5"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="removeBlackButton"> + <property name="toolTip"> + <string>Press to remove selected origins from the blacklist</string> + </property> + <property name="text"> + <string>Remove</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="addBlackButton"> + <property name="toolTip"> + <string>Press to add an origin to the blacklist</string> + </property> + <property name="text"> + <string>Add...</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_6"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>cookiesTabWidget</tabstop> + <tabstop>filterEdit</tabstop> + <tabstop>cookiesList</tabstop> + <tabstop>reloadButton</tabstop> + <tabstop>contentsEdit</tabstop> + <tabstop>pathEdit</tabstop> + <tabstop>removeAllButton</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>whiteList</tabstop> + <tabstop>removeWhiteButton</tabstop> + <tabstop>addWhiteButton</tabstop> + <tabstop>blackList</tabstop> + <tabstop>removeBlackButton</tabstop> + <tabstop>addBlackButton</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>FlashCookieManagerDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>FlashCookieManagerDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FlashCookieManager/FlashCookieNotification.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the feature permission bar widget. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QLabel, QHBoxLayout, QPushButton + +from E5Gui.E5AnimatedWidget import E5AnimatedWidget + +import UI.PixmapCache + + +class FlashCookieNotification(E5AnimatedWidget): + """ + Class implementing the feature permission bar widget. + """ + DefaultHeight = 30 + + def __init__(self, view, manager, noCookies): + """ + Constructor + + @param view reference to the web view + @type WebBrowserView + @param manager reference to the Flash cookie manager object + @type FlashCookieManager + @param noCookies number of newly detected Flash cookies + @type int + """ + super(FlashCookieNotification, self).__init__(parent=view) + + self.__manager = manager + + if noCookies == 1: + msg = self.tr("A new flash cookie was detected.") + else: + msg = self.tr("{0} new flash cookies were detected.")\ + .format(noCookies) + self.setAutoFillBackground(True) + self.__layout = QHBoxLayout() + self.setLayout(self.__layout) + self.__layout.setContentsMargins(9, 0, 0, 0) + self.__iconLabel = QLabel(self) + self.__iconLabel.setPixmap(UI.PixmapCache.getPixmap("flashCookie.png")) + self.__layout.addWidget(self.__iconLabel) + self.__messageLabel = QLabel(msg, self) + self.__layout.addWidget(self.__messageLabel) + self.__viewButton = QPushButton(self.tr("View"), self) + self.__layout.addWidget(self.__viewButton) + self.__layout.addStretch() + self.__discardButton = QPushButton(UI.PixmapCache.getIcon("close.png"), + "", self) + self.__layout.addWidget(self.__discardButton) + + self.__viewButton.clicked.connect(manager.showFlashCookieManagerDialog) + self.__viewButton.clicked.connect(self.hide) + self.__discardButton.clicked.connect(self.hide) + + self.resize(view.width(), self.height()) + self.startAnimation()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FlashCookieManager/FlashCookieReader.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,474 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to read flash cookies. +""" + +# +# Note: The code is based on s2x.py +# + +from __future__ import unicode_literals + +import struct +import io + +from PyQt5.QtCore import QDateTime + + +class FlashCookieReaderError(Exception): + """ + Class containing data of a reader error. + """ + def __init__(self, msg): + """ + Constructor + + @param msg error message + @type str + """ + self.msg = msg + + +class FlashCookieReader(object): + """ + Class implementing a reader for flash cookies (*.sol files). + """ + Number = b'\x00' + Boolean = b'\x01' + String = b'\x02' + ObjObj = b'\x03' + Null = b'\x05' + Undef = b'\x06' + ObjArr = b'\x08' + ObjDate = b'\x0B' + ObjM = b'\x0D' + ObjXml = b'\x0F' + ObjCc = b'\x10' + + EpochCorrectionMsecs = 31 * 24 * 60 * 60 * 1000 + # Flash Epoch starts at 1969-12-01 + + def __init__(self): + """ + Constructor + """ + self.__result = {} + # dictionary with element name as key and tuple of + # type and value as value + self.__data = None + self.__parsed = False + + def setBytes(self, solData): + """ + Public method to set the contents of a sol file to be parsed. + + @param solData contents of the file + @type bytes + """ + self.__data = io.BytesIO(solData) + + def setFileName(self, solFilename): + """ + Public method to set the name of a sol file to be parsed. + + @param solFilename name of the sol file + @type str + """ + self.__data = open(solFilename, "rb") + + def setFile(self, solFile): + """ + Public method to set an open sol file to be parsed. + + @param solFile sol file to be parsed + @type io.FileIO + """ + self.__data = solFile + + def parse(self): + """ + Public method to parse the sol file. + + @exception FlashCookieReaderError raised when encountering a parse + issue + """ + if self.__data is None: + return + + self.__data.seek(0, 2) + lenSolData = self.__data.tell() + self.__data.seek(0) + self.__data.read(2) + sLenData = self.__data.read(4) + lenData, = struct.unpack(">L", sLenData) # unsigned long, big-endian + if lenSolData != lenData + 6: + raise FlashCookieReaderError( + "Flash cookie data lengths don't match\n" + " file length: {0}\n" + " data length: {1}" + .format(lenSolData - 6, lenData)) + sDataType = self.__data.read(4).decode("utf-8") # 'TCSO' + if sDataType != "TCSO": + raise FlashCookieReaderError( + "Flash cookie type is not 'TCSO'; found '{0}'." + .format(sDataType)) + self.__data.read(6) + lenSolName, = struct.unpack(">H", self.__data.read(2)) + # unsigned short, big-endian + solName = self.__data.read(lenSolName) + solName = solName.decode("utf-8", "replace") + self.__result["SolName"] = ("string", solName) + self.__data.read(4) + while self.__data.tell() < lenSolData: + lenVariableName, = struct.unpack(">H", self.__data.read(2)) + # unsigned short, big-endian + variableName = self.__data.read(lenVariableName) + variableName = variableName.decode("utf-8", "replace") + variableType = self.__data.read(1) + if len(variableType): + if variableType == self.Number: + self.__parseNumber(variableName, self.__result) + elif variableType == self.Boolean: + self.__parseBoolean(variableName, self.__result) + elif variableType == self.String: + self.__parseString(variableName, self.__result) + elif variableType == self.ObjObj: + self.__parseObject(variableName, self.__result) + elif variableType == self.ObjArr: + self.__parseArray(variableName, self.__result) + elif variableType == self.ObjDate: + self.__parseDate(variableName, self.__result) + elif variableType == self.ObjXml: + self.__parseXml(variableName, self.__result) + elif variableType == self.ObjCc: + self.__parseOcc(variableName, self.__result) + elif variableType == self.ObjM: + self.__parseOjm(variableName, self.__result) + elif variableType == self.Null: + self.__parseNull(variableName, self.__result) + elif variableType == self.Undef: + self.__parseUndefined(variableName, self.__result) + else: + raise FlashCookieReaderError( + "Unexpected Data Type: " + hex(ord(variableType))) + self.__data.read(1) # '\x00' + self.__data.close() + self.__parsed = True + + def __parseNumber(self, variableName, parent): + """ + Private method to parse a number. + + @param variableName name of the variable to be parsed + @type str + @param parent reference to the dictionary to insert the result into + @type dict + """ + b = self.__data.read(8) + if b == b"\x7F\xF0\x00\x00\x00\x00\x00\x00": + value = "Infinity" + elif b == b"\xFF\xF0\x00\x00\x00\x00\x00\x00": + value = "-Infinity" + elif b == b"\x7F\xF8\x00\x00\x00\x00\x00\x00": + value = "NaN" + else: + value, = struct.unpack(">d", b) # double, big-endian + value = str(value) + parent[variableName] = ("number", value) + + def __parseBoolean(self, variableName, parent): + """ + Private method to parse a boolean. + + @param variableName name of the variable to be parsed + @type str + @param parent reference to the dictionary to insert the result into + @type dict + """ + b = self.__data.read(1) + if b == b"\x00": + value = "False" + elif b == b"\x01": + value = "True" + else: + # boolean value error; default to True + value = "True" + parent[variableName] = ("boolean", value) + + def __parseString(self, variableName, parent): + """ + Private method to parse a string. + + @param variableName name of the variable to be parsed + @type str + @param parent reference to the dictionary to insert the result into + @type dict + """ + lenStr, = struct.unpack(">H", self.__data.read(2)) + # unsigned short, big-endian + b = self.__data.read(lenStr) + value = b.decode("utf-8", "replace") + parent[variableName] = ("string", value) + + def __parseDate(self, variableName, parent): + """ + Private method to parse a date. + + @param variableName name of the variable to be parsed + @type str + @param parent reference to the dictionary to insert the result into + @type dict + """ + msec, = struct.unpack(">d", self.__data.read(8)) + # double, big-endian + # DateObject: Milliseconds Count From Dec. 1, 1969 + msec -= self.EpochCorrectionMsecs # correct for Unix epoch + minOffset, = struct.unpack(">h", self.__data.read(2)) + # short, big-endian + offset = minOffset // 60 # offset in hours + # Timezone: UTC + Offset + value = QDateTime() + value.setMSecsSinceEpoch(msec) + value.setOffsetFromUtc(offset * 3600) + parent[variableName] = ("date", + value.toString("yyyy-MM-dd HH:mm:ss t")) + + def __parseXml(self, variableName, parent): + """ + Private method to parse XML. + + @param variableName name of the variable to be parsed + @type str + @param parent reference to the dictionary to insert the result into + @type dict + """ + lenCData, = struct.unpack(">L", self.__data.read(4)) + # unsigned long, big-endian + cData = self.__data.read(lenCData) + value = cData.decode("utf-8", "replace") + parent[variableName] = ("xml", value) + + def __parseOjm(self, variableName, parent): + """ + Private method to parse an m_object. + + @param variableName name of the variable to be parsed + @type str + @param parent reference to the dictionary to insert the result into + @type dict + """ + parent[variableName] = ("m_object", "") + + def __parseNull(self, variableName, parent): + """ + Private method to parse a null object. + + @param variableName name of the variable to be parsed + @type str + @param parent reference to the dictionary to insert the result into + @type dict + """ + parent[variableName] = ("null", "") + + def __parseUndefined(self, variableName, parent): + """ + Private method to parse an undefined object. + + @param variableName name of the variable to be parsed + @type str + @param parent reference to the dictionary to insert the result into + @type dict + """ + parent[variableName] = ("undefined", "") + + def __parseObject(self, variableName, parent): + """ + Private method to parse an object. + + @param variableName name of the variable to be parsed + @type str + @param parent reference to the dictionary to insert the result into + @type dict + @exception FlashCookieReaderError raised when an issue with the cookie + file is found + """ + value = {} + parent[variableName] = ("object", value) + + lenVariableName, = struct.unpack(">H", self.__data.read(2)) + # unsigned short, big-endian + while lenVariableName != 0: + variableName = self.__data.read(lenVariableName) + variableName = variableName.decode("utf-8", "replace") + variableType = self.__data.read(1) + if variableType == self.Number: + self.__parseNumber(variableName, value) + elif variableType == self.Boolean: + self.__parseBoolean(variableName, value) + elif variableType == self.String: + self.__parseString(variableName, value) + elif variableType == self.ObjObj: + self.__parseObject(variableName, value) + elif variableType == self.ObjArr: + self.__parseArray(variableName, value) + elif variableType == self.ObjDate: + self.__parseDate(variableName, value) + elif variableType == self.ObjXml: + self.__parseXml(variableName, value) + elif variableType == self.ObjCc: + self.__parseOcc(variableName, value) + elif variableType == self.ObjM: + self.__parseOjm(variableName, value) + elif variableType == self.Null: + self.__parseNull(variableName, value) + elif variableType == self.Undef: + self.__parseUndefined(variableName, value) + else: + raise FlashCookieReaderError( + "Unexpected Data Type: " + hex(ord(variableType))) + lenVariableName, = struct.unpack(">H", self.__data.read(2)) + self.__data.read(1) # '\x09' + + def __parseArray(self, variableName, parent): + """ + Private method to parse an array. + + @param variableName name of the variable to be parsed + @type str + @param parent reference to the dictionary to insert the result into + @type dict + @exception FlashCookieReaderError raised when an issue with the cookie + file is found + """ + arrayLength, = struct.unpack(">L", self.__data.read(4)) + # unsigned long, big-endian + + value = {} + parent[variableName] = ("array; length={0}".format(arrayLength), value) + + lenVariableName, = struct.unpack(">H", self.__data.read(2)) + # unsigned short, big-endian + while lenVariableName != 0: + variableName = self.__data.read(lenVariableName) + variableName = variableName.decode("utf-8", "replace") + variableType = self.__data.read(1) + if variableType == self.Number: + self.__parseNumber(variableName, value) + elif variableType == self.Boolean: + self.__parseBoolean(variableName, value) + elif variableType == self.String: + self.__parseString(variableName, value) + elif variableType == self.ObjObj: + self.__parseObject(variableName, value) + elif variableType == self.ObjArr: + self.__parseArray(variableName, value) + elif variableType == self.ObjDate: + self.__parseDate(variableName, value) + elif variableType == self.ObjXml: + self.__parseXml(variableName, value) + elif variableType == self.ObjCc: + self.__parseOcc(variableName, value) + elif variableType == self.ObjM: + self.__parseOjm(variableName, value) + elif variableType == self.Null: + self.__parseNull(variableName, value) + elif variableType == self.Undef: + self.__parseUndefined(variableName, value) + else: + raise FlashCookieReaderError( + "Unexpected Data Type: " + hex(ord(variableType))) + lenVariableName, = struct.unpack(">H", self.__data.read(2)) + self.__data.read(1) # '\x09' + + def __parseOcc(self, variableName, parent): + """ + Private method to parse a c_object. + + @param variableName name of the variable to be parsed + @type str + @param parent reference to the dictionary to insert the result into + @type dict + @exception FlashCookieReaderError raised when an issue with the cookie + file is found + """ + lenCname = struct.unpack(">H", self.__data.read(2)) + # unsigned short, big-endian + cname = self.__data.read(lenCname) + cname = cname.decode("utf-8", "replace") + + value = {} + parent[variableName] = ("c_object; cname={0}".format(cname), value) + + lenVariableName, = struct.unpack(">H", self.__data.read(2)) + # unsigned short, big-endian + while lenVariableName != 0: + variableName = self.__data.read(lenVariableName) + variableName = variableName.decode("utf-8", "replace") + variableType = self.__data.read(1) + if variableType == self.Number: + self.__parseNumber(variableName, value) + elif variableType == self.Boolean: + self.__parseBoolean(variableName, value) + elif variableType == self.String: + self.__parseString(variableName, value) + elif variableType == self.ObjObj: + self.__parseObject(variableName, value) + elif variableType == self.ObjArr: + self.__parseArray(variableName, value) + elif variableType == self.ObjDate: + self.__parseDate(variableName, value) + elif variableType == self.ObjXml: + self.__parseXml(variableName, value) + elif variableType == self.ObjCc: + self.__parseOcc(variableName, value) + elif variableType == self.ObjM: + self.__parseOjm(variableName, value) + elif variableType == self.Null: + self.__parseNull(variableName, value) + elif variableType == self.Undef: + self.__parseUndefined(variableName, value) + else: + raise FlashCookieReaderError( + "Unexpected Data Type: " + hex(ord(variableType))) + lenVariableName, = struct.unpack(">H", self.__data.read(2)) + self.__data.read(1) # '\x09' + + def toString(self, indent=0, parent=None): + """ + Public method to convert the parsed cookie to a string representation. + + @param indent indentation level + @type int + @param parent reference to the dictionary to be converted + @type dict + @return string representation of the cookie + @rtype str + """ + indentStr = " " * indent + strArr = [] + + if parent is None: + parent = self.__result + + if not parent: + return "" + + for variableName in sorted(parent.keys()): + variableType, value = parent[variableName] + if isinstance(value, dict): + resultStr = self.toString(indent + 1, value) + if resultStr: + strArr.append("{0}{1}:\n{2}" + .format(indentStr, variableName, resultStr)) + else: + strArr.append("{0}{1}:" + .format(indentStr, variableName)) + else: + strArr.append("{0}{1}: {2}" + .format(indentStr, variableName, value)) + + return "\n".join(strArr)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FlashCookieManager/FlashCookieUtilities.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing some utility functions. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import QProcessEnvironment + +import Globals + + +def flashDataPathForOS(): + """ + Function to determine the OS dependent path where Flash cookies + are stored. + + @return Flash data path + @rtype str + """ + # On Microsoft Windows NT 5.x and 6.x, they are stored in: + # %APPDATA%\Macromedia\Flash Player\#SharedObjects\ + # %APPDATA%\Macromedia\Flash Player\macromedia.com\support\flashplayer\sys\ + # On Mac OS X, they are stored in: + # ~/Library/Preferences/Macromedia/Flash Player/#SharedObjects/ + # ~/Library/Preferences/Macromedia/Flash Player/macromedia.com/support/⏎ + # flashplayer/sys/ + # On Linux or Unix, they are stored in: + # ~/.macromedia/Flash_Player/#SharedObjects/ + # ~/.macromedia/Flash_Player/macromedia.com/support/flashplayer/sys/ + # For Linux and Unix systems, if the open-source Gnash plugin is being used + # instead of the official Adobe Flash, they will instead be found at: + # ~/.gnash/SharedObjects/ + + flashPath = "" + + if Globals.isWindowsPlatform(): + appData = QProcessEnvironment.systemEnvironment().value("APPDATA") + appData = appData.replace("\\", "/") + flashPath = appData + "/Macromedia/Flash Player" + elif Globals.isMacPlatform(): + flashPath = os.path.expanduser( + "~/Library/Preferences/Macromedia/Flash Player") + else: + if os.path.exists(os.path.expanduser("~/.macromedia")): + flashPath = os.path.expanduser("~/.macromedia/Flash_Player") + elif os.path.exists(os.path.expanduser("~/.gnash")): + flashPath = os.path.expanduser("~/.gnash") + + return flashPath
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/FlashCookieManager/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the Flash cookie manager and associated objects. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyAddScriptDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + + +""" +Module implementing a dialog for adding GreaseMonkey scripts.. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSlot, QDir, QFile +from PyQt5.QtWidgets import QDialog + +from E5Gui import E5MessageBox + +from .Ui_GreaseMonkeyAddScriptDialog import Ui_GreaseMonkeyAddScriptDialog + +import UI.PixmapCache + + +class GreaseMonkeyAddScriptDialog(QDialog, Ui_GreaseMonkeyAddScriptDialog): + """ + Class implementing a dialog for adding GreaseMonkey scripts.. + """ + def __init__(self, manager, script, parent=None): + """ + Constructor + + @param manager reference to the GreaseMonkey manager + (GreaseMonkeyManager) + @param script GreaseMonkey script to be added (GreaseMonkeyScript) + @param parent reference to the parent widget (QWidget) + """ + super(GreaseMonkeyAddScriptDialog, self).__init__(parent) + self.setupUi(self) + + self.iconLabel.setPixmap( + UI.PixmapCache.getPixmap("greaseMonkey48.png")) + + self.__manager = manager + self.__script = script + + runsAt = "" + doesNotRunAt = "" + + include = script.include() + exclude = script.exclude() + + if include: + runsAt = self.tr("<p>runs at:<br/><i>{0}</i></p>").format( + "<br/>".join(include)) + + if exclude: + doesNotRunAt = self.tr( + "<p>does not run at:<br/><i>{0}</i></p>").format( + "<br/>".join(exclude)) + + scriptInfoTxt = "<p><b>{0}</b> {1}<br/>{2}</p>{3}{4}".format( + script.name(), script.version(), script.description(), runsAt, + doesNotRunAt) + self.scriptInfo.setHtml(scriptInfoTxt) + + self.accepted.connect(self.__accepted) + + @pyqtSlot() + def on_showScriptSourceButton_clicked(self): + """ + Private slot to show an editor window with the source code. + """ + from WebBrowser.Tools import WebBrowserTools + + tmpFileName = WebBrowserTools.ensureUniqueFilename( + os.path.join(QDir.tempPath(), "tmp-userscript.js")) + if QFile.copy(self.__script.fileName(), tmpFileName): + from QScintilla.MiniEditor import MiniEditor + editor = MiniEditor(tmpFileName, "JavaScript", self) + editor.show() + + def __accepted(self): + """ + Private slot handling the accepted signal. + """ + if self.__manager.addScript(self.__script): + msg = self.tr( + "<p><b>{0}</b> installed successfully.</p>").format( + self.__script.name()) + success = True + else: + msg = self.tr("<p>Cannot install script.</p>") + success = False + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if success and WebBrowserWindow.notificationsEnabled(): + WebBrowserWindow.showNotification( + UI.PixmapCache.getPixmap("greaseMonkey48.png"), + self.tr("GreaseMonkey Script Installation"), + msg) + else: + E5MessageBox.information( + self, + self.tr("GreaseMonkey Script Installation"), + msg)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyAddScriptDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>GreaseMonkeyAddScriptDialog</class> + <widget class="QDialog" name="GreaseMonkeyAddScriptDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>550</width> + <height>400</height> + </rect> + </property> + <property name="windowTitle"> + <string>GreaseMonkey Script Installation</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="iconLabel"> + <property name="minimumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="text"> + <string notr="true">Icon</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string><h2>GreaseMonkey Script Installation</h2></string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>You are about to install this userscript into GreaseMonkey:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QTextBrowser" name="scriptInfo"/> + </item> + <item> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string><b>You should only install scripts from sources you trust!</b></string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Are you sure you want to install it?</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="showScriptSourceButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Press to open an editor with the script's source</string> + </property> + <property name="text"> + <string>Show source code of script</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::No|QDialogButtonBox::Yes</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>GreaseMonkeyAddScriptDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>401</x> + <y>389</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>GreaseMonkeyAddScriptDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>439</x> + <y>389</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the GreaseMonkey scripts configuration dialog. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt, QUrl +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtWidgets import QDialog, QListWidgetItem + +from E5Gui import E5MessageBox + +from .Ui_GreaseMonkeyConfigurationDialog import \ + Ui_GreaseMonkeyConfigurationDialog + +import UI.PixmapCache + + +class GreaseMonkeyConfigurationDialog( + QDialog, Ui_GreaseMonkeyConfigurationDialog): + """ + Class implementing the GreaseMonkey scripts configuration dialog. + """ + ScriptVersionRole = Qt.UserRole + ScriptDescriptionRole = Qt.UserRole + 1 + ScriptRole = Qt.UserRole + 2 + + def __init__(self, manager, parent=None): + """ + Constructor + + @param manager reference to the manager object (GreaseMonkeyManager) + @param parent reference to the parent widget (QWidget) + """ + super(GreaseMonkeyConfigurationDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.iconLabel.setPixmap( + UI.PixmapCache.getPixmap("greaseMonkey48.png")) + + self.__manager = manager + + self.__loadScripts() + + self.scriptsList.removeItemRequested.connect(self.__removeItem) + self.scriptsList.itemChanged.connect(self.__itemChanged) + + @pyqtSlot() + def on_openDirectoryButton_clicked(self): + """ + Private slot to open the GreaseMonkey scripts directory. + """ + QDesktopServices.openUrl( + QUrl.fromLocalFile(self.__manager.scriptsDirectory())) + + @pyqtSlot(str) + def on_downloadLabel_linkActivated(self, link): + """ + Private slot to open the greasyfork.org web site. + + @param link URL (string) + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if not link or "userscript.org" in link: + # userscript.org is down, default to Greasy Fork. + link = "https://greasyfork.org/" + WebBrowserWindow.mainWindow().newTab(QUrl(link)) + self.close() + + @pyqtSlot(QListWidgetItem) + def on_scriptsList_itemDoubleClicked(self, item): + """ + Private slot to show information about the selected script. + + @param item reference to the double clicked item (QListWidgetItem) + """ + script = self.__getScript(item) + if script is not None: + from .GreaseMonkeyConfigurationScriptInfoDialog import \ + GreaseMonkeyConfigurationScriptInfoDialog + infoDlg = GreaseMonkeyConfigurationScriptInfoDialog(script, self) + infoDlg.exec_() + + def __loadScripts(self): + """ + Private method to load all the available scripts. + """ + for script in self.__manager.allScripts(): + itm = QListWidgetItem( + UI.PixmapCache.getIcon("greaseMonkeyScript.png"), + script.name(), self.scriptsList) + itm.setData( + GreaseMonkeyConfigurationDialog.ScriptVersionRole, + script.version()) + itm.setData( + GreaseMonkeyConfigurationDialog.ScriptDescriptionRole, + script.description()) + itm.setFlags(itm.flags() | Qt.ItemIsUserCheckable) + if script.isEnabled(): + itm.setCheckState(Qt.Checked) + else: + itm.setCheckState(Qt.Unchecked) + itm.setData(GreaseMonkeyConfigurationDialog.ScriptRole, script) + self.scriptsList.addItem(itm) + + self.scriptsList.sortItems() + + itemMoved = True + while itemMoved: + itemMoved = False + for row in range(self.scriptsList.count()): + topItem = self.scriptsList.item(row) + bottomItem = self.scriptsList.item(row + 1) + if topItem is None or bottomItem is None: + continue + + if topItem.checkState() == Qt.Unchecked and \ + bottomItem.checkState == Qt.Checked: + itm = self.scriptsList.takeItem(row + 1) + self.scriptsList.insertItem(row, itm) + itemMoved = True + + def __getScript(self, itm): + """ + Private method to get the script for the given item. + + @param itm item to get script for (QListWidgetItem) + @return reference to the script object (GreaseMonkeyScript) + """ + if itm is None: + return None + + script = itm.data(GreaseMonkeyConfigurationDialog.ScriptRole) + return script + + def __removeItem(self, itm): + """ + Private slot to remove a script item. + + @param itm item to be removed (QListWidgetItem) + """ + script = self.__getScript(itm) + if script is None: + return + + removeIt = E5MessageBox.yesNo( + self, + self.tr("Remove Script"), + self.tr( + """<p>Are you sure you want to remove <b>{0}</b>?</p>""") + .format(script.name())) + if removeIt and self.__manager.removeScript(script): + self.scriptsList.takeItem(self.scriptsList.row(itm)) + del itm + + def __itemChanged(self, itm): + """ + Private slot to handle changes of a script item. + + @param itm changed item (QListWidgetItem) + """ + script = self.__getScript(itm) + if script is None: + return + + if itm.checkState() == Qt.Checked: + self.__manager.enableScript(script) + else: + self.__manager.disableScript(script)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,191 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>GreaseMonkeyConfigurationDialog</class> + <widget class="QDialog" name="GreaseMonkeyConfigurationDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>550</width> + <height>450</height> + </rect> + </property> + <property name="windowTitle"> + <string>GreaseMonkey Scripts Configuration</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="iconLabel"> + <property name="minimumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="text"> + <string notr="true">Icon</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string><h2>GreaseMonkey Scripts</h2></string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Double clicking script will show additional information.</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="GreaseMonkeyConfigurationListWidget" name="scriptsList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="verticalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <property name="uniformItemSizes"> + <bool>true</bool> + </property> + <property name="selectionRectVisible"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="downloadLabel"> + <property name="text"> + <string><p>Get more scripts from <a href="https://greasyfork.org/">greasyfork.org</a> or via <a href="http://wiki.greasespot.net/User_Script_Hosting">Greasespot Wiki.</a></p></string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QPushButton" name="openDirectoryButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Press to open the scripts directory</string> + </property> + <property name="text"> + <string>Open Scripts Directory</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>GreaseMonkeyConfigurationListWidget</class> + <extends>QListWidget</extends> + <header>.GreaseMonkeyConfigurationListWidget.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>scriptsList</tabstop> + <tabstop>openDirectoryButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>GreaseMonkeyConfigurationDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>GreaseMonkeyConfigurationDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationListDelegate.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a delegate for the special list widget for GreaseMonkey +scripts. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QSize, QRect +from PyQt5.QtGui import QFontMetrics, QPalette, QFont +from PyQt5.QtWidgets import QStyle, QStyledItemDelegate, QApplication +from PyQt5.QtWidgets import QStyleOptionViewItem + +import UI.PixmapCache +import Globals + + +class GreaseMonkeyConfigurationListDelegate(QStyledItemDelegate): + """ + Class implementing a delegate for the special list widget for GreaseMonkey + scripts. + """ + IconSize = 32 + RemoveIconSize = 16 + CheckBoxSize = 18 + MinPadding = 5 + ItemWidth = 200 + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(GreaseMonkeyConfigurationListDelegate, self).__init__(parent) + + self.__removePixmap = \ + UI.PixmapCache.getIcon("greaseMonkeyTrash.png").pixmap( + GreaseMonkeyConfigurationListDelegate.RemoveIconSize) + self.__rowHeight = 0 + self.__padding = 0 + + def padding(self): + """ + Public method to get the padding used. + + @return padding used (integer) + """ + return self.__padding + + def paint(self, painter, option, index): + """ + Public method to paint the specified list item. + + @param painter painter object to paint to (QPainter) + @param option style option used for painting (QStyleOptionViewItem) + @param index model index of the item (QModelIndex) + """ + opt = QStyleOptionViewItem(option) + self.initStyleOption(opt, index) + + widget = opt.widget + style = widget.style() if widget is not None else QApplication.style() + height = opt.rect.height() + center = height // 2 + opt.rect.top() + + # Prepare title font + titleFont = QFont(opt.font) + titleFont.setBold(True) + titleFont.setPointSize(titleFont.pointSize() + 1) + + titleMetrics = QFontMetrics(titleFont) + if Globals.isWindowsPlatform(): + colorRole = QPalette.Text + else: + colorRole = QPalette.HighlightedText \ + if opt.state & QStyle.State_Selected else QPalette.Text + + leftPos = self.__padding + rightPos = opt.rect.right() - self.__padding - \ + GreaseMonkeyConfigurationListDelegate.RemoveIconSize + + # Draw background + style.drawPrimitive(QStyle.PE_PanelItemViewItem, opt, painter, widget) + + # Draw checkbox + checkBoxYPos = center - \ + GreaseMonkeyConfigurationListDelegate.CheckBoxSize // 2 + opt2 = QStyleOptionViewItem(opt) + if opt2.checkState == Qt.Checked: + opt2.state |= QStyle.State_On + else: + opt2.state |= QStyle.State_Off + styleCheckBoxRect = style.subElementRect( + QStyle.SE_ViewItemCheckIndicator, opt2, widget) + opt2.rect = QRect( + leftPos, checkBoxYPos, + styleCheckBoxRect.width(), styleCheckBoxRect.height()) + style.drawPrimitive(QStyle.PE_IndicatorViewItemCheck, opt2, painter, + widget) + leftPos = opt2.rect.right() + self.__padding + + # Draw icon + iconYPos = center - GreaseMonkeyConfigurationListDelegate.IconSize // 2 + iconRect = QRect(leftPos, iconYPos, + GreaseMonkeyConfigurationListDelegate.IconSize, + GreaseMonkeyConfigurationListDelegate.IconSize) + pixmap = index.data(Qt.DecorationRole).pixmap( + GreaseMonkeyConfigurationListDelegate.IconSize) + painter.drawPixmap(iconRect, pixmap) + leftPos = iconRect.right() + self.__padding + + # Draw script name + name = index.data(Qt.DisplayRole) + leftTitleEdge = leftPos + 2 + rightTitleEdge = rightPos - self.__padding + leftPosForVersion = titleMetrics.width(name) + self.__padding + nameRect = QRect(leftTitleEdge, opt.rect.top() + self.__padding, + rightTitleEdge - leftTitleEdge, titleMetrics.height()) + painter.setFont(titleFont) + style.drawItemText(painter, nameRect, Qt.AlignLeft, opt.palette, True, + name, colorRole) + + # Draw version + version = index.data(Qt.UserRole) + versionRect = QRect( + nameRect.x() + leftPosForVersion, nameRect.y(), + rightTitleEdge - leftTitleEdge, titleMetrics.height()) + versionFont = titleFont + painter.setFont(versionFont) + style.drawItemText(painter, versionRect, Qt.AlignLeft, opt.palette, + True, version, colorRole) + + # Draw description + infoYPos = nameRect.bottom() + opt.fontMetrics.leading() + infoRect = QRect( + nameRect.x(), infoYPos, + nameRect.width(), opt.fontMetrics.height()) + info = opt.fontMetrics.elidedText( + index.data(Qt.UserRole + 1), Qt.ElideRight, infoRect.width()) + painter.setFont(opt.font) + style.drawItemText(painter, infoRect, Qt.AlignLeft | Qt.TextSingleLine, + opt.palette, True, info, colorRole) + + # Draw remove button + removeIconYPos = center - \ + GreaseMonkeyConfigurationListDelegate.RemoveIconSize // 2 + removeIconRect = QRect( + rightPos, removeIconYPos, + GreaseMonkeyConfigurationListDelegate.RemoveIconSize, + GreaseMonkeyConfigurationListDelegate.RemoveIconSize) + painter.drawPixmap(removeIconRect, self.__removePixmap) + + def sizeHint(self, option, index): + """ + Public method to get a size hint for the specified list item. + + @param option style option used for painting (QStyleOptionViewItem) + @param index model index of the item (QModelIndex) + @return size hint (QSize) + """ + if not self.__rowHeight: + opt = QStyleOptionViewItem(option) + self.initStyleOption(opt, index) + + widget = opt.widget + style = widget.style() if widget is not None \ + else QApplication.style() + padding = style.pixelMetric(QStyle.PM_FocusFrameHMargin) + 1 + + titleFont = opt.font + titleFont.setBold(True) + titleFont.setPointSize(titleFont.pointSize() + 1) + + self.__padding = padding \ + if padding > GreaseMonkeyConfigurationListDelegate.MinPadding \ + else GreaseMonkeyConfigurationListDelegate.MinPadding + + titleMetrics = QFontMetrics(titleFont) + + self.__rowHeight = 2 * self.__padding + \ + opt.fontMetrics.leading() + \ + opt.fontMetrics.height() + \ + titleMetrics.height() + + return QSize(GreaseMonkeyConfigurationListDelegate.ItemWidth, + self.__rowHeight)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationListWidget.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a special list widget for GreaseMonkey scripts. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, QRect +from PyQt5.QtWidgets import QListWidget, QListWidgetItem + +from .GreaseMonkeyConfigurationListDelegate import \ + GreaseMonkeyConfigurationListDelegate + + +class GreaseMonkeyConfigurationListWidget(QListWidget): + """ + Class implementing a special list widget for GreaseMonkey scripts. + """ + removeItemRequested = pyqtSignal(QListWidgetItem) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(GreaseMonkeyConfigurationListWidget, self).__init__(parent) + + self.__delegate = GreaseMonkeyConfigurationListDelegate(self) + self.setItemDelegate(self.__delegate) + + def __containsRemoveIcon(self, pos): + """ + Private method to check, if the given position is inside the remove + icon. + + @param pos position to check for (QPoint) + @return flag indicating success (boolean) + """ + itm = self.itemAt(pos) + if itm is None: + return False + + rect = self.visualItemRect(itm) + iconSize = GreaseMonkeyConfigurationListDelegate.RemoveIconSize + removeIconXPos = rect.right() - self.__delegate.padding() - iconSize + center = rect.height() // 2 + rect.top() + removeIconYPos = center - iconSize // 2 + + removeIconRect = QRect(removeIconXPos, removeIconYPos, + iconSize, iconSize) + return removeIconRect.contains(pos) + + def mousePressEvent(self, evt): + """ + Protected method handling presses of mouse buttons. + + @param evt mouse press event (QMouseEvent) + """ + if self.__containsRemoveIcon(evt.pos()): + self.removeItemRequested.emit(self.itemAt(evt.pos())) + return + + super(GreaseMonkeyConfigurationListWidget, self).mousePressEvent(evt) + + def mouseDoubleClickEvent(self, evt): + """ + Protected method handling mouse double click events. + + @param evt mouse press event (QMouseEvent) + """ + if self.__containsRemoveIcon(evt.pos()): + self.removeItemRequested.emit(self.itemAt(evt.pos())) + return + + super(GreaseMonkeyConfigurationListWidget, self).mouseDoubleClickEvent( + evt)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationScriptInfoDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show GreaseMonkey script information. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QDialog + +from .Ui_GreaseMonkeyConfigurationScriptInfoDialog import \ + Ui_GreaseMonkeyConfigurationScriptInfoDialog + +from ..GreaseMonkeyScript import GreaseMonkeyScript + +import UI.PixmapCache + + +class GreaseMonkeyConfigurationScriptInfoDialog( + QDialog, Ui_GreaseMonkeyConfigurationScriptInfoDialog): + """ + Class implementing a dialog to show GreaseMonkey script information. + """ + def __init__(self, script, parent=None): + """ + Constructor + + @param script reference to the script (GreaseMonkeyScript) + @param parent reference to the parent widget (QWidget) + """ + super(GreaseMonkeyConfigurationScriptInfoDialog, self).__init__(parent) + self.setupUi(self) + + self.iconLabel.setPixmap( + UI.PixmapCache.getPixmap("greaseMonkey48.png")) + + self.__scriptFileName = script.fileName() + + self.setWindowTitle( + self.tr("Script Details of {0}").format(script.name())) + + self.nameLabel.setText(script.fullName()) + self.versionLabel.setText(script.version()) + self.urlLabel.setText(script.downloadUrl().toString()) + if script.startAt() == GreaseMonkeyScript.DocumentStart: + self.startAtLabel.setText("document-start") + else: + self.startAtLabel.setText("document-end") + self.descriptionBrowser.setHtml(script.description()) + self.runsAtBrowser.setHtml("<br/>".join(script.include())) + self.doesNotRunAtBrowser.setHtml("<br/>".join(script.exclude())) + + @pyqtSlot() + def on_showScriptSourceButton_clicked(self): + """ + Private slot to show an editor window with the script source code. + """ + from QScintilla.MiniEditor import MiniEditor + editor = MiniEditor(self.__scriptFileName, "JavaScript", self) + editor.show()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationScriptInfoDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,246 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>GreaseMonkeyConfigurationScriptInfoDialog</class> + <widget class="QDialog" name="GreaseMonkeyConfigurationScriptInfoDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>550</width> + <height>500</height> + </rect> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="iconLabel"> + <property name="minimumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="text"> + <string notr="true">Icon</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string><h2>GreaseMonkey Script Details</h2></string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Name:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string/> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Version:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="versionLabel"> + <property name="text"> + <string/> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>URL:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="urlLabel"> + <property name="text"> + <string/> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Start at:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLabel" name="startAtLabel"> + <property name="text"> + <string/> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Description:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QTextBrowser" name="descriptionBrowser"/> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Runs at:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QTextBrowser" name="runsAtBrowser"/> + </item> + <item row="6" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Does not run at:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="6" column="1"> + <widget class="QTextBrowser" name="doesNotRunAtBrowser"/> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="showScriptSourceButton"> + <property name="toolTip"> + <string>Press to open an editor with the script's source</string> + </property> + <property name="text"> + <string>Show source code of script</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <tabstops> + <tabstop>descriptionBrowser</tabstop> + <tabstop>runsAtBrowser</tabstop> + <tabstop>doesNotRunAtBrowser</tabstop> + <tabstop>showScriptSourceButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>GreaseMonkeyConfigurationScriptInfoDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>402</x> + <y>484</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>GreaseMonkeyConfigurationScriptInfoDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>470</x> + <y>490</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the GreaseMonkey configuration dialogs. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyDownloader.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the downloader for GreaseMonkey scripts. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, QObject, QSettings, QRegExp, QUrl +from PyQt5.QtWidgets import QDialog +from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest + +from E5Gui import E5MessageBox + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + + +class GreaseMonkeyDownloader(QObject): + """ + Class implementing the downloader for GreaseMonkey scripts. + """ + finished = pyqtSignal() + + def __init__(self, url, manager): + """ + Constructor + + @param url URL to download script from + @type QUrl + @param manager reference to the GreaseMonkey manager + @type GreaseMonkeyManager + """ + super(GreaseMonkeyDownloader, self).__init__() + + self.__manager = manager + + self.__reply = WebBrowserWindow.networkManager().get( + QNetworkRequest(url)) + self.__reply.finished.connect(self.__scriptDownloaded) + + self.__fileName = "" + self.__requireUrls = [] + + def __scriptDownloaded(self): + """ + Private slot to handle the finished download of a script. + """ + if self.sender() != self.__reply: + self.finished.emit() + return + + response = bytes(self.__reply.readAll()).decode() + + if self.__reply.error() == QNetworkReply.NoError and \ + "// ==UserScript==" in response: + from WebBrowser.Tools import WebBrowserTools + filePath = os.path.join( + self.__manager.scriptsDirectory(), + WebBrowserTools.getFileNameFromUrl(self.__reply.url())) + self.__fileName = WebBrowserTools.ensureUniqueFilename(filePath) + + try: + f = open(self.__fileName, "w", encoding="utf-8") + except (IOError, OSError) as err: + E5MessageBox.critical( + None, + self.tr("GreaseMonkey Download"), + self.tr( + """<p>The file <b>{0}</b> could not be opened""" + """ for writing.<br/>Reason: {1}</p>""").format( + self.__fileName, str(err))) + self.finished.emit() + return + f.write(response) + f.close() + + settings = QSettings( + os.path.join(self.__manager.requireScriptsDirectory(), + "requires.ini"), + QSettings.IniFormat) + settings.beginGroup("Files") + + rx = QRegExp("@require(.*)\\n") + rx.setMinimal(True) + rx.indexIn(response) + + for i in range(1, rx.captureCount() + 1): + url = rx.cap(i).strip() + if url and not settings.contains(url): + self.__requireUrls.append(QUrl(url)) + + self.__reply.deleteLater() + self.__reply = None + + self.__downloadRequires() + + def __requireDownloaded(self): + """ + Private slot to handle the finished download of a required script. + """ + if self.sender() != self.__reply: + self.finished.emit() + return + + response = bytes(self.__reply.readAll()).decode() + + if self.__reply.error() == QNetworkReply.NoError and response: + from WebBrowser.Tools import WebBrowserTools + filePath = os.path.join(self.__manager.requireScriptsDirectory(), + "require.js") + fileName = WebBrowserTools.ensureUniqueFilename(filePath, "{0}") + + try: + f = open(fileName, "w", encoding="utf-8") + except (IOError, OSError) as err: + E5MessageBox.critical( + None, + self.tr("GreaseMonkey Download"), + self.tr( + """<p>The file <b>{0}</b> could not be opened""" + """ for writing.<br/>Reason: {1}</p>""").format( + fileName, str(err))) + self.finished.emit() + return + f.write(response) + f.close() + + settings = QSettings( + os.path.join(self.__manager.requireScriptsDirectory(), + "requires.ini"), + QSettings.IniFormat) + settings.beginGroup("Files") + settings.setValue(self.__reply.originalUrl().toString(), fileName) + + self.__reply.deleteLater() + self.__reply = None + + self.__downloadRequires() + + def __downloadRequires(self): + """ + Private slot to initiate the download of required scripts. + """ + if self.__requireUrls: + self.__reply = WebBrowserWindow.networkManager().get( + QNetworkRequest(self.__requireUrls.pop(0))) + self.__reply.finished.connect(self.__requireDownloaded) + else: + from .GreaseMonkeyScript import GreaseMonkeyScript + deleteScript = True + script = GreaseMonkeyScript(self.__manager, self.__fileName) + + if script.isValid(): + if not self.__manager.containsScript(script.fullName()): + from .GreaseMonkeyAddScriptDialog import \ + GreaseMonkeyAddScriptDialog + dlg = GreaseMonkeyAddScriptDialog(self.__manager, script) + deleteScript = dlg.exec_() != QDialog.Accepted + else: + E5MessageBox.information( + None, + self.tr("GreaseMonkey Download"), + self.tr( + """<p><b>{0}</b> is already installed.</p>""") + .format(script.name())) + + if deleteScript: + try: + os.remove(self.__fileName) + except (IOError, OSError): + # ignore + pass + + self.finished.emit()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyJavaScript.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module containing some JavaScript resources. +""" + +from __future__ import unicode_literals + +bootstrap_js = """ +if(typeof GM_xmlhttpRequest === "undefined") { + GM_xmlhttpRequest = function(/* object */ details) { + details.method = details.method.toUpperCase() || "GET"; + + if(!details.url) { + throw("GM_xmlhttpRequest requires an URL."); + } + + // build XMLHttpRequest object + var oXhr = new XMLHttpRequest; + // run it + if(oXhr) { + if("onreadystatechange" in details) + oXhr.onreadystatechange = function() { + details.onreadystatechange(oXhr) + }; + if("onload" in details) + oXhr.onload = function() { details.onload(oXhr) }; + if("onerror" in details) + oXhr.onerror = function() { details.onerror(oXhr) }; + + oXhr.open(details.method, details.url, true); + + if("headers" in details) + for(var header in details.headers) + oXhr.setRequestHeader(header, details.headers[header]); + + if("data" in details) + oXhr.send(details.data); + else + oXhr.send(); + } else + throw ("This Browser is not supported, please upgrade.") + } +} + +if(typeof GM_addStyle === "undefined") { + function GM_addStyle(/* String */ styles) { + var head = document.getElementsByTagName("head")[0]; + if (head === undefined) { + document.onreadystatechange = function() { + if (document.readyState == "interactive") { + var oStyle = document.createElement("style"); + oStyle.setAttribute("type", "text\/css"); + oStyle.appendChild(document.createTextNode(styles)); + document.getElementsByTagName("head")[0].appendChild(oStyle); + } + } + } + else { + var oStyle = document.createElement("style"); + oStyle.setAttribute("type", "text\/css"); + oStyle.appendChild(document.createTextNode(styles)); + head.appendChild(oStyle); + } + } +} + +if(typeof GM_log === "undefined") { + function GM_log(log) { + if(console) + console.log(log); + } +} + +if(typeof GM_openInTab === "undefined") { + function GM_openInTab(url) { + window.open(url) + } +} + +// Define unsafe window +var unsafeWindow = window; +window.wrappedJSObject = unsafeWindow; + +// GM_registerMenuCommand not supported +if(typeof GM_registerMenuCommand === "undefined") { + function GM_registerMenuCommand(caption, commandFunc, accessKey) { } +} + +// GM Resource not supported +if(typeof GM_getResourceText === "undefined") { + function GM_getResourceText(resourceName) { + throw ("eric6 Web Browser: GM Resource is not supported!"); + } +} + +if(typeof GM_getResourceURL === "undefined") { + function GM_getResourceURL(resourceName) { + throw ("eric6 Web Browser: GM Resource is not supported!"); + } +} + +// GM Settings not supported +if(typeof GM_getValue === "undefined") { + function GM_getValue(name, defaultValue) { + return defaultValue; + } +} + +if(typeof GM_setValue === "undefined") { + function GM_setValue(name, value) { } +} + +if(typeof GM_deleteValue === "undefined") { + function GM_deleteValue(name) { } +} + +if(typeof GM_listValues === "undefined") { + function GM_listValues() { + return new Array(""); + } +} +""" + + +# {0} - unique script id +values_js = """ +function GM_deleteValue(aKey) {{ + localStorage.removeItem("{0}" + aKey); +}} + +function GM_getValue(aKey, aDefault) {{ + var val = localStorage.getItem("{0}" + aKey) + if (null === val && 'undefined' != typeof aDefault) return aDefault; + return val; +}} + +function GM_listValues() {{ + var values = []; + for (var i = 0; i < localStorage.length; i++) {{ + var k = localStorage.key(i); + if (k.indexOf("{0}") === 0) {{ + values.push(k.replace("{0}", "")); + }} + }} + return values; +}} + +function GM_setValue(aKey, aVal) {{ + localStorage.setItem("{0}" + aKey, aVal); +}} +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the manager for GreaseMonkey scripts. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject, QTimer, QFile, \ + QDir, QSettings, QMetaObject, QUrl, Q_ARG + +import Utilities +import Preferences + +from WebBrowser.WebBrowserWindow import WebBrowserWindow +from .GreaseMonkeyUrlInterceptor import GreaseMonkeyUrlInterceptor + + +class GreaseMonkeyManager(QObject): + """ + Class implementing the manager for GreaseMonkey scripts. + """ + scriptsChanged = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(GreaseMonkeyManager, self).__init__(parent) + + self.__disabledScripts = [] + self.__scripts = [] + self.__downloaders = [] + + self.__interceptor = GreaseMonkeyUrlInterceptor(self) + WebBrowserWindow.networkManager().installUrlInterceptor( + GreaseMonkeyUrlInterceptor(self)) + + QTimer.singleShot(0, self.__load) + + def __del__(self): + """ + Special method called during object destruction. + """ + WebBrowserWindow.networkManager().removeUrlInterceptor( + self.__interceptor) + + def showConfigurationDialog(self, parent=None): + """ + Public method to show the configuration dialog. + + @param parent reference to the parent widget (QWidget) + """ + from .GreaseMonkeyConfiguration.GreaseMonkeyConfigurationDialog \ + import GreaseMonkeyConfigurationDialog + self.__configDiaolg = GreaseMonkeyConfigurationDialog(self, parent) + self.__configDiaolg.show() + + def downloadScript(self, url): + """ + Public method to download a GreaseMonkey script. + + @param url URL to download script from + @type QUrl + """ + QMetaObject.invokeMethod( + self, "doDownloadScript", Qt.QueuedConnection, + Q_ARG(QUrl, url)) + + @pyqtSlot(QUrl) + def doDownloadScript(self, url): + from .GreaseMonkeyDownloader import GreaseMonkeyDownloader + downloader = GreaseMonkeyDownloader(url, self) + downloader.finished.connect(self.__downloaderFinished) + self.__downloaders.append(downloader) + + def __downloaderFinished(self): + """ + Private slot to handle the completion of a script download. + """ + downloader = self.sender() + if downloader is None or downloader not in self.__downloaders: + return + + self.__downloaders.remove(downloader) + + def scriptsDirectory(self): + """ + Public method to get the path of the scripts directory. + + @return path of the scripts directory (string) + """ + return os.path.join( + Utilities.getConfigDir(), "web_browser", "greasemonkey") + + def requireScriptsDirectory(self): + """ + Public method to get the path of the scripts directory. + + @return path of the scripts directory (string) + """ + return os.path.join(self.scriptsDirectory(), "requires") + + def requireScripts(self, urlList): + """ + Public method to get the sources of all required scripts. + + @param urlList list of URLs (list of string) + @return sources of all required scripts (string) + """ + requiresDir = QDir(self.requireScriptsDirectory()) + if not requiresDir.exists() or len(urlList) == 0: + return "" + + script = "" + + settings = QSettings( + os.path.join(self.requireScriptsDirectory(), "requires.ini"), + QSettings.IniFormat) + settings.beginGroup("Files") + for url in urlList: + if settings.contains(url): + fileName = settings.value(url) + try: + f = open(fileName, "r", encoding="utf-8") + source = f.read() + f.close() + except (IOError, OSError): + source = "" + script += source.strip() + "\n" + + return script + + def saveConfiguration(self): + """ + Public method to save the configuration. + """ + Preferences.setWebBrowser("GreaseMonkeyDisabledScripts", + self.__disabledScripts) + + def allScripts(self): + """ + Public method to get a list of all scripts. + + @return list of all scripts (list of GreaseMonkeyScript) + """ + return self.__scripts[:] + + def containsScript(self, fullName): + """ + Public method to check, if the given script exists. + + @param fullName full name of the script (string) + @return flag indicating the existence (boolean) + """ + for script in self.__scripts: + if script.fullName() == fullName: + return True + + return False + + def enableScript(self, script): + """ + Public method to enable the given script. + + @param script script to be enabled (GreaseMonkeyScript) + """ + script.setEnabled(True) + fullName = script.fullName() + if fullName in self.__disabledScripts: + self.__disabledScripts.remove(fullName) + + collection = WebBrowserWindow.webProfile().scripts() + collection.insert(script.webScript()) + + def disableScript(self, script): + """ + Public method to disable the given script. + + @param script script to be disabled (GreaseMonkeyScript) + """ + script.setEnabled(False) + fullName = script.fullName() + if fullName not in self.__disabledScripts: + self.__disabledScripts.append(fullName) + + collection = WebBrowserWindow.webProfile().scripts() + collection.remove(collection.findScript(fullName)) + + def addScript(self, script): + """ + Public method to add a script. + + @param script script to be added (GreaseMonkeyScript) + @return flag indicating success (boolean) + """ + if not script or not script.isValid(): + return False + + self.__scripts.append(script) + script.scriptChanged.connect(self.__scriptChanged) + + collection = WebBrowserWindow.webProfile().scripts() + collection.insert(script.webScript()) + + self.scriptsChanged.emit() + return True + + def removeScript(self, script, removeFile=True): + """ + Public method to remove a script. + + @param script script to be removed (GreaseMonkeyScript) + @param removeFile flag indicating to remove the script file as well + (bool) + @return flag indicating success (boolean) + """ + if not script: + return False + + try: + self.__scripts.remove(script) + except ValueError: + pass + + fullName = script.fullName() + collection = WebBrowserWindow.webProfile().scripts() + collection.remove(collection.findScript(fullName)) + + if fullName in self.__disabledScripts: + self.__disabledScripts.remove(fullName) + + if removeFile: + QFile.remove(script.fileName()) + del script + + self.scriptsChanged.emit() + return True + + def canRunOnScheme(self, scheme): + """ + Public method to check, if scripts can be run on a scheme. + + @param scheme scheme to check (string) + @return flag indicating, that scripts can be run (boolean) + """ + return scheme in ["http", "https", "data", "ftp"] + + def __load(self): + """ + Private slot to load the available scripts into the manager. + """ + scriptsDir = QDir(self.scriptsDirectory()) + if not scriptsDir.exists(): + scriptsDir.mkpath(self.scriptsDirectory()) + + if not scriptsDir.exists("requires"): + scriptsDir.mkdir("requires") + + self.__disabledScripts = \ + Preferences.getWebBrowser("GreaseMonkeyDisabledScripts") + + from .GreaseMonkeyScript import GreaseMonkeyScript + for fileName in scriptsDir.entryList(["*.js"], QDir.Files): + absolutePath = scriptsDir.absoluteFilePath(fileName) + script = GreaseMonkeyScript(self, absolutePath) + + if not script.isValid(): + del script + continue + + self.__scripts.append(script) + + if script.fullName() in self.__disabledScripts: + script.setEnabled(False) + else: + collection = WebBrowserWindow.webProfile().scripts() + collection.insert(script.webScript()) + + def __scriptChanged(self): + """ + Private slot handling a changed script. + """ + script = self.sender() + if not script: + return + + fullName = script.fullName() + collection = WebBrowserWindow.webProfile().scripts() + collection.remove(collection.findScript(fullName)) + collection.insert(script.webScript())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyScript.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,391 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the GreaseMonkey script. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QRegExp, \ + QByteArray, QCryptographicHash +from PyQt5.QtWebEngineWidgets import QWebEngineScript + +from .GreaseMonkeyJavaScript import bootstrap_js, values_js + +from ..Tools.DelayedFileWatcher import DelayedFileWatcher + + +class GreaseMonkeyScript(QObject): + """ + Class implementing the GreaseMonkey script. + """ + DocumentStart = 0 + DocumentEnd = 1 + DocumentIdle = 2 + + scriptChanged = pyqtSignal() + + def __init__(self, manager, path): + """ + Constructor + + @param manager reference to the manager object (GreaseMonkeyManager) + @param path path of the Javascript file (string) + """ + super(GreaseMonkeyScript, self).__init__(manager) + + self.__manager = manager + self.__fileWatcher = DelayedFileWatcher(parent=None) + + self.__name = "" + self.__namespace = "GreaseMonkeyNS" + self.__description = "" + self.__version = "" + + self.__include = [] + self.__exclude = [] + + self.__downloadUrl = QUrl() + self.__updateUrl = QUrl() + self.__startAt = GreaseMonkeyScript.DocumentEnd + + self.__script = "" + self.__fileName = path + self.__enabled = True + self.__valid = False + self.__noFrames = False + + self.__parseScript() + + self.__fileWatcher.delayedFileChanged.connect( + self.__watchedFileChanged) + + def isValid(self): + """ + Public method to check the validity of the script. + + @return flag indicating a valid script (boolean) + """ + return self.__valid + + def name(self): + """ + Public method to get the name of the script. + + @return name of the script (string) + """ + return self.__name + + def nameSpace(self): + """ + Public method to get the name space of the script. + + @return name space of the script (string) + """ + return self.__namespace + + def fullName(self): + """ + Public method to get the full name of the script. + + @return full name of the script (string) + """ + return "{0}/{1}".format(self.__namespace, self.__name) + + def description(self): + """ + Public method to get the description of the script. + + @return description of the script (string) + """ + return self.__description + + def version(self): + """ + Public method to get the version of the script. + + @return version of the script (string) + """ + return self.__version + + def downloadUrl(self): + """ + Public method to get the download URL of the script. + + @return download URL of the script (QUrl) + """ + return QUrl(self.__downloadUrl) + + def updateUrl(self): + """ + Public method to get the update URL of the script. + + @return update URL of the script (QUrl) + """ + return QUrl(self.__updateUrl) + + def startAt(self): + """ + Public method to get the start point of the script. + + @return start point of the script (DocumentStart or DocumentEnd) + """ + return self.__startAt + + def noFrames(self): + """ + Public method to get the noFrames flag. + + @return flag indicating to not run on sub frames + @rtype bool + """ + return self.__noFrames + + def isEnabled(self): + """ + Public method to check, if the script is enabled. + + @return flag indicating an enabled state (boolean) + """ + return self.__enabled and self.__valid + + def setEnabled(self, enable): + """ + Public method to enable a script. + + @param enable flag indicating the new enabled state (boolean) + """ + self.__enabled = enable + + def include(self): + """ + Public method to get the list of included URLs. + + @return list of included URLs (list of strings) + """ + return self.__include[:] + + def exclude(self): + """ + Public method to get the list of excluded URLs. + + @return list of excluded URLs (list of strings) + """ + return self.__exclude[:] + + def script(self): + """ + Public method to get the Javascript source. + + @return Javascript source (string) + """ + return self.__script + + def fileName(self): + """ + Public method to get the path of the Javascript file. + + @return path path of the Javascript file (string) + """ + return self.__fileName + + @pyqtSlot(str) + def __watchedFileChanged(self, fileName): + """ + Private slot handling changes of the script file. + + @param fileName path of the script file + @type str + """ + if self.__fileName == fileName: + self.__parseScript() + + self.__manager.removeScript(self, False) + self.__manager.addScript(self) + + self.scriptChanged.emit() + + def __parseScript(self): + """ + Private method to parse the given script and populate the data + structure. + """ + self.__name = "" + self.__namespace = "GreaseMonkeyNS" + self.__description = "" + self.__version = "" + + self.__include = [] + self.__exclude = [] + + self.__downloadUrl = QUrl() + self.__updateUrl = QUrl() + self.__startAt = GreaseMonkeyScript.DocumentEnd + + self.__script = "" + self.__enabled = True + self.__valid = False + self.__noFrames = False + + try: + f = open(self.__fileName, "r", encoding="utf-8") + fileData = f.read() + f.close() + except (IOError, OSError): + # silently ignore because it shouldn't happen + return + + if self.__fileName not in self.__fileWatcher.files(): + self.__fileWatcher.addPath(self.__fileName) + + rx = QRegExp("// ==UserScript==(.*)// ==/UserScript==") + rx.indexIn(fileData) + metaDataBlock = rx.cap(1).strip() + + if metaDataBlock == "": + # invalid script file + return + + requireList = [] + for line in metaDataBlock.splitlines(): + if not line.strip(): + continue + + if not line.startswith("// @"): + continue + + line = line[3:].replace("\t", " ") + index = line.find(" ") + if index < 0: + continue + + key = line[:index].strip() + value = line[index + 1:].strip() + + # Ignored values: @resource, @unwrap + + if not key or not value: + continue + + if key == "@name": + self.__name = value + + elif key == "@namespace": + self.__namespace = value + + elif key == "@description": + self.__description = value + + elif key == "@version": + self.__version = value + + elif key in ["@include", "@match"]: + self.__include.append(value) + + elif key in ["@exclude", "@exclude_match"]: + self.__exclude.append(value) + + elif key == "@require": + requireList.append(value) + + elif key == "@run-at": + if value == "document-end": + self.__startAt = GreaseMonkeyScript.DocumentEnd + elif value == "document-start": + self.__startAt = GreaseMonkeyScript.DocumentStart + elif value == "document-idle": + self.__startAt = GreaseMonkeyScript.DocumentIdle + + elif key == "@downloadURL" and self.__downloadUrl.isEmpty(): + self.__downloadUrl = QUrl(value) + + elif key == "@updateURL" and self.__updateUrl.isEmpty(): + self.__updateUrl = QUrl(value) + + if not self.__include: + self.__include.append("*") + + nspace = bytes(QCryptographicHash.hash( + QByteArray(self.fullName().encode("utf-8")), + QCryptographicHash.Md4).toHex()).decode("ascii") + valuesScript = values_js.format(nspace) + runCheck = """ + for (var value of {0}) {{ + var re = new RegExp(value); + if (re.test(window.location.href)) {{ + return; + }} + }} + __eric_includes = false; + for (var value of {1}) {{ + var re = new RegExp(value); + if (re.test(window.location.href)) {{ + __eric_includes = true; + break; + }} + }} + if (!__eric_includes) {{ + return; + }} + delete __eric_includes;""".format( + self.__toJavaScriptList(self.__exclude[:]), + self.__toJavaScriptList(self.__include[:]) + ) + runCheck = "" + self.__script = "(function(){{{0}\n{1}\n{2}\n{3}\n}})();".format( + runCheck, valuesScript, + self.__manager.requireScripts(requireList), fileData + ) + self.__valid = True + + def webScript(self): + """ + Public method to create a script object. + + @return prepared script object + @rtype QWebEngineScript + """ + if self.startAt() == GreaseMonkeyScript.DocumentStart: + injectionPoint = QWebEngineScript.DocumentCreation + elif self.startAt() == GreaseMonkeyScript.DocumentEnd: + injectionPoint = QWebEngineScript.DocumentReady + elif self.startAt() == GreaseMonkeyScript.DocumentIdle: + injectionPoint = QWebEngineScript.Deferred + else: + raise ValueError("Wrong script start point.") + + script = QWebEngineScript() + script.setName(self.fullName()) + script.setInjectionPoint(injectionPoint) + script.setWorldId(QWebEngineScript.MainWorld) + script.setRunsOnSubFrames(not self.__noFrames) + script.setSourceCode("{0}\n{1}".format( + bootstrap_js, self.__script + )) + return script + + def __toJavaScriptList(self, patterns): + """ + Private method to convert a list of str to a string containing a valid + JavaScript list definition. + + @param patterns list of match patterns + @type list of str + @return JavaScript script containing the list + @rtype str + """ + patternList = [] + for pattern in patterns: + if pattern.startswith("/") and pattern.endswith("/") and \ + len(pattern) > 1: + pattern = pattern[1:-1] + else: + pattern = pattern.replace(".", "\\.").replace("*", ".*") + pattern = "'{0}'".format(pattern) + patternList.append(pattern) + + script = "[{0}]".format(",".join(patternList)) + return script
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyUrlInterceptor.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a handler for GreaseMonkey related URLs. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInfo + +from ..Network.UrlInterceptor import UrlInterceptor + + +class GreaseMonkeyUrlInterceptor(UrlInterceptor): + """ + Class implementing a handler for GreaseMonkey related URLs. + """ + def __init__(self, manager): + """ + Constructor + + @param manager reference to the GreaseMonkey manager + @type GreaseMonkeyManager + """ + super(GreaseMonkeyUrlInterceptor, self).__init__(manager) + + self.__manager = manager + + def interceptRequest(self, info): + """ + Public method to handle a GreaseMonkey request. + + @param info request info object + @type QWebEngineUrlRequestInfo + """ + if info.navigationType() != \ + QWebEngineUrlRequestInfo.NavigationTypeLink: + return + + if info.requestUrl().toString().endswith(".user.js"): + self.__manager.downloadScript(info.requestUrl()) + info.block(True)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/GreaseMonkey/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the GreaseMonkey support. +""" + +# +# The code in this package was inspired and ported in parts from QupZilla +# Copyright (C) David Rosca <nowrep@gmail.com> +#
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/History/HistoryCompleter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,303 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a special completer for the history. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QRegExp, QTimer, QSortFilterProxyModel +from PyQt5.QtWidgets import QTableView, QAbstractItemView, QCompleter + +from .HistoryModel import HistoryModel +from .HistoryFilterModel import HistoryFilterModel + + +class HistoryCompletionView(QTableView): + """ + Class implementing a special completer view for history based completions. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(HistoryCompletionView, self).__init__(parent) + + self.horizontalHeader().hide() + self.verticalHeader().hide() + + self.setShowGrid(False) + + self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setSelectionMode(QAbstractItemView.SingleSelection) + self.setTextElideMode(Qt.ElideRight) + + metrics = self.fontMetrics() + self.verticalHeader().setDefaultSectionSize(metrics.height()) + + def resizeEvent(self, evt): + """ + Protected method handling resize events. + + @param evt reference to the resize event (QResizeEvent) + """ + self.horizontalHeader().resizeSection(0, 0.65 * self.width()) + self.horizontalHeader().setStretchLastSection(True) + + super(HistoryCompletionView, self).resizeEvent(evt) + + def sizeHintForRow(self, row): + """ + Public method to give a size hint for rows. + + @param row row number (integer) + @return desired row height (integer) + """ + metrics = self.fontMetrics() + return metrics.height() + + +class HistoryCompletionModel(QSortFilterProxyModel): + """ + Class implementing a special model for history based completions. + """ + HistoryCompletionRole = HistoryFilterModel.MaxRole + 1 + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(HistoryCompletionModel, self).__init__(parent) + + self.__searchString = "" + self.__searchMatcher = QRegExp( + "", Qt.CaseInsensitive, QRegExp.FixedString) + self.__wordMatcher = QRegExp("", Qt.CaseInsensitive) + self.__isValid = False + + self.setDynamicSortFilter(True) + + def data(self, index, role=Qt.DisplayRole): + """ + Public method to get data from the model. + + @param index index of history entry to get data for (QModelIndex) + @param role data role (integer) + @return history entry data + """ + # If the model is valid, tell QCompleter that everything we have + # filtered matches what the user typed; if not, nothing matches + if role == self.HistoryCompletionRole and index.isValid(): + if self.isValid(): + return "t" + else: + return "f" + + if role == Qt.DisplayRole: + if index.column() == 0: + role = HistoryModel.UrlStringRole + else: + role = HistoryModel.TitleRole + + return QSortFilterProxyModel.data(self, index, role) + + def searchString(self): + """ + Public method to get the current search string. + + @return current search string (string) + """ + return self.__searchString + + def setSearchString(self, string): + """ + Public method to set the current search string. + + @param string new search string (string) + """ + if string == self.__searchString: + return + + self.__searchString = string + self.__searchMatcher.setPattern(self.__searchString) + self.__wordMatcher.setPattern( + "\\b" + QRegExp.escape(self.__searchString)) + self.invalidateFilter() + + def isValid(self): + """ + Public method to check the model for validity. + + @return flag indicating a valid status (boolean) + """ + return self.__isValid + + def setValid(self, valid): + """ + Public method to set the model's validity. + + @param valid flag indicating the new valid status (boolean) + """ + if valid == self.__isValid: + return + + self.__isValid = valid + + # tell the history completer that the model has changed + self.dataChanged.emit(self.index(0, 0), self.index(0, + self.rowCount() - 1)) + + def filterAcceptsRow(self, sourceRow, sourceParent): + """ + Public method to determine, if the row is acceptable. + + @param sourceRow row number in the source model (integer) + @param sourceParent index of the source item (QModelIndex) + @return flag indicating acceptance (boolean) + """ + # Do a case-insensitive substring match against both the url and title. + # It's already ensured, that the user doesn't accidentally use regexp + # metacharacters (s. setSearchString()). + idx = self.sourceModel().index(sourceRow, 0, sourceParent) + + url = self.sourceModel().data(idx, HistoryModel.UrlStringRole) + if self.__searchMatcher.indexIn(url) != -1: + return True + + title = self.sourceModel().data(idx, HistoryModel.TitleRole) + if self.__searchMatcher.indexIn(title) != -1: + return True + + return False + + def lessThan(self, left, right): + """ + Public method used to sort the displayed items. + + It implements a special sorting function based on the history entry's + frequency giving a bonus to hits that match on a word boundary so that + e.g. "dot.python-projects.org" is a better result for typing "dot" than + "slashdot.org". However, it only looks for the string in the host name, + not the entire URL, since while it makes sense to e.g. give + "www.phoronix.com" a bonus for "ph", it does NOT make sense to give + "www.yadda.com/foo.php" the bonus. + + @param left index of left item (QModelIndex) + @param right index of right item (QModelIndex) + @return true, if left is less than right (boolean) + """ + frequency_L = \ + self.sourceModel().data(left, HistoryFilterModel.FrequencyRole) + url_L = self.sourceModel().data(left, HistoryModel.UrlRole).host() + title_L = self.sourceModel().data(left, HistoryModel.TitleRole) + + if self.__wordMatcher.indexIn(url_L) != -1 or \ + self.__wordMatcher.indexIn(title_L) != -1: + frequency_L *= 2 + + frequency_R = \ + self.sourceModel().data(right, HistoryFilterModel.FrequencyRole) + url_R = self.sourceModel().data(right, HistoryModel.UrlRole).host() + title_R = self.sourceModel().data(right, HistoryModel.TitleRole) + + if self.__wordMatcher.indexIn(url_R) != -1 or \ + self.__wordMatcher.indexIn(title_R) != -1: + frequency_R *= 2 + + # Sort results in descending frequency-derived score. + return frequency_R < frequency_L + + +class HistoryCompleter(QCompleter): + """ + Class implementing a completer for the browser history. + """ + def __init__(self, model, parent=None): + """ + Constructor + + @param model reference to the model (QAbstractItemModel) + @param parent reference to the parent object (QObject) + """ + super(HistoryCompleter, self).__init__(model, parent) + + self.setPopup(HistoryCompletionView()) + + # Completion should be against the faked role. + self.setCompletionRole(HistoryCompletionModel.HistoryCompletionRole) + + # Since the completion role is faked, advantage of the sorted-model + # optimizations in QCompleter can be taken. + self.setCaseSensitivity(Qt.CaseSensitive) + self.setModelSorting(QCompleter.CaseSensitivelySortedModel) + + self.__searchString = "" + self.__filterTimer = QTimer(self) + self.__filterTimer.setSingleShot(True) + self.__filterTimer.timeout.connect(self.__updateFilter) + + def pathFromIndex(self, idx): + """ + Public method to get a path for a given index. + + @param idx reference to the index (QModelIndex) + @return the actual URL from the history (string) + """ + return self.model().data(idx, HistoryModel.UrlStringRole) + + def splitPath(self, path): + """ + Public method to split the given path into strings, that are used to + match at each level in the model. + + @param path path to be split (string) + @return list of path elements (list of strings) + """ + if path == self.__searchString: + return ["t"] + + # Queue an update to the search string. Wait a bit, so that if the user + # is quickly typing, the completer doesn't try to complete until they + # pause. + if self.__filterTimer.isActive(): + self.__filterTimer.stop() + self.__filterTimer.start(150) + + # If the previous search results are not a superset of the current + # search results, tell the model that it is not valid yet. + if not path.startswith(self.__searchString): + self.model().setValid(False) + + self.__searchString = path + + # The actual filtering is done by the HistoryCompletionModel. Just + # return a short dummy here so that QCompleter thinks everything + # matched. + return ["t"] + + def __updateFilter(self): + """ + Private slot to update the search string. + """ + completionModel = self.model() + + # Tell the HistoryCompletionModel about the new search string. + completionModel.setSearchString(self.__searchString) + + # Sort the model. + completionModel.sort(0) + + # Mark it valid. + completionModel.setValid(True) + + # Now update the QCompleter widget, but only if the user is still + # typing a URL. + if self.widget() is not None and self.widget().hasFocus(): + self.complete()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/History/HistoryDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to manage history. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, Qt, QUrl +from PyQt5.QtGui import QFontMetrics, QCursor +from PyQt5.QtWidgets import QDialog, QMenu, QApplication + +from E5Gui.E5TreeSortFilterProxyModel import E5TreeSortFilterProxyModel + +from .HistoryModel import HistoryModel + +from .Ui_HistoryDialog import Ui_HistoryDialog + + +class HistoryDialog(QDialog, Ui_HistoryDialog): + """ + Class implementing a dialog to manage history. + + @signal openUrl(QUrl, str) emitted to open a URL in the current tab + @signal newUrl(QUrl, str) emitted to open a URL in a new tab + """ + openUrl = pyqtSignal(QUrl, str) + newUrl = pyqtSignal(QUrl, str) + + def __init__(self, parent=None, manager=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget + @param manager reference to the history manager object (HistoryManager) + """ + super(HistoryDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.__historyManager = manager + if self.__historyManager is None: + import WebBrowser.WebBrowserWindow + self.__historyManager = \ + WebBrowser.WebBrowserWindow.WebBrowserWindow.historyManager() + + self.__model = self.__historyManager.historyTreeModel() + self.__proxyModel = E5TreeSortFilterProxyModel(self) + self.__proxyModel.setSortRole(HistoryModel.DateTimeRole) + self.__proxyModel.setFilterKeyColumn(-1) + self.__proxyModel.setSourceModel(self.__model) + self.historyTree.setModel(self.__proxyModel) + self.historyTree.expandAll() + fm = QFontMetrics(self.font()) + header = fm.width("m") * 40 + self.historyTree.header().resizeSection(0, header) + self.historyTree.header().setStretchLastSection(True) + self.historyTree.setContextMenuPolicy(Qt.CustomContextMenu) + + self.historyTree.activated.connect(self.__activated) + self.historyTree.customContextMenuRequested.connect( + self.__customContextMenuRequested) + + self.searchEdit.textChanged.connect( + self.__proxyModel.setFilterFixedString) + self.removeButton.clicked.connect(self.historyTree.removeSelected) + self.removeAllButton.clicked.connect(self.__historyManager.clear) + + self.__proxyModel.modelReset.connect(self.__modelReset) + + def __modelReset(self): + """ + Private slot handling a reset of the tree view's model. + """ + self.historyTree.expandAll() + + def __customContextMenuRequested(self, pos): + """ + Private slot to handle the context menu request for the bookmarks tree. + + @param pos position the context menu was requested (QPoint) + """ + menu = QMenu() + idx = self.historyTree.indexAt(pos) + idx = idx.sibling(idx.row(), 0) + if idx.isValid() and not self.historyTree.model().hasChildren(idx): + menu.addAction( + self.tr("&Open"), self.__openHistoryInCurrentTab) + menu.addAction( + self.tr("Open in New &Tab"), self.__openHistoryInNewTab) + menu.addSeparator() + menu.addAction(self.tr("&Copy"), self.__copyHistory) + menu.addAction(self.tr("&Remove"), self.historyTree.removeSelected) + menu.exec_(QCursor.pos()) + + def __activated(self, idx): + """ + Private slot to handle the activation of an entry. + + @param idx reference to the entry index (QModelIndex) + """ + self.__openHistory( + QApplication.keyboardModifiers() & Qt.ControlModifier) + + def __openHistoryInCurrentTab(self): + """ + Private slot to open a history entry in the current browser tab. + """ + self.__openHistory(False) + + def __openHistoryInNewTab(self): + """ + Private slot to open a history entry in a new browser tab. + """ + self.__openHistory(True) + + def __openHistory(self, newTab): + """ + Private method to open a history entry. + + @param newTab flag indicating to open the history entry in a new tab + (boolean) + """ + idx = self.historyTree.currentIndex() + if newTab: + self.newUrl.emit( + idx.data(HistoryModel.UrlRole), + idx.data(HistoryModel.TitleRole)) + else: + self.openUrl.emit( + idx.data(HistoryModel.UrlRole), + idx.data(HistoryModel.TitleRole)) + + def __copyHistory(self): + """ + Private slot to copy a history entry's URL to the clipboard. + """ + idx = self.historyTree.currentIndex() + if not idx.parent().isValid(): + return + + url = idx.data(HistoryModel.UrlStringRole) + + clipboard = QApplication.clipboard() + clipboard.setText(url)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/History/HistoryDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,176 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>HistoryDialog</class> + <widget class="QDialog" name="HistoryDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>750</width> + <height>450</height> + </rect> + </property> + <property name="windowTitle"> + <string>Manage History</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="E5ClearableLineEdit" name="searchEdit"> + <property name="toolTip"> + <string>Enter search term for history entries</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <widget class="E5TreeView" name="historyTree"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="textElideMode"> + <enum>Qt::ElideMiddle</enum> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected entries</string> + </property> + <property name="text"> + <string>&Remove</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all entries</string> + </property> + <property name="text"> + <string>Remove &All</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="spacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + <customwidget> + <class>E5TreeView</class> + <extends>QTreeView</extends> + <header>E5Gui/E5TreeView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>historyTree</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>removeAllButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>HistoryDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>252</x> + <y>445</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>HistoryDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>320</x> + <y>445</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/History/HistoryFilterModel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,375 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the history filter model. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QDateTime, QModelIndex, QAbstractProxyModel + +from .HistoryModel import HistoryModel + + +class HistoryData(object): + """ + Class storing some history data. + """ + def __init__(self, offset, frequency=0): + """ + Constructor + + @param offset tail offset (integer) + @param frequency frequency (integer) + """ + self.tailOffset = offset + self.frequency = frequency + + def __eq__(self, other): + """ + Special method implementing equality. + + @param other reference to the object to check against (HistoryData) + @return flag indicating equality (boolean) + """ + return self.tailOffset == other.tailOffset and \ + (self.frequency == -1 or other.frequency == -1 or + self.frequency == other.frequency) + + def __lt__(self, other): + """ + Special method determining less relation. + + Note: Like the actual history entries the index mapping is sorted in + reverse order by offset + + @param other reference to the history data object to compare against + (HistoryEntry) + @return flag indicating less (boolean) + """ + return self.tailOffset > other.tailOffset + + +class HistoryFilterModel(QAbstractProxyModel): + """ + Class implementing the history filter model. + """ + FrequencyRole = HistoryModel.MaxRole + 1 + MaxRole = FrequencyRole + + def __init__(self, sourceModel, parent=None): + """ + Constructor + + @param sourceModel reference to the source model (QAbstractItemModel) + @param parent reference to the parent object (QObject) + """ + super(HistoryFilterModel, self).__init__(parent) + + self.__loaded = False + self.__filteredRows = [] + self.__historyDict = {} + self.__scaleTime = QDateTime() + + self.setSourceModel(sourceModel) + + def historyContains(self, url): + """ + Public method to check the history for an entry. + + @param url URL to check for (string) + @return flag indicating success (boolean) + """ + self.__load() + return url in self.__historyDict + + def historyLocation(self, url): + """ + Public method to get the row number of an entry in the source model. + + @param url URL to check for (tring) + @return row number in the source model (integer) + """ + self.__load() + if url not in self.__historyDict: + return 0 + + return self.sourceModel().rowCount() - self.__historyDict[url] + + def data(self, index, role=Qt.DisplayRole): + """ + Public method to get data from the model. + + @param index index of history entry to get data for (QModelIndex) + @param role data role (integer) + @return history entry data + """ + if role == self.FrequencyRole and index.isValid(): + return self.__filteredRows[index.row()].frequency + + return QAbstractProxyModel.data(self, index, role) + + def setSourceModel(self, sourceModel): + """ + Public method to set the source model. + + @param sourceModel reference to the source model (QAbstractItemModel) + """ + if self.sourceModel() is not None: + self.sourceModel().modelReset.disconnect(self.__sourceReset) + self.sourceModel().dataChanged.disconnect(self.__sourceDataChanged) + self.sourceModel().rowsInserted.disconnect( + self.__sourceRowsInserted) + self.sourceModel().rowsRemoved.disconnect(self.__sourceRowsRemoved) + + super(HistoryFilterModel, self).setSourceModel(sourceModel) + + if self.sourceModel() is not None: + self.__loaded = False + self.sourceModel().modelReset.connect(self.__sourceReset) + self.sourceModel().dataChanged.connect(self.__sourceDataChanged) + self.sourceModel().rowsInserted.connect(self.__sourceRowsInserted) + self.sourceModel().rowsRemoved.connect(self.__sourceRowsRemoved) + + def __sourceDataChanged(self, topLeft, bottomRight): + """ + Private slot to handle the change of data of the source model. + + @param topLeft index of top left data element (QModelIndex) + @param bottomRight index of bottom right data element (QModelIndex) + """ + self.dataChanged.emit( + self.mapFromSource(topLeft), self.mapFromSource(bottomRight)) + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Public method to get the header data. + + @param section section number (integer) + @param orientation header orientation (Qt.Orientation) + @param role data role (integer) + @return header data + """ + return self.sourceModel().headerData(section, orientation, role) + + def recalculateFrequencies(self): + """ + Public method to recalculate the frequencies. + """ + self.__sourceReset() + + def __sourceReset(self): + """ + Private slot to handle a reset of the source model. + """ + self.beginResetModel() + self.__loaded = False + self.endResetModel() + + def rowCount(self, parent=QModelIndex()): + """ + Public method to determine the number of rows. + + @param parent index of parent (QModelIndex) + @return number of rows (integer) + """ + self.__load() + if parent.isValid(): + return 0 + return len(self.__historyDict) + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns. + + @param parent index of parent (QModelIndex) + @return number of columns (integer) + """ + return self.sourceModel().columnCount(self.mapToSource(parent)) + + def mapToSource(self, proxyIndex): + """ + Public method to map an index to the source model index. + + @param proxyIndex reference to a proxy model index (QModelIndex) + @return source model index (QModelIndex) + """ + self.__load() + sourceRow = self.sourceModel().rowCount() - proxyIndex.internalId() + return self.sourceModel().index(sourceRow, proxyIndex.column()) + + def mapFromSource(self, sourceIndex): + """ + Public method to map an index to the proxy model index. + + @param sourceIndex reference to a source model index (QModelIndex) + @return proxy model index (QModelIndex) + """ + self.__load() + url = sourceIndex.data(HistoryModel.UrlStringRole) + if url not in self.__historyDict: + return QModelIndex() + + sourceOffset = self.sourceModel().rowCount() - sourceIndex.row() + + try: + row = self.__filteredRows.index(HistoryData(sourceOffset, -1)) + except ValueError: + return QModelIndex() + + return self.createIndex(row, sourceIndex.column(), sourceOffset) + + def index(self, row, column, parent=QModelIndex()): + """ + Public method to create an index. + + @param row row number for the index (integer) + @param column column number for the index (integer) + @param parent index of the parent item (QModelIndex) + @return requested index (QModelIndex) + """ + self.__load() + if row < 0 or row >= self.rowCount(parent) or \ + column < 0 or column >= self.columnCount(parent): + return QModelIndex() + + return self.createIndex(row, column, + self.__filteredRows[row].tailOffset) + + def parent(self, index): + """ + Public method to get the parent index. + + @param index index of item to get parent (QModelIndex) + @return index of parent (QModelIndex) + """ + return QModelIndex() + + def __load(self): + """ + Private method to load the model data. + """ + if self.__loaded: + return + + self.__filteredRows = [] + self.__historyDict = {} + self.__scaleTime = QDateTime.currentDateTime() + + for sourceRow in range(self.sourceModel().rowCount()): + idx = self.sourceModel().index(sourceRow, 0) + url = idx.data(HistoryModel.UrlStringRole) + if url not in self.__historyDict: + sourceOffset = self.sourceModel().rowCount() - sourceRow + self.__filteredRows.append( + HistoryData(sourceOffset, self.__frequencyScore(idx))) + self.__historyDict[url] = sourceOffset + else: + # the url is known already, so just update the frequency score + row = self.__filteredRows.index( + HistoryData(self.__historyDict[url], -1)) + self.__filteredRows[row].frequency += \ + self.__frequencyScore(idx) + + self.__loaded = True + + def __sourceRowsInserted(self, parent, start, end): + """ + Private slot to handle the insertion of data in the source model. + + @param parent reference to the parent index (QModelIndex) + @param start start row (integer) + @param end end row (integer) + """ + if start == end and start == 0: + if not self.__loaded: + return + + idx = self.sourceModel().index(start, 0, parent) + url = idx.data(HistoryModel.UrlStringRole) + currentFrequency = 0 + if url in self.__historyDict: + row = self.__filteredRows.index( + HistoryData(self.__historyDict[url], -1)) + currentFrequency = self.__filteredRows[row].frequency + self.beginRemoveRows(QModelIndex(), row, row) + del self.__filteredRows[row] + del self.__historyDict[url] + self.endRemoveRows() + + self.beginInsertRows(QModelIndex(), 0, 0) + self.__filteredRows.insert( + 0, HistoryData( + self.sourceModel().rowCount(), + self.__frequencyScore(idx) + currentFrequency)) + self.__historyDict[url] = self.sourceModel().rowCount() + self.endInsertRows() + + def __sourceRowsRemoved(self, parent, start, end): + """ + Private slot to handle the removal of data in the source model. + + @param parent reference to the parent index (QModelIndex) + @param start start row (integer) + @param end end row (integer) + """ + self.__sourceReset() + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove entries from the model. + + @param row row of the first entry to remove (integer) + @param count number of entries to remove (integer) + @param parent index of the parent entry (QModelIndex) + @return flag indicating successful removal (boolean) + """ + if row < 0 or \ + count <= 0 or \ + row + count > self.rowCount(parent) or \ + parent.isValid(): + return False + + lastRow = row + count - 1 + self.sourceModel().rowsRemoved.disconnect(self.__sourceRowsRemoved) + self.beginRemoveRows(parent, row, lastRow) + oldCount = self.rowCount() + start = self.sourceModel().rowCount() - \ + self.__filteredRows[row].tailOffset + end = self.sourceModel().rowCount() - \ + self.__filteredRows[lastRow].tailOffset + self.sourceModel().removeRows(start, end - start + 1) + self.endRemoveRows() + self.sourceModel().rowsRemoved.connect(self.__sourceRowsRemoved) + self.__loaded = False + if oldCount - count != self.rowCount(): + self.beginResetModel() + self.endResetModel() + return True + + def __frequencyScore(self, sourceIndex): + """ + Private method to calculate the frequency score. + + @param sourceIndex index of the source model (QModelIndex) + @return frequency score (integer) + """ + loadTime = \ + self.sourceModel().data(sourceIndex, HistoryModel.DateTimeRole) + days = loadTime.daysTo(self.__scaleTime) + + if days <= 1: + return 100 + elif days < 8: # within the last week + return 90 + elif days < 15: # within the last two weeks + return 70 + elif days < 31: # within the last month + return 50 + elif days < 91: # within the last 3 months + return 30 + else: + return 10
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/History/HistoryManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,505 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the history manager. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QFileInfo, QDateTime, QDate, \ + QTime, QUrl, QTimer, QFile, QIODevice, QByteArray, QDataStream, \ + QTemporaryFile, QObject + +from E5Gui import E5MessageBox + +from Utilities.AutoSaver import AutoSaver +import Utilities +import Preferences + +HISTORY_VERSION = 42 + + +class HistoryEntry(object): + """ + Class implementing a history entry. + """ + def __init__(self, url=None, dateTime=None, title=None): + """ + Constructor + + @param url URL of the history entry (string) + @param dateTime date and time this entry was created (QDateTime) + @param title title string for the history entry (string) + """ + self.url = url and url or "" + self.dateTime = dateTime and dateTime or QDateTime() + self.title = title and title or "" + + def __eq__(self, other): + """ + Special method determining equality. + + @param other reference to the history entry to compare against + (HistoryEntry) + @return flag indicating equality (boolean) + """ + return other.title == self.title and \ + other.url == self.url and \ + other.dateTime == self.dateTime + + def __lt__(self, other): + """ + Special method determining less relation. + + Note: History is sorted in reverse order by date and time + + @param other reference to the history entry to compare against + (HistoryEntry) + @return flag indicating less (boolean) + """ + return self.dateTime > other.dateTime + + def userTitle(self): + """ + Public method to get the title of the history entry. + + @return title of the entry (string) + """ + if not self.title: + page = QFileInfo(QUrl(self.url).path()).fileName() + if page: + return page + return self.url + return self.title + + +class HistoryManager(QObject): + """ + Class implementing the history manager. + + @signal historyCleared() emitted after the history has been cleared + @signal historyReset() emitted after the history has been reset + @signal entryAdded(HistoryEntry) emitted after a history entry has been + added + @signal entryRemoved(HistoryEntry) emitted after a history entry has been + removed + @signal entryUpdated(int) emitted after a history entry has been updated + @signal historySaved() emitted after the history was saved + """ + historyCleared = pyqtSignal() + historyReset = pyqtSignal() + entryAdded = pyqtSignal(HistoryEntry) + entryRemoved = pyqtSignal(HistoryEntry) + entryUpdated = pyqtSignal(int) + historySaved = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(HistoryManager, self).__init__(parent) + + self.__saveTimer = AutoSaver(self, self.save) + self.__daysToExpire = Preferences.getWebBrowser("HistoryLimit") + self.__history = [] + self.__lastSavedUrl = "" + + self.__expiredTimer = QTimer(self) + self.__expiredTimer.setSingleShot(True) + self.__expiredTimer.timeout.connect(self.__checkForExpired) + + self.__frequencyTimer = QTimer(self) + self.__frequencyTimer.setSingleShot(True) + self.__frequencyTimer.timeout.connect(self.__refreshFrequencies) + + self.entryAdded.connect(self.__saveTimer.changeOccurred) + self.entryRemoved.connect(self.__saveTimer.changeOccurred) + + self.__load() + + from .HistoryModel import HistoryModel + from .HistoryFilterModel import HistoryFilterModel + from .HistoryTreeModel import HistoryTreeModel + + self.__historyModel = HistoryModel(self, self) + self.__historyFilterModel = \ + HistoryFilterModel(self.__historyModel, self) + self.__historyTreeModel = \ + HistoryTreeModel(self.__historyFilterModel, self) + + self.__startFrequencyTimer() + + def close(self): + """ + Public method to close the history manager. + """ + # remove history items on application exit + if self.__daysToExpire == -2: + self.clear() + self.__saveTimer.saveIfNeccessary() + + def history(self): + """ + Public method to return the history. + + @return reference to the list of history entries (list of HistoryEntry) + """ + return self.__history[:] + + def setHistory(self, history, loadedAndSorted=False): + """ + Public method to set a new history. + + @param history reference to the list of history entries to be set + (list of HistoryEntry) + @param loadedAndSorted flag indicating that the list is sorted + (boolean) + """ + self.__history = history[:] + if not loadedAndSorted: + self.__history.sort() + + self.__checkForExpired() + + if loadedAndSorted: + try: + self.__lastSavedUrl = self.__history[0].url + except IndexError: + self.__lastSavedUrl = "" + else: + self.__lastSavedUrl = "" + self.__saveTimer.changeOccurred() + self.historyReset.emit() + + def addHistoryEntry(self, view): + """ + Public method to add a history entry. + + @param view reference to the view to add an entry for + @type WebBrowserView + """ + import WebBrowser.WebBrowserWindow + if WebBrowser.WebBrowserWindow.WebBrowserWindow.isPrivate(): + return + + url = view.url() + title = view.title() + + if url.scheme() not in ["eric", "about", "data"]: + if url.password(): + # don't save the password in the history + url.setPassword("") + if url.host(): + url.setHost(url.host().lower()) + itm = HistoryEntry(url.toString(), + QDateTime.currentDateTime(), + title) + self.__history.insert(0, itm) + self.entryAdded.emit(itm) + if len(self.__history) == 1: + self.__checkForExpired() + + def updateHistoryEntry(self, url, title): + """ + Public method to update a history entry. + + @param url URL of the entry to update (string) + @param title title of the entry to update (string) + """ + cleanurl = QUrl(url) + if cleanurl.scheme() not in ["eric", "about"]: + for index in range(len(self.__history)): + if url == self.__history[index].url: + self.__history[index].title = title + self.__saveTimer.changeOccurred() + if not self.__lastSavedUrl: + self.__lastSavedUrl = self.__history[index].url + self.entryUpdated.emit(index) + break + + def removeHistoryEntry(self, url, title=""): + """ + Public method to remove a history entry. + + @param url URL of the entry to remove (QUrl) + @param title title of the entry to remove (string) + """ + for index in range(len(self.__history)): + if url == QUrl(self.__history[index].url) and \ + (not title or title == self.__history[index].title): + itm = self.__history[index] + self.__lastSavedUrl = "" + self.__history.remove(itm) + self.entryRemoved.emit(itm) + break + + def historyModel(self): + """ + Public method to get a reference to the history model. + + @return reference to the history model (HistoryModel) + """ + return self.__historyModel + + def historyFilterModel(self): + """ + Public method to get a reference to the history filter model. + + @return reference to the history filter model (HistoryFilterModel) + """ + return self.__historyFilterModel + + def historyTreeModel(self): + """ + Public method to get a reference to the history tree model. + + @return reference to the history tree model (HistoryTreeModel) + """ + return self.__historyTreeModel + + def __checkForExpired(self): + """ + Private slot to check entries for expiration. + """ + if self.__daysToExpire < 0 or len(self.__history) == 0: + return + + now = QDateTime.currentDateTime() + nextTimeout = 0 + + while self.__history: + checkForExpired = QDateTime(self.__history[-1].dateTime) + checkForExpired.setDate( + checkForExpired.date().addDays(self.__daysToExpire)) + if now.daysTo(checkForExpired) > 7: + nextTimeout = 7 * 86400 + else: + nextTimeout = now.secsTo(checkForExpired) + if nextTimeout > 0: + break + + itm = self.__history.pop(-1) + self.__lastSavedUrl = "" + self.entryRemoved.emit(itm) + self.__saveTimer.saveIfNeccessary() + + if nextTimeout > 0: + self.__expiredTimer.start(nextTimeout * 1000) + + def daysToExpire(self): + """ + Public method to get the days for entry expiration. + + @return days for entry expiration (integer) + """ + return self.__daysToExpire + + def setDaysToExpire(self, limit): + """ + Public method to set the days for entry expiration. + + @param limit days for entry expiration (integer) + """ + if self.__daysToExpire == limit: + return + + self.__daysToExpire = limit + self.__checkForExpired() + self.__saveTimer.changeOccurred() + + def preferencesChanged(self): + """ + Public method to indicate a change of preferences. + """ + self.setDaysToExpire(Preferences.getWebBrowser("HistoryLimit")) + + @pyqtSlot() + def clear(self, period=0): + """ + Public slot to clear the complete history. + + @param period history period in milliseconds to be cleared (integer) + """ + if period == 0: + self.__history = [] + self.historyReset.emit() + else: + breakMS = QDateTime.currentMSecsSinceEpoch() - period + while self.__history and \ + (QDateTime(self.__history[0].dateTime).toMSecsSinceEpoch() > + breakMS): + itm = self.__history.pop(0) + self.entryRemoved.emit(itm) + self.__lastSavedUrl = "" + self.__saveTimer.changeOccurred() + self.__saveTimer.saveIfNeccessary() + self.historyCleared.emit() + + def getFileName(self): + """ + Public method to get the file name of the history file. + + @return name of the history file (string) + """ + return os.path.join(Utilities.getConfigDir(), "web_browser", "history") + + def reload(self): + """ + Public method to reload the history. + """ + self.__load() + + def __load(self): + """ + Private method to load the saved history entries from disk. + """ + historyFile = QFile(self.getFileName()) + if not historyFile.exists(): + return + if not historyFile.open(QIODevice.ReadOnly): + E5MessageBox.warning( + None, + self.tr("Loading History"), + self.tr( + """<p>Unable to open history file <b>{0}</b>.<br/>""" + """Reason: {1}</p>""") + .format(historyFile.fileName, historyFile.errorString())) + return + + history = [] + + # double check, that the history file is sorted as it is read + needToSort = False + lastInsertedItem = HistoryEntry() + data = QByteArray(historyFile.readAll()) + stream = QDataStream(data, QIODevice.ReadOnly) + stream.setVersion(QDataStream.Qt_4_6) + while not stream.atEnd(): + ver = stream.readUInt32() + if ver != HISTORY_VERSION: + continue + itm = HistoryEntry() + itm.url = Utilities.readStringFromStream(stream) + stream >> itm.dateTime + itm.title = Utilities.readStringFromStream(stream) + + if not itm.dateTime.isValid(): + continue + + if itm == lastInsertedItem: + if not lastInsertedItem.title and len(history) > 0: + history[0].title = itm.title + continue + + if not needToSort and history and lastInsertedItem < itm: + needToSort = True + + history.insert(0, itm) + lastInsertedItem = itm + historyFile.close() + + if needToSort: + history.sort() + + self.setHistory(history, True) + + # if the history had to be sorted, rewrite the history sorted + if needToSort: + self.__lastSavedUrl = "" + self.__saveTimer.changeOccurred() + + def save(self): + """ + Public slot to save the history entries to disk. + """ + historyFile = QFile(self.getFileName()) + if not historyFile.exists(): + self.__lastSavedUrl = "" + + saveAll = self.__lastSavedUrl == "" + first = len(self.__history) - 1 + if not saveAll: + # find the first one to save + for index in range(len(self.__history)): + if self.__history[index].url == self.__lastSavedUrl: + first = index - 1 + break + if first == len(self.__history) - 1: + saveAll = True + + if saveAll: + # use a temporary file when saving everything + f = QTemporaryFile() + f.setAutoRemove(False) + opened = f.open() + else: + f = historyFile + opened = f.open(QIODevice.Append) + + if not opened: + E5MessageBox.warning( + None, + self.tr("Saving History"), + self.tr( + """<p>Unable to open history file <b>{0}</b>.<br/>""" + """Reason: {1}</p>""") + .format(f.fileName(), f.errorString())) + return + + for index in range(first, -1, -1): + data = QByteArray() + stream = QDataStream(data, QIODevice.WriteOnly) + stream.setVersion(QDataStream.Qt_4_6) + itm = self.__history[index] + stream.writeUInt32(HISTORY_VERSION) + stream.writeString(itm.url.encode("utf-8")) + stream << itm.dateTime + stream.writeString(itm.title.encode('utf-8')) + f.write(data) + + f.close() + if saveAll: + if historyFile.exists() and not historyFile.remove(): + E5MessageBox.warning( + None, + self.tr("Saving History"), + self.tr( + """<p>Error removing old history file <b>{0}</b>.""" + """<br/>Reason: {1}</p>""") + .format(historyFile.fileName(), + historyFile.errorString())) + if not f.copy(historyFile.fileName()): + E5MessageBox.warning( + None, + self.tr("Saving History"), + self.tr( + """<p>Error moving new history file over old one """ + """(<b>{0}</b>).<br/>Reason: {1}</p>""") + .format(historyFile.fileName(), f.errorString())) + self.historySaved.emit() + try: + self.__lastSavedUrl = self.__history[0].url + except IndexError: + self.__lastSavedUrl = "" + + def __refreshFrequencies(self): + """ + Private slot to recalculate the refresh frequencies. + """ + self.__historyFilterModel.recalculateFrequencies() + self.__startFrequencyTimer() + + def __startFrequencyTimer(self): + """ + Private method to start the timer to recalculate the frequencies. + """ + tomorrow = QDateTime(QDate.currentDate().addDays(1), QTime(3, 0)) + self.__frequencyTimer.start( + QDateTime.currentDateTime().secsTo(tomorrow) * 1000)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/History/HistoryMenu.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,476 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the history menu. +""" + +from __future__ import unicode_literals + +import sys + +from PyQt5.QtCore import pyqtSignal, Qt, QMimeData, QUrl, QModelIndex, \ + QSortFilterProxyModel, QAbstractProxyModel +from PyQt5.QtWidgets import QMenu + +from E5Gui.E5ModelMenu import E5ModelMenu +from E5Gui import E5MessageBox + +from .HistoryModel import HistoryModel + +import UI.PixmapCache + + +class HistoryMenuModel(QAbstractProxyModel): + """ + Class implementing a model for the history menu. + + It maps the first bunch of items of the source model to the root. + """ + MOVEDROWS = 15 + + def __init__(self, sourceModel, parent=None): + """ + Constructor + + @param sourceModel reference to the source model (QAbstractItemModel) + @param parent reference to the parent object (QObject) + """ + super(HistoryMenuModel, self).__init__(parent) + + self.__treeModel = sourceModel + + self.setSourceModel(sourceModel) + + def bumpedRows(self): + """ + Public method to determine the number of rows moved to the root. + + @return number of rows moved to the root (integer) + """ + first = self.__treeModel.index(0, 0) + if not first.isValid(): + return 0 + return min(self.__treeModel.rowCount(first), self.MOVEDROWS) + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns. + + @param parent index of parent (QModelIndex) + @return number of columns (integer) + """ + return self.__treeModel.columnCount(self.mapToSource(parent)) + + def rowCount(self, parent=QModelIndex()): + """ + Public method to determine the number of rows. + + @param parent index of parent (QModelIndex) + @return number of rows (integer) + """ + if parent.column() > 0: + return 0 + + if not parent.isValid(): + folders = self.sourceModel().rowCount() + bumpedItems = self.bumpedRows() + if bumpedItems <= self.MOVEDROWS and \ + bumpedItems == self.sourceModel().rowCount( + self.sourceModel().index(0, 0)): + folders -= 1 + return bumpedItems + folders + + if parent.internalId() == sys.maxsize: + if parent.row() < self.bumpedRows(): + return 0 + + idx = self.mapToSource(parent) + defaultCount = self.sourceModel().rowCount(idx) + if idx == self.sourceModel().index(0, 0): + return defaultCount - self.bumpedRows() + + return defaultCount + + def mapFromSource(self, sourceIndex): + """ + Public method to map an index to the proxy model index. + + @param sourceIndex reference to a source model index (QModelIndex) + @return proxy model index (QModelIndex) + """ + sourceRow = self.__treeModel.mapToSource(sourceIndex).row() + return self.createIndex( + sourceIndex.row(), sourceIndex.column(), sourceRow) + + def mapToSource(self, proxyIndex): + """ + Public method to map an index to the source model index. + + @param proxyIndex reference to a proxy model index (QModelIndex) + @return source model index (QModelIndex) + """ + if not proxyIndex.isValid(): + return QModelIndex() + + if proxyIndex.internalId() == sys.maxsize: + bumpedItems = self.bumpedRows() + if proxyIndex.row() < bumpedItems: + return self.__treeModel.index( + proxyIndex.row(), proxyIndex.column(), + self.__treeModel.index(0, 0)) + if bumpedItems <= self.MOVEDROWS and \ + bumpedItems == self.sourceModel().rowCount( + self.__treeModel.index(0, 0)): + bumpedItems -= 1 + return self.__treeModel.index(proxyIndex.row() - bumpedItems, + proxyIndex.column()) + + historyIndex = self.__treeModel.sourceModel()\ + .index(proxyIndex.internalId(), proxyIndex.column()) + treeIndex = self.__treeModel.mapFromSource(historyIndex) + return treeIndex + + def index(self, row, column, parent=QModelIndex()): + """ + Public method to create an index. + + @param row row number for the index (integer) + @param column column number for the index (integer) + @param parent index of the parent item (QModelIndex) + @return requested index (QModelIndex) + """ + if row < 0 or \ + column < 0 or \ + column >= self.columnCount(parent) or \ + parent.column() > 0: + return QModelIndex() + + if not parent.isValid(): + return self.createIndex(row, column, sys.maxsize) + + treeIndexParent = self.mapToSource(parent) + + bumpedItems = 0 + if treeIndexParent == self.sourceModel().index(0, 0): + bumpedItems = self.bumpedRows() + treeIndex = self.__treeModel.index( + row + bumpedItems, column, treeIndexParent) + historyIndex = self.__treeModel.mapToSource(treeIndex) + historyRow = historyIndex.row() + if historyRow == -1: + historyRow = treeIndex.row() + return self.createIndex(row, column, historyRow) + + def parent(self, index): + """ + Public method to get the parent index. + + @param index index of item to get parent (QModelIndex) + @return index of parent (QModelIndex) + """ + offset = index.internalId() + if offset == sys.maxsize or not index.isValid(): + return QModelIndex() + + historyIndex = self.__treeModel.sourceModel().index( + index.internalId(), 0) + treeIndex = self.__treeModel.mapFromSource(historyIndex) + treeIndexParent = treeIndex.parent() + + sourceRow = self.sourceModel().mapToSource(treeIndexParent).row() + bumpedItems = self.bumpedRows() + if bumpedItems <= self.MOVEDROWS and \ + bumpedItems == self.sourceModel().rowCount( + self.sourceModel().index(0, 0)): + bumpedItems -= 1 + + return self.createIndex(bumpedItems + treeIndexParent.row(), + treeIndexParent.column(), + sourceRow) + + def mimeData(self, indexes): + """ + Public method to return the mime data. + + @param indexes list of indexes (QModelIndexList) + @return mime data (QMimeData) + """ + urls = [] + for index in indexes: + url = index.data(HistoryModel.UrlRole) + urls.append(url) + + mdata = QMimeData() + mdata.setUrls(urls) + return mdata + + +class HistoryMostVisitedMenuModel(QSortFilterProxyModel): + """ + Class implementing a model to show the most visited history entries. + """ + def __init__(self, sourceModel, parent=None): + """ + Constructor + + @param sourceModel reference to the source model (QAbstractItemModel) + @param parent reference to the parent object (QObject) + """ + super(HistoryMostVisitedMenuModel, self).__init__(parent) + + self.setDynamicSortFilter(True) + self.setSourceModel(sourceModel) + + def lessThan(self, left, right): + """ + Public method used to sort the displayed items. + + @param left index of left item (QModelIndex) + @param right index of right item (QModelIndex) + @return true, if left is less than right (boolean) + """ + from .HistoryFilterModel import HistoryFilterModel + frequency_L = \ + self.sourceModel().data(left, HistoryFilterModel.FrequencyRole) + dateTime_L = \ + self.sourceModel().data(left, HistoryModel.DateTimeRole) + frequency_R = \ + self.sourceModel().data(right, HistoryFilterModel.FrequencyRole) + dateTime_R = \ + self.sourceModel().data(right, HistoryModel.DateTimeRole) + + # Sort results in descending frequency-derived score. If frequencies + # are equal, sort on most recently viewed + if frequency_R == frequency_L: + return dateTime_R < dateTime_L + + return frequency_R < frequency_L + + +class HistoryMenu(E5ModelMenu): + """ + Class implementing the history menu. + + @signal openUrl(QUrl, str) emitted to open a URL in the current tab + @signal newUrl(QUrl, str) emitted to open a URL in a new tab + """ + openUrl = pyqtSignal(QUrl, str) + newUrl = pyqtSignal(QUrl, str) + + def __init__(self, parent=None, tabWidget=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + @param tabWidget reference to the tab widget managing the browser + tabs (HelpTabWidget + """ + E5ModelMenu.__init__(self, parent) + + self.__tabWidget = tabWidget + + self.__historyManager = None + self.__historyMenuModel = None + self.__initialActions = [] + self.__mostVisitedMenu = None + + self.__closedTabsMenu = QMenu(self.tr("Closed Tabs")) + self.__closedTabsMenu.aboutToShow.connect( + self.__aboutToShowClosedTabsMenu) + self.__tabWidget.closedTabsManager().closedTabAvailable.connect( + self.__closedTabAvailable) + + self.setMaxRows(7) + + self.activated.connect(self.__activated) + self.setStatusBarTextRole(HistoryModel.UrlStringRole) + + def __activated(self, idx): + """ + Private slot handling the activated signal. + + @param idx index of the activated item (QModelIndex) + """ + if self._keyboardModifiers & Qt.ControlModifier: + self.newUrl.emit( + idx.data(HistoryModel.UrlRole), + idx.data(HistoryModel.TitleRole)) + else: + self.openUrl.emit( + idx.data(HistoryModel.UrlRole), + idx.data(HistoryModel.TitleRole)) + + def prePopulated(self): + """ + Public method to add any actions before the tree. + + @return flag indicating if any actions were added (boolean) + """ + if self.__historyManager is None: + import WebBrowser.WebBrowserWindow + self.__historyManager = \ + WebBrowser.WebBrowserWindow.WebBrowserWindow.historyManager() + self.__historyMenuModel = HistoryMenuModel( + self.__historyManager.historyTreeModel(), self) + self.setModel(self.__historyMenuModel) + + # initial actions + for act in self.__initialActions: + self.addAction(act) + if len(self.__initialActions) != 0: + self.addSeparator() + self.setFirstSeparator(self.__historyMenuModel.bumpedRows()) + + return False + + def postPopulated(self): + """ + Public method to add any actions after the tree. + """ + if len(self.__historyManager.history()) > 0: + self.addSeparator() + + if self.__mostVisitedMenu is None: + self.__mostVisitedMenu = HistoryMostVisitedMenu(10, self) + self.__mostVisitedMenu.setTitle(self.tr("Most Visited")) + self.__mostVisitedMenu.openUrl.connect(self.openUrl) + self.__mostVisitedMenu.newUrl.connect(self.newUrl) + self.addMenu(self.__mostVisitedMenu) + act = self.addMenu(self.__closedTabsMenu) + act.setIcon(UI.PixmapCache.getIcon("trash.png")) + act.setEnabled(self.__tabWidget.canRestoreClosedTab()) + self.addSeparator() + + act = self.addAction(UI.PixmapCache.getIcon("history.png"), + self.tr("Show All History...")) + act.triggered.connect(self.__showHistoryDialog) + act = self.addAction(UI.PixmapCache.getIcon("historyClear.png"), + self.tr("Clear History...")) + act.triggered.connect(self.__clearHistoryDialog) + + def setInitialActions(self, actions): + """ + Public method to set the list of actions that should appear first in + the menu. + + @param actions list of initial actions (list of QAction) + """ + self.__initialActions = actions[:] + for act in self.__initialActions: + self.addAction(act) + + def __showHistoryDialog(self): + """ + Private slot to show the history dialog. + """ + from .HistoryDialog import HistoryDialog + dlg = HistoryDialog(self) + dlg.newUrl.connect(self.newUrl) + dlg.openUrl.connect(self.openUrl) + dlg.show() + + def __clearHistoryDialog(self): + """ + Private slot to clear the history. + """ + if self.__historyManager is not None and E5MessageBox.yesNo( + self, + self.tr("Clear History"), + self.tr("""Do you want to clear the history?""")): + self.__historyManager.clear() + self.__tabWidget.clearClosedTabsList() + + def __aboutToShowClosedTabsMenu(self): + """ + Private slot to populate the closed tabs menu. + """ + fm = self.__closedTabsMenu.fontMetrics() + maxWidth = fm.width('m') * 40 + + import WebBrowser.WebBrowserWindow + self.__closedTabsMenu.clear() + index = 0 + for tab in self.__tabWidget.closedTabsManager().allClosedTabs(): + title = fm.elidedText(tab.title, Qt.ElideRight, maxWidth) + self.__closedTabsMenu.addAction( + WebBrowser.WebBrowserWindow.WebBrowserWindow.icon(tab.url), + title, + self.__tabWidget.restoreClosedTab).setData(index) + index += 1 + self.__closedTabsMenu.addSeparator() + self.__closedTabsMenu.addAction( + self.tr("Restore All Closed Tabs"), + self.__tabWidget.restoreAllClosedTabs) + self.__closedTabsMenu.addAction( + self.tr("Clear List"), + self.__tabWidget.clearClosedTabsList) + + def __closedTabAvailable(self, avail): + """ + Private slot to handle changes of the availability of closed tabs. + + @param avail flag indicating the availability of closed tabs (boolean) + """ + self.__closedTabsMenu.setEnabled(avail) + + +class HistoryMostVisitedMenu(E5ModelMenu): + """ + Class implementing the most visited history menu. + + @signal openUrl(QUrl, str) emitted to open a URL in the current tab + @signal newUrl(QUrl, str) emitted to open a URL in a new tab + """ + openUrl = pyqtSignal(QUrl, str) + newUrl = pyqtSignal(QUrl, str) + + def __init__(self, count, parent=None): + """ + Constructor + + @param count maximum number of entries to be shown (integer) + @param parent reference to the parent widget (QWidget) + """ + E5ModelMenu.__init__(self, parent) + + self.__historyMenuModel = None + + self.setMaxRows(count + 1) + + self.activated.connect(self.__activated) + self.setStatusBarTextRole(HistoryModel.UrlStringRole) + + def __activated(self, idx): + """ + Private slot handling the activated signal. + + @param idx index of the activated item (QModelIndex) + """ + if self._keyboardModifiers & Qt.ControlModifier: + self.newUrl.emit( + idx.data(HistoryModel.UrlRole), + idx.data(HistoryModel.TitleRole)) + else: + self.openUrl.emit( + idx.data(HistoryModel.UrlRole), + idx.data(HistoryModel.TitleRole)) + + def prePopulated(self): + """ + Public method to add any actions before the tree. + + @return flag indicating if any actions were added (boolean) + """ + if self.__historyMenuModel is None: + import WebBrowser.WebBrowserWindow + historyManager = \ + WebBrowser.WebBrowserWindow.WebBrowserWindow.historyManager() + self.__historyMenuModel = HistoryMostVisitedMenuModel( + historyManager.historyFilterModel(), self) + self.setModel(self.__historyMenuModel) + self.__historyMenuModel.sort(0) + + return False
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/History/HistoryModel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the history model. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QAbstractTableModel, QModelIndex, QUrl + +import WebBrowser.WebBrowserWindow + + +class HistoryModel(QAbstractTableModel): + """ + Class implementing the history model. + """ + DateRole = Qt.UserRole + 1 + DateTimeRole = Qt.UserRole + 2 + UrlRole = Qt.UserRole + 3 + UrlStringRole = Qt.UserRole + 4 + TitleRole = Qt.UserRole + 5 + MaxRole = TitleRole + + def __init__(self, historyManager, parent=None): + """ + Constructor + + @param historyManager reference to the history manager object + (HistoryManager) + @param parent reference to the parent object (QObject) + """ + super(HistoryModel, self).__init__(parent) + + self.__historyManager = historyManager + + self.__headers = [ + self.tr("Title"), + self.tr("Address"), + ] + + self.__historyManager.historyReset.connect(self.historyReset) + self.__historyManager.entryRemoved.connect(self.historyReset) + self.__historyManager.entryAdded.connect(self.entryAdded) + self.__historyManager.entryUpdated.connect(self.entryUpdated) + + def historyReset(self): + """ + Public slot to reset the model. + """ + self.beginResetModel() + self.endResetModel() + + def entryAdded(self): + """ + Public slot to handle the addition of a history entry. + """ + self.beginInsertRows(QModelIndex(), 0, 0) + self.endInsertRows() + + def entryUpdated(self, row): + """ + Public slot to handle the update of a history entry. + + @param row row number of the updated entry (integer) + """ + idx = self.index(row, 0) + self.dataChanged.emit(idx, idx) + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Public method to get the header data. + + @param section section number (integer) + @param orientation header orientation (Qt.Orientation) + @param role data role (integer) + @return header data + """ + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + try: + return self.__headers[section] + except IndexError: + pass + return QAbstractTableModel.headerData(self, section, orientation, role) + + def data(self, index, role=Qt.DisplayRole): + """ + Public method to get data from the model. + + @param index index of history entry to get data for (QModelIndex) + @param role data role (integer) + @return history entry data + """ + lst = self.__historyManager.history() + if index.row() < 0 or index.row() > len(lst): + return None + + itm = lst[index.row()] + if role == self.DateTimeRole: + return itm.dateTime + elif role == self.DateRole: + return itm.dateTime.date() + elif role == self.UrlRole: + return QUrl(itm.url) + elif role == self.UrlStringRole: + return itm.url + elif role == self.TitleRole: + return itm.userTitle() + elif role in [Qt.DisplayRole, Qt.EditRole]: + if index.column() == 0: + return itm.userTitle() + elif index.column() == 1: + return itm.url + elif role == Qt.DecorationRole: + if index.column() == 0: + return WebBrowser.WebBrowserWindow.WebBrowserWindow.icon( + QUrl(itm.url)) + + return None + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns. + + @param parent index of parent (QModelIndex) + @return number of columns (integer) + """ + if parent.isValid(): + return 0 + else: + return len(self.__headers) + + def rowCount(self, parent=QModelIndex()): + """ + Public method to determine the number of rows. + + @param parent index of parent (QModelIndex) + @return number of rows (integer) + """ + if parent.isValid(): + return 0 + else: + return len(self.__historyManager.history()) + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove history entries from the model. + + @param row row of the first history entry to remove (integer) + @param count number of history entries to remove (integer) + @param parent index of the parent entry (QModelIndex) + @return flag indicating successful removal (boolean) + """ + if parent.isValid(): + return False + + lastRow = row + count - 1 + self.beginRemoveRows(parent, row, lastRow) + lst = self.__historyManager.history()[:] + for index in range(lastRow, row - 1, -1): + del lst[index] + self.__historyManager.historyReset.disconnect(self.historyReset) + self.__historyManager.setHistory(lst) + self.__historyManager.historyReset.connect(self.historyReset) + self.endRemoveRows() + return True
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/History/HistoryTreeModel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,377 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the history tree model. +""" + +from __future__ import unicode_literals + +import bisect + +from PyQt5.QtCore import Qt, QModelIndex, QDate, QAbstractProxyModel + +from .HistoryModel import HistoryModel + +import UI.PixmapCache + + +class HistoryTreeModel(QAbstractProxyModel): + """ + Class implementing the history tree model. + """ + def __init__(self, sourceModel, parent=None): + """ + Constructor + + @param sourceModel reference to the source model (QAbstractItemModel) + @param parent reference to the parent object (QObject) + """ + super(HistoryTreeModel, self).__init__(parent) + + self.__sourceRowCache = [] + self.__removingDown = False + + self.setSourceModel(sourceModel) + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Public method to get the header data. + + @param section section number (integer) + @param orientation header orientation (Qt.Orientation) + @param role data role (integer) + @return header data + """ + return self.sourceModel().headerData(section, orientation, role) + + def data(self, index, role=Qt.DisplayRole): + """ + Public method to get data from the model. + + @param index index of history entry to get data for (QModelIndex) + @param role data role (integer) + @return history entry data + """ + if role in [Qt.DisplayRole, Qt.EditRole]: + start = index.internalId() + if start == 0: + offset = self.__sourceDateRow(index.row()) + if index.column() == 0: + idx = self.sourceModel().index(offset, 0) + date = idx.data(HistoryModel.DateRole) + if date == QDate.currentDate(): + return self.tr("Earlier Today") + return date.toString("yyyy-MM-dd") + if index.column() == 1: + return self.tr( + "%n item(s)", "", + self.rowCount(index.sibling(index.row(), 0))) + + elif role == Qt.DecorationRole: + if index.column() == 0 and not index.parent().isValid(): + return UI.PixmapCache.getIcon("history.png") + + elif role == HistoryModel.DateRole: + if index.column() == 0 and index.internalId() == 0: + offset = self.__sourceDateRow(index.row()) + idx = self.sourceModel().index(offset, 0) + return idx.data(HistoryModel.DateRole) + + return QAbstractProxyModel.data(self, index, role) + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns. + + @param parent index of parent (QModelIndex) + @return number of columns (integer) + """ + return self.sourceModel().columnCount(self.mapToSource(parent)) + + def rowCount(self, parent=QModelIndex()): + """ + Public method to determine the number of rows. + + @param parent index of parent (QModelIndex) + @return number of rows (integer) + """ + if parent.internalId() != 0 or \ + parent.column() > 0 or \ + self.sourceModel() is None: + return 0 + + # row count OF dates + if not parent.isValid(): + if self.__sourceRowCache: + return len(self.__sourceRowCache) + + currentDate = QDate() + rows = 0 + totalRows = self.sourceModel().rowCount() + + for row in range(totalRows): + rowDate = self.sourceModel().index(row, 0)\ + .data(HistoryModel.DateRole) + if rowDate != currentDate: + self.__sourceRowCache.append(row) + currentDate = rowDate + rows += 1 + return rows + + # row count FOR a date + start = self.__sourceDateRow(parent.row()) + end = self.__sourceDateRow(parent.row() + 1) + return end - start + + def __sourceDateRow(self, row): + """ + Private method to translate the top level date row into the offset + where that date starts. + + @param row row number of the date (integer) + @return offset where that date starts (integer) + """ + if row <= 0: + return 0 + + if len(self.__sourceRowCache) == 0: + self.rowCount(QModelIndex()) + + if row >= len(self.__sourceRowCache): + if self.sourceModel() is None: + return 0 + return self.sourceModel().rowCount() + + return self.__sourceRowCache[row] + + def mapToSource(self, proxyIndex): + """ + Public method to map an index to the source model index. + + @param proxyIndex reference to a proxy model index (QModelIndex) + @return source model index (QModelIndex) + """ + offset = proxyIndex.internalId() + if offset == 0: + return QModelIndex() + startDateRow = self.__sourceDateRow(offset - 1) + return self.sourceModel().index( + startDateRow + proxyIndex.row(), proxyIndex.column()) + + def index(self, row, column, parent=QModelIndex()): + """ + Public method to create an index. + + @param row row number for the index (integer) + @param column column number for the index (integer) + @param parent index of the parent item (QModelIndex) + @return requested index (QModelIndex) + """ + if row < 0 or \ + column < 0 or \ + column >= self.columnCount(parent) or \ + parent.column() > 0: + return QModelIndex() + + if not parent.isValid(): + return self.createIndex(row, column, 0) + return self.createIndex(row, column, parent.row() + 1) + + def parent(self, index): + """ + Public method to get the parent index. + + @param index index of item to get parent (QModelIndex) + @return index of parent (QModelIndex) + """ + offset = index.internalId() + if offset == 0 or not index.isValid(): + return QModelIndex() + return self.createIndex(offset - 1, 0, 0) + + def hasChildren(self, parent=QModelIndex()): + """ + Public method to check, if an entry has some children. + + @param parent index of the entry to check (QModelIndex) + @return flag indicating the presence of children (boolean) + """ + grandparent = parent.parent() + if not grandparent.isValid(): + return True + return False + + def flags(self, index): + """ + Public method to get the item flags. + + @param index index of the item (QModelIndex) + @return flags (Qt.ItemFlags) + """ + if not index.isValid(): + return Qt.ItemFlags(Qt.NoItemFlags) + return Qt.ItemFlags( + Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + + def setSourceModel(self, sourceModel): + """ + Public method to set the source model. + + @param sourceModel reference to the source model (QAbstractItemModel) + """ + if self.sourceModel() is not None: + self.sourceModel().modelReset.disconnect(self.__sourceReset) + self.sourceModel().layoutChanged.disconnect(self.__sourceReset) + self.sourceModel().rowsInserted.disconnect( + self.__sourceRowsInserted) + self.sourceModel().rowsRemoved.disconnect(self.__sourceRowsRemoved) + + super(HistoryTreeModel, self).setSourceModel(sourceModel) + + if self.sourceModel() is not None: + self.__loaded = False + self.sourceModel().modelReset.connect(self.__sourceReset) + self.sourceModel().layoutChanged.connect(self.__sourceReset) + self.sourceModel().rowsInserted.connect(self.__sourceRowsInserted) + self.sourceModel().rowsRemoved.connect(self.__sourceRowsRemoved) + + self.beginResetModel() + self.endResetModel() + + def __sourceReset(self): + """ + Private slot to handle a reset of the source model. + """ + self.beginResetModel() + self.__sourceRowCache = [] + self.endResetModel() + + def __sourceRowsInserted(self, parent, start, end): + """ + Private slot to handle the insertion of data in the source model. + + @param parent reference to the parent index (QModelIndex) + @param start start row (integer) + @param end end row (integer) + """ + if not parent.isValid(): + if start != 0 or start != end: + self.beginResetModel() + self.__sourceRowCache = [] + self.endResetModel() + return + + self.__sourceRowCache = [] + treeIndex = self.mapFromSource(self.sourceModel().index(start, 0)) + treeParent = treeIndex.parent() + if self.rowCount(treeParent) == 1: + self.beginInsertRows(QModelIndex(), 0, 0) + self.endInsertRows() + else: + self.beginInsertRows(treeParent, treeIndex.row(), + treeIndex.row()) + self.endInsertRows() + + def mapFromSource(self, sourceIndex): + """ + Public method to map an index to the proxy model index. + + @param sourceIndex reference to a source model index (QModelIndex) + @return proxy model index (QModelIndex) + """ + if not sourceIndex.isValid(): + return QModelIndex() + + if len(self.__sourceRowCache) == 0: + self.rowCount(QModelIndex()) + + try: + row = self.__sourceRowCache.index(sourceIndex.row()) + except ValueError: + row = bisect.bisect_left(self.__sourceRowCache, sourceIndex.row()) + if row == len(self.__sourceRowCache) or \ + self.__sourceRowCache[row] != sourceIndex.row(): + row -= 1 + dateRow = max(0, row) + row = sourceIndex.row() - self.__sourceRowCache[dateRow] + return self.createIndex(row, sourceIndex.column(), dateRow + 1) + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove entries from the model. + + @param row row of the first entry to remove (integer) + @param count number of entries to remove (integer) + @param parent index of the parent entry (QModelIndex) + @return flag indicating successful removal (boolean) + """ + if row < 0 or \ + count <= 0 or \ + row + count > self.rowCount(parent): + return False + + self.__removingDown = True + if parent.isValid() and self.rowCount(parent) == count - row: + self.beginRemoveRows(QModelIndex(), parent.row(), parent.row()) + else: + self.beginRemoveRows(parent, row, row + count - 1) + if parent.isValid(): + # removing pages + offset = self.__sourceDateRow(parent.row()) + return self.sourceModel().removeRows(offset + row, count) + else: + # removing whole dates + for i in range(row + count - 1, row - 1, -1): + dateParent = self.index(i, 0) + offset = self.__sourceDateRow(dateParent.row()) + if not self.sourceModel().removeRows( + offset, self.rowCount(dateParent)): + return False + return True + + def __sourceRowsRemoved(self, parent, start, end): + """ + Private slot to handle the removal of data in the source model. + + @param parent reference to the parent index (QModelIndex) + @param start start row (integer) + @param end end row (integer) + """ + if not self.__removingDown: + self.beginResetModel() + self.__sourceRowCache = [] + self.endResetModel() + return + + if not parent.isValid(): + if self.__sourceRowCache: + i = end + while i >= start: + try: + ind = self.__sourceRowCache.index(i) + except ValueError: + ind = bisect.bisect_left(self.__sourceRowCache, i) + if ind == len(self.__sourceRowCache) or \ + self.__sourceRowCache[ind] != i: + ind -= 1 + row = max(0, ind) + offset = self.__sourceRowCache[row] + dateParent = self.index(row, 0) + # If we can remove all the rows in the date do that + # and skip over them. + rc = self.rowCount(dateParent) + if i - rc + 1 == offset and start <= i - rc + 1: + del self.__sourceRowCache[row] + i -= rc + 1 + else: + row += 1 + i -= 1 + for j in range(row, len(self.__sourceRowCache)): + self.__sourceRowCache[j] -= 1 + + if self.__removingDown: + self.endRemoveRows() + self.__removingDown = False
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/History/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the history system. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/JavaScript/ExternalJsObject.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the JavaScript external object being the endpoint of +a web channel. +""" + +# +# This code was ported from QupZilla and modified. +# Copyright (C) David Rosca <nowrep@gmail.com> +# + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtProperty, QObject + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + +from .StartPageJsObject import StartPageJsObject +from .PasswordManagerJsObject import PasswordManagerJsObject + + +class ExternalJsObject(QObject): + """ + Class implementing the endpoint of our web channel. + """ + def __init__(self, page): + """ + Constructor + + @param page reference to the web page object + @type WebBrowserPage + """ + super(ExternalJsObject, self).__init__(page) + + self.__page = page + + self.__startPage = None + self.__passwordManager = None + + def page(self): + """ + Public method returning a reference to the web page object. + + @return reference to the web page object + @rtype WebBrowserPage + """ + return self.__page + + @pyqtProperty(QObject, constant=True) + def passwordManager(self): + """ + Public method to get a reference to the password manager JavaScript + object. + + @return reference to the password manager JavaScript object + @rtype StartPageJsObject + """ + if self.__passwordManager is None: + self.__passwordManager = PasswordManagerJsObject(self) + + return self.__passwordManager + + @pyqtProperty(QObject, constant=True) + def speedDial(self): + """ + Public method returning a reference to a speed dial object. + + @return reference to a speed dial object + @rtype SpeedDial + """ + if self.__page.url().toString() != "eric:speeddial": + return None + + return WebBrowserWindow.speedDial() + + @pyqtProperty(QObject, constant=True) + def startPage(self): + """ + Public method to get a reference to the start page JavaScript object. + + @return reference to the start page JavaScript object + @rtype StartPageJsObject + """ + if self.__startPage is None: + self.__startPage = StartPageJsObject(self) + + return self.__startPage
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/JavaScript/PasswordManagerJsObject.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Python side for calling the password manager. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, QObject, QByteArray + + +class PasswordManagerJsObject(QObject): + """ + Class implementing the Python side for calling the password manager. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object + @type ExternalJsObject + """ + super(PasswordManagerJsObject, self).__init__(parent) + + self.__external = parent + + @pyqtSlot(str, str, str, QByteArray) + def formSubmitted(self, urlStr, userName, password, data): + """ + Public slot passing form data to the password manager. + + @param urlStr form submission URL + @type str + @param userName name of the user + @type str + @param password user password + @type str + @param data data to be submitted + @type QByteArray + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + WebBrowserWindow.passwordManager().formSubmitted( + urlStr, userName, password, data, self.__external.page())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/JavaScript/StartPageJsObject.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Python side of the eric home page. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, QObject + + +class StartPageJsObject(QObject): + """ + Class implementing the Python side of the eric home page. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object + @type ExternalJsObject + """ + super(StartPageJsObject, self).__init__(parent) + + self.__external = parent + + @pyqtSlot(result=str) + def providerString(self): + """ + Public method to get a string for the search provider. + + @return string for the search provider (string) + """ + return (self.tr("Search results provided by {0}") + .format(self.__external.page().view().mainWindow() + .openSearchManager().currentEngineName())) + + @pyqtSlot(str, result=str) + def searchUrl(self, searchStr): + """ + Public method to get the search URL for the given search term. + + @param searchStr search term (string) + @return search URL (string) + """ + return bytes( + self.__external.page().view().mainWindow().openSearchManager() + .currentEngine().searchUrl(searchStr).toEncoded()).decode()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/JavaScript/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the external JavaScript objects. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/EricSchemeHandler.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a scheme handler for the eric: scheme. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, QByteArray, QBuffer, QIODevice, \ + QTextStream, QUrlQuery +from PyQt5.QtWidgets import qApp +from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler + +from ..Tools.WebBrowserTools import readAllFileContents + +class EricSchemeHandler(QWebEngineUrlSchemeHandler): + """ + Class implementing a scheme handler for the eric: scheme. + """ + SupportedPages = [ + "adblock", # error page for URLs blocked by AdBlock + "home", "start", "startpage", # eric home page + "speeddial", # eric speeddial + ] + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object + @type QObject + """ + super(EricSchemeHandler, self).__init__(parent) + + self.__replies = [] + + def requestStarted(self, job): + """ + Public method handling the URL request. + + @param job URL request job + @type QWebEngineUrlRequestJob + """ + if job.requestUrl().path() in self.SupportedPages: + reply = EricSchemeReply(job) + reply.closed.connect(self.__replyClosed) + self.__replies.append(reply) + job.reply(b"text/html", reply) + else: + job.reply(QByteArray(), QBuffer()) + # job.fail(QWebEngineUrlRequestJob.UrlNotFound) + + def __replyClosed(self): + """ + Private slot handling the closed signal of a reply. + """ + object = self.sender() + if object and object in self.__replies: + self.__replies.remove(object) + + +class EricSchemeReply(QIODevice): + """ + Class implementing a reply for a requested eric: page. + + @signal closed emitted to signal that the web engine has read + the data + """ + closed = pyqtSignal() + + _speedDialPage = "" + + def __init__(self, job, parent=None): + """ + Constructor + + @param job reference to the URL request + @type QWebEngineUrlRequestJob + @param parent reference to the parent object + @type QObject + """ + super(EricSchemeReply, self).__init__(parent) + + self.__loaded = False + self.__job = job + + self.__pageName = self.__job.requestUrl().path() + self.__buffer = QBuffer() + + self.open(QIODevice.ReadOnly) + self.__buffer.open(QIODevice.ReadWrite) + self.__loadPage() + + def __loadPage(self): + """ + Private method to load the requested page. + """ + if self.__loaded: + return + + stream = QTextStream(self.__buffer) + stream.setCodec("utf-8") + + if self.__pageName == "adblock": + stream << self.__adBlockPage() + elif self.__pageName in ["home", "start", "startpage"]: + stream << self.__startPage() + elif self.__pageName == "speeddial": + stream << self.__speedDialPage() + + stream.flush() + self.__buffer.reset() + self.__loaded = True + + def bytesAvailable(self): + """ + Public method to get the number of available bytes. + + @return number of available bytes + @rtype int + """ + return self.__buffer.bytesAvailable() + + def readData(self, maxlen): + """ + Public method to retrieve data from the reply object. + + @param maxlen maximum number of bytes to read (integer) + @return string containing the data (bytes) + """ + return self.__buffer.read(maxlen) + + def close(self): + """ + Public method used to cloase the reply. + """ + super(EricSchemeReply, self).close() + self.closed.emit() + + def __adBlockPage(self): + """ + Private method to build the AdBlock page. + + @return built AdBlock page + @rtype str + """ + query = QUrlQuery(self.__job.requestUrl()) + rule = query.queryItemValue("rule") + subscription = query.queryItemValue("subscription") + title = self.tr("Content blocked by AdBlock Plus") + message = self.tr( + "Blocked by rule: <i>{0} ({1})</i>").format(rule, subscription) + + page = readAllFileContents(":/html/adblockPage.html") + page = page.replace( + "@FAVICON@", "qrc:icons/adBlockPlus16.png") + page = page.replace( + "@IMAGE@", "qrc:icons/adBlockPlus64.png") + page = page.replace("@TITLE@", title) + page = page.replace("@MESSAGE@", message) + + return page + + def __startPage(self): + """ + Private method to build the Start page. + + @return built Start page + @rtype str + """ + page = readAllFileContents(":/html/startPage.html") + page = page.replace("@FAVICON@", "qrc:icons/ericWeb16.png") + page = page.replace("@IMAGE@", "qrc:icons/ericWeb32.png") + page = page.replace("@TITLE@", + self.tr("Welcome to eric6 Web Browser!")) + page = page.replace("@ERIC_LINK@", self.tr("About eric6")) + page = page.replace("@HEADER_TITLE@", self.tr("eric6 Web Browser")) + page = page.replace("@SUBMIT@", self.tr("Search!")) + if qApp.isLeftToRight(): + ltr = "LTR" + else: + ltr = "RTL" + page = page.replace("@QT_LAYOUT_DIRECTION@", ltr) + + return page + + def __speedDialPage(self): + """ + Private method to create the Speeddial page. + + @return prepared speeddial page (QByteArray) + """ + if not self._speedDialPage: + page = readAllFileContents(":/html/speeddialPage.html") + page = ( + page.replace("@FAVICON@", "qrc:icons/ericWeb16.png") + .replace("@IMG_PLUS@", "qrc:icons/plus.png") + .replace("@IMG_CLOSE@", "qrc:icons/close.png") + .replace("@IMG_EDIT@", "qrc:icons/edit.png") + .replace("@IMG_RELOAD@", "qrc:icons/reload.png") + .replace("@IMG_SETTINGS@", "qrc:icons/setting.png") + .replace("@LOADING-IMG@", "qrc:icons/loading.gif") + .replace("@BOX-BORDER@", "qrc:icons/box-border-small.png") + + .replace("@JQUERY@", "qrc:javascript/jquery.js") + .replace("@JQUERY-UI@", "qrc:javascript/jquery-ui.js") + + .replace("@SITE-TITLE@", self.tr("Speed Dial")) + .replace("@URL@", self.tr("URL")) + .replace("@TITLE@", self.tr("Title")) + .replace("@APPLY@", self.tr("Apply")) + .replace("@CLOSE@", self.tr("Close")) + .replace("@NEW-PAGE@", self.tr("New Page")) + .replace("@TITLE-EDIT@", self.tr("Edit")) + .replace("@TITLE-REMOVE@", self.tr("Remove")) + .replace("@TITLE-RELOAD@", self.tr("Reload")) + .replace("@TITLE-WARN@", + self.tr("Are you sure to remove this speed dial?")) + .replace("@TITLE-WARN-REL@", + self.tr("Are you sure you want to reload all speed" + " dials?")) + .replace("@TITLE-FETCHTITLE@", + self.tr("Load title from page")) + .replace("@SETTINGS-TITLE@", + self.tr("Speed Dial Settings")) + .replace("@ADD-TITLE@", self.tr("Add New Page")) + .replace("@TXT_NRROWS@", + self.tr("Maximum pages in a row:")) + .replace("@TXT_SDSIZE@", self.tr("Change size of pages:")) + ) + + self._speedDialPage = page + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + dial = WebBrowserWindow.speedDial() + page = ( + self._speedDialPage + .replace("@INITIAL-SCRIPT@", dial.initialScript()) + .replace("@ROW-PAGES@", str(dial.pagesInRow())) + .replace("@SD-SIZE@", str(dial.sdSize())) + ) + + return page
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/NetworkManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,323 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a network manager class. +""" + +from __future__ import unicode_literals + +import json + +from PyQt5.QtCore import pyqtSignal, QByteArray +from PyQt5.QtWidgets import QDialog +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkProxy, \ + QNetworkRequest + +from E5Gui import E5MessageBox + +from E5Network.E5NetworkProxyFactory import proxyAuthenticationRequired +try: + from E5Network.E5SslErrorHandler import E5SslErrorHandler + SSL_AVAILABLE = True +except ImportError: + SSL_AVAILABLE = False + +from WebBrowser.WebBrowserWindow import WebBrowserWindow +from .NetworkUrlInterceptor import NetworkUrlInterceptor + +from Utilities.AutoSaver import AutoSaver +import Preferences + + +class NetworkManager(QNetworkAccessManager): + """ + Class implementing a network manager. + + @signal changed() emitted to indicate a change + """ + changed = pyqtSignal() + + def __init__(self, engine, parent=None): + """ + Constructor + + @param engine reference to the help engine (QHelpEngine) + @param parent reference to the parent object (QObject) + """ + super(NetworkManager, self).__init__(parent) + + if not WebBrowserWindow._fromEric: + from PyQt5.QtNetwork import QNetworkProxyFactory + from E5Network.E5NetworkProxyFactory import E5NetworkProxyFactory + + self.__proxyFactory = E5NetworkProxyFactory() + QNetworkProxyFactory.setApplicationProxyFactory( + self.__proxyFactory) + + self.languagesChanged() + + if SSL_AVAILABLE: + self.__sslErrorHandler = E5SslErrorHandler(self) + self.sslErrors.connect(self.__sslErrorHandler.sslErrorsReplySlot) + + self.__temporarilyIgnoredSslErrors = {} + self.__permanentlyIgnoredSslErrors = {} + # dictionaries of permanently and temporarily ignored SSL errors + + self.__loaded = False + self.__saveTimer = AutoSaver(self, self.__save) + + self.changed.connect(self.__saveTimer.changeOccurred) + self.proxyAuthenticationRequired.connect(proxyAuthenticationRequired) + self.authenticationRequired.connect( + lambda reply, auth: self.authentication(reply.url(), auth)) + + from .EricSchemeHandler import EricSchemeHandler + self.__ericSchemeHandler = EricSchemeHandler() + WebBrowserWindow.webProfile().installUrlSchemeHandler( + QByteArray(b"eric"), self.__ericSchemeHandler) + + if engine: + from .QtHelpSchemeHandler import QtHelpSchemeHandler + self.__qtHelpSchemeHandler = QtHelpSchemeHandler(engine) + WebBrowserWindow.webProfile().installUrlSchemeHandler( + QByteArray(b"qthelp"), self.__qtHelpSchemeHandler) + + self.__interceptor = NetworkUrlInterceptor(self) + WebBrowserWindow.webProfile().setRequestInterceptor(self.__interceptor) + + WebBrowserWindow.cookieJar() + + def __save(self): + """ + Private slot to save the permanent SSL error exceptions. + """ + if not self.__loaded: + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if not WebBrowserWindow.isPrivate(): + dbString = json.dumps(self.__permanentlyIgnoredSslErrors) + Preferences.setWebBrowser("SslExceptionsDB", dbString) + + def __load(self): + """ + Private method to load the permanent SSL error exceptions. + """ + if self.__loaded: + return + + dbString = Preferences.getWebBrowser("SslExceptionsDB") + if dbString: + try: + db = json.loads(dbString) + self.__permanentlyIgnoredSslErrors = db + except ValueError: + # ignore silently + pass + + self.__loaded = True + + def shutdown(self): + """ + Public method to shut down the network manager. + """ + self.__saveTimer.saveIfNeccessary() + self.__loaded = False + self.__temporarilyIgnoredSslErrors = {} + self.__permanentlyIgnoredSslErrors = {} + + def showSslErrorExceptionsDialog(self): + """ + Public method to show the SSL error exceptions dialog. + """ + self.__load() + + from .SslErrorExceptionsDialog import SslErrorExceptionsDialog + dlg = SslErrorExceptionsDialog(self.__permanentlyIgnoredSslErrors) + if dlg.exec_() == QDialog.Accepted: + self.__permanentlyIgnoredSslErrors = dlg.getSslErrorExceptions() + self.changed.emit() + + def clearSslExceptions(self): + """ + Public method to clear the permanent SSL certificate error exceptions. + """ + self.__load() + + self.__permanentlyIgnoredSslErrors = {} + self.changed.emit() + self.__saveTimer.saveIfNeccessary() + + def certificateError(self, error, view): + """ + Public method to handle SSL certificate errors. + + @param error object containing the certificate error information + @type QWebEngineCertificateError + @param view reference to a view to be used as parent for the dialog + @type QWidget + @return flag indicating to ignore this error + @rtype bool + """ + self.__load() + + host = error.url().host() + + if host in self.__temporarilyIgnoredSslErrors and \ + error.error() in self.__temporarilyIgnoredSslErrors[host]: + return True + + if host in self.__permanentlyIgnoredSslErrors and \ + error.error() in self.__permanentlyIgnoredSslErrors[host]: + return True + + title = self.tr("SSL Certificate Error") + msgBox = E5MessageBox.E5MessageBox( + E5MessageBox.Warning, + title, + self.tr("""<b>{0}</b>""" + """<p>The page you are trying to access has errors""" + """ in the SSL certificate.</p>""" + """<ul><li>{1}</li></ul>""" + """<p>Would you like to make an exception?</p>""") + .format(title, error.errorDescription()), + modal=True, parent=view) + permButton = msgBox.addButton(self.tr("&Permanent accept"), + E5MessageBox.AcceptRole) + tempButton = msgBox.addButton(self.tr("&Temporary accept"), + E5MessageBox.AcceptRole) + msgBox.addButton(self.tr("&Reject"), E5MessageBox.RejectRole) + msgBox.exec_() + if msgBox.clickedButton() == permButton: + if host not in self.__permanentlyIgnoredSslErrors: + self.__permanentlyIgnoredSslErrors[host] = [] + self.__permanentlyIgnoredSslErrors[host].append(error.error()) + self.changed.emit() + return True + elif msgBox.clickedButton() == tempButton: + if host not in self.__temporarilyIgnoredSslErrors: + self.__temporarilyIgnoredSslErrors[host] = [] + self.__temporarilyIgnoredSslErrors[host].append(error.error()) + return True + else: + return False + + def authentication(self, url, auth): + """ + Public slot to handle an authentication request. + + @param url URL requesting authentication (QUrl) + @param auth reference to the authenticator object (QAuthenticator) + """ + urlRoot = "{0}://{1}"\ + .format(url.scheme(), url.authority()) + realm = auth.realm() + if not realm and 'realm' in auth.options(): + realm = auth.option("realm") + if realm: + info = self.tr("<b>Enter username and password for '{0}', " + "realm '{1}'</b>").format(urlRoot, realm) + else: + info = self.tr("<b>Enter username and password for '{0}'</b>")\ + .format(urlRoot) + + from UI.AuthenticationDialog import AuthenticationDialog + import WebBrowser.WebBrowserWindow + + dlg = AuthenticationDialog(info, auth.user(), + Preferences.getUser("SavePasswords"), + Preferences.getUser("SavePasswords")) + if Preferences.getUser("SavePasswords"): + username, password = \ + WebBrowser.WebBrowserWindow.WebBrowserWindow.passwordManager()\ + .getLogin(url, realm) + if username: + dlg.setData(username, password) + if dlg.exec_() == QDialog.Accepted: + username, password = dlg.getData() + auth.setUser(username) + auth.setPassword(password) + if Preferences.getUser("SavePasswords"): + WebBrowser.WebBrowserWindow.WebBrowserWindow.passwordManager()\ + .setLogin(url, realm, username, password) + + def proxyAuthentication(self, requestUrl, auth, proxyHost): + """ + Public slot to handle a proxy authentication request. + + @param requestUrl requested URL + @type QUrl + @param auth reference to the authenticator object + @type QAuthenticator + @param hostname name of the proxy host + @type str + """ + proxy = QNetworkProxy.applicationProxy() + if proxy.user() and proxy.password(): + auth.setUser(proxy.user()) + auth.setPassword(proxy.password()) + return + + proxyAuthenticationRequired(proxy, auth) + + def languagesChanged(self): + """ + Public slot to (re-)load the list of accepted languages. + """ + from WebBrowser.WebBrowserLanguagesDialog import \ + WebBrowserLanguagesDialog + languages = Preferences.toList( + Preferences.Prefs.settings.value( + "WebBrowser/AcceptLanguages", + WebBrowserLanguagesDialog.defaultAcceptLanguages())) + self.__acceptLanguage = WebBrowserLanguagesDialog.httpString(languages) + + WebBrowserWindow.webProfile().setHttpAcceptLanguage( + self.__acceptLanguage) + + def installUrlInterceptor(self, interceptor): + """ + Public method to install an URL interceptor. + + @param interceptor URL interceptor to be installed + @type UrlInterceptor + """ + self.__interceptor.installUrlInterceptor(interceptor) + + def removeUrlInterceptor(self, interceptor): + """ + Public method to remove an URL interceptor. + + @param interceptor URL interceptor to be removed + @type UrlInterceptor + """ + self.__interceptor.removeUrlInterceptor(interceptor) + + def preferencesChanged(self): + """ + Public slot to handle a change of preferences. + """ + self.__interceptor.preferencesChanged() + + def createRequest(self, op, request, data): + """ + Public method to launch a network action. + + @param op operation to be performed + @type QNetworkAccessManager.Operation + @param request request to be operated on + @type QNetworkRequest + @param data reference to the data to be sent + @type QIODevice + @return reference to the network reply + @rtype QNetworkReply + """ + req = QNetworkRequest(request) + req.setAttribute(QNetworkRequest.SpdyAllowedAttribute, True) + req.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) + + return super(NetworkManager, self).createRequest(op, req, data)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/NetworkUrlInterceptor.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to handle URL requests before they get processed +by QtWebEngine. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor + +from ..WebBrowserPage import WebBrowserPage + +import Preferences + + +class NetworkUrlInterceptor(QWebEngineUrlRequestInterceptor): + """ + Class implementing an URL request handler. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object + @type QObject + """ + super(NetworkUrlInterceptor, self).__init__(parent) + + self.__interceptors = [] + + self.__loadSettings() + + def interceptRequest(self, info): + """ + Public method handling an URL request. + + @param info URL request information + @type QWebEngineUrlRequestInfo + """ + # Do Not Track feature + if self.__doNotTrack: + info.setHttpHeader(b"DNT", b"1") + info.setHttpHeader(b"X-Do-Not-Track", b"1") + + # Send referer header? + if not self.__sendReferer and info.requestUrl().host() not in \ + Preferences.getWebBrowser("SendRefererWhitelist"): + info.setHttpHeader(b"Referer", b"") + + # User Agents header + userAgent = WebBrowserPage.userAgentForUrl(info.requestUrl()) + info.setHttpHeader(b"User-Agent", userAgent.encode()) + + for interceptor in self.__interceptors: + interceptor.interceptRequest(info) + + def installUrlInterceptor(self, interceptor): + """ + Public method to install an URL interceptor. + + @param interceptor URL interceptor to be installed + @type UrlInterceptor + """ + if interceptor not in self.__interceptors: + self.__interceptors.append(interceptor) + + def removeUrlInterceptor(self, interceptor): + """ + Public method to remove an URL interceptor. + + @param interceptor URL interceptor to be removed + @type UrlInterceptor + """ + if interceptor in self.__interceptors: + self.__interceptors.remove(interceptor) + + def __loadSettings(self): + """ + Private method to load the Network Manager settings. + """ + self.__doNotTrack = Preferences.getWebBrowser("DoNotTrack") + self.__sendReferer = Preferences.getWebBrowser("SendReferer") + + def preferencesChanged(self): + """ + Public slot to handle a change of preferences. + """ + self.__loadSettings()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/QtHelpSchemeHandler.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a scheme access handler for QtHelp. +""" + +from __future__ import unicode_literals + +import mimetypes +import os + +from PyQt5.QtCore import pyqtSignal, QByteArray, QIODevice, QBuffer +from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler, \ + QWebEngineUrlRequestJob + +QtDocPath = "qthelp://org.qt-project." + +ExtensionMap = { + ".bmp": "image/bmp", + ".css": "text/css", + ".gif": "image/gif", + ".html": "text/html", + ".htm": "text/html", + ".ico": "image/x-icon", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".js": "application/x-javascript", + ".mng": "video/x-mng", + ".pbm": "image/x-portable-bitmap", + ".pgm": "image/x-portable-graymap", + ".pdf": "application/pdf", + ".png": "image/png", + ".ppm": "image/x-portable-pixmap", + ".rss": "application/rss+xml", + ".svg": "image/svg+xml", + ".svgz": "image/svg+xml", + ".text": "text/plain", + ".tif": "image/tiff", + ".tiff": "image/tiff", + ".txt": "text/plain", + ".xbm": "image/x-xbitmap", + ".xml": "text/xml", + ".xpm": "image/x-xpm", + ".xsl": "text/xsl", + ".xhtml": "application/xhtml+xml", + ".wml": "text/vnd.wap.wml", + ".wmlc": "application/vnd.wap.wmlc", +} + + +class QtHelpSchemeHandler(QWebEngineUrlSchemeHandler): + """ + Class implementing a scheme handler for the qthelp: scheme. + """ + def __init__(self, engine, parent=None): + """ + Constructor + + @param engine reference to the help engine + @type QHelpEngine + @param parent reference to the parent object + @type QObject + """ + super(QtHelpSchemeHandler, self).__init__(parent) + + self.__engine = engine + + self.__replies = [] + + def requestStarted(self, job): + """ + Public method handling the URL request. + + @param job URL request job + @type QWebEngineUrlRequestJob + """ + if job.requestUrl().scheme() == "qthelp": + reply = QtHelpSchemeReply(job, self.__engine) + reply.closed.connect(self.__replyClosed) + self.__replies.append(reply) + job.reply(reply.mimeType(), reply) + else: + job.fail(QWebEngineUrlRequestJob.UrlInvalid) + + def __replyClosed(self): + """ + Private slot handling the closed signal of a reply. + """ + object = self.sender() + if object and object in self.__replies: + self.__replies.remove(object) + + +class QtHelpSchemeReply(QIODevice): + """ + Class implementing a reply for a requested qthelp: page. + + @signal closed emitted to signal that the web engine has read + the data + """ + closed = pyqtSignal() + + def __init__(self, job, engine, parent=None): + """ + Constructor + + @param job reference to the URL request + @type QWebEngineUrlRequestJob + @param engine reference to the help engine + @type QHelpEngine + @param parent reference to the parent object + @type QObject + """ + super(QtHelpSchemeReply, self).__init__(parent) + + url = job.requestUrl() + strUrl = url.toString() + + self.__buffer = QBuffer() + + # For some reason the url to load maybe wrong (passed from web engine) + # though the css file and the references inside should work that way. + # One possible problem might be that the css is loaded at the same + # level as the html, thus a path inside the css like + # (../images/foo.png) might cd out of the virtual folder + if not engine.findFile(url).isValid(): + if strUrl.startswith(QtDocPath): + newUrl = job.requestUrl() + if not newUrl.path().startswith("/qdoc/"): + newUrl.setPath("/qdoc" + newUrl.path()) + url = newUrl + strUrl = url.toString() + + self.__mimeType = mimetypes.guess_type(strUrl)[0] + if self.__mimeType is None: + # do our own (limited) guessing + self.__mimeType = self.__mimeFromUrl(url) + + if engine.findFile(url).isValid(): + data = engine.fileData(url) + else: + data = QByteArray(self.tr( + """<html>""" + """<head><title>Error 404...</title></head>""" + """<body><div align="center"><br><br>""" + """<h1>The page could not be found</h1><br>""" + """<h3>'{0}'</h3></div></body>""" + """</html>""").format(strUrl) + .encode("utf-8")) + + self.__buffer.setData(data) + self.__buffer.open(QIODevice.ReadOnly) + self.open(QIODevice.ReadOnly) + + def bytesAvailable(self): + """ + Public method to get the number of available bytes. + + @return number of available bytes + @rtype int + """ + return self.__buffer.bytesAvailable() + + def readData(self, maxlen): + """ + Public method to retrieve data from the reply object. + + @param maxlen maximum number of bytes to read (integer) + @return string containing the data (bytes) + """ + return self.__buffer.read(maxlen) + + def close(self): + """ + Public method used to cloase the reply. + """ + super(QtHelpSchemeReply, self).close() + self.closed.emit() + + def __mimeFromUrl(self, url): + """ + Private method to guess the mime type given an URL. + + @param url URL to guess the mime type from (QUrl) + @return mime type for the given URL (string) + """ + path = url.path() + ext = os.path.splitext(path)[1].lower() + if ext in ExtensionMap: + return ExtensionMap[ext] + else: + return "application/octet-stream" + + def mimeType(self): + """ + Public method to get the reply mime type. + + @return mime type of the reply + @rtype bytes + """ + return self.__mimeType.encode("utf-8")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/SendRefererWhitelistDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to manage the Send Referer whitelist. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt, QSortFilterProxyModel, QStringListModel +from PyQt5.QtWidgets import QDialog, QInputDialog, QLineEdit + +from .Ui_SendRefererWhitelistDialog import Ui_SendRefererWhitelistDialog + +import Preferences + + +class SendRefererWhitelistDialog(QDialog, Ui_SendRefererWhitelistDialog): + """ + Class implementing a dialog to manage the Send Referer whitelist. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(SendRefererWhitelistDialog, self).__init__(parent) + self.setupUi(self) + + self.__model = QStringListModel( + Preferences.getWebBrowser("SendRefererWhitelist"), self) + self.__model.sort(0) + self.__proxyModel = QSortFilterProxyModel(self) + self.__proxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive) + self.__proxyModel.setSourceModel(self.__model) + self.whitelist.setModel(self.__proxyModel) + + self.searchEdit.textChanged.connect( + self.__proxyModel.setFilterFixedString) + + self.removeButton.clicked.connect(self.whitelist.removeSelected) + self.removeAllButton.clicked.connect(self.whitelist.removeAll) + + @pyqtSlot() + def on_addButton_clicked(self): + """ + Private slot to add an entry to the whitelist. + """ + host, ok = QInputDialog.getText( + self, + self.tr("Send Referer Whitelist"), + self.tr("Enter host name to add to the whitelist:"), + QLineEdit.Normal) + if ok and host != "" and host not in self.__model.stringList(): + self.__model.insertRow(self.__model.rowCount()) + self.__model.setData( + self.__model.index(self.__model.rowCount() - 1), host) + self.__model.sort(0) + + def accept(self): + """ + Public method to accept the dialog data. + """ + Preferences.setWebBrowser( + "SendRefererWhitelist", self.__model.stringList()) + + super(SendRefererWhitelistDialog, self).accept()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/SendRefererWhitelistDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,199 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SendRefererWhitelistDialog</class> + <widget class="QDialog" name="SendRefererWhitelistDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>350</height> + </rect> + </property> + <property name="windowTitle"> + <string>Send Referer Whitelist</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="horizontalSpacing"> + <number>0</number> + </property> + <item row="0" column="1"> + <widget class="E5ClearableLineEdit" name="searchEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + <property name="toolTip"> + <string>Enter search term for hosts</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="4" column="1"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="addButton"> + <property name="toolTip"> + <string>Press to add site to the whitelist</string> + </property> + <property name="text"> + <string>&Add...</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected entries</string> + </property> + <property name="text"> + <string>&Remove</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all entries</string> + </property> + <property name="text"> + <string>R&emove All</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="0" column="0" rowspan="5"> + <widget class="E5ListView" name="whitelist"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + <customwidget> + <class>E5ListView</class> + <extends>QListView</extends> + <header>E5Gui/E5ListView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>whitelist</tabstop> + <tabstop>addButton</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>removeAllButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SendRefererWhitelistDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>227</x> + <y>329</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>SendRefererWhitelistDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>295</x> + <y>335</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/SslErrorExceptionsDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to edit the SSL error exceptions. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt, QPoint +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem, QMenu +from PyQt5.QtWebEngineWidgets import QWebEngineCertificateError + +from .Ui_SslErrorExceptionsDialog import Ui_SslErrorExceptionsDialog + + +class SslErrorExceptionsDialog(QDialog, Ui_SslErrorExceptionsDialog): + """ + Class implementing a dialog to edit the SSL error exceptions. + """ + def __init__(self, errorsDict, parent=None): + """ + Constructor + + @param errorsDict error exceptions + @type dict of list of int + @param parent reference to the parent widget + @type QWidget + """ + super(SslErrorExceptionsDialog, self).__init__(parent) + self.setupUi(self) + + self.__errorDescriptions = { + QWebEngineCertificateError.SslPinnedKeyNotInCertificateChain: + self.tr("The certificate did not match the built-in public" + " keys pinned for the host name."), + QWebEngineCertificateError.CertificateCommonNameInvalid: + self.tr("The certificate's common name did not match the" + " host name."), + QWebEngineCertificateError.CertificateDateInvalid: + self.tr("The certificate is not valid at the current date" + " and time."), + QWebEngineCertificateError.CertificateAuthorityInvalid: + self.tr("The certificate is not signed by a trusted" + " authority."), + QWebEngineCertificateError.CertificateContainsErrors: + self.tr("The certificate contains errors."), + QWebEngineCertificateError.CertificateNoRevocationMechanism: + self.tr("The certificate has no mechanism for determining if" + " it has been revoked."), + QWebEngineCertificateError.CertificateUnableToCheckRevocation: + self.tr("Revocation information for the certificate is" + " not available."), + QWebEngineCertificateError.CertificateRevoked: + self.tr("The certificate has been revoked."), + QWebEngineCertificateError.CertificateInvalid: + self.tr("The certificate is invalid."), + QWebEngineCertificateError.CertificateWeakSignatureAlgorithm: + self.tr("The certificate is signed using a weak signature" + " algorithm."), + QWebEngineCertificateError.CertificateNonUniqueName: + self.tr("The host name specified in the certificate is" + " not unique."), + QWebEngineCertificateError.CertificateWeakKey: + self.tr("The certificate contains a weak key."), + QWebEngineCertificateError.CertificateNameConstraintViolation: + self.tr("The certificate claimed DNS names that are in" + " violation of name constraints."), + } + + for host, errors in errorsDict.items(): + itm = QTreeWidgetItem(self.errorsTree, [host]) + self.errorsTree.setFirstItemColumnSpanned(itm, True) + for error in errors: + try: + errorDesc = self.__errorDescriptions[error] + except KeyError: + errorDesc = self.tr("No error description available.") + QTreeWidgetItem(itm, [str(error), errorDesc]) + + self.errorsTree.expandAll() + for i in range(self.errorsTree.columnCount()): + self.errorsTree.resizeColumnToContents(i) + self.errorsTree.sortItems(0, Qt.AscendingOrder) + + self.__setRemoveButtons() + + def __setRemoveButtons(self): + """ + Private method to set the state of the 'remove' buttons. + """ + if self.errorsTree.topLevelItemCount() == 0: + self.removeButton.setEnabled(False) + self.removeAllButton.setEnabled(False) + else: + self.removeAllButton.setEnabled(True) + self.removeButton.setEnabled( + len(self.errorsTree.selectedItems()) > 0) + + @pyqtSlot(QPoint) + def on_errorsTree_customContextMenuRequested(self, pos): + """ + Private slot to show the context menu. + + @param pos cursor position + @type QPoint + """ + menu = QMenu() + menu.addAction( + self.tr("Remove Selected"), + self.on_removeButton_clicked).setEnabled( + self.errorsTree.topLevelItemCount() > 0 and + len(self.errorsTree.selectedItems()) > 0) + menu.addAction( + self.tr("Remove All"), + self.on_removeAllButton_clicked).setEnabled( + self.errorsTree.topLevelItemCount() > 0) + + menu.exec_(self.errorsTree.mapToGlobal(pos)) + + @pyqtSlot() + def on_errorsTree_itemSelectionChanged(self): + """ + Private slot handling the selection of entries. + """ + self.__setRemoveButtons() + + @pyqtSlot() + def on_removeButton_clicked(self): + """ + Private slot to remove the selected items. + """ + for itm in self.errorsTree.selectedItems(): + pitm = itm.parent() + if pitm: + pitm.removeChild(itm) + else: + index = self.errorsTree.indexOfTopLevelItem(itm) + self.errorsTree.takeTopLevelItem(index) + del itm + + # remove all hosts without an exception + for index in range(self.errorsTree.topLevelItemCount() - 1, -1, -1): + itm = self.errorsTree.topLevelItem(index) + if itm.childCount() == 0: + self.errorsTree.takeTopLevelItem(index) + del itm + + @pyqtSlot() + def on_removeAllButton_clicked(self): + """ + Private slot to remove all entries. + """ + self.errorsTree.clear() + + def getSslErrorExceptions(self): + """ + Public method to retrieve the list of SSL error exceptions. + + @return error exceptions + @rtype dict of list of int + """ + errors = {} + + for index in range(self.errorsTree.topLevelItemCount()): + itm = self.errorsTree.topLevelItem(index) + host = itm.text(0) + errors[host] = [] + for cindex in range(itm.childCount()): + citm = itm.child(cindex) + errors[host].append(int(citm.text(0))) + + return errors
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/SslErrorExceptionsDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,133 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SslErrorExceptionsDialog</class> + <widget class="QDialog" name="SslErrorExceptionsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>751</width> + <height>513</height> + </rect> + </property> + <property name="windowTitle"> + <string>SSL Error Exceptions</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" rowspan="3"> + <widget class="QTreeWidget" name="errorsTree"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <property name="allColumnsShowFocus"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>Code</string> + </property> + </column> + <column> + <property name="text"> + <string>Error Description</string> + </property> + </column> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected entries</string> + </property> + <property name="text"> + <string>&Remove</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all entries</string> + </property> + <property name="text"> + <string>Remove &All</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>128</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SslErrorExceptionsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>388</x> + <y>385</y> + </hint> + <hint type="destinationlabel"> + <x>399</x> + <y>319</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>SslErrorExceptionsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>349</x> + <y>391</y> + </hint> + <hint type="destinationlabel"> + <x>356</x> + <y>286</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/UrlInterceptor.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an URL interceptor base class. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QObject + +class UrlInterceptor(QObject): + """ + Class implementing an URL interceptor base class. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent referemce to the parent object + @type QObject + """ + super(UrlInterceptor, self).__init__(parent) + + def interceptRequest(self, info): + """ + Public method to intercept a request. + + @param info request info object + @type QWebEngineUrlRequestInfo + """ + pass
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Network/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package containing network related modules. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/Amazoncom.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Amazon.com</ShortName> + <Description>Amazon.com Search</Description> + <Url method="get" type="text/html" template="http://www.amazon.com/exec/obidos/external-search/?field-keywords={searchTerms}"/> + <Image>http://www.amazon.com/favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/Bing.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Bing</ShortName> + <Description>Bing Web Search</Description> + <Url method="get" type="text/html" template="http://www.bing.com/search?cc={language}&q={searchTerms}"/> + <Image>http://www.bing.com/s/wlflag.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/DeEn_Beolingus.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>De-En Beolingus</ShortName> + <Description>Beolingus: German-English Dictionary</Description> + <Url method="get" type="text/html" template="http://dict.tu-chemnitz.de/?query={searchTerms}"/> + <Url method="get" type="application/x-suggestions+json" template="http://dict.tu-chemnitz.de/sugg.php?json=1&s={searchTerms}"/> + <Image>http://dict.tu-chemnitz.de/pics/beo-de.png</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/DefaultSearchEngines.qrc Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,21 @@ +<!DOCTYPE RCC> +<RCC version="1.0"> +<qresource> + <file>Amazoncom.xml</file> + <file>Bing.xml</file> + <file>DeEn_Beolingus.xml</file> + <file>DuckDuckGo.xml</file> + <file>Facebook.xml</file> + <file>Google.xml</file> + <file>Google_Im_Feeling_Lucky.xml</file> + <file>LEO_DeuEng.xml</file> + <file>LinuxMagazin.xml</file> + <file>Reddit.xml</file> + <file>Wikia.xml</file> + <file>Wikia_en.xml</file> + <file>Wikipedia.xml</file> + <file>Wiktionary.xml</file> + <file>Yahoo.xml</file> + <file>YouTube.xml</file> +</qresource> +</RCC>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/DefaultSearchEngines_rc.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,728 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.5.1) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x01\x79\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x42\x69\x6e\x67\x3c\x2f\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x0a\x20\x20\x20\x20\x3c\ +\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x42\x69\x6e\x67\ +\x20\x57\x65\x62\x20\x53\x65\x61\x72\x63\x68\x3c\x2f\x44\x65\x73\ +\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x20\x20\x20\x3c\x55\ +\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\x74\x22\x20\ +\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x68\x74\x6d\x6c\x22\ +\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x62\x69\x6e\x67\x2e\x63\x6f\x6d\x2f\x73\ +\x65\x61\x72\x63\x68\x3f\x63\x63\x3d\x7b\x6c\x61\x6e\x67\x75\x61\ +\x67\x65\x7d\x26\x61\x6d\x70\x3b\x71\x3d\x7b\x73\x65\x61\x72\x63\ +\x68\x54\x65\x72\x6d\x73\x7d\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\ +\x49\x6d\x61\x67\x65\x3e\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ +\x2e\x62\x69\x6e\x67\x2e\x63\x6f\x6d\x2f\x73\x2f\x77\x6c\x66\x6c\ +\x61\x67\x2e\x69\x63\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\x0a\x3c\ +\x2f\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\x68\x44\x65\x73\x63\x72\ +\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +\x00\x00\x02\x1e\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x44\x65\x2d\x45\x6e\x20\ +\x42\x65\x6f\x6c\x69\x6e\x67\x75\x73\x3c\x2f\x53\x68\x6f\x72\x74\ +\x4e\x61\x6d\x65\x3e\x0a\x20\x20\x20\x20\x3c\x44\x65\x73\x63\x72\ +\x69\x70\x74\x69\x6f\x6e\x3e\x42\x65\x6f\x6c\x69\x6e\x67\x75\x73\ +\x3a\x20\x47\x65\x72\x6d\x61\x6e\x2d\x45\x6e\x67\x6c\x69\x73\x68\ +\x20\x44\x69\x63\x74\x69\x6f\x6e\x61\x72\x79\x3c\x2f\x44\x65\x73\ +\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x20\x20\x20\x3c\x55\ +\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\x74\x22\x20\ +\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x68\x74\x6d\x6c\x22\ +\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x64\x69\x63\x74\x2e\x74\x75\x2d\x63\x68\x65\x6d\x6e\x69\ +\x74\x7a\x2e\x64\x65\x2f\x3f\x71\x75\x65\x72\x79\x3d\x7b\x73\x65\ +\x61\x72\x63\x68\x54\x65\x72\x6d\x73\x7d\x22\x2f\x3e\x0a\x20\x20\ +\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\ +\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x61\x70\x70\x6c\x69\x63\ +\x61\x74\x69\x6f\x6e\x2f\x78\x2d\x73\x75\x67\x67\x65\x73\x74\x69\ +\x6f\x6e\x73\x2b\x6a\x73\x6f\x6e\x22\x20\x74\x65\x6d\x70\x6c\x61\ +\x74\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x64\x69\x63\x74\x2e\ +\x74\x75\x2d\x63\x68\x65\x6d\x6e\x69\x74\x7a\x2e\x64\x65\x2f\x73\ +\x75\x67\x67\x2e\x70\x68\x70\x3f\x6a\x73\x6f\x6e\x3d\x31\x26\x61\ +\x6d\x70\x3b\x73\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\x6d\ +\x73\x7d\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x49\x6d\x61\x67\x65\ +\x3e\x68\x74\x74\x70\x3a\x2f\x2f\x64\x69\x63\x74\x2e\x74\x75\x2d\ +\x63\x68\x65\x6d\x6e\x69\x74\x7a\x2e\x64\x65\x2f\x70\x69\x63\x73\ +\x2f\x62\x65\x6f\x2d\x64\x65\x2e\x70\x6e\x67\x3c\x2f\x49\x6d\x61\ +\x67\x65\x3e\x0a\x3c\x2f\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\x68\ +\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +\x00\x00\x02\x64\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x47\x6f\x6f\x67\x6c\x65\ +\x20\x28\x49\x27\x6d\x20\x46\x65\x65\x6c\x69\x6e\x67\x20\x4c\x75\ +\x63\x6b\x79\x29\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\ +\x0a\x20\x20\x20\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\ +\x6e\x3e\x47\x6f\x6f\x67\x6c\x65\x20\x57\x65\x62\x20\x53\x65\x61\ +\x72\x63\x68\x3c\x2f\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\ +\x3e\x0a\x20\x20\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\ +\x64\x3d\x22\x67\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\ +\x78\x74\x2f\x68\x74\x6d\x6c\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\ +\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x67\x6f\ +\x6f\x67\x6c\x65\x2e\x63\x6f\x6d\x2f\x73\x65\x61\x72\x63\x68\x3f\ +\x62\x74\x6e\x49\x3d\x26\x61\x6d\x70\x3b\x68\x6c\x3d\x7b\x6c\x61\ +\x6e\x67\x75\x61\x67\x65\x7d\x26\x61\x6d\x70\x3b\x6c\x72\x3d\x6c\ +\x61\x6e\x67\x5f\x7b\x6c\x61\x6e\x67\x75\x61\x67\x65\x7d\x26\x61\ +\x6d\x70\x3b\x71\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\x6d\ +\x73\x7d\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x55\x72\x6c\x20\x6d\ +\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\x74\x22\x20\x74\x79\x70\x65\ +\x3d\x22\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\x78\x2d\ +\x73\x75\x67\x67\x65\x73\x74\x69\x6f\x6e\x73\x2b\x6a\x73\x6f\x6e\ +\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x73\x75\x67\x67\x65\x73\x74\x71\x75\x65\x72\x69\x65\ +\x73\x2e\x67\x6f\x6f\x67\x6c\x65\x2e\x63\x6f\x6d\x2f\x63\x6f\x6d\ +\x70\x6c\x65\x74\x65\x2f\x73\x65\x61\x72\x63\x68\x3f\x6f\x75\x74\ +\x70\x75\x74\x3d\x66\x69\x72\x65\x66\x6f\x78\x26\x61\x6d\x70\x3b\ +\x68\x6c\x3d\x7b\x6c\x61\x6e\x67\x75\x61\x67\x65\x7d\x26\x61\x6d\ +\x70\x3b\x71\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\x6d\x73\ +\x7d\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x49\x6d\x61\x67\x65\x3e\ +\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x67\x6f\x6f\x67\x6c\ +\x65\x2e\x63\x6f\x6d\x2f\x66\x61\x76\x69\x63\x6f\x6e\x2e\x69\x63\ +\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\x0a\x3c\x2f\x4f\x70\x65\x6e\ +\x53\x65\x61\x72\x63\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\ +\x6e\x3e\x0a\ +\x00\x00\x02\x54\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x57\x69\x6b\x69\x61\x3c\ +\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x0a\x20\x20\x20\x20\ +\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x57\x69\x6b\ +\x69\x61\x20\x53\x69\x74\x65\x20\x53\x65\x61\x72\x63\x68\x3c\x2f\ +\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x20\x20\ +\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\ +\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x68\x74\ +\x6d\x6c\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\ +\x74\x70\x3a\x2f\x2f\x7b\x63\x6f\x75\x6e\x74\x72\x79\x7d\x2e\x77\ +\x69\x6b\x69\x61\x2e\x63\x6f\x6d\x2f\x69\x6e\x64\x65\x78\x2e\x70\ +\x68\x70\x3f\x74\x69\x74\x6c\x65\x3d\x53\x70\x65\x63\x69\x61\x6c\ +\x3a\x53\x65\x61\x72\x63\x68\x26\x61\x6d\x70\x3b\x73\x65\x61\x72\ +\x63\x68\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\x6d\x73\x7d\ +\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\ +\x68\x6f\x64\x3d\x22\x67\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\ +\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\x78\x2d\x73\x75\ +\x67\x67\x65\x73\x74\x69\x6f\x6e\x73\x2b\x6a\x73\x6f\x6e\x22\x20\ +\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x7b\x63\x6f\x75\x6e\x74\x72\x79\x7d\x2e\x77\x69\x6b\x69\x61\ +\x2e\x63\x6f\x6d\x2f\x61\x70\x69\x2e\x70\x68\x70\x3f\x61\x63\x74\ +\x69\x6f\x6e\x3d\x6f\x70\x65\x6e\x73\x65\x61\x72\x63\x68\x26\x61\ +\x6d\x70\x3b\x73\x65\x61\x72\x63\x68\x3d\x7b\x73\x65\x61\x72\x63\ +\x68\x54\x65\x72\x6d\x73\x7d\x26\x61\x6d\x70\x3b\x6e\x61\x6d\x65\ +\x73\x70\x61\x63\x65\x3d\x30\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\ +\x49\x6d\x61\x67\x65\x3e\x68\x74\x74\x70\x3a\x2f\x2f\x69\x6d\x61\ +\x67\x65\x73\x2e\x77\x69\x6b\x69\x61\x2e\x63\x6f\x6d\x2f\x77\x69\ +\x6b\x69\x61\x67\x6c\x6f\x62\x61\x6c\x2f\x69\x6d\x61\x67\x65\x73\ +\x2f\x36\x2f\x36\x34\x2f\x46\x61\x76\x69\x63\x6f\x6e\x2e\x69\x63\ +\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\x0a\x3c\x2f\x4f\x70\x65\x6e\ +\x53\x65\x61\x72\x63\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\ +\x6e\x3e\x0a\ +\x00\x00\x02\x27\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x59\x61\x68\x6f\x6f\x21\ +\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x0a\x20\x20\x20\ +\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x59\x61\ +\x68\x6f\x6f\x20\x57\x65\x62\x20\x53\x65\x61\x72\x63\x68\x3c\x2f\ +\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x20\x20\ +\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\ +\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x68\x74\ +\x6d\x6c\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\ +\x74\x70\x3a\x2f\x2f\x73\x65\x61\x72\x63\x68\x2e\x79\x61\x68\x6f\ +\x6f\x2e\x63\x6f\x6d\x2f\x73\x65\x61\x72\x63\x68\x3f\x65\x69\x3d\ +\x75\x74\x66\x2d\x38\x26\x61\x6d\x70\x3b\x66\x72\x3d\x73\x66\x70\ +\x26\x61\x6d\x70\x3b\x69\x73\x63\x71\x72\x79\x3d\x26\x61\x6d\x70\ +\x3b\x70\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\x6d\x73\x7d\ +\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\ +\x68\x6f\x64\x3d\x22\x67\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\ +\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\x78\x2d\x73\x75\ +\x67\x67\x65\x73\x74\x69\x6f\x6e\x73\x2b\x6a\x73\x6f\x6e\x22\x20\ +\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x66\x66\x2e\x73\x65\x61\x72\x63\x68\x2e\x79\x61\x68\x6f\x6f\ +\x2e\x63\x6f\x6d\x2f\x67\x6f\x73\x73\x69\x70\x3f\x6f\x75\x74\x70\ +\x75\x74\x3d\x66\x78\x6a\x73\x6f\x6e\x26\x61\x6d\x70\x3b\x63\x6f\ +\x6d\x6d\x61\x6e\x64\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\ +\x6d\x73\x7d\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x49\x6d\x61\x67\ +\x65\x3e\x68\x74\x74\x70\x3a\x2f\x2f\x6d\x2e\x77\x77\x77\x2e\x79\ +\x61\x68\x6f\x6f\x2e\x63\x6f\x6d\x2f\x66\x61\x76\x69\x63\x6f\x6e\ +\x2e\x69\x63\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\x0a\x3c\x2f\x4f\ +\x70\x65\x6e\x53\x65\x61\x72\x63\x68\x44\x65\x73\x63\x72\x69\x70\ +\x74\x69\x6f\x6e\x3e\x0a\ +\x00\x00\x01\x6f\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x52\x65\x64\x64\x69\x74\ +\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x0a\x20\x20\x20\ +\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x52\x65\ +\x64\x64\x69\x74\x20\x53\x69\x74\x65\x20\x53\x65\x61\x72\x63\x68\ +\x3c\x2f\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\ +\x20\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\ +\x67\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\ +\x68\x74\x6d\x6c\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\ +\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x72\x65\x64\x64\x69\ +\x74\x2e\x63\x6f\x6d\x2f\x73\x65\x61\x72\x63\x68\x3f\x71\x3d\x7b\ +\x73\x65\x61\x72\x63\x68\x54\x65\x72\x6d\x73\x7d\x22\x2f\x3e\x0a\ +\x20\x20\x20\x20\x3c\x49\x6d\x61\x67\x65\x3e\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x72\x65\x64\x64\x69\x74\x2e\x63\x6f\x6d\ +\x2f\x66\x61\x76\x69\x63\x6f\x6e\x2e\x69\x63\x6f\x3c\x2f\x49\x6d\ +\x61\x67\x65\x3e\x0a\x3c\x2f\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +\x00\x00\x02\x7a\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x4c\x45\x4f\x20\x44\x65\ +\x75\x2d\x45\x6e\x67\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\ +\x3e\x0a\x20\x20\x20\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\ +\x6f\x6e\x3e\x44\x65\x75\x74\x73\x63\x68\x2d\x45\x6e\x67\x6c\x69\ +\x73\x63\x68\x20\x57\xc3\xb6\x72\x74\x65\x72\x62\x75\x63\x68\x20\ +\x76\x6f\x6e\x20\x4c\x45\x4f\x3c\x2f\x44\x65\x73\x63\x72\x69\x70\ +\x74\x69\x6f\x6e\x3e\x0a\x20\x20\x20\x20\x3c\x55\x72\x6c\x20\x6d\ +\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\x74\x22\x20\x74\x79\x70\x65\ +\x3d\x22\x74\x65\x78\x74\x2f\x68\x74\x6d\x6c\x22\x20\x74\x65\x6d\ +\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x64\x69\ +\x63\x74\x2e\x6c\x65\x6f\x2e\x6f\x72\x67\x2f\x65\x6e\x64\x65\x3f\ +\x6c\x61\x6e\x67\x3d\x64\x65\x26\x61\x6d\x70\x3b\x73\x65\x61\x72\ +\x63\x68\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\x6d\x73\x7d\ +\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\ +\x68\x6f\x64\x3d\x22\x67\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\ +\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\x78\x2d\x73\x75\ +\x67\x67\x65\x73\x74\x69\x6f\x6e\x73\x2b\x6a\x73\x6f\x6e\x22\x20\ +\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x64\x69\x63\x74\x2e\x6c\x65\x6f\x2e\x6f\x72\x67\x2f\x64\x69\ +\x63\x74\x51\x75\x65\x72\x79\x2f\x6d\x2d\x71\x75\x65\x72\x79\x2f\ +\x63\x6f\x6e\x66\x2f\x65\x6e\x64\x65\x2f\x71\x75\x65\x72\x79\x2e\ +\x63\x6f\x6e\x66\x2f\x73\x74\x72\x6c\x69\x73\x74\x2e\x6a\x73\x6f\ +\x6e\x3f\x71\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\x6d\x73\ +\x7d\x26\x61\x6d\x70\x3b\x73\x6f\x72\x74\x3d\x50\x4c\x61\x26\x61\ +\x6d\x70\x3b\x73\x68\x6f\x72\x74\x51\x75\x65\x72\x79\x26\x61\x6d\ +\x70\x3b\x6e\x6f\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x26\ +\x61\x6d\x70\x3b\x6e\x6f\x51\x75\x65\x72\x79\x55\x52\x4c\x73\x22\ +\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x49\x6d\x61\x67\x65\x3e\x68\x74\ +\x74\x70\x3a\x2f\x2f\x64\x69\x63\x74\x2e\x6c\x65\x6f\x2e\x6f\x72\ +\x67\x2f\x69\x6d\x67\x2f\x66\x61\x76\x69\x63\x6f\x6e\x73\x2f\x65\ +\x6e\x64\x65\x2e\x69\x63\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\x0a\ +\x3c\x2f\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\x68\x44\x65\x73\x63\ +\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +\x00\x00\x01\x85\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x59\x6f\x75\x54\x75\x62\ +\x65\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x0a\x20\x20\ +\x20\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x59\ +\x6f\x75\x54\x75\x62\x65\x3c\x2f\x44\x65\x73\x63\x72\x69\x70\x74\ +\x69\x6f\x6e\x3e\x0a\x20\x20\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\ +\x74\x68\x6f\x64\x3d\x22\x67\x65\x74\x22\x20\x74\x79\x70\x65\x3d\ +\x22\x74\x65\x78\x74\x2f\x68\x74\x6d\x6c\x22\x20\x74\x65\x6d\x70\ +\x6c\x61\x74\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ +\x2e\x79\x6f\x75\x74\x75\x62\x65\x2e\x63\x6f\x6d\x2f\x72\x65\x73\ +\x75\x6c\x74\x73\x3f\x73\x65\x61\x72\x63\x68\x5f\x71\x75\x65\x72\ +\x79\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\x6d\x73\x7d\x26\ +\x61\x6d\x70\x3b\x73\x65\x61\x72\x63\x68\x3d\x53\x65\x61\x72\x63\ +\x68\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x49\x6d\x61\x67\x65\x3e\ +\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x79\x6f\x75\x74\x75\ +\x62\x65\x2e\x63\x6f\x6d\x2f\x66\x61\x76\x69\x63\x6f\x6e\x2e\x69\ +\x63\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\x0a\x3c\x2f\x4f\x70\x65\ +\x6e\x53\x65\x61\x72\x63\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\ +\x6f\x6e\x3e\x0a\ +\x00\x00\x06\xfe\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x44\x75\x63\x6b\x44\x75\ +\x63\x6b\x47\x6f\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\ +\x0a\x20\x20\x20\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\ +\x6e\x3e\x53\x65\x61\x72\x63\x68\x20\x44\x75\x63\x6b\x44\x75\x63\ +\x6b\x47\x6f\x3c\x2f\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\ +\x3e\x0a\x20\x20\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\ +\x64\x3d\x22\x67\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\ +\x78\x74\x2f\x68\x74\x6d\x6c\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\ +\x65\x3d\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x64\x75\x63\x6b\x64\ +\x75\x63\x6b\x67\x6f\x2e\x63\x6f\x6d\x2f\x3f\x71\x3d\x7b\x73\x65\ +\x61\x72\x63\x68\x54\x65\x72\x6d\x73\x7d\x22\x2f\x3e\x0a\x20\x20\ +\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\ +\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x61\x70\x70\x6c\x69\x63\ +\x61\x74\x69\x6f\x6e\x2f\x78\x2d\x73\x75\x67\x67\x65\x73\x74\x69\ +\x6f\x6e\x73\x2b\x6a\x73\x6f\x6e\x22\x20\x74\x65\x6d\x70\x6c\x61\ +\x74\x65\x3d\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x61\x63\x2e\x64\ +\x75\x63\x6b\x64\x75\x63\x6b\x67\x6f\x2e\x63\x6f\x6d\x2f\x61\x63\ +\x2f\x3f\x71\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\x6d\x73\ +\x7d\x26\x61\x6d\x70\x3b\x74\x79\x70\x65\x3d\x6c\x69\x73\x74\x22\ +\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x49\x6d\x61\x67\x65\x3e\x64\x61\ +\x74\x61\x3a\x69\x6d\x61\x67\x65\x2f\x78\x2d\x69\x63\x6f\x6e\x3b\ +\x62\x61\x73\x65\x36\x34\x2c\x69\x56\x42\x4f\x52\x77\x30\x4b\x47\ +\x67\x6f\x41\x41\x41\x41\x4e\x53\x55\x68\x45\x55\x67\x41\x41\x41\ +\x42\x41\x41\x41\x41\x41\x51\x43\x41\x4d\x41\x41\x41\x41\x6f\x4c\ +\x51\x39\x54\x41\x41\x41\x41\x42\x47\x64\x42\x54\x55\x45\x41\x41\ +\x4c\x47\x50\x43\x2f\x78\x68\x42\x51\x41\x41\x41\x43\x42\x6a\x53\ +\x46\x4a\x4e\x41\x41\x42\x36\x4a\x67\x41\x41\x67\x49\x51\x41\x41\ +\x50\x6f\x41\x41\x41\x43\x41\x36\x41\x41\x41\x64\x54\x41\x41\x41\ +\x4f\x70\x67\x41\x41\x41\x36\x6d\x41\x41\x41\x46\x33\x43\x63\x75\ +\x6c\x45\x38\x41\x41\x41\x42\x38\x6c\x42\x4d\x56\x45\x55\x41\x41\ +\x41\x44\x6b\x52\x51\x7a\x6a\x50\x77\x50\x6a\x51\x51\x58\x6b\x52\ +\x51\x33\x69\x50\x77\x54\x69\x51\x51\x58\x67\x50\x51\x50\x65\x51\ +\x67\x72\x63\x4f\x77\x50\x56\x4e\x67\x44\x56\x4e\x51\x44\x57\x4f\ +\x67\x62\x54\x4d\x77\x44\x52\x4d\x67\x44\x51\x4d\x77\x44\x53\x4d\ +\x77\x44\x52\x4e\x77\x54\x51\x4c\x67\x44\x52\x4a\x67\x44\x53\x4a\ +\x77\x44\x53\x4c\x67\x44\x53\x4e\x77\x54\x6a\x4f\x67\x44\x69\x4f\ +\x41\x44\x6a\x4f\x51\x44\x6b\x50\x41\x44\x68\x51\x41\x58\x7a\x73\ +\x35\x76\x2b\x2f\x66\x76\x2f\x2f\x2f\x2f\x30\x76\x4b\x62\x69\x52\ +\x51\x76\x67\x50\x51\x48\x70\x64\x55\x72\x38\x35\x4e\x7a\x75\x6b\ +\x6e\x50\x64\x4b\x67\x44\x63\x49\x77\x44\x6e\x5a\x7a\x6a\x32\x77\ +\x37\x48\x71\x65\x55\x2f\x67\x50\x51\x4c\x73\x69\x6d\x62\x2f\x2b\ +\x50\x66\x74\x6a\x57\x6e\x39\x37\x4f\x62\x70\x62\x30\x4c\x64\x4a\ +\x51\x44\x65\x4c\x51\x44\x74\x6a\x6d\x76\x73\x69\x32\x6a\x67\x53\ +\x42\x44\x6e\x62\x55\x4c\x67\x4f\x51\x44\x2f\x33\x39\x48\x67\x4c\ +\x51\x44\x65\x4d\x67\x44\x70\x65\x46\x4c\x67\x53\x42\x48\x30\x76\ +\x36\x37\x30\x75\x71\x62\x61\x4a\x51\x44\x32\x71\x49\x6d\x57\x76\ +\x50\x2f\x47\x31\x4f\x62\x35\x2b\x2f\x33\x75\x2f\x2f\x2b\x66\x76\ +\x76\x58\x79\x70\x34\x37\x64\x4d\x77\x44\x61\x4c\x77\x44\x30\x75\ +\x36\x76\x30\x76\x36\x2f\x61\x4e\x51\x44\x69\x58\x69\x2f\x61\x4b\ +\x51\x44\x33\x71\x6f\x7a\x55\x37\x2f\x38\x67\x53\x59\x32\x76\x76\ +\x74\x67\x30\x5a\x4b\x2f\x4f\x71\x4c\x44\x61\x4b\x51\x48\x59\x4b\ +\x67\x4c\x67\x57\x54\x66\x61\x4e\x41\x44\x5a\x4d\x67\x44\x5a\x4d\ +\x41\x44\x5a\x4c\x41\x44\x7a\x71\x70\x44\x37\x2f\x2f\x2b\x78\x77\ +\x64\x7a\x2f\x2f\x39\x48\x2f\x35\x42\x6e\x2f\x37\x42\x6e\x2f\x2f\ +\x41\x44\x6f\x66\x41\x44\x59\x4d\x41\x44\x59\x4d\x51\x44\x5a\x4f\ +\x67\x50\x58\x4c\x67\x44\x69\x5a\x44\x6a\x2f\x2f\x39\x37\x2f\x30\ +\x41\x44\x33\x74\x51\x44\x76\x6c\x67\x48\x5a\x4f\x67\x62\x58\x4c\ +\x41\x54\x58\x4d\x41\x44\x57\x4d\x67\x44\x66\x58\x6a\x4c\x56\x4c\ +\x51\x44\x2f\x2f\x2f\x7a\x2b\x30\x41\x44\x2f\x33\x52\x6e\x2f\x79\ +\x52\x6e\x77\x6e\x51\x44\x63\x56\x6a\x62\x56\x4d\x51\x44\x79\x76\ +\x36\x37\x77\x75\x4b\x54\x53\x4a\x77\x44\x52\x48\x51\x44\x2b\x38\ +\x4f\x2f\x74\x67\x33\x2f\x69\x51\x51\x44\x77\x68\x41\x48\x6e\x61\ +\x77\x48\x57\x4d\x41\x44\x76\x74\x4b\x66\x79\x76\x61\x37\x58\x51\ +\x78\x48\x67\x61\x30\x62\x51\x47\x51\x44\x32\x76\x62\x48\x2f\x75\ +\x38\x4c\x58\x49\x51\x43\x6d\x50\x51\x7a\x6a\x61\x30\x37\x58\x51\ +\x78\x4c\x6c\x69\x47\x6e\x39\x39\x66\x50\x6b\x63\x56\x48\x76\x68\ +\x6e\x47\x5a\x35\x56\x67\x75\x76\x55\x55\x35\x77\x6b\x74\x42\x77\ +\x43\x63\x41\x67\x78\x7a\x79\x64\x56\x76\x2f\x38\x2f\x58\x6d\x69\ +\x47\x6e\x67\x64\x6c\x4c\x2b\x79\x73\x69\x33\x2b\x49\x38\x4c\x74\ +\x43\x45\x38\x30\x56\x36\x50\x33\x59\x6d\x58\x34\x73\x44\x6c\x65\ +\x6c\x6a\x53\x4e\x51\x4c\x7a\x72\x36\x44\x37\x73\x4b\x50\x58\x4e\ +\x51\x54\x53\x49\x77\x41\x45\x41\x62\x4d\x72\x41\x41\x41\x41\x46\ +\x33\x52\x53\x54\x6c\x4d\x41\x52\x71\x53\x6b\x52\x76\x50\x7a\x38\ +\x30\x50\x54\x70\x4b\x52\x47\x33\x66\x50\x65\x33\x68\x69\x6f\x39\ +\x2f\x65\x6f\x47\x50\x35\x30\x6a\x4e\x73\x41\x41\x41\x41\x42\x59\ +\x6b\x74\x48\x52\x42\x35\x79\x43\x69\x41\x72\x41\x41\x41\x41\x79\ +\x45\x6c\x45\x51\x56\x51\x59\x47\x51\x58\x42\x76\x55\x71\x43\x59\ +\x52\x69\x41\x34\x66\x75\x32\x56\x39\x54\x6e\x2b\x55\x51\x64\x64\ +\x49\x33\x61\x43\x70\x78\x61\x4f\x6f\x55\x36\x69\x55\x34\x67\x63\ +\x71\x71\x70\x6f\x59\x62\x41\x4c\x58\x42\x75\x43\x75\x6f\x59\x6d\ +\x74\x74\x61\x6d\x71\x4a\x44\x69\x45\x6f\x68\x34\x59\x50\x2b\x4d\ +\x4f\x69\x36\x42\x4e\x43\x68\x2b\x75\x59\x4b\x45\x47\x69\x4f\x56\ +\x4e\x43\x58\x58\x78\x41\x32\x58\x44\x56\x56\x2f\x55\x79\x66\x4b\ +\x62\x52\x43\x58\x54\x4c\x51\x57\x41\x78\x62\x50\x32\x76\x74\x38\ +\x55\x65\x2f\x75\x59\x44\x76\x66\x69\x6d\x39\x31\x36\x31\x35\x73\ +\x62\x32\x75\x6d\x36\x72\x71\x74\x72\x72\x2f\x4e\x46\x62\x31\x63\ +\x55\x66\x31\x59\x62\x64\x30\x36\x61\x72\x65\x55\x36\x6c\x53\x6c\ +\x59\x70\x4b\x37\x39\x6a\x7a\x4b\x31\x53\x79\x4a\x4f\x6b\x66\x68\ +\x4f\x6c\x38\x4a\x47\x45\x63\x71\x56\x35\x7a\x6f\x4b\x72\x54\x52\ +\x71\x4f\x36\x79\x55\x7a\x49\x7a\x4e\x75\x34\x36\x69\x6a\x64\x4d\ +\x31\x56\x56\x39\x62\x68\x75\x55\x4a\x2f\x6e\x5a\x55\x52\x45\x78\ +\x4c\x52\x7a\x55\x69\x50\x51\x6d\x33\x6b\x4b\x58\x48\x69\x34\x42\ +\x41\x45\x47\x4f\x6d\x4f\x69\x37\x38\x41\x2f\x4c\x31\x51\x6f\x55\ +\x2f\x56\x48\x6f\x54\x73\x41\x41\x41\x41\x6c\x64\x45\x56\x59\x64\ +\x47\x52\x68\x64\x47\x55\x36\x59\x33\x4a\x6c\x59\x58\x52\x6c\x41\ +\x44\x49\x77\x4d\x54\x51\x74\x4d\x44\x45\x74\x4d\x54\x6c\x55\x4d\ +\x6a\x41\x36\x4d\x44\x45\x36\x4d\x54\x45\x74\x4d\x44\x55\x36\x4d\ +\x44\x41\x75\x45\x54\x36\x63\x41\x41\x41\x41\x4a\x58\x52\x46\x57\ +\x48\x52\x6b\x59\x58\x52\x6c\x4f\x6d\x31\x76\x5a\x47\x6c\x6d\x65\ +\x51\x41\x79\x4d\x44\x45\x30\x4c\x54\x41\x78\x4c\x54\x45\x35\x56\ +\x44\x49\x77\x4f\x6a\x41\x78\x4f\x6a\x45\x78\x4c\x54\x41\x31\x4f\ +\x6a\x41\x77\x58\x30\x79\x47\x49\x41\x41\x41\x41\x41\x42\x4a\x52\ +\x55\x35\x45\x72\x6b\x4a\x67\x67\x67\x3d\x3d\x3c\x2f\x49\x6d\x61\ +\x67\x65\x3e\x0a\x3c\x2f\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\x68\ +\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +\x00\x00\x01\x7e\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x46\x61\x63\x65\x62\x6f\ +\x6f\x6b\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x0a\x20\ +\x20\x20\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\ +\x53\x65\x61\x72\x63\x68\x20\x46\x61\x63\x65\x62\x6f\x6f\x6b\x3c\ +\x2f\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x20\ +\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\ +\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x68\ +\x74\x6d\x6c\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\ +\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x66\x61\x63\x65\x62\x6f\ +\x6f\x6b\x2e\x63\x6f\x6d\x2f\x73\x65\x61\x72\x63\x68\x2f\x3f\x73\ +\x72\x63\x3d\x6f\x73\x26\x61\x6d\x70\x3b\x71\x3d\x7b\x73\x65\x61\ +\x72\x63\x68\x54\x65\x72\x6d\x73\x7d\x22\x2f\x3e\x0a\x20\x20\x20\ +\x20\x3c\x49\x6d\x61\x67\x65\x3e\x68\x74\x74\x70\x3a\x2f\x2f\x77\ +\x77\x77\x2e\x66\x61\x63\x65\x62\x6f\x6f\x6b\x2e\x63\x6f\x6d\x2f\ +\x66\x61\x76\x69\x63\x6f\x6e\x2e\x69\x63\x6f\x3c\x2f\x49\x6d\x61\ +\x67\x65\x3e\x0a\x3c\x2f\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\x68\ +\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +\x00\x00\x01\xc9\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x4c\x69\x6e\x75\x78\x2d\ +\x4d\x61\x67\x61\x7a\x69\x6e\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\ +\x6d\x65\x3e\x0a\x20\x20\x20\x20\x3c\x44\x65\x73\x63\x72\x69\x70\ +\x74\x69\x6f\x6e\x3e\x53\x75\x63\x68\x65\x20\x61\x75\x66\x20\x77\ +\x77\x77\x2e\x6c\x69\x6e\x75\x78\x2d\x6d\x61\x67\x61\x7a\x69\x6e\ +\x2e\x64\x65\x3c\x2f\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\ +\x3e\x0a\x20\x20\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\ +\x64\x3d\x22\x67\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\ +\x78\x74\x2f\x68\x74\x6d\x6c\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\ +\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x6c\x69\ +\x6e\x75\x78\x2d\x6d\x61\x67\x61\x7a\x69\x6e\x2e\x64\x65\x2f\x63\ +\x6f\x6e\x74\x65\x6e\x74\x2f\x73\x65\x61\x72\x63\x68\x3f\x53\x65\ +\x61\x72\x63\x68\x54\x65\x78\x74\x3d\x7b\x73\x65\x61\x72\x63\x68\ +\x54\x65\x72\x6d\x73\x7d\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x49\ +\x6d\x61\x67\x65\x3e\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ +\x6c\x69\x6e\x75\x78\x2d\x6d\x61\x67\x61\x7a\x69\x6e\x2e\x64\x65\ +\x2f\x65\x78\x74\x65\x6e\x73\x69\x6f\x6e\x2f\x6c\x6e\x6d\x2f\x64\ +\x65\x73\x69\x67\x6e\x2f\x6c\x69\x6e\x75\x78\x5f\x6d\x61\x67\x61\ +\x7a\x69\x6e\x2f\x69\x6d\x61\x67\x65\x73\x2f\x66\x61\x76\x69\x63\ +\x6f\x6e\x2e\x69\x63\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\x0a\x3c\ +\x2f\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\x68\x44\x65\x73\x63\x72\ +\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +\x00\x00\x01\x95\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x41\x6d\x61\x7a\x6f\x6e\ +\x2e\x63\x6f\x6d\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\ +\x0a\x20\x20\x20\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\ +\x6e\x3e\x41\x6d\x61\x7a\x6f\x6e\x2e\x63\x6f\x6d\x20\x53\x65\x61\ +\x72\x63\x68\x3c\x2f\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\ +\x3e\x0a\x20\x20\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\ +\x64\x3d\x22\x67\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\ +\x78\x74\x2f\x68\x74\x6d\x6c\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\ +\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x61\x6d\ +\x61\x7a\x6f\x6e\x2e\x63\x6f\x6d\x2f\x65\x78\x65\x63\x2f\x6f\x62\ +\x69\x64\x6f\x73\x2f\x65\x78\x74\x65\x72\x6e\x61\x6c\x2d\x73\x65\ +\x61\x72\x63\x68\x2f\x3f\x66\x69\x65\x6c\x64\x2d\x6b\x65\x79\x77\ +\x6f\x72\x64\x73\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\x6d\ +\x73\x7d\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x49\x6d\x61\x67\x65\ +\x3e\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x61\x6d\x61\x7a\ +\x6f\x6e\x2e\x63\x6f\x6d\x2f\x66\x61\x76\x69\x63\x6f\x6e\x2e\x69\ +\x63\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\x0a\x3c\x2f\x4f\x70\x65\ +\x6e\x53\x65\x61\x72\x63\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\ +\x6f\x6e\x3e\x0a\ +\x00\x00\x02\x46\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x47\x6f\x6f\x67\x6c\x65\ +\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x0a\x20\x20\x20\ +\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x47\x6f\ +\x6f\x67\x6c\x65\x20\x57\x65\x62\x20\x53\x65\x61\x72\x63\x68\x3c\ +\x2f\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x20\ +\x20\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\ +\x65\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x68\ +\x74\x6d\x6c\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\ +\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x67\x6f\x6f\x67\x6c\x65\ +\x2e\x63\x6f\x6d\x2f\x73\x65\x61\x72\x63\x68\x3f\x68\x6c\x3d\x7b\ +\x6c\x61\x6e\x67\x75\x61\x67\x65\x7d\x26\x61\x6d\x70\x3b\x6c\x72\ +\x3d\x6c\x61\x6e\x67\x5f\x7b\x6c\x61\x6e\x67\x75\x61\x67\x65\x7d\ +\x26\x61\x6d\x70\x3b\x71\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\ +\x72\x6d\x73\x7d\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x55\x72\x6c\ +\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\x74\x22\x20\x74\x79\ +\x70\x65\x3d\x22\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\ +\x78\x2d\x73\x75\x67\x67\x65\x73\x74\x69\x6f\x6e\x73\x2b\x6a\x73\ +\x6f\x6e\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\ +\x74\x70\x3a\x2f\x2f\x73\x75\x67\x67\x65\x73\x74\x71\x75\x65\x72\ +\x69\x65\x73\x2e\x67\x6f\x6f\x67\x6c\x65\x2e\x63\x6f\x6d\x2f\x63\ +\x6f\x6d\x70\x6c\x65\x74\x65\x2f\x73\x65\x61\x72\x63\x68\x3f\x6f\ +\x75\x74\x70\x75\x74\x3d\x66\x69\x72\x65\x66\x6f\x78\x26\x61\x6d\ +\x70\x3b\x68\x6c\x3d\x7b\x6c\x61\x6e\x67\x75\x61\x67\x65\x7d\x26\ +\x61\x6d\x70\x3b\x71\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\x65\x72\ +\x6d\x73\x7d\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x49\x6d\x61\x67\ +\x65\x3e\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x67\x6f\x6f\ +\x67\x6c\x65\x2e\x63\x6f\x6d\x2f\x66\x61\x76\x69\x63\x6f\x6e\x2e\ +\x69\x63\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\x0a\x3c\x2f\x4f\x70\ +\x65\x6e\x53\x65\x61\x72\x63\x68\x44\x65\x73\x63\x72\x69\x70\x74\ +\x69\x6f\x6e\x3e\x0a\ +\x00\x00\x02\x46\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x57\x69\x6b\x69\x61\x20\ +\x28\x65\x6e\x29\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\ +\x0a\x20\x20\x20\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\ +\x6e\x3e\x57\x69\x6b\x69\x61\x20\x28\x65\x6e\x29\x3c\x2f\x44\x65\ +\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x20\x20\x20\x3c\ +\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\x74\x22\ +\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x68\x74\x6d\x6c\ +\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x69\x6b\x69\x61\x2e\x63\x6f\x6d\ +\x2f\x69\x6e\x64\x65\x78\x2e\x70\x68\x70\x3f\x74\x69\x74\x6c\x65\ +\x3d\x53\x70\x65\x63\x69\x61\x6c\x3a\x53\x65\x61\x72\x63\x68\x26\ +\x61\x6d\x70\x3b\x73\x65\x61\x72\x63\x68\x3d\x7b\x73\x65\x61\x72\ +\x63\x68\x54\x65\x72\x6d\x73\x7d\x22\x2f\x3e\x0a\x20\x20\x20\x20\ +\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\x74\ +\x22\x20\x74\x79\x70\x65\x3d\x22\x61\x70\x70\x6c\x69\x63\x61\x74\ +\x69\x6f\x6e\x2f\x78\x2d\x73\x75\x67\x67\x65\x73\x74\x69\x6f\x6e\ +\x73\x2b\x6a\x73\x6f\x6e\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\ +\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x69\x6b\ +\x69\x61\x2e\x63\x6f\x6d\x2f\x61\x70\x69\x2e\x70\x68\x70\x3f\x61\ +\x63\x74\x69\x6f\x6e\x3d\x6f\x70\x65\x6e\x73\x65\x61\x72\x63\x68\ +\x26\x61\x6d\x70\x3b\x73\x65\x61\x72\x63\x68\x3d\x7b\x73\x65\x61\ +\x72\x63\x68\x54\x65\x72\x6d\x73\x7d\x26\x61\x6d\x70\x3b\x6e\x61\ +\x6d\x65\x73\x70\x61\x63\x65\x3d\x31\x22\x2f\x3e\x0a\x20\x20\x20\ +\x20\x3c\x49\x6d\x61\x67\x65\x3e\x68\x74\x74\x70\x3a\x2f\x2f\x69\ +\x6d\x61\x67\x65\x73\x2e\x77\x69\x6b\x69\x61\x2e\x63\x6f\x6d\x2f\ +\x77\x69\x6b\x69\x61\x67\x6c\x6f\x62\x61\x6c\x2f\x69\x6d\x61\x67\ +\x65\x73\x2f\x36\x2f\x36\x34\x2f\x46\x61\x76\x69\x63\x6f\x6e\x2e\ +\x69\x63\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\x0a\x3c\x2f\x4f\x70\ +\x65\x6e\x53\x65\x61\x72\x63\x68\x44\x65\x73\x63\x72\x69\x70\x74\ +\x69\x6f\x6e\x3e\x0a\ +\x00\x00\x01\x9b\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x57\x69\x6b\x74\x69\x6f\ +\x6e\x61\x72\x79\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\ +\x0a\x20\x20\x20\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\ +\x6e\x3e\x57\x69\x6b\x74\x69\x6f\x6e\x61\x72\x79\x3c\x2f\x44\x65\ +\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x20\x20\x20\x3c\ +\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\x74\x22\ +\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x68\x74\x6d\x6c\ +\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x7b\x63\x6f\x75\x6e\x74\x72\x79\x7d\x2e\x77\x69\x6b\ +\x74\x69\x6f\x6e\x61\x72\x79\x2e\x6f\x72\x67\x2f\x77\x2f\x69\x6e\ +\x64\x65\x78\x2e\x70\x68\x70\x3f\x74\x69\x74\x6c\x65\x3d\x53\x70\ +\x65\x63\x69\x61\x6c\x3a\x53\x65\x61\x72\x63\x68\x26\x61\x6d\x70\ +\x3b\x73\x65\x61\x72\x63\x68\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\ +\x65\x72\x6d\x73\x7d\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x49\x6d\ +\x61\x67\x65\x3e\x68\x74\x74\x70\x3a\x2f\x2f\x65\x6e\x2e\x77\x69\ +\x6b\x74\x69\x6f\x6e\x61\x72\x79\x2e\x6f\x72\x67\x2f\x66\x61\x76\ +\x69\x63\x6f\x6e\x2e\x69\x63\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\ +\x0a\x3c\x2f\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\x68\x44\x65\x73\ +\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +\x00\x00\x02\x5b\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\ +\x68\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x78\x6d\x6c\ +\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x61\x39\x2e\x63\x6f\ +\x6d\x2f\x2d\x2f\x73\x70\x65\x63\x2f\x6f\x70\x65\x6e\x73\x65\x61\ +\x72\x63\x68\x2f\x31\x2e\x31\x2f\x22\x3e\x0a\x20\x20\x20\x20\x3c\ +\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x57\x69\x6b\x69\x70\x65\ +\x64\x69\x61\x3c\x2f\x53\x68\x6f\x72\x74\x4e\x61\x6d\x65\x3e\x0a\ +\x20\x20\x20\x20\x3c\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\ +\x3e\x46\x75\x6c\x6c\x20\x74\x65\x78\x74\x20\x73\x65\x61\x72\x63\ +\x68\x20\x69\x6e\x20\x57\x69\x6b\x69\x70\x65\x64\x69\x61\x3c\x2f\ +\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x20\x20\ +\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\ +\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x68\x74\ +\x6d\x6c\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\x65\x3d\x22\x68\x74\ +\x74\x70\x3a\x2f\x2f\x7b\x63\x6f\x75\x6e\x74\x72\x79\x7d\x2e\x77\ +\x69\x6b\x69\x70\x65\x64\x69\x61\x2e\x6f\x72\x67\x2f\x77\x69\x6b\ +\x69\x2f\x53\x70\x65\x63\x69\x61\x6c\x3a\x53\x65\x61\x72\x63\x68\ +\x3f\x73\x65\x61\x72\x63\x68\x3d\x7b\x73\x65\x61\x72\x63\x68\x54\ +\x65\x72\x6d\x73\x7d\x26\x61\x6d\x70\x3b\x66\x75\x6c\x6c\x74\x65\ +\x78\x74\x3d\x53\x65\x61\x72\x63\x68\x22\x2f\x3e\x0a\x20\x20\x20\ +\x20\x3c\x55\x72\x6c\x20\x6d\x65\x74\x68\x6f\x64\x3d\x22\x67\x65\ +\x74\x22\x20\x74\x79\x70\x65\x3d\x22\x61\x70\x70\x6c\x69\x63\x61\ +\x74\x69\x6f\x6e\x2f\x78\x2d\x73\x75\x67\x67\x65\x73\x74\x69\x6f\ +\x6e\x73\x2b\x6a\x73\x6f\x6e\x22\x20\x74\x65\x6d\x70\x6c\x61\x74\ +\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x7b\x63\x6f\x75\x6e\x74\ +\x72\x79\x7d\x2e\x77\x69\x6b\x69\x70\x65\x64\x69\x61\x2e\x6f\x72\ +\x67\x2f\x77\x2f\x61\x70\x69\x2e\x70\x68\x70\x3f\x61\x63\x74\x69\ +\x6f\x6e\x3d\x6f\x70\x65\x6e\x73\x65\x61\x72\x63\x68\x26\x61\x6d\ +\x70\x3b\x73\x65\x61\x72\x63\x68\x3d\x7b\x73\x65\x61\x72\x63\x68\ +\x54\x65\x72\x6d\x73\x7d\x26\x61\x6d\x70\x3b\x6e\x61\x6d\x65\x73\ +\x70\x61\x63\x65\x3d\x30\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x49\ +\x6d\x61\x67\x65\x3e\x68\x74\x74\x70\x3a\x2f\x2f\x65\x6e\x2e\x77\ +\x69\x6b\x69\x70\x65\x64\x69\x61\x2e\x6f\x72\x67\x2f\x66\x61\x76\ +\x69\x63\x6f\x6e\x2e\x69\x63\x6f\x3c\x2f\x49\x6d\x61\x67\x65\x3e\ +\x0a\x3c\x2f\x4f\x70\x65\x6e\x53\x65\x61\x72\x63\x68\x44\x65\x73\ +\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +" + +qt_resource_name = b"\ +\x00\x08\ +\x00\x4a\x56\x1c\ +\x00\x42\ +\x00\x69\x00\x6e\x00\x67\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x12\ +\x0a\xf9\x0f\x7c\ +\x00\x44\ +\x00\x65\x00\x45\x00\x6e\x00\x5f\x00\x42\x00\x65\x00\x6f\x00\x6c\x00\x69\x00\x6e\x00\x67\x00\x75\x00\x73\x00\x2e\x00\x78\x00\x6d\ +\x00\x6c\ +\x00\x1b\ +\x0d\x52\x43\x5c\ +\x00\x47\ +\x00\x6f\x00\x6f\x00\x67\x00\x6c\x00\x65\x00\x5f\x00\x49\x00\x6d\x00\x5f\x00\x46\x00\x65\x00\x65\x00\x6c\x00\x69\x00\x6e\x00\x67\ +\x00\x5f\x00\x4c\x00\x75\x00\x63\x00\x6b\x00\x79\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x09\ +\x01\xf4\xe3\x3c\ +\x00\x57\ +\x00\x69\x00\x6b\x00\x69\x00\x61\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x09\ +\x0f\x62\xe1\xdc\ +\x00\x59\ +\x00\x61\x00\x68\x00\x6f\x00\x6f\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x0a\ +\x0b\x0c\x48\x7c\ +\x00\x52\ +\x00\x65\x00\x64\x00\x64\x00\x69\x00\x74\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x0e\ +\x00\xf1\x12\x1c\ +\x00\x4c\ +\x00\x45\x00\x4f\x00\x5f\x00\x44\x00\x65\x00\x75\x00\x45\x00\x6e\x00\x67\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x0b\ +\x0b\x48\x8a\x5c\ +\x00\x59\ +\x00\x6f\x00\x75\x00\x54\x00\x75\x00\x62\x00\x65\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x0e\ +\x09\x21\x3a\xfc\ +\x00\x44\ +\x00\x75\x00\x63\x00\x6b\x00\x44\x00\x75\x00\x63\x00\x6b\x00\x47\x00\x6f\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x0c\ +\x0f\xd5\x68\x1c\ +\x00\x46\ +\x00\x61\x00\x63\x00\x65\x00\x62\x00\x6f\x00\x6f\x00\x6b\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x10\ +\x09\x73\x65\x7c\ +\x00\x4c\ +\x00\x69\x00\x6e\x00\x75\x00\x78\x00\x4d\x00\x61\x00\x67\x00\x61\x00\x7a\x00\x69\x00\x6e\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x0d\ +\x0a\x2e\x72\x9c\ +\x00\x41\ +\x00\x6d\x00\x61\x00\x7a\x00\x6f\x00\x6e\x00\x63\x00\x6f\x00\x6d\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x0a\ +\x0e\x31\x93\x9c\ +\x00\x47\ +\x00\x6f\x00\x6f\x00\x67\x00\x6c\x00\x65\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x0c\ +\x0e\x81\x61\xdc\ +\x00\x57\ +\x00\x69\x00\x6b\x00\x69\x00\x61\x00\x5f\x00\x65\x00\x6e\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x0e\ +\x08\xce\x7c\x3c\ +\x00\x57\ +\x00\x69\x00\x6b\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x61\x00\x72\x00\x79\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +\x00\x0d\ +\x06\xf8\x53\x3c\ +\x00\x57\ +\x00\x69\x00\x6b\x00\x69\x00\x70\x00\x65\x00\x64\x00\x69\x00\x61\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\xc6\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xfd\ +\x00\x00\x00\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x06\x07\ +\x00\x00\x01\xe4\x00\x00\x00\x00\x00\x01\x00\x00\x22\x21\ +\x00\x00\x01\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x20\x82\ +\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x00\x10\x04\ +\x00\x00\x01\x44\x00\x00\x00\x00\x00\x01\x00\x00\x18\x88\ +\x00\x00\x01\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x1a\x55\ +\x00\x00\x00\x16\x00\x00\x00\x00\x00\x01\x00\x00\x01\x7d\ +\x00\x00\x00\xac\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x8a\ +\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x0e\x7b\ +\x00\x00\x00\x40\x00\x00\x00\x00\x00\x01\x00\x00\x03\x9f\ +\x00\x00\x01\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xee\ +\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x38\ +\x00\x00\x00\x94\x00\x00\x00\x00\x00\x01\x00\x00\x08\x5f\ +\x00\x00\x01\x26\x00\x00\x00\x00\x00\x01\x00\x00\x17\x06\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/DuckDuckGo.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>DuckDuckGo</ShortName> + <Description>Search DuckDuckGo</Description> + <Url method="get" type="text/html" template="https://duckduckgo.com/?q={searchTerms}"/> + <Url method="get" type="application/x-suggestions+json" template="https://ac.duckduckgo.com/ac/?q={searchTerms}&type=list"/> + <Image>data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAB8lBMVEUAAADkRQzjPwPjQQXkRQ3iPwTiQQXgPQPeQgrcOwPVNgDVNQDWOgbTMwDRMgDQMwDSMwDRNwTQLgDRJgDSJwDSLgDSNwTjOgDiOADjOQDkPADhQAXzs5v+/fv////0vKbiRQvgPQHpdUr85NzuknPdKgDcIwDnZzj2w7HqeU/gPQLsimb/+PftjWn97Obpb0LdJQDeLQDtjmvsi2jgSBDnbULgOQD/39HgLQDeMgDpeFLgSBH0v670uqbaJQD2qImWvP/G1Ob5+/3u//+fvvXyp47dMwDaLwD0u6v0v6/aNQDiXi/aKQD3qozU7/8gSY2vvtg0ZK/OqLDaKQHYKgLgWTfaNADZMgDZMADZLADzqpD7//+xwdz//9H/5Bn/7Bn//ADofADYMADYMQDZOgPXLgDiZDj//97/0AD3tQDvlgHZOgbXLATXMADWMgDfXjLVLQD///z+0AD/3Rn/yRnwnQDcVjbVMQDyv67wuKTSJwDRHQD+8O/tg3/iQQDwhAHnawHWMADvtKfyva7XQxHga0bQGQD2vbH/u8LXIQCmPQzja07XQxLliGn99fPkcVHvhnGZ5VguvUU5wktBwCcAgxzydVv/8/XmiGngdlL+ysi3+I8LtCE80V6P3YmX4sDleljSNQLzr6D7sKPXNQTSIwAEAbMrAAAAF3RSTlMARqSkRvPz80PTpKRG3fPe3hio9/eoGP50jNsAAAABYktHRB5yCiArAAAAyElEQVQYGQXBvUqCYRiA4fu2V9Tn+UQddI3aCpxaOoU6iU4gcqqpoYbALXBuCuoYmttamqJDiEoh4YP+MOi6BNCh+uYKEGiOVNCXXxA2XDVV/UyfKbRCXTLQWAxbP2vt8Ue/uYDvfim91615sb2um6rqtrr/NFb1cUf1Ybd06areU6lSlYpK79jzK1SyJOkfhOl8JGEcqV5zoKrTRqO6yUzIzNu46ijdM1VV9bhuUJ/nZURExLRzUiPQm3kKXHi4BAEGOmOi78A/L1QoU/VHoTsAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTQtMDEtMTlUMjA6MDE6MTEtMDU6MDAuET6cAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE0LTAxLTE5VDIwOjAxOjExLTA1OjAwX0yGIAAAAABJRU5ErkJggg==</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/Facebook.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Facebook</ShortName> + <Description>Search Facebook</Description> + <Url method="get" type="text/html" template="http://www.facebook.com/search/?src=os&q={searchTerms}"/> + <Image>http://www.facebook.com/favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/Google.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Google</ShortName> + <Description>Google Web Search</Description> + <Url method="get" type="text/html" template="http://www.google.com/search?hl={language}&lr=lang_{language}&q={searchTerms}"/> + <Url method="get" type="application/x-suggestions+json" template="http://suggestqueries.google.com/complete/search?output=firefox&hl={language}&q={searchTerms}"/> + <Image>http://www.google.com/favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/Google_Im_Feeling_Lucky.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Google (I'm Feeling Lucky)</ShortName> + <Description>Google Web Search</Description> + <Url method="get" type="text/html" template="http://www.google.com/search?btnI=&hl={language}&lr=lang_{language}&q={searchTerms}"/> + <Url method="get" type="application/x-suggestions+json" template="http://suggestqueries.google.com/complete/search?output=firefox&hl={language}&q={searchTerms}"/> + <Image>http://www.google.com/favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/LEO_DeuEng.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>LEO Deu-Eng</ShortName> + <Description>Deutsch-Englisch Wörterbuch von LEO</Description> + <Url method="get" type="text/html" template="http://dict.leo.org/ende?lang=de&search={searchTerms}"/> + <Url method="get" type="application/x-suggestions+json" template="http://dict.leo.org/dictQuery/m-query/conf/ende/query.conf/strlist.json?q={searchTerms}&sort=PLa&shortQuery&noDescription&noQueryURLs"/> + <Image>http://dict.leo.org/img/favicons/ende.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/LinuxMagazin.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Linux-Magazin</ShortName> + <Description>Suche auf www.linux-magazin.de</Description> + <Url method="get" type="text/html" template="http://www.linux-magazin.de/content/search?SearchText={searchTerms}"/> + <Image>http://www.linux-magazin.de/extension/lnm/design/linux_magazin/images/favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/Reddit.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Reddit</ShortName> + <Description>Reddit Site Search</Description> + <Url method="get" type="text/html" template="http://www.reddit.com/search?q={searchTerms}"/> + <Image>http://www.reddit.com/favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/Wikia.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Wikia</ShortName> + <Description>Wikia Site Search</Description> + <Url method="get" type="text/html" template="http://{country}.wikia.com/index.php?title=Special:Search&search={searchTerms}"/> + <Url method="get" type="application/x-suggestions+json" template="http://{country}.wikia.com/api.php?action=opensearch&search={searchTerms}&namespace=0"/> + <Image>http://images.wikia.com/wikiaglobal/images/6/64/Favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/Wikia_en.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Wikia (en)</ShortName> + <Description>Wikia (en)</Description> + <Url method="get" type="text/html" template="http://www.wikia.com/index.php?title=Special:Search&search={searchTerms}"/> + <Url method="get" type="application/x-suggestions+json" template="http://www.wikia.com/api.php?action=opensearch&search={searchTerms}&namespace=1"/> + <Image>http://images.wikia.com/wikiaglobal/images/6/64/Favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/Wikipedia.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Wikipedia</ShortName> + <Description>Full text search in Wikipedia</Description> + <Url method="get" type="text/html" template="http://{country}.wikipedia.org/wiki/Special:Search?search={searchTerms}&fulltext=Search"/> + <Url method="get" type="application/x-suggestions+json" template="http://{country}.wikipedia.org/w/api.php?action=opensearch&search={searchTerms}&namespace=0"/> + <Image>http://en.wikipedia.org/favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/Wiktionary.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Wiktionary</ShortName> + <Description>Wiktionary</Description> + <Url method="get" type="text/html" template="http://{country}.wiktionary.org/w/index.php?title=Special:Search&search={searchTerms}"/> + <Image>http://en.wiktionary.org/favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/Yahoo.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>Yahoo!</ShortName> + <Description>Yahoo Web Search</Description> + <Url method="get" type="text/html" template="http://search.yahoo.com/search?ei=utf-8&fr=sfp&iscqry=&p={searchTerms}"/> + <Url method="get" type="application/x-suggestions+json" template="http://ff.search.yahoo.com/gossip?output=fxjson&command={searchTerms}"/> + <Image>http://m.www.yahoo.com/favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/YouTube.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>YouTube</ShortName> + <Description>YouTube</Description> + <Url method="get" type="text/html" template="http://www.youtube.com/results?search_query={searchTerms}&search=Search"/> + <Image>http://www.youtube.com/favicon.ico</Image> +</OpenSearchDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/DefaultSearchEngines/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2013 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package conatining the default search engine definitions. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/OpenSearchDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog for the configuration of search engines. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QDialog +from PyQt5.QtCore import pyqtSlot + +from E5Gui import E5MessageBox, E5FileDialog + +from .OpenSearchEngineModel import OpenSearchEngineModel + +from .Ui_OpenSearchDialog import Ui_OpenSearchDialog + + +class OpenSearchDialog(QDialog, Ui_OpenSearchDialog): + """ + Class implementing a dialog for the configuration of search engines. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QWidget) + """ + super(OpenSearchDialog, self).__init__(parent) + self.setupUi(self) + + self.setModal(True) + + self.__mw = parent + + self.__model = \ + OpenSearchEngineModel(self.__mw.openSearchManager(), self) + self.enginesTable.setModel(self.__model) + self.enginesTable.horizontalHeader().resizeSection(0, 200) + self.enginesTable.horizontalHeader().setStretchLastSection(True) + self.enginesTable.verticalHeader().hide() + self.enginesTable.verticalHeader().setDefaultSectionSize( + 1.2 * self.fontMetrics().height()) + + self.enginesTable.selectionModel().selectionChanged.connect( + self.__selectionChanged) + self.editButton.setEnabled(False) + + @pyqtSlot() + def on_addButton_clicked(self): + """ + Private slot to add a new search engine. + """ + fileNames = E5FileDialog.getOpenFileNames( + self, + self.tr("Add search engine"), + "", + self.tr("OpenSearch (*.xml);;All Files (*)")) + + osm = self.__mw.openSearchManager() + for fileName in fileNames: + if not osm.addEngine(fileName): + E5MessageBox.critical( + self, + self.tr("Add search engine"), + self.tr( + """{0} is not a valid OpenSearch 1.1 description or""" + """ is already on your list.""").format(fileName)) + + @pyqtSlot() + def on_deleteButton_clicked(self): + """ + Private slot to delete the selected search engines. + """ + if self.enginesTable.model().rowCount() == 1: + E5MessageBox.critical( + self, + self.tr("Delete selected engines"), + self.tr("""You must have at least one search engine.""")) + + self.enginesTable.removeSelected() + + @pyqtSlot() + def on_restoreButton_clicked(self): + """ + Private slot to restore the default search engines. + """ + self.__mw.openSearchManager().restoreDefaults() + + @pyqtSlot() + def on_editButton_clicked(self): + """ + Private slot to edit the data of the current search engine. + """ + from .OpenSearchEditDialog import OpenSearchEditDialog + + rows = self.enginesTable.selectionModel().selectedRows() + if len(rows) == 0: + row = self.enginesTable.selectionModel().currentIndex().row() + else: + row = rows[0].row() + + osm = self.__mw.openSearchManager() + engineName = osm.allEnginesNames()[row] + engine = osm.engine(engineName) + dlg = OpenSearchEditDialog(engine, self) + if dlg.exec_() == QDialog.Accepted: + osm.enginesChanged() + + def __selectionChanged(self, selected, deselected): + """ + Private slot to handle a change of the selection. + + @param selected item selection of selected items (QItemSelection) + @param deselected item selection of deselected items (QItemSelection) + """ + self.editButton.setEnabled( + len(self.enginesTable.selectionModel().selectedRows()) <= 1)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/OpenSearchDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,164 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>OpenSearchDialog</class> + <widget class="QDialog" name="OpenSearchDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>350</height> + </rect> + </property> + <property name="windowTitle"> + <string>Open Search Engines Configuration</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" rowspan="5"> + <widget class="E5TableView" name="enginesTable"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="showGrid"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="addButton"> + <property name="toolTip"> + <string>Press to add a new search engine from file</string> + </property> + <property name="text"> + <string>&Add...</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="deleteButton"> + <property name="toolTip"> + <string>Press to delete the selected engines</string> + </property> + <property name="text"> + <string>&Delete</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QPushButton" name="editButton"> + <property name="toolTip"> + <string>Press to edit the data of the current engine</string> + </property> + <property name="text"> + <string>Edit...</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QPushButton" name="restoreButton"> + <property name="toolTip"> + <string>Press to restore the default engines</string> + </property> + <property name="text"> + <string>&Restore Defaults</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="4" column="1"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>38</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5TableView</class> + <extends>QTableView</extends> + <header>E5Gui/E5TableView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>enginesTable</tabstop> + <tabstop>addButton</tabstop> + <tabstop>deleteButton</tabstop> + <tabstop>editButton</tabstop> + <tabstop>restoreButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>OpenSearchDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>OpenSearchDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/OpenSearchEditDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to edit the data of a search engine. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QDialog + +from .Ui_OpenSearchEditDialog import Ui_OpenSearchEditDialog + + +class OpenSearchEditDialog(QDialog, Ui_OpenSearchEditDialog): + """ + Class implementing a dialog to edit the data of a search engine. + """ + def __init__(self, engine, parent=None): + """ + Constructor + + @param engine reference to the search engine (OpenSearchEngine) + @param parent reference to the parent object (QWidget) + """ + super(OpenSearchEditDialog, self).__init__(parent) + self.setupUi(self) + + self.__engine = engine + + self.nameEdit.setText(engine.name()) + self.descriptionEdit.setText(engine.description()) + self.imageEdit.setText(engine.imageUrl()) + self.searchEdit.setText(engine.searchUrlTemplate()) + self.suggestionsEdit.setText(engine.suggestionsUrlTemplate()) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + def accept(self): + """ + Public slot to accept the data entered. + """ + self.__engine.setName(self.nameEdit.text()) + self.__engine.setDescription(self.descriptionEdit.text()) + self.__engine.setImageUrlAndLoad(self.imageEdit.text()) + self.__engine.setSearchUrlTemplate(self.searchEdit.text()) + self.__engine.setSuggestionsUrlTemplate(self.suggestionsEdit.text()) + + super(OpenSearchEditDialog, self).accept()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/OpenSearchEditDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>OpenSearchEditDialog</class> + <widget class="QDialog" name="OpenSearchEditDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>690</width> + <height>218</height> + </rect> + </property> + <property name="windowTitle"> + <string>Edit search engine data</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>&Name:</string> + </property> + <property name="buddy"> + <cstring>nameEdit</cstring> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="nameEdit"> + <property name="focusPolicy"> + <enum>Qt::NoFocus</enum> + </property> + <property name="toolTip"> + <string>Shows the name of the search engine</string> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>&Description:</string> + </property> + <property name="buddy"> + <cstring>descriptionEdit</cstring> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="descriptionEdit"> + <property name="toolTip"> + <string>Enter a description</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>&Image URL:</string> + </property> + <property name="buddy"> + <cstring>imageEdit</cstring> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="imageEdit"> + <property name="toolTip"> + <string>Enter the URL of the image</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>&Search URL Template:</string> + </property> + <property name="buddy"> + <cstring>searchEdit</cstring> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="searchEdit"> + <property name="toolTip"> + <string>Enter the template of the search URL</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Su&ggestions URL Template:</string> + </property> + <property name="buddy"> + <cstring>suggestionsEdit</cstring> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="suggestionsEdit"> + <property name="toolTip"> + <string>Enter the template of the suggestions URL</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>nameEdit</tabstop> + <tabstop>descriptionEdit</tabstop> + <tabstop>imageEdit</tabstop> + <tabstop>searchEdit</tabstop> + <tabstop>suggestionsEdit</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>OpenSearchEditDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>222</x> + <y>232</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>246</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>OpenSearchEditDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>290</x> + <y>238</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>246</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/OpenSearchEngine.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,524 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the open search engine. +""" + +from __future__ import unicode_literals + +import re +import json + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QLocale, QUrl, QUrlQuery, \ + QByteArray, QBuffer, QIODevice, QObject +from PyQt5.QtGui import QImage +from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager, \ + QNetworkReply + +from UI.Info import Program + +import Preferences +import Utilities + + +class OpenSearchEngine(QObject): + """ + Class implementing the open search engine. + + @signal imageChanged() emitted after the icon has been changed + @signal suggestions(list of strings) emitted after the suggestions have + been received + """ + imageChanged = pyqtSignal() + suggestions = pyqtSignal(list) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(OpenSearchEngine, self).__init__(parent) + + self.__suggestionsReply = None + self.__networkAccessManager = None + self._name = "" + self._description = "" + self._searchUrlTemplate = "" + self._suggestionsUrlTemplate = "" + self._searchParameters = [] # list of two tuples + self._suggestionsParameters = [] # list of two tuples + self._imageUrl = "" + self.__image = QImage() + self.__iconMoved = False + self.__searchMethod = "get" + self.__suggestionsMethod = "get" + self.__requestMethods = { + "get": QNetworkAccessManager.GetOperation, + "post": QNetworkAccessManager.PostOperation, + } + + self.__replies = [] + + @classmethod + def parseTemplate(cls, searchTerm, searchTemplate): + """ + Class method to parse a search template. + + @param searchTerm term to search for (string) + @param searchTemplate template to be parsed (string) + @return parsed template (string) + """ + locale = QLocale(Preferences.getWebBrowser("SearchLanguage")) + language = locale.name().replace("_", "-") + country = locale.name().split("_")[0].lower() + + result = searchTemplate + result = result.replace("{count}", "20") + result = result.replace("{startIndex}", "0") + result = result.replace("{startPage}", "0") + result = result.replace("{language}", language) + result = result.replace("{country}", country) + result = result.replace("{inputEncoding}", "UTF-8") + result = result.replace("{outputEncoding}", "UTF-8") + result = result.replace( + "{searchTerms}", + bytes(QUrl.toPercentEncoding(searchTerm)).decode()) + result = re.sub(r"""\{([^\}]*:|)source\??\}""", Program, result) + + return result + + @pyqtSlot(result=str) + def name(self): + """ + Public method to get the name of the engine. + + @return name of the engine (string) + """ + return self._name + + def setName(self, name): + """ + Public method to set the engine name. + + @param name name of the engine (string) + """ + self._name = name + + def description(self): + """ + Public method to get the description of the engine. + + @return description of the engine (string) + """ + return self._description + + def setDescription(self, description): + """ + Public method to set the engine description. + + @param description description of the engine (string) + """ + self._description = description + + def searchUrlTemplate(self): + """ + Public method to get the search URL template of the engine. + + @return search URL template of the engine (string) + """ + return self._searchUrlTemplate + + def setSearchUrlTemplate(self, searchUrlTemplate): + """ + Public method to set the engine search URL template. + + The URL template is processed according to the specification: + <a + href="http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_URL_template_syntax"> + http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_URL_template_syntax</a> + + A list of template parameters currently supported and what they are + replaced with: + <table> + <tr><td><b>Parameter</b></td><td><b>Value</b></td></tr> + <tr><td>{count}</td><td>20</td></tr> + <tr><td>{startIndex}</td><td>0</td></tr> + <tr><td>{startPage}</td><td>0</td></tr> + <tr><td>{language}</td> + <td>the default language code (RFC 3066)</td></tr> + <tr><td>{country}</td> + <td>the default country code (first part of language)</td></tr> + <tr><td>{inputEncoding}</td><td>UTF-8</td></tr> + <tr><td>{outputEncoding}</td><td>UTF-8</td></tr> + <tr><td>{searchTerms}</td><td>the string supplied by the user</td></tr> + <tr><td>{*:source}</td> + <td>application name, QCoreApplication::applicationName()</td></tr> + </table> + + @param searchUrlTemplate search URL template of the engine (string) + """ + self._searchUrlTemplate = searchUrlTemplate + + def searchUrl(self, searchTerm): + """ + Public method to get a URL ready for searching. + + @param searchTerm term to search for (string) + @return URL (QUrl) + """ + if not self._searchUrlTemplate: + return QUrl() + + ret = QUrl.fromEncoded( + self.parseTemplate(searchTerm, self._searchUrlTemplate) + .encode("utf-8")) + + if self.__searchMethod != "post": + urlQuery = QUrlQuery(ret) + for parameter in self._searchParameters: + urlQuery.addQueryItem( + parameter[0], + self.parseTemplate(searchTerm, parameter[1])) + ret.setQuery(urlQuery) + + return ret + + def providesSuggestions(self): + """ + Public method to check, if the engine provides suggestions. + + @return flag indicating suggestions are provided (boolean) + """ + return self._suggestionsUrlTemplate != "" + + def suggestionsUrlTemplate(self): + """ + Public method to get the search URL template of the engine. + + @return search URL template of the engine (string) + """ + return self._suggestionsUrlTemplate + + def setSuggestionsUrlTemplate(self, suggestionsUrlTemplate): + """ + Public method to set the engine suggestions URL template. + + @param suggestionsUrlTemplate suggestions URL template of the + engine (string) + """ + self._suggestionsUrlTemplate = suggestionsUrlTemplate + + def suggestionsUrl(self, searchTerm): + """ + Public method to get a URL ready for suggestions. + + @param searchTerm term to search for (string) + @return URL (QUrl) + """ + if not self._suggestionsUrlTemplate: + return QUrl() + + ret = QUrl.fromEncoded(QByteArray(self.parseTemplate( + searchTerm, self._suggestionsUrlTemplate).encode("utf-8"))) + + if self.__searchMethod != "post": + urlQuery = QUrlQuery(ret) + for parameter in self._suggestionsParameters: + urlQuery.addQueryItem( + parameter[0], + self.parseTemplate(searchTerm, parameter[1])) + ret.setQuery(urlQuery) + + return ret + + def searchParameters(self): + """ + Public method to get the search parameters of the engine. + + @return search parameters of the engine (list of two tuples) + """ + return self._searchParameters[:] + + def setSearchParameters(self, searchParameters): + """ + Public method to set the engine search parameters. + + @param searchParameters search parameters of the engine + (list of two tuples) + """ + self._searchParameters = searchParameters[:] + + def suggestionsParameters(self): + """ + Public method to get the suggestions parameters of the engine. + + @return suggestions parameters of the engine (list of two tuples) + """ + return self._suggestionsParameters[:] + + def setSuggestionsParameters(self, suggestionsParameters): + """ + Public method to set the engine suggestions parameters. + + @param suggestionsParameters suggestions parameters of the + engine (list of two tuples) + """ + self._suggestionsParameters = suggestionsParameters[:] + + def searchMethod(self): + """ + Public method to get the HTTP request method used to perform search + requests. + + @return HTTP request method (string) + """ + return self.__searchMethod + + def setSearchMethod(self, method): + """ + Public method to set the HTTP request method used to perform search + requests. + + @param method HTTP request method (string) + """ + requestMethod = method.lower() + if requestMethod not in self.__requestMethods: + return + + self.__searchMethod = requestMethod + + def suggestionsMethod(self): + """ + Public method to get the HTTP request method used to perform + suggestions requests. + + @return HTTP request method (string) + """ + return self.__suggestionsMethod + + def setSuggestionsMethod(self, method): + """ + Public method to set the HTTP request method used to perform + suggestions requests. + + @param method HTTP request method (string) + """ + requestMethod = method.lower() + if requestMethod not in self.__requestMethods: + return + + self.__suggestionsMethod = requestMethod + + def imageUrl(self): + """ + Public method to get the image URL of the engine. + + @return image URL of the engine (string) + """ + return self._imageUrl + + def setImageUrl(self, imageUrl): + """ + Public method to set the engine image URL. + + @param imageUrl image URL of the engine (string) + """ + self._imageUrl = imageUrl + + def setImageUrlAndLoad(self, imageUrl): + """ + Public method to set the engine image URL. + + @param imageUrl image URL of the engine (string) + """ + self.setImageUrl(imageUrl) + self.__iconMoved = False + self.loadImage() + + def loadImage(self): + """ + Public method to load the image of the engine. + """ + if self.__networkAccessManager is None or not self._imageUrl: + return + + reply = self.__networkAccessManager.get( + QNetworkRequest(QUrl.fromEncoded(self._imageUrl.encode("utf-8")))) + reply.finished.connect(self.__imageObtained) + self.__replies.append(reply) + + def __imageObtained(self): + """ + Private slot to receive the image of the engine. + """ + reply = self.sender() + if reply is None: + return + + response = reply.readAll() + + reply.close() + if reply in self.__replies: + self.__replies.remove(reply) + reply.deleteLater() + + if response.isEmpty(): + return + + if response.startsWith(b"<html>") or response.startsWith(b"HTML"): + self.__iconMoved = True + self.__image = QImage() + else: + self.__image.loadFromData(response) + self.imageChanged.emit() + + def image(self): + """ + Public method to get the image of the engine. + + @return image of the engine (QImage) + """ + if not self.__iconMoved and self.__image.isNull(): + self.loadImage() + + return self.__image + + def setImage(self, image): + """ + Public method to set the image of the engine. + + @param image image to be set (QImage) + """ + if not self._imageUrl: + imageBuffer = QBuffer() + imageBuffer.open(QIODevice.ReadWrite) + if image.save(imageBuffer, "PNG"): + self._imageUrl = "data:image/png;base64,{0}".format( + bytes(imageBuffer.buffer().toBase64()).decode()) + + self.__image = QImage(image) + self.imageChanged.emit() + + def isValid(self): + """ + Public method to check, if the engine is valid. + + @return flag indicating validity (boolean) + """ + return self._name and self._searchUrlTemplate + + def __eq__(self, other): + """ + Special method implementing the == operator. + + @param other reference to an open search engine (OpenSearchEngine) + @return flag indicating equality (boolean) + """ + if not isinstance(other, OpenSearchEngine): + return NotImplemented + + return self._name == other._name and \ + self._description == other._description and \ + self._imageUrl == other._imageUrl and \ + self._searchUrlTemplate == other._searchUrlTemplate and \ + self._suggestionsUrlTemplate == other._suggestionsUrlTemplate and \ + self._searchParameters == other._searchParameters and \ + self._suggestionsParameters == other._suggestionsParameters + + def __lt__(self, other): + """ + Special method implementing the < operator. + + @param other reference to an open search engine (OpenSearchEngine) + @return flag indicating less than (boolean) + """ + if not isinstance(other, OpenSearchEngine): + return NotImplemented + + return self._name < other._name + + def requestSuggestions(self, searchTerm): + """ + Public method to request suggestions. + + @param searchTerm term to get suggestions for (string) + """ + if not searchTerm or not self.providesSuggestions(): + return + + if self.__networkAccessManager is None: + return + + if self.__suggestionsReply is not None: + self.__suggestionsReply.finished.disconnect( + self.__suggestionsObtained) + self.__suggestionsReply.abort() + self.__suggestionsReply.deleteLater() + self.__suggestionsReply = None + + if self.__suggestionsMethod not in self.__requestMethods: + # ignore + return + + if self.__suggestionsMethod == "get": + self.__suggestionsReply = self.networkAccessManager().get( + QNetworkRequest(self.suggestionsUrl(searchTerm))) + else: + parameters = [] + for parameter in self._suggestionsParameters: + parameters.append(parameter[0] + "=" + parameter[1]) + data = "&".join(parameters) + self.__suggestionsReply = self.networkAccessManager().post( + QNetworkRequest(self.suggestionsUrl(searchTerm)), data) + self.__suggestionsReply.finished.connect( + self.__suggestionsObtained) + + def __suggestionsObtained(self): + """ + Private slot to receive the suggestions. + """ + if self.__suggestionsReply.error() == QNetworkReply.NoError: + buffer = bytes(self.__suggestionsReply.readAll()) + response = Utilities.decodeBytes(buffer) + response = response.strip() + + self.__suggestionsReply.close() + self.__suggestionsReply.deleteLater() + self.__suggestionsReply = None + + if len(response) == 0: + return + + try: + result = json.loads(response) + except ValueError: + return + + try: + suggestions = result[1] + except IndexError: + return + + self.suggestions.emit(suggestions) + + def networkAccessManager(self): + """ + Public method to get a reference to the network access manager object. + + @return reference to the network access manager object + (QNetworkAccessManager) + """ + return self.__networkAccessManager + + def setNetworkAccessManager(self, networkAccessManager): + """ + Public method to set the reference to the network access manager. + + @param networkAccessManager reference to the network access manager + object (QNetworkAccessManager) + """ + self.__networkAccessManager = networkAccessManager
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/OpenSearchEngineAction.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a QAction subclass for open search. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QPixmap, QIcon +from PyQt5.QtWidgets import QAction + + +class OpenSearchEngineAction(QAction): + """ + Class implementing a QAction subclass for open search. + """ + def __init__(self, engine, parent=None): + """ + Constructor + + @param engine reference to the open search engine object + (OpenSearchEngine) + @param parent reference to the parent object (QObject) + """ + super(OpenSearchEngineAction, self).__init__(parent) + + self.__engine = engine + if self.__engine.networkAccessManager() is None: + import WebBrowser.WebBrowserWindow + self.__engine.setNetworkAccessManager( + WebBrowser.WebBrowserWindow.WebBrowserWindow.networkManager()) + + self.setText(engine.name()) + self.__imageChanged() + + engine.imageChanged.connect(self.__imageChanged) + + def __imageChanged(self): + """ + Private slot handling a change of the associated image. + """ + image = self.__engine.image() + if image.isNull(): + import WebBrowser.WebBrowserWindow + self.setIcon( + WebBrowser.WebBrowserWindow.WebBrowserWindow.icon( + QUrl(self.__engine.imageUrl()))) + else: + self.setIcon(QIcon(QPixmap.fromImage(image)))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/OpenSearchEngineModel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a model for search engines. +""" + +from __future__ import unicode_literals + +import re + +from PyQt5.QtCore import Qt, QUrl, QAbstractTableModel, QModelIndex +from PyQt5.QtGui import QPixmap, QIcon + + +class OpenSearchEngineModel(QAbstractTableModel): + """ + Class implementing a model for search engines. + """ + def __init__(self, manager, parent=None): + """ + Constructor + + @param manager reference to the search engine manager + (OpenSearchManager) + @param parent reference to the parent object (QObject) + """ + super(OpenSearchEngineModel, self).__init__(parent) + + self.__manager = manager + manager.changed.connect(self.__enginesChanged) + + self.__headers = [ + self.tr("Name"), + self.tr("Keywords"), + ] + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove entries from the model. + + @param row start row (integer) + @param count number of rows to remove (integer) + @param parent parent index (QModelIndex) + @return flag indicating success (boolean) + """ + if parent.isValid(): + return False + + if count <= 0: + return False + + if self.rowCount() <= 1: + return False + + lastRow = row + count - 1 + + self.beginRemoveRows(parent, row, lastRow) + + nameList = self.__manager.allEnginesNames() + for index in range(row, lastRow + 1): + self.__manager.removeEngine(nameList[index]) + + # removeEngine emits changed() + #self.endRemoveRows() + + return True + + def rowCount(self, parent=QModelIndex()): + """ + Public method to get the number of rows of the model. + + @param parent parent index (QModelIndex) + @return number of rows (integer) + """ + if parent.isValid(): + return 0 + else: + return self.__manager.enginesCount() + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns of the model. + + @param parent parent index (QModelIndex) + @return number of columns (integer) + """ + return 2 + + def flags(self, index): + """ + Public method to get flags for a model cell. + + @param index index of the model cell (QModelIndex) + @return flags (Qt.ItemFlags) + """ + if index.column() == 1: + return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable + else: + return Qt.ItemIsEnabled | Qt.ItemIsSelectable + + def data(self, index, role): + """ + Public method to get data from the model. + + @param index index to get data for (QModelIndex) + @param role role of the data to retrieve (integer) + @return requested data + """ + if index.row() >= self.__manager.enginesCount() or index.row() < 0: + return None + + engine = self.__manager.engine( + self.__manager.allEnginesNames()[index.row()]) + + if engine is None: + return None + + if index.column() == 0: + if role == Qt.DisplayRole: + return engine.name() + + elif role == Qt.DecorationRole: + image = engine.image() + if image.isNull(): + from WebBrowser.WebBrowserWindow import WebBrowserWindow + icon = WebBrowserWindow.icon(QUrl(engine.imageUrl())) + else: + icon = QIcon(QPixmap.fromImage(image)) + return icon + + elif role == Qt.ToolTipRole: + description = self.tr("<strong>Description:</strong> {0}")\ + .format(engine.description()) + if engine.providesSuggestions(): + description += "<br/>" + description += self.tr( + "<strong>Provides contextual suggestions</strong>") + + return description + elif index.column() == 1: + if role in [Qt.EditRole, Qt.DisplayRole]: + return ",".join(self.__manager.keywordsForEngine(engine)) + elif role == Qt.ToolTipRole: + return self.tr( + "Comma-separated list of keywords that may" + " be entered in the location bar followed by search terms" + " to search with this engine") + + return None + + def setData(self, index, value, role=Qt.EditRole): + """ + Public method to set the data of a model cell. + + @param index index of the model cell (QModelIndex) + @param value value to be set + @param role role of the data (integer) + @return flag indicating success (boolean) + """ + if not index.isValid() or index.column() != 1: + return False + + if index.row() >= self.rowCount() or index.row() < 0: + return False + + if role != Qt.EditRole: + return False + + engineName = self.__manager.allEnginesNames()[index.row()] + keywords = re.split("[ ,]+", value) + self.__manager.setKeywordsForEngine( + self.__manager.engine(engineName), keywords) + + return True + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Public method to get the header data. + + @param section section number (integer) + @param orientation header orientation (Qt.Orientation) + @param role data role (integer) + @return header data + """ + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + try: + return self.__headers[section] + except IndexError: + pass + + return None + + def __enginesChanged(self): + """ + Private slot handling a change of the registered engines. + """ + self.beginResetModel() + self.endResetModel()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/OpenSearchManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,598 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a manager for open search engines. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QFile, QDir, QIODevice, \ + QUrlQuery +from PyQt5.QtWidgets import QLineEdit, QInputDialog +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply + +from E5Gui.E5Application import e5App +from E5Gui import E5MessageBox + +from Utilities.AutoSaver import AutoSaver +import Utilities +import Preferences + + +class OpenSearchManager(QObject): + """ + Class implementing a manager for open search engines. + + @signal changed() emitted to indicate a change + @signal currentEngineChanged() emitted to indicate a change of + the current search engine + """ + changed = pyqtSignal() + currentEngineChanged = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + if parent is None: + parent = e5App() + super(OpenSearchManager, self).__init__(parent) + + self.__replies = [] + self.__engines = {} + self.__keywords = {} + self.__current = "" + self.__loading = False + self.__saveTimer = AutoSaver(self, self.save) + + self.changed.connect(self.__saveTimer.changeOccurred) + + self.load() + + def close(self): + """ + Public method to close the open search engines manager. + """ + self.__saveTimer.saveIfNeccessary() + + def currentEngineName(self): + """ + Public method to get the name of the current search engine. + + @return name of the current search engine (string) + """ + return self.__current + + def setCurrentEngineName(self, name): + """ + Public method to set the current engine by name. + + @param name name of the new current engine (string) + """ + if name not in self.__engines: + return + + self.__current = name + self.currentEngineChanged.emit() + self.changed.emit() + + def currentEngine(self): + """ + Public method to get a reference to the current engine. + + @return reference to the current engine (OpenSearchEngine) + """ + if not self.__current or self.__current not in self.__engines: + return None + + return self.__engines[self.__current] + + def setCurrentEngine(self, engine): + """ + Public method to set the current engine. + + @param engine reference to the new current engine (OpenSearchEngine) + """ + if engine is None: + return + + for engineName in self.__engines: + if self.__engines[engineName] == engine: + self.setCurrentEngineName(engineName) + break + + def engine(self, name): + """ + Public method to get a reference to the named engine. + + @param name name of the engine (string) + @return reference to the engine (OpenSearchEngine) + """ + if name not in self.__engines: + return None + + return self.__engines[name] + + def engineExists(self, name): + """ + Public method to check, if an engine exists. + + @param name name of the engine (string) + @return flag indicating an existing engine (boolean) + """ + return name in self.__engines + + def allEnginesNames(self): + """ + Public method to get a list of all engine names. + + @return sorted list of all engine names (list of strings) + """ + return sorted(self.__engines.keys()) + + def enginesCount(self): + """ + Public method to get the number of available engines. + + @return number of engines (integer) + """ + return len(self.__engines) + + def addEngine(self, engine): + """ + Public method to add a new search engine. + + @param engine URL of the engine definition file (QUrl) or + name of a file containing the engine definition (string) + or reference to an engine object (OpenSearchEngine) + @return flag indicating success (boolean) + """ + from .OpenSearchEngine import OpenSearchEngine + if isinstance(engine, QUrl): + return self.__addEngineByUrl(engine) + elif isinstance(engine, OpenSearchEngine): + return self.__addEngineByEngine(engine) + else: + return self.__addEngineByFile(engine) + + def __addEngineByUrl(self, url): + """ + Private method to add a new search engine given its URL. + + @param url URL of the engine definition file (QUrl) + @return flag indicating success (boolean) + """ + if not url.isValid(): + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + + reply = WebBrowserWindow.networkManager().get(QNetworkRequest(url)) + reply.finished.connect(self.__engineFromUrlAvailable) + reply.setParent(self) + self.__replies.append(reply) + + return True + + def __addEngineByFile(self, filename): + """ + Private method to add a new search engine given a filename. + + @param filename name of a file containing the engine definition + (string) + @return flag indicating success (boolean) + """ + file_ = QFile(filename) + if not file_.open(QIODevice.ReadOnly): + return False + + from .OpenSearchReader import OpenSearchReader + reader = OpenSearchReader() + engine = reader.read(file_) + + if not self.__addEngineByEngine(engine): + return False + + return True + + def __addEngineByEngine(self, engine): + """ + Private method to add a new search engine given a reference to an + engine. + + @param engine reference to an engine object (OpenSearchEngine) + @return flag indicating success (boolean) + """ + if engine is None: + return False + + if not engine.isValid(): + return False + + if engine.name() in self.__engines: + return False + + engine.setParent(self) + self.__engines[engine.name()] = engine + + self.changed.emit() + + return True + + def addEngineFromForm(self, res, view): + """ + Private method to add a new search engine from a form. + + @param res result of the JavaScript run on by + WebBrowserView.__addSearchEngine() + @type dict or None + @param view reference to the web browser view + @type WebBrowserView + """ + if not res: + return + + method = res["method"] + actionUrl = QUrl(res["action"]) + inputName = res["inputName"] + + if method != "get": + E5MessageBox.warning( + self, + self.tr("Method not supported"), + self.tr( + """{0} method is not supported.""").format(method.upper())) + return + + if actionUrl.isRelative(): + actionUrl = view.url().resolved(actionUrl) + + searchUrlQuery = QUrlQuery(actionUrl) + searchUrlQuery.addQueryItem(inputName, "{searchTerms}") + + inputFields = res["inputs"] + for inputField in inputFields: + name = inputField[0] + value = inputField[1] + + if not name or name == inputName or not value: + continue + + searchUrlQuery.addQueryItem(name, value) + + engineName, ok = QInputDialog.getText( + view, + self.tr("Engine name"), + self.tr("Enter a name for the engine"), + QLineEdit.Normal) + if not ok: + return + + actionUrl.setQuery(searchUrlQuery) + + from .OpenSearchEngine import OpenSearchEngine + engine = OpenSearchEngine() + engine.setName(engineName) + engine.setDescription(engineName) + engine.setSearchUrlTemplate( + actionUrl.toDisplayString(QUrl.FullyDecoded)) + engine.setImage(view.icon().pixmap(16, 16).toImage()) + + self.__addEngineByEngine(engine) + + def removeEngine(self, name): + """ + Public method to remove an engine. + + @param name name of the engine (string) + """ + if len(self.__engines) <= 1: + return + + if name not in self.__engines: + return + + engine = self.__engines[name] + for keyword in [k for k in self.__keywords + if self.__keywords[k] == engine]: + del self.__keywords[keyword] + del self.__engines[name] + + file_ = QDir(self.enginesDirectory()).filePath( + self.generateEngineFileName(name)) + QFile.remove(file_) + + if name == self.__current: + self.setCurrentEngineName(list(self.__engines.keys())[0]) + + self.changed.emit() + + def generateEngineFileName(self, engineName): + """ + Public method to generate a valid engine file name. + + @param engineName name of the engine (string) + @return valid engine file name (string) + """ + fileName = "" + + # strip special characters + for c in engineName: + if c.isspace(): + fileName += '_' + continue + + if c.isalnum(): + fileName += c + + fileName += ".xml" + + return fileName + + def saveDirectory(self, dirName): + """ + Public method to save the search engine definitions to files. + + @param dirName name of the directory to write the files to (string) + """ + dir = QDir() + if not dir.mkpath(dirName): + return + dir.setPath(dirName) + + from .OpenSearchWriter import OpenSearchWriter + writer = OpenSearchWriter() + + for engine in list(self.__engines.values()): + name = self.generateEngineFileName(engine.name()) + fileName = dir.filePath(name) + + file = QFile(fileName) + if not file.open(QIODevice.WriteOnly): + continue + + writer.write(file, engine) + + def save(self): + """ + Public method to save the search engines configuration. + """ + if self.__loading: + return + + self.saveDirectory(self.enginesDirectory()) + + Preferences.setWebBrowser("WebSearchEngine", self.__current) + keywords = [] + for k in self.__keywords: + if self.__keywords[k]: + keywords.append((k, self.__keywords[k].name())) + Preferences.setWebBrowser("WebSearchKeywords", keywords) + + def loadDirectory(self, dirName): + """ + Public method to load the search engine definitions from files. + + @param dirName name of the directory to load the files from (string) + @return flag indicating success (boolean) + """ + if not QFile.exists(dirName): + return False + + success = False + + dir = QDir(dirName) + for name in dir.entryList(["*.xml"]): + fileName = dir.filePath(name) + if self.__addEngineByFile(fileName): + success = True + + return success + + def load(self): + """ + Public method to load the search engines configuration. + """ + self.__loading = True + self.__current = Preferences.getWebBrowser("WebSearchEngine") + keywords = Preferences.getWebBrowser("WebSearchKeywords") + + if not self.loadDirectory(self.enginesDirectory()): + self.restoreDefaults() + + for keyword, engineName in keywords: + self.__keywords[keyword] = self.engine(engineName) + + if self.__current not in self.__engines and \ + len(self.__engines) > 0: + self.__current = list(self.__engines.keys())[0] + + self.__loading = False + self.currentEngineChanged.emit() + + def restoreDefaults(self): + """ + Public method to restore the default search engines. + """ + from .OpenSearchReader import OpenSearchReader + from .DefaultSearchEngines import DefaultSearchEngines_rc # __IGNORE_WARNING__ + + defaultEngineFiles = ["Amazoncom.xml", "Bing.xml", + "DeEn_Beolingus.xml", "DuckDuckGo.xml", + "Facebook.xml", "Google.xml", + "Google_Im_Feeling_Lucky.xml", "LEO_DeuEng.xml", + "LinuxMagazin.xml", "Reddit.xml", "Wikia.xml", + "Wikia_en.xml", "Wikipedia.xml", + "Wiktionary.xml", "Yahoo.xml", "YouTube.xml", ] + # Keep this list in sync with the contents of the resource file. + + reader = OpenSearchReader() + for engineFileName in defaultEngineFiles: + engineFile = QFile(":/" + engineFileName) + if not engineFile.open(QIODevice.ReadOnly): + continue + engine = reader.read(engineFile) + self.__addEngineByEngine(engine) + + def enginesDirectory(self): + """ + Public method to determine the directory containing the search engine + descriptions. + + @return directory name (string) + """ + return os.path.join( + Utilities.getConfigDir(), "web_browser", "searchengines") + + def __confirmAddition(self, engine): + """ + Private method to confirm the addition of a new search engine. + + @param engine reference to the engine to be added (OpenSearchEngine) + @return flag indicating the engine shall be added (boolean) + """ + if engine is None or not engine.isValid(): + return False + + host = QUrl(engine.searchUrlTemplate()).host() + + res = E5MessageBox.yesNo( + None, + "", + self.tr( + """<p>Do you want to add the following engine to your""" + """ list of search engines?<br/><br/>Name: {0}<br/>""" + """Searches on: {1}</p>""").format(engine.name(), host)) + return res + + def __engineFromUrlAvailable(self): + """ + Private slot to add a search engine from the net. + """ + reply = self.sender() + if reply is None: + return + + if reply.error() != QNetworkReply.NoError: + reply.close() + if reply in self.__replies: + self.__replies.remove(reply) + return + + from .OpenSearchReader import OpenSearchReader + reader = OpenSearchReader() + engine = reader.read(reply) + + reply.close() + if reply in self.__replies: + self.__replies.remove(reply) + + if not engine.isValid(): + return + + if self.engineExists(engine.name()): + return + + if not self.__confirmAddition(engine): + return + + if not self.__addEngineByEngine(engine): + return + + def convertKeywordSearchToUrl(self, keywordSearch): + """ + Public method to get the search URL for a keyword search. + + @param keywordSearch search string for keyword search (string) + @return search URL (QUrl) + """ + try: + keyword, term = keywordSearch.split(" ", 1) + except ValueError: + return QUrl() + + if not term: + return QUrl() + + engine = self.engineForKeyword(keyword) + if engine: + return engine.searchUrl(term) + + return QUrl() + + def engineForKeyword(self, keyword): + """ + Public method to get the engine for a keyword. + + @param keyword keyword to get engine for (string) + @return reference to the search engine object (OpenSearchEngine) + """ + if keyword and keyword in self.__keywords: + return self.__keywords[keyword] + + return None + + def setEngineForKeyword(self, keyword, engine): + """ + Public method to set the engine for a keyword. + + @param keyword keyword to get engine for (string) + @param engine reference to the search engine object (OpenSearchEngine) + or None to remove the keyword + """ + if not keyword: + return + + if engine is None: + try: + del self.__keywords[keyword] + except KeyError: + pass + else: + self.__keywords[keyword] = engine + + self.changed.emit() + + def keywordsForEngine(self, engine): + """ + Public method to get the keywords for a given engine. + + @param engine reference to the search engine object (OpenSearchEngine) + @return list of keywords (list of strings) + """ + return [k for k in self.__keywords if self.__keywords[k] == engine] + + def setKeywordsForEngine(self, engine, keywords): + """ + Public method to set the keywords for an engine. + + @param engine reference to the search engine object (OpenSearchEngine) + @param keywords list of keywords (list of strings) + """ + if engine is None: + return + + for keyword in self.keywordsForEngine(engine): + del self.__keywords[keyword] + + for keyword in keywords: + if not keyword: + continue + + self.__keywords[keyword] = engine + + self.changed.emit() + + def enginesChanged(self): + """ + Public slot to tell the search engine manager, that something has + changed. + """ + self.changed.emit()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/OpenSearchReader.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a reader for open search engine descriptions. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamReader, QIODevice, QCoreApplication + + +class OpenSearchReader(QXmlStreamReader): + """ + Class implementing a reader for open search engine descriptions. + """ + def read(self, device): + """ + Public method to read the description. + + @param device device to read the description from (QIODevice) + @return search engine object (OpenSearchEngine) + """ + self.clear() + + if not device.isOpen(): + device.open(QIODevice.ReadOnly) + + self.setDevice(device) + return self.__read() + + def __read(self): + """ + Private method to read and parse the description. + + @return search engine object (OpenSearchEngine) + """ + from .OpenSearchEngine import OpenSearchEngine + engine = OpenSearchEngine() + + while not self.isStartElement() and not self.atEnd(): + self.readNext() + + if self.name() != "OpenSearchDescription" or \ + self.namespaceUri() != "http://a9.com/-/spec/opensearch/1.1/": + self.raiseError(QCoreApplication.translate( + "OpenSearchReader", + "The file is not an OpenSearch 1.1 file.")) + return engine + + while not self.atEnd(): + self.readNext() + + if not self.isStartElement(): + continue + + if self.name() == "ShortName": + engine.setName(self.readElementText()) + + elif self.name() == "Description": + engine.setDescription(self.readElementText()) + + elif self.name() == "Url": + type_ = self.attributes().value("type") + url = self.attributes().value("template") + method = self.attributes().value("method") + + if type_ == "application/x-suggestions+json" and \ + engine.suggestionsUrlTemplate(): + continue + + if (not type_ or + type_ == "text/html" or + type_ == "application/xhtml+xml") and \ + engine.suggestionsUrlTemplate(): + continue + + if not url: + continue + + parameters = [] + + self.readNext() + + while not (self.isEndElement() and self.name() == "Url"): + if not self.isStartElement() or \ + (self.name() != "Param" and self.name() != "Parameter"): + self.readNext() + continue + + key = self.attributes().value("name") + value = self.attributes().value("value") + + if key and value: + parameters.append((key, value)) + + while not self.isEndElement(): + self.readNext() + + if type_ == "application/x-suggestions+json": + engine.setSuggestionsUrlTemplate(url) + engine.setSuggestionsParameters(parameters) + engine.setSuggestionsMethod(method) + elif not type_ or \ + type_ == "text/html" or \ + type_ == "application/xhtml+xml": + engine.setSearchUrlTemplate(url) + engine.setSearchParameters(parameters) + engine.setSearchMethod(method) + + elif self.name() == "Image": + engine.setImageUrl(self.readElementText()) + + if engine.name() and \ + engine.description() and \ + engine.suggestionsUrlTemplate() and \ + engine.searchUrlTemplate() and \ + engine.imageUrl(): + break + + return engine
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/OpenSearchWriter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a writer for open search engine descriptions. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamWriter, QIODevice + + +class OpenSearchWriter(QXmlStreamWriter): + """ + Class implementing a writer for open search engine descriptions. + """ + def __init__(self): + """ + Constructor + """ + super(OpenSearchWriter, self).__init__() + + self.setAutoFormatting(True) + + def write(self, device, engine): + """ + Public method to write the description of an engine. + + @param device reference to the device to write to (QIODevice) + @param engine reference to the engine (OpenSearchEngine) + @return flag indicating success (boolean) + """ + if engine is None: + return False + + if not device.isOpen(): + if not device.open(QIODevice.WriteOnly): + return False + + self.setDevice(device) + self.__write(engine) + return True + + def __write(self, engine): + """ + Private method to write the description of an engine. + + @param engine reference to the engine (OpenSearchEngine) + """ + self.writeStartDocument() + self.writeStartElement("OpenSearchDescription") + self.writeDefaultNamespace("http://a9.com/-/spec/opensearch/1.1/") + + if engine.name(): + self.writeTextElement("ShortName", engine.name()) + + if engine.description(): + self.writeTextElement("Description", engine.description()) + + if engine.searchUrlTemplate(): + self.writeStartElement("Url") + self.writeAttribute("method", engine.searchMethod()) + self.writeAttribute("type", "text/html") + self.writeAttribute("template", engine.searchUrlTemplate()) + + if len(engine.searchParameters()) > 0: + self.writeNamespace( + "http://a9.com/-/spec/opensearch/extensions/" + "parameters/1.0/", "p") + for parameter in engine.searchParameters(): + self.writeStartElement("p:Parameter") + self.writeAttribute("name", parameter[0]) + self.writeAttribute("value", parameter[1]) + + self.writeEndElement() + + if engine.suggestionsUrlTemplate(): + self.writeStartElement("Url") + self.writeAttribute("method", engine.suggestionsMethod()) + self.writeAttribute("type", "application/x-suggestions+json") + self.writeAttribute("template", engine.suggestionsUrlTemplate()) + + if len(engine.suggestionsParameters()) > 0: + self.writeNamespace( + "http://a9.com/-/spec/opensearch/extensions/" + "parameters/1.0/", "p") + for parameter in engine.suggestionsParameters(): + self.writeStartElement("p:Parameter") + self.writeAttribute("name", parameter[0]) + self.writeAttribute("value", parameter[1]) + + self.writeEndElement() + + if engine.imageUrl(): + self.writeTextElement("Image", engine.imageUrl()) + + self.writeEndElement() + self.writeEndDocument()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/OpenSearch/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the opensearch search engine interfaces. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/PageScreenDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to save a screenshot of a web page. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt, QFile, QFileInfo, QSize +from PyQt5.QtGui import QImage, QPainter, QPixmap +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton + +from E5Gui import E5FileDialog, E5MessageBox + +from .Ui_PageScreenDialog import Ui_PageScreenDialog + + +class PageScreenDialog(QDialog, Ui_PageScreenDialog): + """ + Class documentation goes here. + """ + def __init__(self, view, visibleOnly=False, parent=None): + """ + Constructor + + @param view reference to the web view containing the page to be saved + (WebBrowserView) + @param visibleOnly flag indicating to just save the visible part + of the page (boolean) + @param parent reference to the parent widget (QWidget) + """ + super(PageScreenDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.__view = view + self.__createPixmap(visibleOnly) + self.pageScreenLabel.setPixmap(self.__pagePixmap) + + def __createPixmap(self, visibleOnly): + """ + Private slot to create a pixmap of the associated view's page. + + @param visibleOnly flag indicating to just save the visible part + of the page (boolean) + """ + res = self.__view.page().execJavaScript( + "(function() {" + "var res = {" + " width: 0," + " height: 0," + "};" + "res.width = document.body.scrollWidth;" + "res.height = document.body.scrollHeight;" + "return res;" + "})()" + ) + if res is not None: + if visibleOnly: + image = QImage(QSize(res["width"], self.__view.height()), + QImage.Format_ARGB32) + painter = QPainter(image) + self.__view.render(painter) + painter.end() + else: + # TODO: once QWebEngineView supports this + image = QImage(QSize(res["width"], self.__view.height()), + QImage.Format_ARGB32) + painter = QPainter(image) + self.__view.render(painter) + painter.end() + + self.__pagePixmap = QPixmap.fromImage(image) + + def __savePageScreen(self): + """ + Private slot to save the page screen. + + @return flag indicating success (boolean) + """ + fileName = E5FileDialog.getSaveFileName( + self, + self.tr("Save Page Screen"), + self.tr("screen.png"), + self.tr("Portable Network Graphics File (*.png)"), + E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) + if not fileName: + return False + + if QFileInfo(fileName).exists(): + res = E5MessageBox.yesNo( + self, + self.tr("Save Page Screen"), + self.tr("<p>The file <b>{0}</b> already exists." + " Overwrite it?</p>").format(fileName), + icon=E5MessageBox.Warning) + if not res: + return False + + file = QFile(fileName) + if not file.open(QFile.WriteOnly): + E5MessageBox.warning( + self, + self.tr("Save Page Screen"), + self.tr("Cannot write file '{0}:\n{1}.") + .format(fileName, file.errorString())) + return False + + res = self.__pagePixmap.save(file) + file.close() + + if not res: + E5MessageBox.warning( + self, + self.tr("Save Page Screen"), + self.tr("Cannot write file '{0}:\n{1}.") + .format(fileName, file.errorString())) + return False + + return True + + @pyqtSlot(QAbstractButton) + def on_buttonBox_clicked(self, button): + """ + Private slot to handle clicks of the dialog buttons. + + @param button button that was clicked (QAbstractButton) + """ + if button == self.buttonBox.button(QDialogButtonBox.Cancel): + self.reject() + elif button == self.buttonBox.button(QDialogButtonBox.Save): + if self.__savePageScreen(): + self.accept()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/PageScreenDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PageScreenDialog</class> + <widget class="QDialog" name="PageScreenDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>450</height> + </rect> + </property> + <property name="windowTitle"> + <string>Page Screen</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QScrollArea" name="scrollArea"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="lineWidth"> + <number>0</number> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>482</width> + <height>403</height> + </rect> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="margin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="pageScreenLabel"> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>scrollArea</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/LoginForm.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a data structure for login forms. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QUrl + + +class LoginForm(object): + """ + Class implementing a data structure for login forms. + """ + def __init__(self): + """ + Constructor + """ + self.url = QUrl() + self.name = "" + self.postData = "" + + def isValid(self): + """ + Public method to test for validity. + + @return flag indicating a valid form (boolean) + """ + return not self.url.isEmpty() and \ + bool(self.postData)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,415 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the password manager. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, QObject, QByteArray, QUrl, \ + QCoreApplication, QXmlStreamReader +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWebEngineWidgets import QWebEngineScript + +from E5Gui import E5MessageBox +from E5Gui.E5ProgressDialog import E5ProgressDialog + +from Utilities.AutoSaver import AutoSaver +import Utilities +import Utilities.crypto +import Preferences + +import WebBrowser.WebBrowserWindow +from ..Tools import Scripts + + +class PasswordManager(QObject): + """ + Class implementing the password manager. + + @signal changed() emitted to indicate a change + @signal passwordsSaved() emitted after the passwords were saved + """ + changed = pyqtSignal() + passwordsSaved = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(PasswordManager, self).__init__(parent) + + # setup userscript to monitor forms + script = QWebEngineScript() + script.setName("_eric_passwordmonitor") + script.setInjectionPoint(QWebEngineScript.DocumentReady) + script.setWorldId(QWebEngineScript.MainWorld) + script.setRunsOnSubFrames(True) + script.setSourceCode(Scripts.setupFormObserver()) + profile = WebBrowser.WebBrowserWindow.WebBrowserWindow.webProfile() + profile.scripts().insert(script) + + self.__logins = {} + self.__loginForms = {} + self.__never = [] + self.__loaded = False + self.__saveTimer = AutoSaver(self, self.save) + + self.changed.connect(self.__saveTimer.changeOccurred) + + def clear(self): + """ + Public slot to clear the saved passwords. + """ + if not self.__loaded: + self.__load() + + self.__logins = {} + self.__loginForms = {} + self.__never = [] + self.__saveTimer.changeOccurred() + self.__saveTimer.saveIfNeccessary() + + self.changed.emit() + + def getLogin(self, url, realm): + """ + Public method to get the login credentials. + + @param url URL to get the credentials for (QUrl) + @param realm realm to get the credentials for (string) + @return tuple containing the user name (string) and password (string) + """ + if not self.__loaded: + self.__load() + + key = self.__createKey(url, realm) + try: + return self.__logins[key][0], Utilities.crypto.pwConvert( + self.__logins[key][1], encode=False) + except KeyError: + return "", "" + + def setLogin(self, url, realm, username, password): + """ + Public method to set the login credentials. + + @param url URL to set the credentials for (QUrl) + @param realm realm to set the credentials for (string) + @param username username for the login (string) + @param password password for the login (string) + """ + if not self.__loaded: + self.__load() + + key = self.__createKey(url, realm) + self.__logins[key] = ( + username, + Utilities.crypto.pwConvert(password, encode=True) + ) + self.changed.emit() + + def __createKey(self, url, realm): + """ + Private method to create the key string for the login credentials. + + @param url URL to get the credentials for (QUrl) + @param realm realm to get the credentials for (string) + @return key string (string) + """ + authority = url.authority() + if authority.startswith("@"): + authority = authority[1:] + if realm: + key = "{0}://{1} ({2})".format( + url.scheme(), authority, realm) + else: + key = "{0}://{1}".format(url.scheme(), authority) + return key + + def getFileName(self): + """ + Public method to get the file name of the passwords file. + + @return name of the passwords file (string) + """ + return os.path.join(Utilities.getConfigDir(), + "web_browser", "logins.xml") + + def save(self): + """ + Public slot to save the login entries to disk. + """ + if not self.__loaded: + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if not WebBrowserWindow.isPrivate(): + from .PasswordWriter import PasswordWriter + loginFile = self.getFileName() + writer = PasswordWriter() + if not writer.write( + loginFile, self.__logins, self.__loginForms, self.__never): + E5MessageBox.critical( + None, + self.tr("Saving login data"), + self.tr( + """<p>Login data could not be saved to""" + """ <b>{0}</b></p>""" + ).format(loginFile)) + else: + self.passwordsSaved.emit() + + def __load(self): + """ + Private method to load the saved login credentials. + """ + if self.__loaded: + return + + loginFile = self.getFileName() + if os.path.exists(loginFile): + from .PasswordReader import PasswordReader + reader = PasswordReader() + self.__logins, self.__loginForms, self.__never = \ + reader.read(loginFile) + if reader.error() != QXmlStreamReader.NoError: + E5MessageBox.warning( + None, + self.tr("Loading login data"), + self.tr("""Error when loading login data on""" + """ line {0}, column {1}:\n{2}""") + .format(reader.lineNumber(), + reader.columnNumber(), + reader.errorString())) + + self.__loaded = True + + def reload(self): + """ + Public method to reload the login data. + """ + if not self.__loaded: + return + + self.__loaded = False + self.__load() + + def close(self): + """ + Public method to close the passwords manager. + """ + self.__saveTimer.saveIfNeccessary() + + def removePassword(self, site): + """ + Public method to remove a password entry. + + @param site web site name (string) + """ + if site in self.__logins: + del self.__logins[site] + if site in self.__loginForms: + del self.__loginForms[site] + self.changed.emit() + + def allSiteNames(self): + """ + Public method to get a list of all site names. + + @return sorted list of all site names (list of strings) + """ + if not self.__loaded: + self.__load() + + return sorted(self.__logins.keys()) + + def sitesCount(self): + """ + Public method to get the number of available sites. + + @return number of sites (integer) + """ + if not self.__loaded: + self.__load() + + return len(self.__logins) + + def siteInfo(self, site): + """ + Public method to get a reference to the named site. + + @param site web site name (string) + @return tuple containing the user name (string) and password (string) + """ + if not self.__loaded: + self.__load() + + if site not in self.__logins: + return None + + return self.__logins[site][0], Utilities.crypto.pwConvert( + self.__logins[site][1], encode=False) + + def formSubmitted(self, urlStr, userName, password, data, page): + """ + Public method to record login data. + + @param urlStr form submission URL + @type str + @param userName name of the user + @type str + @param password user password + @type str + @param data data to be submitted + @type QByteArray + @param page reference to the calling page + @type QWebEnginePage + """ + # shall passwords be saved? + if not Preferences.getUser("SavePasswords"): + return + + if WebBrowser.WebBrowserWindow.WebBrowserWindow.isPrivate(): + return + + if not self.__loaded: + self.__load() + + if urlStr in self.__never: + return + + if userName and password: + url = QUrl(urlStr) + url = self.__stripUrl(url) + key = self.__createKey(url, "") + if key not in self.__loginForms: + mb = E5MessageBox.E5MessageBox( + E5MessageBox.Question, + self.tr("Save password"), + self.tr( + """<b>Would you like to save this password?</b><br/>""" + """To review passwords you have saved and remove""" + """ them, use the password management dialog of the""" + """ Settings menu."""), + modal=True, parent=page.view()) + neverButton = mb.addButton( + self.tr("Never for this site"), + E5MessageBox.DestructiveRole) + noButton = mb.addButton( + self.tr("Not now"), E5MessageBox.RejectRole) + mb.addButton(E5MessageBox.Yes) + mb.exec_() + if mb.clickedButton() == neverButton: + self.__never.append(url.toString()) + return + elif mb.clickedButton() == noButton: + return + + self.__logins[key] = \ + (userName, + Utilities.crypto.pwConvert(password, encode=True)) + from .LoginForm import LoginForm + form = LoginForm() + form.url = url + form.name = userName + form.postData = Utilities.crypto.pwConvert( + bytes(data).decode("utf-8"), encode=True) + self.__loginForms[key] = form + self.changed.emit() + + def __stripUrl(self, url): + """ + Private method to strip off all unneeded parts of a URL. + + @param url URL to be stripped (QUrl) + @return stripped URL (QUrl) + """ + cleanUrl = QUrl(url) + cleanUrl.setQuery("") + cleanUrl.setUserInfo("") + + authority = cleanUrl.authority() + if authority.startswith("@"): + authority = authority[1:] + cleanUrl = QUrl("{0}://{1}{2}".format( + cleanUrl.scheme(), authority, cleanUrl.path())) + cleanUrl.setFragment("") + return cleanUrl + + def completePage(self, page): + """ + Public slot to complete login forms with saved data. + + @param page reference to the web page (WebBrowserPage) + """ + if page is None: + return + + if not self.__loaded: + self.__load() + + url = page.url() + url = self.__stripUrl(url) + key = self.__createKey(url, "") + if key not in self.__loginForms or \ + key not in self.__logins: + return + + form = self.__loginForms[key] + if form.url != url: + return + + postData = QByteArray(Utilities.crypto.pwConvert( + form.postData, encode=False).encode("utf-8")) + script = Scripts.completeFormData(postData) + page.runJavaScript(script) + + def masterPasswordChanged(self, oldPassword, newPassword): + """ + Public slot to handle the change of the master password. + + @param oldPassword current master password (string) + @param newPassword new master password (string) + """ + if not self.__loaded: + self.__load() + + progress = E5ProgressDialog( + self.tr("Re-encoding saved passwords..."), + None, 0, len(self.__logins) + len(self.__loginForms), + self.tr("%v/%m Passwords"), + QApplication.activeModalWidget()) + progress.setMinimumDuration(0) + progress.setWindowTitle(self.tr("Passwords")) + count = 0 + + # step 1: do the logins + for key in self.__logins: + progress.setValue(count) + QCoreApplication.processEvents() + username, hash = self.__logins[key] + hash = Utilities.crypto.pwRecode(hash, oldPassword, newPassword) + self.__logins[key] = (username, hash) + count += 1 + + # step 2: do the login forms + for key in self.__loginForms: + progress.setValue(count) + QCoreApplication.processEvents() + postData = self.__loginForms[key].postData + postData = Utilities.crypto.pwRecode( + postData, oldPassword, newPassword) + self.__loginForms[key].postData = postData + count += 1 + + progress.setValue(len(self.__logins) + len(self.__loginForms)) + QCoreApplication.processEvents() + self.changed.emit()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordModel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a model for password management. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QModelIndex, QAbstractTableModel + + +class PasswordModel(QAbstractTableModel): + """ + Class implementing a model for password management. + """ + def __init__(self, manager, parent=None): + """ + Constructor + + @param manager reference to the password manager (PasswordManager) + @param parent reference to the parent object (QObject) + """ + super(PasswordModel, self).__init__(parent) + + self.__manager = manager + manager.changed.connect(self.__passwordsChanged) + + self.__headers = [ + self.tr("Website"), + self.tr("Username"), + self.tr("Password") + ] + + self.__showPasswords = False + + def setShowPasswords(self, on): + """ + Public methods to show passwords. + + @param on flag indicating if passwords shall be shown (boolean) + """ + self.__showPasswords = on + self.beginResetModel() + self.endResetModel() + + def showPasswords(self): + """ + Public method to indicate, if passwords shall be shown. + + @return flag indicating if passwords shall be shown (boolean) + """ + return self.__showPasswords + + def __passwordsChanged(self): + """ + Private slot handling a change of the registered passwords. + """ + self.beginResetModel() + self.endResetModel() + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove entries from the model. + + @param row start row (integer) + @param count number of rows to remove (integer) + @param parent parent index (QModelIndex) + @return flag indicating success (boolean) + """ + if parent.isValid(): + return False + + if count <= 0: + return False + + lastRow = row + count - 1 + + self.beginRemoveRows(parent, row, lastRow) + + siteList = self.__manager.allSiteNames() + for index in range(row, lastRow + 1): + self.__manager.removePassword(siteList[index]) + + # removeEngine emits changed() + #self.endRemoveRows() + + return True + + def rowCount(self, parent=QModelIndex()): + """ + Public method to get the number of rows of the model. + + @param parent parent index (QModelIndex) + @return number of rows (integer) + """ + if parent.isValid(): + return 0 + else: + return self.__manager.sitesCount() + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns of the model. + + @param parent parent index (QModelIndex) + @return number of columns (integer) + """ + if self.__showPasswords: + return 3 + else: + return 2 + + def data(self, index, role): + """ + Public method to get data from the model. + + @param index index to get data for (QModelIndex) + @param role role of the data to retrieve (integer) + @return requested data + """ + if index.row() >= self.__manager.sitesCount() or index.row() < 0: + return None + + site = self.__manager.allSiteNames()[index.row()] + siteInfo = self.__manager.siteInfo(site) + + if siteInfo is None: + return None + + if role == Qt.DisplayRole: + if index.column() == 0: + return site + elif index.column() in [1, 2]: + return siteInfo[index.column() - 1] + + return None + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Public method to get the header data. + + @param section section number (integer) + @param orientation header orientation (Qt.Orientation) + @param role data role (integer) + @return header data + """ + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + try: + return self.__headers[section] + except IndexError: + pass + + return None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordReader.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to read login data files. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamReader, QIODevice, QFile, \ + QCoreApplication, QUrl + + +class PasswordReader(QXmlStreamReader): + """ + Class implementing a reader object for login data files. + """ + def __init__(self): + """ + Constructor + """ + super(PasswordReader, self).__init__() + + def read(self, fileNameOrDevice): + """ + Public method to read a login data file. + + @param fileNameOrDevice name of the file to read (string) + or reference to the device to read (QIODevice) + @return tuple containing the logins, forms and never URLs + """ + self.__logins = {} + self.__loginForms = {} + self.__never = [] + + if isinstance(fileNameOrDevice, QIODevice): + self.setDevice(fileNameOrDevice) + else: + f = QFile(fileNameOrDevice) + if not f.exists(): + return self.__logins, self.__loginForms, self.__never + f.open(QFile.ReadOnly) + self.setDevice(f) + + while not self.atEnd(): + self.readNext() + if self.isStartElement(): + version = self.attributes().value("version") + if self.name() == "Password" and \ + (not version or version == "2.0"): + self.__readPasswords() + else: + self.raiseError(QCoreApplication.translate( + "PasswordReader", + "The file is not a Passwords version 2.0 file.")) + + return self.__logins, self.__loginForms, self.__never + + def __readPasswords(self): + """ + Private method to read and parse the login data file. + """ + if not self.isStartElement() and self.name() != "Password": + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + break + + if self.isStartElement(): + if self.name() == "Logins": + self.__readLogins() + elif self.name() == "Forms": + self.__readForms() + elif self.name() == "Nevers": + self.__readNevers() + else: + self.__skipUnknownElement() + + def __readLogins(self): + """ + Private method to read the login information. + """ + if not self.isStartElement() and self.name() != "Logins": + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + if self.name() == "Login": + continue + else: + break + + if self.isStartElement(): + if self.name() == "Login": + attributes = self.attributes() + key = attributes.value("key") + user = attributes.value("user") + password = attributes.value("password") + self.__logins[key] = (user, password) + else: + self.__skipUnknownElement() + + def __readForms(self): + """ + Private method to read the forms information. + """ + if not self.isStartElement() and self.name() != "Forms": + return + + while not self.atEnd(): + self.readNext() + if self.isStartElement(): + if self.name() == "Form": + from .LoginForm import LoginForm + attributes = self.attributes() + key = attributes.value("key") + form = LoginForm() + form.url = QUrl(attributes.value("url")) + form.name = attributes.value("name") + + elif self.name() == "PostData": + form.postData = self.readElementText() + else: + self.__skipUnknownElement() + + if self.isEndElement(): + if self.name() == "Form": + self.__loginForms[key] = form + continue + elif self.name() == "PostData": + continue + elif self.name() in ["Elements", "Element"]: + continue + else: + break + + def __readNevers(self): + """ + Private method to read the never URLs. + """ + if not self.isStartElement() and self.name() != "Nevers": + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + if self.name() == "Never": + continue + else: + break + + if self.isStartElement(): + if self.name() == "Never": + self.__never.append(self.attributes().value("url")) + else: + self.__skipUnknownElement() + + def __skipUnknownElement(self): + """ + Private method to skip over all unknown elements. + """ + if not self.isStartElement(): + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + break + + if self.isStartElement(): + self.__skipUnknownElement()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordWriter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to write login data files. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamWriter, QIODevice, QFile + + +class PasswordWriter(QXmlStreamWriter): + """ + Class implementing a writer object to generate login data files. + """ + def __init__(self): + """ + Constructor + """ + super(PasswordWriter, self).__init__() + + self.setAutoFormatting(True) + + def write(self, fileNameOrDevice, logins, forms, nevers): + """ + Public method to write an login data file. + + @param fileNameOrDevice name of the file to write (string) + or device to write to (QIODevice) + @param logins dictionary with login data (user name, password) + @param forms list of forms data (list of LoginForm) + @param nevers list of URLs to never store data for (list of strings) + @return flag indicating success (boolean) + """ + if isinstance(fileNameOrDevice, QIODevice): + f = fileNameOrDevice + else: + f = QFile(fileNameOrDevice) + if not f.open(QFile.WriteOnly): + return False + + self.setDevice(f) + return self.__write(logins, forms, nevers) + + def __write(self, logins, forms, nevers): + """ + Private method to write an login data file. + + @param logins dictionary with login data (user name, password) + @param forms list of forms data (list of LoginForm) + @param nevers list of URLs to never store data for (list of strings) + @return flag indicating success (boolean) + """ + self.writeStartDocument() + self.writeDTD("<!DOCTYPE passwords>") + self.writeStartElement("Password") + self.writeAttribute("version", "2.0") + + if logins: + self.__writeLogins(logins) + if forms: + self.__writeForms(forms) + if nevers: + self.__writeNevers(nevers) + + self.writeEndDocument() + return True + + def __writeLogins(self, logins): + """ + Private method to write the login data. + + @param logins dictionary with login data (user name, password) + """ + self.writeStartElement("Logins") + for key, login in logins.items(): + self.writeEmptyElement("Login") + self.writeAttribute("key", key) + self.writeAttribute("user", login[0]) + self.writeAttribute("password", login[1]) + self.writeEndElement() + + def __writeForms(self, forms): + """ + Private method to write forms data. + + @param forms list of forms data (list of LoginForm) + """ + self.writeStartElement("Forms") + for key, form in forms.items(): + self.writeStartElement("Form") + self.writeAttribute("key", key) + self.writeAttribute("url", form.url.toString()) + self.writeAttribute("name", str(form.name)) + self.writeTextElement("PostData", form.postData) + self.writeEndElement() + self.writeEndElement() + + def __writeNevers(self, nevers): + """ + Private method to write the URLs never to store login data for. + + @param nevers list of URLs to never store data for (list of strings) + """ + self.writeStartElement("Nevers") + for never in nevers: + self.writeEmptyElement("Never") + self.writeAttribute("url", never) + self.writeEndElement()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordsDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show all saved logins. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, QSortFilterProxyModel +from PyQt5.QtGui import QFont, QFontMetrics +from PyQt5.QtWidgets import QDialog + +from E5Gui import E5MessageBox + +from .Ui_PasswordsDialog import Ui_PasswordsDialog + + +class PasswordsDialog(QDialog, Ui_PasswordsDialog): + """ + Class implementing a dialog to show all saved logins. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(PasswordsDialog, self).__init__(parent) + self.setupUi(self) + + self.__showPasswordsText = self.tr("Show Passwords") + self.__hidePasswordsText = self.tr("Hide Passwords") + self.passwordsButton.setText(self.__showPasswordsText) + + self.removeButton.clicked.connect( + self.passwordsTable.removeSelected) + self.removeAllButton.clicked.connect(self.passwordsTable.removeAll) + + import WebBrowser.WebBrowserWindow + from .PasswordModel import PasswordModel + + self.passwordsTable.verticalHeader().hide() + self.__passwordModel = PasswordModel( + WebBrowser.WebBrowserWindow.WebBrowserWindow.passwordManager(), + self) + self.__proxyModel = QSortFilterProxyModel(self) + self.__proxyModel.setSourceModel(self.__passwordModel) + self.searchEdit.textChanged.connect( + self.__proxyModel.setFilterFixedString) + self.passwordsTable.setModel(self.__proxyModel) + + fm = QFontMetrics(QFont()) + height = fm.height() + fm.height() // 3 + self.passwordsTable.verticalHeader().setDefaultSectionSize(height) + self.passwordsTable.verticalHeader().setMinimumSectionSize(-1) + + self.__calculateHeaderSizes() + + def __calculateHeaderSizes(self): + """ + Private method to calculate the section sizes of the horizontal header. + """ + fm = QFontMetrics(QFont()) + for section in range(self.__passwordModel.columnCount()): + header = self.passwordsTable.horizontalHeader()\ + .sectionSizeHint(section) + if section == 0: + header = fm.width("averagebiglongsitename") + elif section == 1: + header = fm.width("averagelongusername") + elif section == 2: + header = fm.width("averagelongpassword") + buffer = fm.width("mm") + header += buffer + self.passwordsTable.horizontalHeader()\ + .resizeSection(section, header) + self.passwordsTable.horizontalHeader().setStretchLastSection(True) + + @pyqtSlot() + def on_passwordsButton_clicked(self): + """ + Private slot to switch the password display mode. + """ + if self.__passwordModel.showPasswords(): + self.__passwordModel.setShowPasswords(False) + self.passwordsButton.setText(self.__showPasswordsText) + else: + res = E5MessageBox.yesNo( + self, + self.tr("Saved Passwords"), + self.tr("""Do you really want to show passwords?""")) + if res: + self.__passwordModel.setShowPasswords(True) + self.passwordsButton.setText(self.__hidePasswordsText) + self.__calculateHeaderSizes()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/PasswordsDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,202 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PasswordsDialog</class> + <widget class="QDialog" name="PasswordsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>350</height> + </rect> + </property> + <property name="windowTitle"> + <string>Saved Passwords</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="E5ClearableLineEdit" name="searchEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + <property name="toolTip"> + <string>Enter search term</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <widget class="E5TableView" name="passwordsTable"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="textElideMode"> + <enum>Qt::ElideMiddle</enum> + </property> + <property name="showGrid"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected entries</string> + </property> + <property name="text"> + <string>&Remove</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all entries</string> + </property> + <property name="text"> + <string>Remove &All</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>208</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="passwordsButton"> + <property name="toolTip"> + <string>Press to toggle the display of passwords</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + <customwidget> + <class>E5TableView</class> + <extends>QTableView</extends> + <header>E5Gui/E5TableView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>passwordsTable</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>removeAllButton</tabstop> + <tabstop>passwordsButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>PasswordsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>237</x> + <y>340</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>PasswordsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>325</x> + <y>340</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Passwords/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the password management interface. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/PersonalInformationManager/PersonalDataDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to enter personal data. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QDialog + +from .Ui_PersonalDataDialog import Ui_PersonalDataDialog + +import UI.PixmapCache +import Preferences + + +class PersonalDataDialog(QDialog, Ui_PersonalDataDialog): + """ + Class implementing a dialog to enter personal data. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(PersonalDataDialog, self).__init__(parent) + self.setupUi(self) + + self.iconLabel.setPixmap(UI.PixmapCache.getPixmap("pim48.png")) + + self.firstnameEdit.setText(Preferences.getWebBrowser("PimFirstName")) + self.lastnameEdit.setText(Preferences.getWebBrowser("PimLastName")) + self.fullnameEdit.setText(Preferences.getWebBrowser("PimFullName")) + self.emailEdit.setText(Preferences.getWebBrowser("PimEmail")) + self.phoneEdit.setText(Preferences.getWebBrowser("PimPhone")) + self.mobileEdit.setText(Preferences.getWebBrowser("PimMobile")) + self.addressEdit.setText(Preferences.getWebBrowser("PimAddress")) + self.cityEdit.setText(Preferences.getWebBrowser("PimCity")) + self.zipEdit.setText(Preferences.getWebBrowser("PimZip")) + self.stateEdit.setText(Preferences.getWebBrowser("PimState")) + self.countryEdit.setText(Preferences.getWebBrowser("PimCountry")) + self.homepageEdit.setText(Preferences.getWebBrowser("PimHomePage")) + self.special1Edit.setText(Preferences.getWebBrowser("PimSpecial1")) + self.special2Edit.setText(Preferences.getWebBrowser("PimSpecial2")) + self.special3Edit.setText(Preferences.getWebBrowser("PimSpecial3")) + self.special4Edit.setText(Preferences.getWebBrowser("PimSpecial4")) + + def storeData(self): + """ + Public method to store the entered personal information. + """ + Preferences.setWebBrowser("PimFirstName", self.firstnameEdit.text()) + Preferences.setWebBrowser("PimLastName", self.lastnameEdit.text()) + Preferences.setWebBrowser("PimFullName", self.fullnameEdit.text()) + Preferences.setWebBrowser("PimEmail", self.emailEdit.text()) + Preferences.setWebBrowser("PimPhone", self.phoneEdit.text()) + Preferences.setWebBrowser("PimMobile", self.mobileEdit.text()) + Preferences.setWebBrowser("PimAddress", self.addressEdit.text()) + Preferences.setWebBrowser("PimCity", self.cityEdit.text()) + Preferences.setWebBrowser("PimZip", self.zipEdit.text()) + Preferences.setWebBrowser("PimState", self.stateEdit.text()) + Preferences.setWebBrowser("PimCountry", self.countryEdit.text()) + Preferences.setWebBrowser("PimHomePage", self.homepageEdit.text()) + Preferences.setWebBrowser("PimSpecial1", self.special1Edit.text()) + Preferences.setWebBrowser("PimSpecial2", self.special2Edit.text()) + Preferences.setWebBrowser("PimSpecial3", self.special3Edit.text()) + Preferences.setWebBrowser("PimSpecial4", self.special4Edit.text())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/PersonalInformationManager/PersonalDataDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,384 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PersonalDataDialog</class> + <widget class="QDialog" name="PersonalDataDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>600</width> + <height>400</height> + </rect> + </property> + <property name="windowTitle"> + <string>Personal Information</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="iconLabel"> + <property name="minimumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="text"> + <string notr="true">Icon</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string><h2>Personal Information</h2></string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="label_01"> + <property name="text"> + <string>Your personal information that will be used on webpages.</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_02"> + <property name="text"> + <string>First Name:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="firstnameEdit"/> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="label_08"> + <property name="text"> + <string>ZIP Code:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QLineEdit" name="zipEdit"/> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_03"> + <property name="text"> + <string>Last Name:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="lastnameEdit"/> + </item> + <item row="1" column="2"> + <widget class="QLabel" name="label_09"> + <property name="text"> + <string>State/Region:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QLineEdit" name="stateEdit"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_18"> + <property name="text"> + <string>Full Name:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="fullnameEdit"/> + </item> + <item row="2" column="2"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>Country:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QLineEdit" name="countryEdit"/> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_12"> + <property name="text"> + <string>E-mail:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLineEdit" name="emailEdit"/> + </item> + <item row="3" column="2"> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>Home Page:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="3"> + <widget class="QLineEdit" name="homepageEdit"/> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_04"> + <property name="text"> + <string>Phone:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLineEdit" name="phoneEdit"/> + </item> + <item row="4" column="2"> + <widget class="QLabel" name="label_13"> + <property name="text"> + <string>Custom 1:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="4" column="3"> + <widget class="QLineEdit" name="special1Edit"/> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_05"> + <property name="text"> + <string>Mobile Phone:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QLineEdit" name="mobileEdit"/> + </item> + <item row="5" column="2"> + <widget class="QLabel" name="label_14"> + <property name="text"> + <string>Custom 2:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="5" column="3"> + <widget class="QLineEdit" name="special2Edit"/> + </item> + <item row="6" column="0"> + <widget class="QLabel" name="label_06"> + <property name="text"> + <string>Address:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="6" column="1"> + <widget class="QLineEdit" name="addressEdit"/> + </item> + <item row="6" column="2"> + <widget class="QLabel" name="label_15"> + <property name="text"> + <string>Custom 3:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="6" column="3"> + <widget class="QLineEdit" name="special3Edit"/> + </item> + <item row="7" column="0"> + <widget class="QLabel" name="label_07"> + <property name="text"> + <string>City:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="7" column="1"> + <widget class="QLineEdit" name="cityEdit"/> + </item> + <item row="7" column="2"> + <widget class="QLabel" name="label_17"> + <property name="text"> + <string>Custom 4:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="7" column="3"> + <widget class="QLineEdit" name="special4Edit"/> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="label_16"> + <property name="text"> + <string><b>Note:</b> Press Ctrl+ENTER to autofill form fields for which personal entries were found.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>31</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>firstnameEdit</tabstop> + <tabstop>lastnameEdit</tabstop> + <tabstop>fullnameEdit</tabstop> + <tabstop>emailEdit</tabstop> + <tabstop>phoneEdit</tabstop> + <tabstop>mobileEdit</tabstop> + <tabstop>addressEdit</tabstop> + <tabstop>cityEdit</tabstop> + <tabstop>zipEdit</tabstop> + <tabstop>stateEdit</tabstop> + <tabstop>countryEdit</tabstop> + <tabstop>homepageEdit</tabstop> + <tabstop>special1Edit</tabstop> + <tabstop>special2Edit</tabstop> + <tabstop>special3Edit</tabstop> + <tabstop>special4Edit</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>PersonalDataDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>PersonalDataDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/PersonalInformationManager/PersonalInformationManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a personal information manager used to complete form +fields. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QObject, QPoint +from PyQt5.QtWidgets import QDialog, QMenu + +import Preferences +import UI.PixmapCache + + +class PersonalInformationManager(QObject): + """ + Class implementing the personal information manager used to complete form + fields. + """ + FullName = 0 + LastName = 1 + FirstName = 2 + Email = 3 + Mobile = 4 + Phone = 5 + Address = 6 + City = 7 + Zip = 8 + State = 9 + Country = 10 + HomePage = 11 + Special1 = 12 + Special2 = 13 + Special3 = 14 + Special4 = 15 + Max = 16 + Invalid = 256 + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(PersonalInformationManager, self).__init__(parent) + + self.__loaded = False + self.__allInfo = {} + self.__infoMatches = {} + self.__translations = {} + + self.__view = None + self.__clickedPos = QPoint() + + def __loadSettings(self): + """ + Private method to load the settings. + """ + self.__allInfo[self.FullName] = \ + Preferences.getWebBrowser("PimFullName") + self.__allInfo[self.LastName] = \ + Preferences.getWebBrowser("PimLastName") + self.__allInfo[self.FirstName] = \ + Preferences.getWebBrowser("PimFirstName") + self.__allInfo[self.Email] = Preferences.getWebBrowser("PimEmail") + self.__allInfo[self.Mobile] = Preferences.getWebBrowser("PimMobile") + self.__allInfo[self.Phone] = Preferences.getWebBrowser("PimPhone") + self.__allInfo[self.Address] = Preferences.getWebBrowser("PimAddress") + self.__allInfo[self.City] = Preferences.getWebBrowser("PimCity") + self.__allInfo[self.Zip] = Preferences.getWebBrowser("PimZip") + self.__allInfo[self.State] = Preferences.getWebBrowser("PimState") + self.__allInfo[self.Country] = Preferences.getWebBrowser("PimCountry") + self.__allInfo[self.HomePage] = \ + Preferences.getWebBrowser("PimHomePage") + self.__allInfo[self.Special1] = \ + Preferences.getWebBrowser("PimSpecial1") + self.__allInfo[self.Special2] = \ + Preferences.getWebBrowser("PimSpecial2") + self.__allInfo[self.Special3] = \ + Preferences.getWebBrowser("PimSpecial3") + self.__allInfo[self.Special4] = \ + Preferences.getWebBrowser("PimSpecial4") + + self.__translations[self.FullName] = self.tr("Full Name") + self.__translations[self.LastName] = self.tr("Last Name") + self.__translations[self.FirstName] = self.tr("First Name") + self.__translations[self.Email] = self.tr("E-mail") + self.__translations[self.Mobile] = self.tr("Mobile") + self.__translations[self.Phone] = self.tr("Phone") + self.__translations[self.Address] = self.tr("Address") + self.__translations[self.City] = self.tr("City") + self.__translations[self.Zip] = self.tr("ZIP Code") + self.__translations[self.State] = self.tr("State/Region") + self.__translations[self.Country] = self.tr("Country") + self.__translations[self.HomePage] = self.tr("Home Page") + self.__translations[self.Special1] = self.tr("Custom 1") + self.__translations[self.Special2] = self.tr("Custom 2") + self.__translations[self.Special3] = self.tr("Custom 3") + self.__translations[self.Special4] = self.tr("Custom 4") + + self.__infoMatches[self.FullName] = ["fullname", "realname"] + self.__infoMatches[self.LastName] = ["lastname", "surname"] + self.__infoMatches[self.FirstName] = ["firstname", "name"] + self.__infoMatches[self.Email] = ["email", "e-mail", "mail"] + self.__infoMatches[self.Mobile] = ["mobile", "mobilephone"] + self.__infoMatches[self.Phone] = ["phone", "telephone"] + self.__infoMatches[self.Address] = ["address"] + self.__infoMatches[self.City] = ["city"] + self.__infoMatches[self.Zip] = ["zip"] + self.__infoMatches[self.State] = ["state", "region"] + self.__infoMatches[self.Country] = ["country"] + self.__infoMatches[self.HomePage] = ["homepage", "www"] + + self.__loaded = True + + def showConfigurationDialog(self): + """ + Public method to show the configuration dialog. + """ + from .PersonalDataDialog import PersonalDataDialog + dlg = PersonalDataDialog() + if dlg.exec_() == QDialog.Accepted: + dlg.storeData() + self.__loadSettings() + + def createSubMenu(self, menu, view, hitTestResult): + """ + Public method to create the personal information sub-menu. + + @param menu reference to the main menu (QMenu) + @param view reference to the view (HelpBrowser) + @param hitTestResult reference to the hit test result + (WebHitTestResult) + """ + self.__view = view + self.__clickedPos = hitTestResult.pos() + + if not hitTestResult.isContentEditable(): + return + + if not self.__loaded: + self.__loadSettings() + + submenu = QMenu(self.tr("Insert Personal Information"), menu) + submenu.setIcon(UI.PixmapCache.getIcon("pim.png")) + + for key, info in sorted(self.__allInfo.items()): + if info: + act = submenu.addAction( + self.__translations[key], self.__insertData) + act.setData(info) + + submenu.addSeparator() + submenu.addAction(self.tr("Edit Personal Information"), + self.showConfigurationDialog) + + menu.addMenu(submenu) + menu.addSeparator() + + def __insertData(self): + """ + Private slot to insert the selected personal information. + """ + if self.__view is None or self.__clickedPos.isNull(): + return + + act = self.sender() + if act is None: + return + + info = act.data() + info = info.replace('"', '\\"') + + source = """ + var e = document.elementFromPoint({0}, {1}); + if (e) {{ + var v = e.value.substring(0, e.selectionStart); + v += "{2}" + e.value.substring(e.selectionEnd); + e.value = v; + }}""".format(self.__clickedPos.x(), self.__clickedPos.y(), info) + self.__view.page().runJavaScript(source) + + def viewKeyPressEvent(self, view, evt): + """ + Protected method to handle key press events we are interested in. + + @param view reference to the view (HelpBrowser) + @param evt reference to the key event (QKeyEvent) + @return flag indicating handling of the event (boolean) + """ + if view is None: + return False + + isEnter = evt.key() in [Qt.Key_Return, Qt.Key_Enter] + isControlModifier = evt.modifiers() & Qt.ControlModifier + if not isEnter or not isControlModifier: + return False + + if not self.__loaded: + self.__loadSettings() + + source = """ + var inputs = document.getElementsByTagName('input'); + var table = {0}; + for (var i = 0; i < inputs.length; ++i) {{ + var input = inputs[i]; + if (input.type != 'text' || input.name == '') + continue; + for (var key in table) {{ + if (!table.hasOwnProperty(key)) + continue; + if (key == input.name || input.name.indexOf(key) != -1) {{ + input.value = table[key]; + break; + }} + }} + }}""".format(self.__matchingJsTable()) + view.page().runJavaScript(source) + + return True + + def connectPage(self, page): + """ + Public method to allow the personal information manager to connect to + the page. + + @param page reference to the web page (HelpWebPage) + """ + page.loadFinished.connect(self.__pageLoadFinished) + + def __pageLoadFinished(self, ok): + """ + Private slot to handle the completion of a page load. + + @param ok flag indicating a successful load (boolean) + """ + page = self.sender() + if page is None or not ok: + return + + if not self.__loaded: + self.__loadSettings() + + source = """ + var inputs = document.getElementsByTagName('input'); + var table = {0}; + for (var i = 0; i < inputs.length; ++i) {{ + var input = inputs[i]; + if (input.type != 'text' || input.name == '') + continue; + for (var key in table) {{ + if (!table.hasOwnProperty(key) || table[key] == '') + continue; + if (key == input.name || input.name.indexOf(key) != -1) {{ + input.style['-webkit-appearance'] = 'none'; + input.style['-webkit-box-shadow'] = + 'inset 0 0 2px 1px #000EEE'; + break; + }} + }} + }}""".format(self.__matchingJsTable()) + page.runJavaScript(source) + + def __matchingJsTable(self): + """ + Private method to create the common part of the JavaScript sources. + + @return JavaScript source + @rtype str + """ + values = [] + for key, names in self.__infoMatches.items(): + for name in names: + value = self.__allInfo[key].replace('"', '\\"') + values.append('"{0}":"{1}"'.format(name, value)) + return "{{ {0} }}".format(",".join(values))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/PersonalInformationManager/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the personal information manager for the completion of +forms. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/QtHelp/HelpDocsInstaller.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a thread class populating and updating the QtHelp +documentation database. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, qVersion, QThread, Qt, QMutex, \ + QDateTime, QDir, QLibraryInfo, QFileInfo +from PyQt5.QtHelp import QHelpEngineCore + +from eric6config import getConfig + + +class HelpDocsInstaller(QThread): + """ + Class implementing the worker thread populating and updating the QtHelp + documentation database. + + @signal errorMessage(str) emitted, if an error occurred during + the installation of the documentation + @signal docsInstalled(bool) emitted after the installation has finished + """ + errorMessage = pyqtSignal(str) + docsInstalled = pyqtSignal(bool) + + def __init__(self, collection): + """ + Constructor + + @param collection full pathname of the collection file (string) + """ + super(HelpDocsInstaller, self).__init__() + + self.__abort = False + self.__collection = collection + self.__mutex = QMutex() + + def stop(self): + """ + Public slot to stop the installation procedure. + """ + if not self.isRunning(): + return + + self.__mutex.lock() + self.__abort = True + self.__mutex.unlock() + self.wait() + + def installDocs(self): + """ + Public method to start the installation procedure. + """ + self.start(QThread.LowPriority) + + def run(self): + """ + Public method executed by the thread. + """ + engine = QHelpEngineCore(self.__collection) + engine.setupData() + changes = False + + qt4Docs = ["designer", "linguist", "qt"] + qt5Docs = [ + "activeqt", "qdoc", "qmake", "qt3d", "qt3drenderer", + "qtandroidextras", "qtassistant", "qtbluetooth", "qtcanvas3d", + "qtconcurrent", "qtcore", "qtdbus", "qtdesigner", "qtdoc", + "qtenginio", "qtenginiooverview", "qtenginoqml", + "qtgraphicaleffects", "qtgui", "qthelp", "qtimageformats", + "qtlabscontrols", "qtlinguist", "qtlocation", "qtmaxextras", + "qtmultimedia", "qtmultimediawidgets", "qtnetwork", "qtnfc", + "qtopengl", "qtplatformheaders", "qtpositioning", "qtprintsupport", + "qtqml", "qtquick", "qtquickcontrols", "qtquickdialogs", + "qtquickextras", "qtquicklayouts", "qtscript", "qtscripttools", + "qtsensors", "qtserialbus", "qtserialport", "qtsql", "qtsvg", + "qttestlib", "qtuitools", "qtwebchannel", "qtwebengine", + "qtwebenginewidgets", "qtwebkit", "qtwebkitexamples", + "qtwebsockets", "qtwebview", "qtwidgets", "qtwinextras", + "qtx11extras", "qtxml", "qtxmlpatterns"] + for qtDocs, version in [(qt4Docs, 4), (qt5Docs, 5)]: + for doc in qtDocs: + changes |= self.__installQtDoc(doc, version, engine) + self.__mutex.lock() + if self.__abort: + engine = None + self.__mutex.unlock() + return + self.__mutex.unlock() + + changes |= self.__installEric6Doc(engine) + engine = None + del engine + self.docsInstalled.emit(changes) + + def __installQtDoc(self, name, version, engine): + """ + Private method to install/update a Qt help document. + + @param name name of the Qt help document (string) + @param version Qt version of the help documens (integer) + @param engine reference to the help engine (QHelpEngineCore) + @return flag indicating success (boolean) + """ + versionKey = "qt_version_{0}@@{1}".format(version, name) + info = engine.customValue(versionKey, "") + lst = info.split('|') + + dt = QDateTime() + if len(lst) and lst[0]: + dt = QDateTime.fromString(lst[0], Qt.ISODate) + + qchFile = "" + if len(lst) == 2: + qchFile = lst[1] + + if version == 4: + docsPath = QDir( + QLibraryInfo.location(QLibraryInfo.DocumentationPath) + + QDir.separator() + "qch") + elif version == 5: + docsPath = QLibraryInfo.location(QLibraryInfo.DocumentationPath) + if not os.path.isdir(docsPath) or \ + len(QDir(docsPath).entryList(["*.qch"])) == 0: + # Qt installer is a bit buggy; it's missing a symbolic link + docsPathList = QDir.fromNativeSeparators(docsPath).split("/") + docsPath = os.sep.join( + docsPathList[:-3] + + ["Docs", "Qt-{0}".format(qVersion()[:3])]) + docsPath = QDir(docsPath) + else: + # unsupported Qt version + return False + + files = docsPath.entryList(["*.qch"]) + if not files: + engine.setCustomValue( + versionKey, + QDateTime().toString(Qt.ISODate) + '|') + return False + + for f in files: + if f.startswith(name + "."): + fi = QFileInfo(docsPath.absolutePath() + QDir.separator() + f) + namespace = QHelpEngineCore.namespaceName( + fi.absoluteFilePath()) + if not namespace: + continue + + if dt.isValid() and \ + namespace in engine.registeredDocumentations() and \ + fi.lastModified().toString(Qt.ISODate) == \ + dt.toString(Qt.ISODate) and \ + qchFile == fi.absoluteFilePath(): + return False + + if namespace in engine.registeredDocumentations(): + engine.unregisterDocumentation(namespace) + + if not engine.registerDocumentation(fi.absoluteFilePath()): + self.errorMessage.emit( + self.tr( + """<p>The file <b>{0}</b> could not be""" + """ registered. <br/>Reason: {1}</p>""") + .format(fi.absoluteFilePath, engine.error()) + ) + return False + + engine.setCustomValue( + versionKey, + fi.lastModified().toString(Qt.ISODate) + '|' + + fi.absoluteFilePath()) + return True + + return False + + def __installEric6Doc(self, engine): + """ + Private method to install/update the eric6 help documentation. + + @param engine reference to the help engine (QHelpEngineCore) + @return flag indicating success (boolean) + """ + versionKey = "eric6_ide" + info = engine.customValue(versionKey, "") + lst = info.split('|') + + dt = QDateTime() + if len(lst) and lst[0]: + dt = QDateTime.fromString(lst[0], Qt.ISODate) + + qchFile = "" + if len(lst) == 2: + qchFile = lst[1] + + docsPath = QDir(getConfig("ericDocDir") + QDir.separator() + "Help") + + files = docsPath.entryList(["*.qch"]) + if not files: + engine.setCustomValue( + versionKey, QDateTime().toString(Qt.ISODate) + '|') + return False + + for f in files: + if f == "source.qch": + fi = QFileInfo(docsPath.absolutePath() + QDir.separator() + f) + namespace = QHelpEngineCore.namespaceName( + fi.absoluteFilePath()) + if not namespace: + continue + + if dt.isValid() and \ + namespace in engine.registeredDocumentations() and \ + fi.lastModified().toString(Qt.ISODate) == \ + dt.toString(Qt.ISODate) and \ + qchFile == fi.absoluteFilePath(): + return False + + if namespace in engine.registeredDocumentations(): + engine.unregisterDocumentation(namespace) + + if not engine.registerDocumentation(fi.absoluteFilePath()): + self.errorMessage.emit( + self.tr( + """<p>The file <b>{0}</b> could not be""" + """ registered. <br/>Reason: {1}</p>""") + .format(fi.absoluteFilePath, engine.error()) + ) + return False + + engine.setCustomValue( + versionKey, + fi.lastModified().toString(Qt.ISODate) + '|' + + fi.absoluteFilePath()) + return True + + return False
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/QtHelp/HelpIndexWidget.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a window for showing the QtHelp index. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, Qt, QUrl, QEvent +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit, QMenu, \ + QDialog + + +class HelpIndexWidget(QWidget): + """ + Class implementing a window for showing the QtHelp index. + + @signal linkActivated(QUrl) emitted when an index entry is activated + @signal linksActivated(links, keyword) emitted when an index entry + referencing multiple targets is activated + @signal escapePressed() emitted when the ESC key was pressed + """ + linkActivated = pyqtSignal(QUrl) + linksActivated = pyqtSignal(dict, str) + escapePressed = pyqtSignal() + + def __init__(self, engine, mainWindow, parent=None): + """ + Constructor + + @param engine reference to the help engine (QHelpEngine) + @param mainWindow reference to the main window object (QMainWindow) + @param parent reference to the parent widget (QWidget) + """ + super(HelpIndexWidget, self).__init__(parent) + + self.__engine = engine + self.__mw = mainWindow + + self.__searchEdit = None + self.__index = None + + self.__layout = QVBoxLayout(self) + label = QLabel(self.tr("&Look for:")) + self.__layout.addWidget(label) + + self.__searchEdit = QLineEdit() + label.setBuddy(self.__searchEdit) + self.__searchEdit.textChanged.connect(self.__filterIndices) + self.__searchEdit.installEventFilter(self) + self.__layout.addWidget(self.__searchEdit) + + self.__index = self.__engine.indexWidget() + self.__index.installEventFilter(self) + self.__engine.indexModel().indexCreationStarted.connect( + self.__disableSearchEdit) + self.__engine.indexModel().indexCreated.connect( + self.__enableSearchEdit) + self.__index.activated.connect(self.__activated) + self.__searchEdit.returnPressed.connect( + self.__index.activateCurrentItem) + self.__layout.addWidget(self.__index) + + self.__index.viewport().installEventFilter(self) + + def __activated(self, idx): + """ + Private slot to handle the activation of a keyword entry. + + @param idx index of the activated entry (QModelIndex) + """ + model = self.__index.model() + if model is not None: + keyword = model.data(idx, Qt.DisplayRole) + links = model.linksForKeyword(keyword) + if len(links) == 1: + self.linkActivated.emit(QUrl(links[list(links.keys())[0]])) + else: + self.linksActivated.emit(links, keyword) + + def __filterIndices(self, filter): + """ + Private slot to filter the indices according to the given filter. + + @param filter filter to be used (string) + """ + if '*' in filter: + self.__index.filterIndices(filter, filter) + else: + self.__index.filterIndices(filter) + + def __enableSearchEdit(self): + """ + Private slot to enable the search edit. + """ + self.__searchEdit.setEnabled(True) + self.__filterIndices(self.__searchEdit.text()) + + def __disableSearchEdit(self): + """ + Private slot to enable the search edit. + """ + self.__searchEdit.setEnabled(False) + + def focusInEvent(self, evt): + """ + Protected method handling focus in events. + + @param evt reference to the focus event object (QFocusEvent) + """ + if evt.reason() != Qt.MouseFocusReason: + self.__searchEdit.selectAll() + self.__searchEdit.setFocus() + + def eventFilter(self, watched, event): + """ + Public method called to filter the event queue. + + @param watched the QObject being watched (QObject) + @param event the event that occurred (QEvent) + @return flag indicating whether the event was handled (boolean) + """ + if self.__searchEdit and watched == self.__searchEdit and \ + event.type() == QEvent.KeyPress: + idx = self.__index.currentIndex() + if event.key() == Qt.Key_Up: + idx = self.__index.model().index( + idx.row() - 1, idx.column(), idx.parent()) + if idx.isValid(): + self.__index.setCurrentIndex(idx) + elif event.key() == Qt.Key_Down: + idx = self.__index.model().index( + idx.row() + 1, idx.column(), idx.parent()) + if idx.isValid(): + self.__index.setCurrentIndex(idx) + elif event.key() == Qt.Key_Escape: + self.escapePressed.emit() + elif self.__index and watched == self.__index and \ + event.type() == QEvent.ContextMenu: + idx = self.__index.indexAt(event.pos()) + if idx.isValid(): + menu = QMenu() + curTab = menu.addAction(self.tr("Open Link")) + newTab = menu.addAction(self.tr("Open Link in New Tab")) + menu.move(self.__index.mapToGlobal(event.pos())) + + act = menu.exec_() + if act == curTab: + self.__activated(idx) + elif act == newTab: + model = self.__index.model() + if model is not None: + keyword = model.data(idx, Qt.DisplayRole) + links = model.linksForKeyword(keyword) + if len(links) == 1: + self.__mw.newTab(list(links.values())[0]) + elif len(links) > 1: + from .HelpTopicDialog import HelpTopicDialog + dlg = HelpTopicDialog(self, keyword, links) + if dlg.exec_() == QDialog.Accepted: + self.__mw.newTab(dlg.link()) + elif self.__index and watched == self.__index.viewport() and \ + event.type() == QEvent.MouseButtonRelease: + idx = self.__index.indexAt(event.pos()) + if idx.isValid() and event.button() == Qt.MidButton: + model = self.__index.model() + if model is not None: + keyword = model.data(idx, Qt.DisplayRole) + links = model.linksForKeyword(keyword) + if len(links) == 1: + self.__mw.newTab(list(links.values())[0]) + elif len(links) > 1: + from .HelpTopicDialog import HelpTopicDialog + dlg = HelpTopicDialog(self, keyword, links) + if dlg.exec_() == QDialog.Accepted: + self.__mw.newTab(dlg.link()) + + return QWidget.eventFilter(self, watched, event)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/QtHelp/HelpSearchWidget.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a window for showing the QtHelp index. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, Qt, QEvent, QUrl +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextBrowser, QApplication, \ + QMenu + + +class HelpSearchWidget(QWidget): + """ + Class implementing a window for showing the QtHelp index. + + @signal linkActivated(QUrl) emitted when a search result entry is activated + @signal escapePressed() emitted when the ESC key was pressed + """ + linkActivated = pyqtSignal(QUrl) + escapePressed = pyqtSignal() + + def __init__(self, engine, mainWindow, parent=None): + """ + Constructor + + @param engine reference to the help search engine (QHelpSearchEngine) + @param mainWindow reference to the main window object (QMainWindow) + @param parent reference to the parent widget (QWidget) + """ + super(HelpSearchWidget, self).__init__(parent) + + self.__engine = engine + self.__mw = mainWindow + + self.__layout = QVBoxLayout(self) + + self.__result = self.__engine.resultWidget() + self.__query = self.__engine.queryWidget() + + self.__layout.addWidget(self.__query) + self.__layout.addWidget(self.__result) + + self.setFocusProxy(self.__query) + + self.__query.search.connect(self.__search) + self.__result.requestShowLink.connect(self.linkActivated) + + self.__engine.searchingStarted.connect(self.__searchingStarted) + self.__engine.searchingFinished.connect(self.__searchingFinished) + + self.__browser = self.__result.findChildren(QTextBrowser)[0] + if self.__browser: + self.__browser.viewport().installEventFilter(self) + + def __search(self): + """ + Private slot to perform a search of the database. + """ + query = self.__query.query() + self.__engine.search(query) + + def __searchingStarted(self): + """ + Private slot to handle the start of a search. + """ + QApplication.setOverrideCursor(Qt.WaitCursor) + + def __searchingFinished(self, hits): + """ + Private slot to handle the end of the search. + + @param hits number of hits (integer) (unused) + """ + QApplication.restoreOverrideCursor() + + def eventFilter(self, watched, event): + """ + Public method called to filter the event queue. + + @param watched the QObject being watched (QObject) + @param event the event that occurred (QEvent) + @return flag indicating whether the event was handled (boolean) + """ + if self.__browser and watched == self.__browser.viewport() and \ + event.type() == QEvent.MouseButtonRelease: + link = self.__result.linkAt(event.pos()) + if not link.isEmpty() and link.isValid(): + ctrl = event.modifiers() & Qt.ControlModifier + if (event.button() == Qt.LeftButton and ctrl) or \ + event.button() == Qt.MidButton: + self.__mw.newTab(link) + + return QWidget.eventFilter(self, watched, event) + + def keyPressEvent(self, evt): + """ + Protected method handling key press events. + + @param evt reference to the key press event (QKeyEvent) + """ + if evt.key() == Qt.Key_Escape: + self.escapePressed.emit() + else: + evt.ignore() + + def contextMenuEvent(self, evt): + """ + Protected method handling context menu events. + + @param evt reference to the context menu event (QContextMenuEvent) + """ + point = evt.globalPos() + + if self.__browser: + point = self.__browser.mapFromGlobal(point) + if not self.__browser.rect().contains(point, True): + return + link = QUrl(self.__browser.anchorAt(point)) + else: + point = self.__result.mapFromGlobal(point) + link = self.__result.linkAt(point) + + if link.isEmpty() or not link.isValid(): + return + + menu = QMenu() + curTab = menu.addAction(self.tr("Open Link")) + newTab = menu.addAction(self.tr("Open Link in New Tab")) + menu.move(evt.globalPos()) + act = menu.exec_() + if act == curTab: + self.linkActivated.emit(link) + elif act == newTab: + self.__mw.newTab(link)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/QtHelp/HelpTocWidget.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a window for showing the QtHelp TOC. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, Qt, QEvent, QUrl +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMenu + + +class HelpTocWidget(QWidget): + """ + Class implementing a window for showing the QtHelp TOC. + + @signal linkActivated(QUrl) emitted when a TOC entry is activated + @signal escapePressed() emitted when the ESC key was pressed + """ + linkActivated = pyqtSignal(QUrl) + escapePressed = pyqtSignal() + + def __init__(self, engine, mainWindow, parent=None): + """ + Constructor + + @param engine reference to the help engine (QHelpEngine) + @param mainWindow reference to the main window object (QMainWindow) + @param parent reference to the parent widget (QWidget) + """ + super(HelpTocWidget, self).__init__(parent) + + self.__engine = engine + self.__mw = mainWindow + self.__expandDepth = -2 + + self.__tocWidget = self.__engine.contentWidget() + self.__tocWidget.viewport().installEventFilter(self) + self.__tocWidget.setContextMenuPolicy(Qt.CustomContextMenu) + self.__tocWidget.setSortingEnabled(True) + + self.__layout = QVBoxLayout(self) + self.__layout.addWidget(self.__tocWidget) + + self.__tocWidget.customContextMenuRequested.connect( + self.__showContextMenu) + self.__tocWidget.linkActivated.connect(self.linkActivated) + + model = self.__tocWidget.model() + model.contentsCreated.connect(self.__contentsCreated) + + def __contentsCreated(self): + """ + Private slot to be run after the contents was generated. + """ + self.__tocWidget.sortByColumn(0, Qt.AscendingOrder) + self.__expandTOC() + + def __expandTOC(self): + """ + Private slot to expand the table of contents. + """ + if self.__expandDepth > -2: + self.expandToDepth(self.__expandDepth) + self.__expandDepth = -2 + + def expandToDepth(self, depth): + """ + Public slot to expand the table of contents to a specific depth. + + @param depth depth to expand to (integer) + """ + self.__expandDepth = depth + if depth == -1: + self.__tocWidget.expandAll() + else: + self.__tocWidget.expandToDepth(depth) + + def focusInEvent(self, evt): + """ + Protected method handling focus in events. + + @param evt reference to the focus event object (QFocusEvent) + """ + if evt.reason() != Qt.MouseFocusReason: + self.__tocWidget.setFocus() + + def keyPressEvent(self, evt): + """ + Protected method handling key press events. + + @param evt reference to the key press event (QKeyEvent) + """ + if evt.key() == Qt.Key_Escape: + self.escapePressed.emit() + + def eventFilter(self, watched, event): + """ + Public method called to filter the event queue. + + @param watched the QObject being watched (QObject) + @param event the event that occurred (QEvent) + @return flag indicating whether the event was handled (boolean) + """ + if self.__tocWidget and watched == self.__tocWidget.viewport() and \ + event.type() == QEvent.MouseButtonRelease: + if self.__tocWidget.indexAt(event.pos()).isValid() and \ + event.button() == Qt.LeftButton: + self.itemClicked(self.__tocWidget.currentIndex()) + elif self.__tocWidget.indexAt(event.pos()).isValid() and \ + event.button() == Qt.MidButton: + model = self.__tocWidget.model() + itm = model.contentItemAt(self.__tocWidget.currentIndex()) + self.__mw.newTab(itm.url()) + + return QWidget.eventFilter(self, watched, event) + + def itemClicked(self, index): + """ + Public slot handling a click of a TOC entry. + + @param index index of the TOC clicked (QModelIndex) + """ + if not index.isValid(): + return + + model = self.__tocWidget.model() + itm = model.contentItemAt(index) + if itm: + self.linkActivated.emit(itm.url()) + + def syncToContent(self, url): + """ + Public method to sync the TOC to the displayed page. + + @param url URL of the displayed page (QUrl) + @return flag indicating a successful synchronization (boolean) + """ + idx = self.__tocWidget.indexOf(url) + if not idx.isValid(): + return False + self.__tocWidget.setCurrentIndex(idx) + return True + + def __showContextMenu(self, pos): + """ + Private slot showing the context menu. + + @param pos position to show the menu at (QPoint) + """ + if not self.__tocWidget.indexAt(pos).isValid(): + return + + menu = QMenu() + curTab = menu.addAction(self.tr("Open Link")) + newTab = menu.addAction(self.tr("Open Link in New Tab")) + menu.move(self.__tocWidget.mapToGlobal(pos)) + + model = self.__tocWidget.model() + itm = model.contentItemAt(self.__tocWidget.currentIndex()) + + act = menu.exec_() + if act == curTab: + self.linkActivated.emit(itm.url()) + elif act == newTab: + self.__mw.newTab(itm.url())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/QtHelp/HelpTopicDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to select a help topic to display. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QDialog +from PyQt5.QtCore import QUrl + +from .Ui_HelpTopicDialog import Ui_HelpTopicDialog + + +class HelpTopicDialog(QDialog, Ui_HelpTopicDialog): + """ + Class implementing a dialog to select a help topic to display. + """ + def __init__(self, parent, keyword, links): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + @param keyword keyword for the link set (string) + @param links dictionary with help topic as key (string) and + URL as value (QUrl) + """ + super(HelpTopicDialog, self).__init__(parent) + self.setupUi(self) + + self.label.setText(self.tr("Choose a &topic for <b>{0}</b>:") + .format(keyword)) + + self.__links = links + for topic in sorted(self.__links): + self.topicsList.addItem(topic) + if self.topicsList.count() > 0: + self.topicsList.setCurrentRow(0) + self.topicsList.setFocus() + + self.topicsList.itemActivated.connect(self.accept) + + def link(self): + """ + Public method to the link of the selected topic. + + @return URL of the selected topic (QUrl) + """ + itm = self.topicsList.currentItem() + if itm is None: + return QUrl() + + topic = itm.text() + if topic == "" or topic not in self.__links: + return QUrl() + + return self.__links[topic]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/QtHelp/HelpTopicDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,80 @@ +<ui version="4.0" > + <class>HelpTopicDialog</class> + <widget class="QDialog" name="HelpTopicDialog" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle" > + <string>Select Help Topic</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout" > + <item> + <widget class="QLabel" name="label" > + <property name="text" > + <string>&Topics:</string> + </property> + <property name="buddy" > + <cstring>topicsList</cstring> + </property> + </widget> + </item> + <item> + <widget class="QListWidget" name="topicsList" /> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox" > + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons" > + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>topicsList</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>HelpTopicDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel" > + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel" > + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>HelpTopicDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel" > + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel" > + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/QtHelp/QtHelpDocumentationDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to manage the QtHelp documentation database. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt, QItemSelectionModel +from PyQt5.QtWidgets import QDialog +from PyQt5.QtHelp import QHelpEngineCore + +from E5Gui import E5MessageBox, E5FileDialog + +from .Ui_QtHelpDocumentationDialog import Ui_QtHelpDocumentationDialog + + +class QtHelpDocumentationDialog(QDialog, Ui_QtHelpDocumentationDialog): + """ + Class implementing a dialog to manage the QtHelp documentation database. + """ + def __init__(self, engine, parent): + """ + Constructor + + @param engine reference to the help engine (QHelpEngine) + @param parent reference to the parent widget (QWidget) + """ + super(QtHelpDocumentationDialog, self).__init__(parent) + self.setupUi(self) + + self.removeButton.setEnabled(False) + + self.__engine = engine + self.__mw = parent + + docs = self.__engine.registeredDocumentations() + self.documentsList.addItems(docs) + + self.__registeredDocs = [] + self.__unregisteredDocs = [] + self.__tabsToClose = [] + + @pyqtSlot() + def on_documentsList_itemSelectionChanged(self): + """ + Private slot handling a change of the documents selection. + """ + self.removeButton.setEnabled( + len(self.documentsList.selectedItems()) != 0) + + @pyqtSlot() + def on_addButton_clicked(self): + """ + Private slot to add documents to the help database. + """ + fileNames = E5FileDialog.getOpenFileNames( + self, + self.tr("Add Documentation"), + "", + self.tr("Qt Compressed Help Files (*.qch)")) + if not fileNames: + return + + for fileName in fileNames: + ns = QHelpEngineCore.namespaceName(fileName) + if not ns: + E5MessageBox.warning( + self, + self.tr("Add Documentation"), + self.tr( + """The file <b>{0}</b> is not a valid""" + """ Qt Help File.""").format(fileName) + ) + continue + + if len(self.documentsList.findItems(ns, Qt.MatchFixedString)): + E5MessageBox.warning( + self, + self.tr("Add Documentation"), + self.tr( + """The namespace <b>{0}</b> is already registered.""") + .format(ns) + ) + continue + + self.__engine.registerDocumentation(fileName) + self.documentsList.addItem(ns) + self.__registeredDocs.append(ns) + if ns in self.__unregisteredDocs: + self.__unregisteredDocs.remove(ns) + + @pyqtSlot() + def on_removeButton_clicked(self): + """ + Private slot to remove a document from the help database. + """ + res = E5MessageBox.yesNo( + self, + self.tr("Remove Documentation"), + self.tr( + """Do you really want to remove the selected documentation """ + """sets from the database?""")) + if not res: + return + + openedDocs = self.__mw.getSourceFileList() + + items = self.documentsList.selectedItems() + for item in items: + ns = item.text() + if ns in list(openedDocs.values()): + res = E5MessageBox.yesNo( + self, + self.tr("Remove Documentation"), + self.tr( + """Some documents currently opened reference the """ + """documentation you are attempting to remove. """ + """Removing the documentation will close those """ + """documents. Remove anyway?"""), + icon=E5MessageBox.Warning) + if not res: + return + self.__unregisteredDocs.append(ns) + for id in openedDocs: + if openedDocs[id] == ns and id not in self.__tabsToClose: + self.__tabsToClose.append(id) + itm = self.documentsList.takeItem(self.documentsList.row(item)) + del itm + + self.__engine.unregisterDocumentation(ns) + + if self.documentsList.count(): + self.documentsList.setCurrentRow( + 0, QItemSelectionModel.ClearAndSelect) + + def hasChanges(self): + """ + Public slot to test the dialog for changes. + + @return flag indicating presence of changes + """ + return len(self.__registeredDocs) > 0 or \ + len(self.__unregisteredDocs) > 0 + + def getTabsToClose(self): + """ + Public method to get the list of tabs to close. + + @return list of tab ids to be closed (list of integers) + """ + return self.__tabsToClose
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/QtHelp/QtHelpDocumentationDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QtHelpDocumentationDialog</class> + <widget class="QDialog" name="QtHelpDocumentationDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>425</width> + <height>391</height> + </rect> + </property> + <property name="windowTitle"> + <string>Manage QtHelp Documentation Database</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Registered Documents</string> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" rowspan="3"> + <widget class="QListWidget" name="documentsList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="addButton"> + <property name="toolTip"> + <string>Press to select QtHelp documents to add to the database</string> + </property> + <property name="text"> + <string>Add...</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected documents from the database</string> + </property> + <property name="text"> + <string>Remove</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>98</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>documentsList</tabstop> + <tabstop>addButton</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>QtHelpDocumentationDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>QtHelpDocumentationDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/QtHelp/QtHelpFiltersDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to manage the QtHelp filters. +""" + +from __future__ import unicode_literals + +import sqlite3 + +from PyQt5.QtCore import pyqtSlot, Qt, QItemSelectionModel +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem, QListWidgetItem, \ + QInputDialog, QLineEdit +from PyQt5.QtHelp import QHelpEngineCore + +from E5Gui import E5MessageBox + +from .Ui_QtHelpFiltersDialog import Ui_QtHelpFiltersDialog + + +class QtHelpFiltersDialog(QDialog, Ui_QtHelpFiltersDialog): + """ + Class implementing a dialog to manage the QtHelp filters. + """ + def __init__(self, engine, parent=None): + """ + Constructor + + @param engine reference to the help engine (QHelpEngine) + @param parent reference to the parent widget (QWidget) + """ + super(QtHelpFiltersDialog, self).__init__(parent) + self.setupUi(self) + + self.removeButton.setEnabled(False) + self.removeAttributeButton.setEnabled(False) + + self.__engine = engine + + self.filtersList.clear() + self.attributesList.clear() + + help = QHelpEngineCore(self.__engine.collectionFile()) + help.setupData() + + self.__removedFilters = [] + self.__filterMap = {} + self.__filterMapBackup = {} + self.__removedAttributes = [] + + for filter in help.customFilters(): + atts = help.filterAttributes(filter) + self.__filterMapBackup[filter] = atts + if filter not in self.__filterMap: + self.__filterMap[filter] = atts + + self.filtersList.addItems(sorted(self.__filterMap.keys())) + for attr in help.filterAttributes(): + QTreeWidgetItem(self.attributesList, [attr]) + self.attributesList.sortItems(0, Qt.AscendingOrder) + + if self.__filterMap: + self.filtersList.setCurrentRow(0) + + @pyqtSlot(QListWidgetItem, QListWidgetItem) + def on_filtersList_currentItemChanged(self, current, previous): + """ + Private slot to update the attributes depending on the current filter. + + @param current reference to the current item (QListWidgetitem) + @param previous reference to the previous current item + (QListWidgetItem) + """ + checkedList = [] + if current is not None: + checkedList = self.__filterMap[current.text()] + for index in range(0, self.attributesList.topLevelItemCount()): + itm = self.attributesList.topLevelItem(index) + if itm.text(0) in checkedList: + itm.setCheckState(0, Qt.Checked) + else: + itm.setCheckState(0, Qt.Unchecked) + + @pyqtSlot() + def on_filtersList_itemSelectionChanged(self): + """ + Private slot handling a change of selected filters. + """ + self.removeButton.setEnabled( + len(self.filtersList.selectedItems()) > 0) + + @pyqtSlot(QTreeWidgetItem, int) + def on_attributesList_itemChanged(self, item, column): + """ + Private slot to handle a change of an attribute. + + @param item reference to the changed item (QTreeWidgetItem) + @param column column containing the change (integer) + """ + if self.filtersList.currentItem() is None: + return + + filter = self.filtersList.currentItem().text() + if filter not in self.__filterMap: + return + + newAtts = [] + for index in range(0, self.attributesList.topLevelItemCount()): + itm = self.attributesList.topLevelItem(index) + if itm.checkState(0) == Qt.Checked: + newAtts.append(itm.text(0)) + self.__filterMap[filter] = newAtts + + @pyqtSlot() + def on_attributesList_itemSelectionChanged(self): + """ + Private slot handling the selection of attributes. + """ + self.removeAttributeButton.setEnabled( + len(self.attributesList.selectedItems()) != 0) + + @pyqtSlot() + def on_addButton_clicked(self): + """ + Private slot to add a new filter. + """ + filter, ok = QInputDialog.getText( + None, + self.tr("Add Filter"), + self.tr("Filter name:"), + QLineEdit.Normal) + if not filter: + return + + if filter not in self.__filterMap: + self.__filterMap[filter] = [] + self.filtersList.addItem(filter) + + itm = self.filtersList.findItems(filter, Qt.MatchCaseSensitive)[0] + self.filtersList.setCurrentItem(itm) + + @pyqtSlot() + def on_removeButton_clicked(self): + """ + Private slot to remove the selected filters. + """ + ok = E5MessageBox.yesNo( + self, + self.tr("Remove Filters"), + self.tr( + """Do you really want to remove the selected filters """ + """from the database?""")) + if not ok: + return + + items = self.filtersList.selectedItems() + for item in items: + itm = self.filtersList.takeItem(self.filtersList.row(item)) + if itm is None: + continue + + del self.__filterMap[itm.text()] + self.__removedFilters.append(itm.text()) + del itm + + if self.filtersList.count(): + self.filtersList.setCurrentRow( + 0, QItemSelectionModel.ClearAndSelect) + + @pyqtSlot() + def on_removeAttributeButton_clicked(self): + """ + Private slot to remove the selected filter attributes. + """ + ok = E5MessageBox.yesNo( + self, + self.tr("Remove Attributes"), + self.tr( + """Do you really want to remove the selected attributes """ + """from the database?""")) + if not ok: + return + + items = self.attributesList.selectedItems() + for item in items: + itm = self.attributesList.takeTopLevelItem( + self.attributesList.indexOfTopLevelItem(item)) + if itm is None: + continue + + attr = itm.text(0) + self.__removedAttributes.append(attr) + for filter in self.__filterMap: + if attr in self.__filterMap[filter]: + self.__filterMap[filter].remove(attr) + + del itm + + @pyqtSlot() + def on_unusedAttributesButton_clicked(self): + """ + Private slot to select all unused attributes. + """ + # step 1: determine all used attributes + attributes = set() + for filter in self.__filterMap: + attributes |= set(self.__filterMap[filter]) + + # step 2: select all unused attribute items + self.attributesList.clearSelection() + for row in range(self.attributesList.topLevelItemCount()): + itm = self.attributesList.topLevelItem(row) + if itm.text(0) not in attributes: + itm.setSelected(True) + + def __removeAttributes(self): + """ + Private method to remove attributes from the Qt Help database. + """ + try: + self.__db = sqlite3.connect(self.__engine.collectionFile()) + except sqlite3.DatabaseError: + pass # ignore database errors + + for attr in self.__removedAttributes: + self.__db.execute( + "DELETE FROM FilterAttributeTable WHERE Name = '{0}'" + .format(attr)) + self.__db.commit() + self.__db.close() + + @pyqtSlot() + def on_buttonBox_accepted(self): + """ + Private slot to update the database, if the dialog is accepted. + """ + filtersChanged = False + if len(self.__filterMapBackup) != len(self.__filterMap): + filtersChanged = True + else: + for filter in self.__filterMapBackup: + if filter not in self.__filterMap: + filtersChanged = True + else: + oldFilterAtts = self.__filterMapBackup[filter] + newFilterAtts = self.__filterMap[filter] + if len(oldFilterAtts) != len(newFilterAtts): + filtersChanged = True + else: + for attr in oldFilterAtts: + if attr not in newFilterAtts: + filtersChanged = True + break + + if filtersChanged: + break + + if filtersChanged: + for filter in self.__removedFilters: + self.__engine.removeCustomFilter(filter) + for filter in self.__filterMap: + self.__engine.addCustomFilter(filter, self.__filterMap[filter]) + + if self.__removedAttributes: + self.__removeAttributes() + + if filtersChanged or self.__removedAttributes: + self.__engine.setupData() + + self.accept()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/QtHelp/QtHelpFiltersDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QtHelpFiltersDialog</class> + <widget class="QDialog" name="QtHelpFiltersDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>570</width> + <height>391</height> + </rect> + </property> + <property name="windowTitle"> + <string>Manage QtHelp Filters</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Filters:</string> + </property> + </widget> + </item> + <item row="0" column="2" colspan="2"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Attributes:</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QListWidget" name="filtersList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="2" colspan="2"> + <widget class="QTreeWidget" name="attributesList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <property name="headerHidden"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>1</string> + </property> + </column> + </widget> + </item> + <item row="2" column="0"> + <widget class="QPushButton" name="addButton"> + <property name="toolTip"> + <string>Press to add a new filter</string> + </property> + <property name="text"> + <string>Add Filter ...</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected filters</string> + </property> + <property name="text"> + <string>Remove Filters</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QPushButton" name="removeAttributeButton"> + <property name="toolTip"> + <string>Press to remove the selected attributes</string> + </property> + <property name="text"> + <string>Remove Attributes</string> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QPushButton" name="unusedAttributesButton"> + <property name="statusTip"> + <string>Press to select all unused attributes</string> + </property> + <property name="text"> + <string>Select Unused</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>filtersList</tabstop> + <tabstop>addButton</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>attributesList</tabstop> + <tabstop>removeAttributeButton</tabstop> + <tabstop>unusedAttributesButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>QtHelpFiltersDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>320</x> + <y>386</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/QtHelp/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package containing the interface to QtHelp. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SearchWidget.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the search bar for the web browser. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt +from PyQt5.QtGui import QPalette, QBrush, QColor +from PyQt5.QtWidgets import QWidget + +from .Ui_SearchWidget import Ui_SearchWidget + +import UI.PixmapCache + + +class SearchWidget(QWidget, Ui_SearchWidget): + """ + Class implementing the search bar for the web browser. + """ + def __init__(self, mainWindow, parent=None): + """ + Constructor + + @param mainWindow reference to the main window (QMainWindow) + @param parent parent widget of this dialog (QWidget) + """ + super(SearchWidget, self).__init__(parent) + self.setupUi(self) + + self.__mainWindow = mainWindow + + self.closeButton.setIcon(UI.PixmapCache.getIcon("close.png")) + self.findPrevButton.setIcon(UI.PixmapCache.getIcon("1leftarrow.png")) + self.findNextButton.setIcon(UI.PixmapCache.getIcon("1rightarrow.png")) + + self.__defaultBaseColor = \ + self.findtextCombo.lineEdit().palette().color(QPalette.Base) + self.__defaultTextColor = \ + self.findtextCombo.lineEdit().palette().color(QPalette.Text) + + self.__findHistory = [] + self.__havefound = False + self.__findBackwards = False + + self.findtextCombo.setCompleter(None) + self.findtextCombo.lineEdit().returnPressed.connect( + self.__findByReturnPressed) + self.findtextCombo.lineEdit().textEdited.connect( + self.__searchTextEdited) + + def on_findtextCombo_editTextChanged(self, txt): + """ + Private slot to enable/disable the find buttons. + + @param txt text of the combobox (string) + """ + self.findPrevButton.setEnabled(txt != "") + self.findNextButton.setEnabled(txt != "") + + def __searchTextEdited(self, txt): + """ + Private slot to perform an incremental search. + + @param txt current text of the search combos line edit (string) + (unused) + """ + self.__findNextPrev() + + def __findNextPrev(self): + """ + Private slot to find the next occurrence of text. + """ + self.infoLabel.clear() + self.__setFindtextComboBackground(False) + + if not self.findtextCombo.currentText(): + return + + self.__mainWindow.currentBrowser().findNextPrev( + self.findtextCombo.currentText(), + self.caseCheckBox.isChecked(), + self.__findBackwards, + self.__findNextPrevCallback) + + def __findNextPrevCallback(self, found): + """ + Private method to process the result of the last search. + + @param found flag indicating if the last search succeeded + @type bool + """ + if not found: + self.infoLabel.setText(self.tr("Expression was not found.")) + self.__setFindtextComboBackground(True) + + @pyqtSlot() + def on_findNextButton_clicked(self): + """ + Private slot to find the next occurrence. + """ + txt = self.findtextCombo.currentText() + + # This moves any previous occurrence of this statement to the head + # of the list and updates the combobox + if txt in self.__findHistory: + self.__findHistory.remove(txt) + self.__findHistory.insert(0, txt) + self.findtextCombo.clear() + self.findtextCombo.addItems(self.__findHistory) + + self.__findBackwards = False + self.__findNextPrev() + + def findNext(self): + """ + Public slot to find the next occurrence. + """ + if not self.__havefound or not self.findtextCombo.currentText(): + self.showFind() + return + + self.on_findNextButton_clicked() + + @pyqtSlot() + def on_findPrevButton_clicked(self): + """ + Private slot to find the previous occurrence. + """ + txt = self.findtextCombo.currentText() + + # This moves any previous occurrence of this statement to the head + # of the list and updates the combobox + if txt in self.__findHistory: + self.__findHistory.remove(txt) + self.__findHistory.insert(0, txt) + self.findtextCombo.clear() + self.findtextCombo.addItems(self.__findHistory) + + self.__findBackwards = True + self.__findNextPrev() + + def findPrevious(self): + """ + Public slot to find the previous occurrence. + """ + if not self.__havefound or not self.findtextCombo.currentText(): + self.showFind() + return + + self.on_findPrevButton_clicked() + + def __findByReturnPressed(self): + """ + Private slot to handle the returnPressed signal of the findtext + combobox. + """ + if self.__findBackwards: + self.on_findPrevButton_clicked() + else: + self.on_findNextButton_clicked() + + def showFind(self): + """ + Public method to display this dialog. + """ + self.__havefound = True + self.__findBackwards = False + + self.findtextCombo.clear() + self.findtextCombo.addItems(self.__findHistory) + self.findtextCombo.setEditText('') + self.findtextCombo.setFocus() + + self.caseCheckBox.setChecked(False) + + if self.__mainWindow.currentBrowser().hasSelection(): + self.findtextCombo.setEditText( + self.__mainWindow.currentBrowser().selectedText()) + + self.__setFindtextComboBackground(False) + self.show() + + @pyqtSlot() + def on_closeButton_clicked(self): + """ + Private slot to close the widget. + """ + self.close() + + def keyPressEvent(self, event): + """ + Protected slot to handle key press events. + + @param event reference to the key press event (QKeyEvent) + """ + if event.key() == Qt.Key_Escape: + cb = self.__mainWindow.currentBrowser() + if cb: + cb.setFocus(Qt.ActiveWindowFocusReason) + event.accept() + self.close() + + def __setFindtextComboBackground(self, error): + """ + Private slot to change the findtext combo background to indicate + errors. + + @param error flag indicating an error condition (boolean) + """ + le = self.findtextCombo.lineEdit() + p = le.palette() + if error: + p.setBrush(QPalette.Base, QBrush(QColor("#FF6666"))) + p.setBrush(QPalette.Text, QBrush(QColor("#000000"))) + else: + p.setBrush(QPalette.Base, self.__defaultBaseColor) + p.setBrush(QPalette.Text, self.__defaultTextColor) + le.setPalette(p) + le.update()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SearchWidget.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,117 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SearchWidget</class> + <widget class="QWidget" name="SearchWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>747</width> + <height>26</height> + </rect> + </property> + <property name="windowTitle"> + <string>Find</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QToolButton" name="closeButton"> + <property name="toolTip"> + <string>Press to close the window</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Find:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="findtextCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="editable"> + <bool>true</bool> + </property> + <property name="insertPolicy"> + <enum>QComboBox::InsertAtTop</enum> + </property> + <property name="duplicatesEnabled"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="findPrevButton"> + <property name="toolTip"> + <string>Press to find the previous occurrence</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="findNextButton"> + <property name="toolTip"> + <string>Press to find the next occurrence</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="caseCheckBox"> + <property name="text"> + <string>Match case</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="infoLine"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="infoLabel"> + <property name="minimumSize"> + <size> + <width>200</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>findtextCombo</tabstop> + <tabstop>caseCheckBox</tabstop> + <tabstop>findNextButton</tabstop> + <tabstop>findPrevButton</tabstop> + <tabstop>closeButton</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SiteInfo/SiteInfoDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show some information about a site. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, QUrl, Qt +from PyQt5.QtGui import QPixmap, QImage +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem, QGraphicsScene, QMenu, \ + QApplication, QGraphicsPixmapItem + +from E5Gui import E5MessageBox, E5FileDialog + +from .Ui_SiteInfoDialog import Ui_SiteInfoDialog + +from ..Tools import Scripts, WebBrowserTools + +import UI.PixmapCache + + +class SiteInfoDialog(QDialog, Ui_SiteInfoDialog): + """ + Class implementing a dialog to show some information about a site. + """ + okStyle = "QLabel { color : white; background-color : green; }" + nokStyle = "QLabel { color : white; background-color : red; }" + + def __init__(self, browser, parent=None): + """ + Constructor + + @param browser reference to the browser window (HelpBrowser) + @param parent reference to the parent widget (QWidget) + """ + super(SiteInfoDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + # put icons + self.tabWidget.setTabIcon( + 0, UI.PixmapCache.getIcon("siteinfo-general.png")) + self.tabWidget.setTabIcon( + 1, UI.PixmapCache.getIcon("siteinfo-media.png")) + + self.__imageReply = None + + self.__baseUrl = browser.url() + title = browser.title() + + # populate General tab + self.heading.setText("<b>{0}</b>".format(title)) + self.siteAddressLabel.setText(self.__baseUrl.toString()) + if self.__baseUrl.scheme() in ["https"]: + self.securityLabel.setStyleSheet(SiteInfoDialog.okStyle) + self.securityLabel.setText('<b>Connection is encrypted.</b>') + else: + self.securityLabel.setStyleSheet(SiteInfoDialog.nokStyle) + self.securityLabel.setText('<b>Connection is not encrypted.</b>') + browser.page().runJavaScript( + "document.charset", + lambda res: self.encodingLabel.setText(res)) + + # populate Meta tags + browser.page().runJavaScript(Scripts.getAllMetaAttributes(), + self.__processMetaAttributes) + + # populate Media tab + browser.page().runJavaScript(Scripts.getAllImages(), + self.__processImageTags) + + def __processImageTags(self, res): + """ + Private method to process the image tags. + + @param res result of the JavaScript script + @type list of dict + """ + for img in res: + src = img["src"] + alt = img["alt"] + if not alt: + if src.find("/") == -1: + alt = src + else: + pos = src.rfind("/") + alt = src[pos + 1:] + + if not src or not alt: + continue + + QTreeWidgetItem(self.imagesTree, [alt, src]) + + for col in range(self.imagesTree.columnCount()): + self.imagesTree.resizeColumnToContents(col) + if self.imagesTree.columnWidth(0) > 300: + self.imagesTree.setColumnWidth(0, 300) + self.imagesTree.setCurrentItem(self.imagesTree.topLevelItem(0)) + self.imagesTree.setContextMenuPolicy(Qt.CustomContextMenu) + self.imagesTree.customContextMenuRequested.connect( + self.__imagesTreeContextMenuRequested) + + def __processMetaAttributes(self, res): + """ + Private method to process the meta attributes. + + @param res result of the JavaScript script + @type list of dict + """ + for meta in res: + content = meta["content"] + name = meta["name"] + if not name: + name = meta["httpequiv"] + + if not name or not content: + continue + + if meta["charset"]: + self.encodingLabel.setText(meta["charset"]) + if "charset=" in content: + self.encodingLabel.setText( + content[content.index("charset=") + 8:]) + + QTreeWidgetItem(self.tagsTree, [name, content]) + for col in range(self.tagsTree.columnCount()): + self.tagsTree.resizeColumnToContents(col) + + @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) + def on_imagesTree_currentItemChanged(self, current, previous): + """ + Private slot to show a preview of the selected image. + + @param current current image entry (QTreeWidgetItem) + @param previous old current entry (QTreeWidgetItem) + """ + if current is None: + return + + imageUrl = QUrl(current.text(1)) + if imageUrl.isRelative(): + imageUrl = self.__baseUrl.resolved(imageUrl) + + pixmap = QPixmap() + loading = False + + if imageUrl.scheme() == "data": + encodedUrl = current.text(1).encode("utf-8") + imageData = encodedUrl[encodedUrl.find(",") + 1:] + pixmap = WebBrowserTools.pixmapFromByteArray(imageData) + elif imageUrl.scheme() == "file": + pixmap = QPixmap(imageUrl.toLocalFile()) + elif imageUrl.scheme() == "qrc": + pixmap = QPixmap(imageUrl.toString()[3:]) + else: + if self.__imageReply is not None: + self.__imageReply.deleteLater() + self.__imageReply = None + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + self.__imageReply = WebBrowserWindow.networkManager().get( + QNetworkRequest(imageUrl)) + self.__imageReply.finished.connect(self.__imageReplyFinished) + loading = True + self.__showLoadingText() + + if not loading: + self.__showPixmap(pixmap) + + @pyqtSlot() + def __imageReplyFinished(self): + """ + Private slot handling the loading of an image. + """ + if self.__imageReply.error() != QNetworkReply.NoError: + return + + data = self.__imageReply.readAll() + self.__showPixmap(QPixmap.fromImage(QImage.fromData(data))) + + def __showPixmap(self, pixmap): + """ + Private method to show a pixmap in the preview pane. + + @param pixmap pixmap to be shown + @type QPixmap + """ + scene = QGraphicsScene(self.imagePreview) + if pixmap.isNull(): + scene.addText(self.tr("Preview not available.")) + else: + scene.addPixmap(pixmap) + self.imagePreview.setScene(scene) + + def __showLoadingText(self): + """ + Private method to show some text while loading an image. + """ + scene = QGraphicsScene(self.imagePreview) + scene.addText(self.tr("Loading...")) + self.imagePreview.setScene(scene) + + def __imagesTreeContextMenuRequested(self, pos): + """ + Private slot to show a context menu for the images list. + + @param pos position for the menu (QPoint) + """ + itm = self.imagesTree.itemAt(pos) + if itm is None: + return + + menu = QMenu() + menu.addAction( + self.tr("Copy Image Location to Clipboard"), + self.__copyAction).setData(itm.text(1)) + menu.addAction( + self.tr("Copy Image Name to Clipboard"), + self.__copyAction).setData(itm.text(0)) + menu.addSeparator() + menu.addAction( + self.tr("Save Image"), + self.__saveImage).setData(self.imagesTree.indexOfTopLevelItem(itm)) + menu.exec_(self.imagesTree.viewport().mapToGlobal(pos)) + + def __copyAction(self): + """ + Private slot to copy the image URL or the image name to the clipboard. + """ + act = self.sender() + QApplication.clipboard().setText(act.data()) + + def __saveImage(self): + """ + Private slot to save the selected image to disk. + """ + act = self.sender() + index = act.data() + itm = self.imagesTree.topLevelItem(index) + if itm is None: + return + + if not self.imagePreview.scene() or \ + len(self.imagePreview.scene().items()) == 0: + return + + pixmapItem = self.imagePreview.scene().items()[0] + if not isinstance(pixmapItem, QGraphicsPixmapItem): + return + + if pixmapItem.pixmap().isNull(): + E5MessageBox.warning( + self, + self.tr("Save Image"), + self.tr( + """<p>This preview is not available.</p>""")) + return + + imageFileName = WebBrowserTools.getFileNameFromUrl(QUrl(itm.text(1))) + index = imageFileName.rfind(".") + if index != -1: + imageFileName = imageFileName[:index] + ".png" + + filename = E5FileDialog.getSaveFileName( + self, + self.tr("Save Image"), + imageFileName, + self.tr("All Files (*)"), + E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) + + if not filename: + return + + if not pixmapItem.pixmap().save(filename, "PNG"): + E5MessageBox.critical( + self, + self.tr("Save Image"), + self.tr( + """<p>Cannot write to file <b>{0}</b>.</p>""") + .format(filename)) + return
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SiteInfo/SiteInfoDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,262 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SiteInfoDialog</class> + <widget class="QDialog" name="SiteInfoDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>700</width> + <height>550</height> + </rect> + </property> + <property name="windowTitle"> + <string>Site Information</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="heading"> + <property name="text"> + <string notr="true"/> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="generalTab"> + <attribute name="title"> + <string>General</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Site Address:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="siteAddressLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Encoding:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="encodingLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Meta tags of site:</string> + </property> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="tagsTree"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="wordWrap"> + <bool>false</bool> + </property> + <column> + <property name="text"> + <string>Tag</string> + </property> + </column> + <column> + <property name="text"> + <string>Value</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0" colspan="4"> + <widget class="QLabel" name="label_4"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string><b>Security information</b></string> + </property> + </widget> + </item> + <item row="1" column="0"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="securityLabel"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="1" column="2"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QWidget" name="mediaTab"> + <attribute name="title"> + <string>Media</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QTreeWidget" name="imagesTree"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <column> + <property name="text"> + <string>Image</string> + </property> + </column> + <column> + <property name="text"> + <string>Image Address</string> + </property> + </column> + </widget> + </item> + <item> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string><b>Preview</b></string> + </property> + </widget> + </item> + <item> + <widget class="QGraphicsView" name="imagePreview"/> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>tagsTree</tabstop> + <tabstop>imagesTree</tabstop> + <tabstop>imagePreview</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SiteInfoDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>SiteInfoDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SiteInfo/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the site info widgets. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SpeedDial/Page.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a structure to hold the data for a speed dial page. +""" + + +from __future__ import unicode_literals + + +class Page(object): + """ + Class to hold the data for a speed dial page. + """ + def __init__(self, url="", title="", broken=False): + """ + Constructor + + @param url URL of the page (string) + @param title title of the page (string) + @param broken flag indicating a broken connection (boolean) + """ + self.url = url + self.title = title + self.broken = broken + + def __eq__(self, other): + """ + Special method implementing the equality operator. + + @param other reference to the other page object (Page) + @return flag indicating equality (boolean) + """ + return self.title == other.title and \ + self.url == other.url + + def isValid(self): + """ + Public method to check the validity. + + @return flag indicating a valid object + @rtype bool + """ + return bool(self.url)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SpeedDial/PageThumbnailer.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an object to create a thumbnail image of a web site. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSize, Qt, QUrl, \ + QTimer +from PyQt5.QtGui import QPixmap +from PyQt5.QtQuickWidgets import QQuickWidget + + +class PageThumbnailer(QObject): + """ + Class implementing a thumbnail creator for web sites. + + @signal thumbnailCreated(QPixmap) emitted after the thumbnail has been + created + """ + thumbnailCreated = pyqtSignal(QPixmap) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(PageThumbnailer, self).__init__(parent) + + self.__size = QSize(231, 130) + self.__loadTitle = False + self.__title = "" + self.__url = QUrl() + + self.__view = QQuickWidget() + self.__view.setAttribute(Qt.WA_DontShowOnScreen) + self.__view.setSource(QUrl("qrc:qml/thumbnailer.qml")) + self.__view.rootContext().setContextProperty("thumbnailer", self) + self.__view.show() + + def setSize(self, size): + """ + Public method to set the size of the image. + + @param size size of the image (QSize) + """ + if size.isValid(): + self.__size = QSize(size) + + def setUrl(self, url): + """ + Public method to set the URL of the site to be thumbnailed. + + @param url URL of the web site (QUrl) + """ + if url.isValid(): + self.__url = QUrl(url) + + def url(self): + """ + Public method to get the URL of the thumbnail. + + @return URL of the thumbnail (QUrl) + """ + return QUrl(self.__url) + + def loadTitle(self): + """ + Public method to check, if the title is loaded from the web site. + + @return flag indicating, that the title is loaded (boolean) + """ + return self.__loadTitle + + def setLoadTitle(self, load): + """ + Public method to set a flag indicating to load the title from + the web site. + + @param load flag indicating to load the title (boolean) + """ + self.__loadTitle = load + + def title(self): + """ + Public method to get the title of the thumbnail. + + @return title of the thumbnail (string) + """ + if self.__title: + title = self.__title + else: + title = self.__url.host() + if not title: + title = self.__url.toString() + return title + + def start(self): + """ + Public method to start the thumbnailing action. + """ + if self.__view.rootObject(): + self.__view.rootObject().setProperty("url", self.__url) + else: + QTimer.singleShot(0, lambda: self.thumbnailCreated.emit(QPixmap())) + + @pyqtSlot(bool) + def createThumbnail(self, status): + """ + Private slot creating the thumbnail of the web site. + + @param status flag indicating a successful load of the web site + (boolean) + """ + if not status: + self.thumbnailCreated.emit(QPixmap()) + return + + QTimer.singleShot(1000, self.__grabThumbnail) + + def __grabThumbnail(self): + """ + Private slot to grab the thumbnail image from the view. + """ + self.__title = self.__view.rootObject().property("title") + pixmap = QPixmap.fromImage( + self.__view.grabFramebuffer().scaled( + self.__size, Qt.KeepAspectRatioByExpanding, + Qt.SmoothTransformation)) + self.thumbnailCreated.emit(pixmap)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SpeedDial/SpeedDial.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,431 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the speed dial. +""" + +from __future__ import unicode_literals +try: + str = unicode +except NameError: + pass + +import os + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QCryptographicHash, \ + QByteArray, QUrl, qWarning +from PyQt5.QtGui import QPixmap + +from E5Gui import E5MessageBox + +from ..Tools.WebBrowserTools import pixmapToDataUrl + +from Utilities.AutoSaver import AutoSaver +import Utilities + + +class SpeedDial(QObject): + """ + Class implementing the speed dial. + + @signal pagesChanged() emitted after the list of pages changed + @signal thumbnailLoaded(url, src) emitted after a thumbnail was loaded + @signal titleLoaded(url, title) emitted after a title was loaded + @signal speedDialSaved() emitted after the speed dial data was saved + """ + pagesChanged = pyqtSignal() + thumbnailLoaded = pyqtSignal(str, str) + pageTitleLoaded = pyqtSignal(str, str) + speedDialSaved = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(SpeedDial, self).__init__(parent) + + self.__regenerateScript = True + + self.__webPages = [] + + self.__initialScript = "" + self.__thumbnailsDirectory = "" + + self.__thumbnailers = [] + + self.__initialize() + + self.__saveTimer = AutoSaver(self, self.save) + self.pagesChanged.connect(self.__saveTimer.changeOccurred) + + def addPage(self, url, title): + """ + Public method to add a page for the given data. + + @param url URL of the page (QUrl) + @param title title of the page (string) + """ + if url.isEmpty(): + return + + from .Page import Page + page = Page( + self.__escapeUrl(url.toString()), + self.__escapeTitle(title)) + self.__webPages.append(page) + self.__regenerateScript = True + + self.pagesChanged.emit() + + def removePage(self, url): + """ + Public method to remove a page. + + @param url URL of the page (QUrl) + """ + page = self.pageForUrl(url) + if not page.isValid(): + return + + self.removeImageForUrl(page.url) + self.__webPages.remove(page) + self.__regenerateScript = True + + self.pagesChanged.emit() + + def __imageFileName(self, url): + """ + Private method to generate the image file name for a URL. + + @param url URL to generate the file name from (string) + @return name of the image file (string) + """ + return os.path.join( + self.__thumbnailsDirectory, + str(QCryptographicHash.hash(QByteArray(url.encode("utf-8")), + QCryptographicHash.Md5).toHex(), encoding="utf-8") + ".png") + + def initialScript(self): + """ + Public method to get the 'initial' JavaScript script. + + @return initial JavaScript script (string) + """ + if self.__regenerateScript: + self.__regenerateScript = False + self.__initialScript = "" + + for page in self.__webPages: + if page.broken: + imgSource = "qrc:icons/brokenPage.png" + else: + imgSource = self.__imageFileName(page.url) + if not os.path.exists(imgSource): + self.loadThumbnail(page.url) + imgSource = "qrc:icons/loading.gif" + + if not page.url: + imgSource = "" + else: + imgSource = \ + pixmapToDataUrl(QPixmap(imgSource)).toString() + + self.__initialScript += \ + "addBox('{0}', '{1}', '{2}');\n".format( + page.url, Utilities.html_uencode(page.title), + imgSource) + + return self.__initialScript + + def getFileName(self): + """ + Public method to get the file name of the user agents file. + + @return name of the user agents file (string) + """ + return os.path.join( + Utilities.getConfigDir(), "web_browser", "speedDial.xml") + + def __initialize(self): + """ + Private method to initialize the speed dial. + """ + self.__thumbnailsDirectory = os.path.join( + Utilities.getConfigDir(), "web_browser", "thumbnails") + # Create directory if it does not exist yet + if not os.path.exists(self.__thumbnailsDirectory): + os.makedirs(self.__thumbnailsDirectory) + + self.__load() + + def reload(self): + """ + Public method to reload the speed dial data. + """ + self.__load() + + def __load(self): + """ + Private method to load the speed dial configuration. + """ + allPages, pagesPerRow, speedDialSize = [], 0, 0 + + speedDialFile = self.getFileName() + if os.path.exists(speedDialFile): + from .SpeedDialReader import SpeedDialReader + reader = SpeedDialReader() + allPages, pagesPerRow, speedDialSize = reader.read(speedDialFile) + + self.__pagesPerRow = pagesPerRow if pagesPerRow else 4 + self.__speedDialSize = speedDialSize if speedDialSize else 231 + + if allPages: + self.__webPages = allPages + self.pagesChanged.emit() + else: + allPages = \ + 'url:"http://eric-ide.python-projects.org/"|'\ + 'title:"Eric Web Site";'\ + 'url:"https://www.riverbankcomputing.com/"|'\ + 'title:"PyQt Web Site";'\ + 'url:"http://www.qt.io/"|title:"Qt Web Site";'\ + 'url:"http://blog.qt.io/"|title:"Qt Blog";'\ + 'url:"https://www.python.org"|'\ + 'title:"Python Language Website";'\ + 'url:"http://www.google.com"|title:"Google";' + self.changed(allPages) + + def save(self): + """ + Public method to save the speed dial configuration. + """ + from .SpeedDialWriter import SpeedDialWriter + speedDialFile = self.getFileName() + writer = SpeedDialWriter() + if not writer.write(speedDialFile, self.__webPages, + self.__pagesPerRow, self.__speedDialSize): + E5MessageBox.critical( + None, + self.tr("Saving Speed Dial data"), + self.tr( + """<p>Speed Dial data could not be saved to""" + """ <b>{0}</b></p>""").format(speedDialFile)) + else: + self.speedDialSaved.emit() + + def close(self): + """ + Public method to close the user agents manager. + """ + self.__saveTimer.saveIfNeccessary() + + def pageForUrl(self, url): + """ + Public method to get the page for the given URL. + + @param url URL to be searched for (QUrl) + @return page for the URL (Page) + """ + urlString = url.toString() + if urlString.endswith("/"): + urlString = urlString[:-1] + + for page in self.__webPages: + if page.url == urlString: + return page + + from .Page import Page + return Page() + + def urlForShortcut(self, key): + """ + Public method to get the URL for the given shortcut key. + + @param key shortcut key (integer) + @return URL for the key (QUrl) + """ + if key < 0 or len(self.__webPages) <= key: + return QUrl() + + return QUrl.fromEncoded(self.__webPages[key].url.encode("utf-8")) + + @pyqtSlot(str) + def changed(self, allPages): + """ + Public slot to react on changed pages. + + @param allPages string giving all pages (string) + """ + if not allPages: + return + + entries = allPages.split('";') + self.__webPages = [] + + from .Page import Page + for entry in entries: + if not entry: + continue + + tmp = entry.split('"|') + if len(tmp) == 2: + broken = False + elif len(tmp) == 3: + broken = "brokenPage" in tmp[2][5:] + else: + continue + + url = tmp[0][5:] + if url.endswith("/"): + url = url[:-1] + title = tmp[1][7:] + page = Page(url, title, broken) + self.__webPages.append(page) + + self.pagesChanged.emit() + + @pyqtSlot(str, bool) + def loadThumbnail(self, url, loadTitle): + """ + Public slot to load a thumbnail of the given URL. + + @param url URL of the thumbnail (string) + @param loadTitle flag indicating to get the title for the thumbnail + from the site (boolean) + """ + if not url: + return + + from .PageThumbnailer import PageThumbnailer + thumbnailer = PageThumbnailer(self) + thumbnailer.setUrl(QUrl.fromEncoded(url.encode("utf-8"))) + thumbnailer.setLoadTitle(loadTitle) + thumbnailer.thumbnailCreated.connect(self.__thumbnailCreated) + self.__thumbnailers.append(thumbnailer) + + thumbnailer.start() + + @pyqtSlot(str) + def removeImageForUrl(self, url): + """ + Public slot to remove the image for a URL. + + @param url URL to remove the image for (string) + """ + fileName = self.__imageFileName(url) + if os.path.exists(fileName): + os.remove(fileName) + + @pyqtSlot(str, result=str) + def urlFromUserInput(self, url): + """ + Public slot to get the URL from user input. + + @param url URL entered by the user (string) + @return sanitized URL (string) + """ + return QUrl.fromUserInput(url).toString() + + @pyqtSlot(int) + def setPagesInRow(self, count): + """ + Public slot to set the number of pages per row. + + @param count number of pages per row (integer) + """ + self.__pagesPerRow = count + self.__saveTimer.changeOccurred() + + def pagesInRow(self): + """ + Public method to get the number of dials per row. + + @return number of dials per row (integer) + """ + return self.__pagesPerRow + + @pyqtSlot(int) + def setSdSize(self, size): + """ + Public slot to set the size of the speed dial. + + @param size size of the speed dial (integer) + """ + self.__speedDialSize = size + self.__saveTimer.changeOccurred() + + def sdSize(self): + """ + Public method to get the speed dial size. + + @return speed dial size (integer) + """ + return self.__speedDialSize + + def __thumbnailCreated(self, image): + """ + Private slot to handle the creation of a thumbnail image. + + @param image thumbnail image (QPixmap) + """ + from .PageThumbnailer import PageThumbnailer + thumbnailer = self.sender() + if not isinstance(thumbnailer, PageThumbnailer) or \ + thumbnailer not in self.__thumbnailers: + return + + loadTitle = thumbnailer.loadTitle() + title = thumbnailer.title() + url = thumbnailer.url().toString() + fileName = self.__imageFileName(url) + + if image.isNull(): + fileName = "qrc:icons/brokenPage.png" + title = self.tr("Unable to load") + loadTitle = True + page = self.pageForUrl(thumbnailer.url()) + page.broken = True + else: + if not image.save(fileName, "PNG"): + qWarning( + "SpeedDial.__thumbnailCreated: Cannot save thumbnail" + " to {0}".format(fileName)) + + self.__regenerateScript = True + thumbnailer.deleteLater() + self.__thumbnailers.remove(thumbnailer) + + if loadTitle: + self.pageTitleLoaded.emit(url, title) + + self.thumbnailLoaded.emit( + url, pixmapToDataUrl(QPixmap(fileName)).toString()) + + def __escapeTitle(self, title): + """ + Private method to escape a title string. + + @param title title string to be escaped + @type str + @return escaped title string + @rtype str + """ + title = title.replace('"', """).replace("'", "'") + return title + + def __escapeUrl(self, url): + """ + Private method to escape an URL string. + + @param url URL to be escaped + @type str + @return escaped URL string + @rtype str + """ + url = url.replace('"', "").replace("'", "") + return url
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SpeedDial/SpeedDialReader.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + + +""" +Module implementing a class to read speed dial data files. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamReader, QIODevice, QFile, QCoreApplication + + +class SpeedDialReader(QXmlStreamReader): + """ + Class implementing a reader object for speed dial data files. + """ + def __init__(self): + """ + Constructor + """ + super(SpeedDialReader, self).__init__() + + def read(self, fileNameOrDevice): + """ + Public method to read a user agent file. + + @param fileNameOrDevice name of the file to read (string) + or reference to the device to read (QIODevice) + @return list of speed dial pages (list of Page), number of pages per + row (integer) and size of the speed dial pages (integer) + """ + self.__pages = [] + self.__pagesPerRow = 0 + self.__sdSize = 0 + + if isinstance(fileNameOrDevice, QIODevice): + self.setDevice(fileNameOrDevice) + else: + f = QFile(fileNameOrDevice) + if not f.exists(): + return self.__pages, self.__pagesPerRow, self.__sdSize + opened = f.open(QFile.ReadOnly) + if not opened: + self.raiseError(QCoreApplication.translate( + "SpeedDialReader", + "The file {0} could not be opened. Error: {1}").format( + fileNameOrDevice, f.errorString())) + return self.__pages, self.__pagesPerRow, self.__sdSize + self.setDevice(f) + + while not self.atEnd(): + self.readNext() + if self.isStartElement(): + version = self.attributes().value("version") + if self.name() == "SpeedDial" and \ + (not version or version == "1.0"): + self.__readSpeedDial() + else: + self.raiseError(QCoreApplication.translate( + "SpeedDialReader", + "The file is not a SpeedDial version 1.0 file.")) + + return self.__pages, self.__pagesPerRow, self.__sdSize + + def __readSpeedDial(self): + """ + Private method to read the speed dial data. + """ + if not self.isStartElement() and self.name() != "SpeedDial": + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + if self.name() in ["Pages", "Page"]: + continue + else: + break + + if self.isStartElement(): + if self.name() == "Pages": + attributes = self.attributes() + pagesPerRow = attributes.value("row") + if pagesPerRow.isdigit(): + self.__pagesPerRow = int(pagesPerRow) + sdSize = attributes.value("size") + if sdSize.isdigit(): + self.__sdSize = int(sdSize) + elif self.name() == "Page": + attributes = self.attributes() + url = attributes.value("url") + title = attributes.value("title") + if url: + if not title: + title = url + from .Page import Page + page = Page(url, title) + self.__pages.append(page) + else: + self.__skipUnknownElement() + + def __skipUnknownElement(self): + """ + Private method to skip over all unknown elements. + """ + if not self.isStartElement(): + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + break + + if self.isStartElement(): + self.__skipUnknownElement()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SpeedDial/SpeedDialWriter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to write speed dial data files. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamWriter, QIODevice, QFile + + +class SpeedDialWriter(QXmlStreamWriter): + """ + Class implementing a writer object to generate speed dial data files. + """ + def __init__(self): + """ + Constructor + """ + super(SpeedDialWriter, self).__init__() + + self.setAutoFormatting(True) + + def write(self, fileNameOrDevice, pages, pagesPerRow, speedDialSize): + """ + Public method to write a speed dial data file. + + @param fileNameOrDevice name of the file to write (string) + or device to write to (QIODevice) + @param pages list of speed dial pages (list of Page) + @param pagesPerRow number of pages per row (integer) + @param speedDialSize size of the speed dial pages (integer) + @return flag indicating success (boolean) + """ + if isinstance(fileNameOrDevice, QIODevice): + f = fileNameOrDevice + else: + f = QFile(fileNameOrDevice) + if not f.open(QFile.WriteOnly): + return False + + self.setDevice(f) + return self.__write(pages, pagesPerRow, speedDialSize) + + def __write(self, pages, pagesPerRow, speedDialSize): + """ + Private method to write a speed dial file. + + @param pages list of speed dial pages (list of Page) + @param pagesPerRow number of pages per row (integer) + @param speedDialSize size of the speed dial pages (integer) + @return flag indicating success (boolean) + """ + self.writeStartDocument() + self.writeDTD("<!DOCTYPE speeddial>") + self.writeStartElement("SpeedDial") + self.writeAttribute("version", "1.0") + + self.writeStartElement("Pages") + self.writeAttribute("row", str(pagesPerRow)) + self.writeAttribute("size", str(speedDialSize)) + + for page in pages: + self.writeEmptyElement("Page") + self.writeAttribute("url", page.url) + self.writeAttribute("title", page.title) + + self.writeEndDocument() + return True
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SpeedDial/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the speed dial functionality. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/DirectorySyncHandler.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a synchronization handler using a shared directory. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, QByteArray, QFileInfo, QCoreApplication + +from .SyncHandler import SyncHandler + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + +import Preferences + + +class DirectorySyncHandler(SyncHandler): + """ + Class implementing a synchronization handler using a shared directory. + + @signal syncStatus(type_, message) emitted to indicate the synchronization + status (string one of "bookmarks", "history", "passwords", + "useragents" or "speeddial", string) + @signal syncError(message) emitted for a general error with the error + message (string) + @signal syncMessage(message) emitted to send a message about + synchronization (string) + @signal syncFinished(type_, done, download) emitted after a + synchronization has finished (string one of "bookmarks", "history", + "passwords", "useragents" or "speeddial", boolean, boolean) + """ + syncStatus = pyqtSignal(str, str) + syncError = pyqtSignal(str) + syncMessage = pyqtSignal(str) + syncFinished = pyqtSignal(str, bool, bool) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(DirectorySyncHandler, self).__init__(parent) + self.__forceUpload = False + + self.__remoteFilesFound = [] + + def initialLoadAndCheck(self, forceUpload): + """ + Public method to do the initial check. + + @keyparam forceUpload flag indicating a forced upload of the files + (boolean) + """ + if not Preferences.getWebBrowser("SyncEnabled"): + return + + self.__forceUpload = forceUpload + + self.__remoteFilesFound = [] + + # check the existence of the shared directory; create it, if it is + # not there + if not os.path.exists(Preferences.getWebBrowser("SyncDirectoryPath")): + try: + os.makedirs(Preferences.getWebBrowser("SyncDirectoryPath")) + except OSError as err: + self.syncError.emit( + self.tr("Error creating the shared directory.\n{0}") + .format(str(err))) + return + + self.__initialSync() + + def __downloadFile(self, type_, fileName, timestamp): + """ + Private method to downlaod the given file. + + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param fileName name of the file to be downloaded (string) + @param timestamp time stamp in seconds of the file to be downloaded + (integer) + """ + self.syncStatus.emit(type_, self._messages[type_]["RemoteExists"]) + try: + f = open(os.path.join( + Preferences.getWebBrowser("SyncDirectoryPath"), + self._remoteFiles[type_]), "rb") + data = f.read() + f.close() + except IOError as err: + self.syncStatus.emit( + type_, + self.tr("Cannot read remote file.\n{0}").format(str(err))) + self.syncFinished.emit(type_, False, True) + return + + QCoreApplication.processEvents() + ok, error = self.writeFile(QByteArray(data), fileName, type_, + timestamp) + if not ok: + self.syncStatus.emit(type_, error) + self.syncFinished.emit(type_, ok, True) + + def __uploadFile(self, type_, fileName): + """ + Private method to upload the given file. + + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param fileName name of the file to be uploaded (string) + """ + QCoreApplication.processEvents() + data = self.readFile(fileName, type_) + if data.isEmpty(): + self.syncStatus.emit(type_, self._messages[type_]["LocalMissing"]) + self.syncFinished.emit(type_, False, False) + return + else: + try: + f = open(os.path.join( + Preferences.getWebBrowser("SyncDirectoryPath"), + self._remoteFiles[type_]), "wb") + f.write(bytes(data)) + f.close() + except IOError as err: + self.syncStatus.emit( + type_, + self.tr("Cannot write remote file.\n{0}").format( + str(err))) + self.syncFinished.emit(type_, False, False) + return + + self.syncFinished.emit(type_, True, False) + + def __initialSyncFile(self, type_, fileName): + """ + Private method to do the initial synchronization of the given file. + + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param fileName name of the file to be synchronized (string) + """ + if not self.__forceUpload and \ + os.path.exists(os.path.join( + Preferences.getWebBrowser("SyncDirectoryPath"), + self._remoteFiles[type_])) and \ + QFileInfo(fileName).lastModified() <= QFileInfo( + os.path.join( + Preferences.getWebBrowser("SyncDirectoryPath"), + self._remoteFiles[type_])).lastModified(): + self.__downloadFile( + type_, fileName, + QFileInfo(os.path.join( + Preferences.getWebBrowser("SyncDirectoryPath"), + self._remoteFiles[type_])).lastModified().toTime_t()) + else: + if not os.path.exists(os.path.join( + Preferences.getWebBrowser("SyncDirectoryPath"), + self._remoteFiles[type_])): + self.syncStatus.emit( + type_, self._messages[type_]["RemoteMissing"]) + else: + self.syncStatus.emit( + type_, self._messages[type_]["LocalNewer"]) + self.__uploadFile(type_, fileName) + + def __initialSync(self): + """ + Private slot to do the initial synchronization. + """ + QCoreApplication.processEvents() + # Bookmarks + if Preferences.getWebBrowser("SyncBookmarks"): + self.__initialSyncFile( + "bookmarks", + WebBrowserWindow.bookmarksManager().getFileName()) + + QCoreApplication.processEvents() + # History + if Preferences.getWebBrowser("SyncHistory"): + self.__initialSyncFile( + "history", + WebBrowserWindow.historyManager().getFileName()) + + QCoreApplication.processEvents() + # Passwords + if Preferences.getWebBrowser("SyncPasswords"): + self.__initialSyncFile( + "passwords", + WebBrowserWindow.passwordManager().getFileName()) + + QCoreApplication.processEvents() + # User Agent Settings + if Preferences.getWebBrowser("SyncUserAgents"): + self.__initialSyncFile( + "useragents", + WebBrowserWindow.userAgentsManager().getFileName()) + + QCoreApplication.processEvents() + # Speed Dial Settings + if Preferences.getWebBrowser("SyncSpeedDial"): + self.__initialSyncFile( + "speeddial", + WebBrowserWindow.speedDial().getFileName()) + + self.__forceUpload = False + self.syncMessage.emit(self.tr("Synchronization finished")) + + def __syncFile(self, type_, fileName): + """ + Private method to synchronize the given file. + + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param fileName name of the file to be synchronized (string) + """ + self.syncStatus.emit(type_, self._messages[type_]["Uploading"]) + self.__uploadFile(type_, fileName) + + def syncBookmarks(self): + """ + Public method to synchronize the bookmarks. + """ + self.__syncFile( + "bookmarks", + WebBrowserWindow.bookmarksManager().getFileName()) + + def syncHistory(self): + """ + Public method to synchronize the history. + """ + self.__syncFile( + "history", + WebBrowserWindow.historyManager().getFileName()) + + def syncPasswords(self): + """ + Public method to synchronize the passwords. + """ + self.__syncFile( + "passwords", + WebBrowserWindow.passwordManager().getFileName()) + + def syncUserAgents(self): + """ + Public method to synchronize the user agents. + """ + self.__syncFile( + "useragents", + WebBrowserWindow.userAgentsManager().getFileName()) + + def syncSpeedDial(self): + """ + Public method to synchronize the speed dial data. + """ + self.__syncFile( + "speeddial", + WebBrowserWindow.speedDial().getFileName()) + + def shutdown(self): + """ + Public method to shut down the handler. + """ + # nothing to do + return
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/FtpSyncHandler.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,410 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a synchronization handler using FTP. +""" + +from __future__ import unicode_literals + +import ftplib +import io + +from PyQt5.QtCore import pyqtSignal, QTimer, QFileInfo, QCoreApplication, \ + QByteArray + +from E5Network.E5Ftp import E5Ftp, E5FtpProxyType, E5FtpProxyError + +from .SyncHandler import SyncHandler + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + +import Preferences + +from Utilities.FtpUtilities import FtpDirLineParser, FtpDirLineParserError + + +class FtpSyncHandler(SyncHandler): + """ + Class implementing a synchronization handler using FTP. + + @signal syncStatus(type_, message) emitted to indicate the synchronization + status (string one of "bookmarks", "history", "passwords", + "useragents" or "speeddial", string) + @signal syncError(message) emitted for a general error with the error + message (string) + @signal syncMessage(message) emitted to send a message about + synchronization (string) + @signal syncFinished(type_, done, download) emitted after a + synchronization has finished (string one of "bookmarks", "history", + "passwords", "useragents" or "speeddial", boolean, boolean) + """ + syncStatus = pyqtSignal(str, str) + syncError = pyqtSignal(str) + syncMessage = pyqtSignal(str) + syncFinished = pyqtSignal(str, bool, bool) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(FtpSyncHandler, self).__init__(parent) + + self.__state = "idle" + self.__forceUpload = False + self.__connected = False + + self.__remoteFilesFound = {} + + def initialLoadAndCheck(self, forceUpload): + """ + Public method to do the initial check. + + @keyparam forceUpload flag indicating a forced upload of the files + (boolean) + """ + if not Preferences.getWebBrowser("SyncEnabled"): + return + + self.__state = "initializing" + self.__forceUpload = forceUpload + + self.__dirLineParser = FtpDirLineParser() + self.__remoteFilesFound = {} + + self.__idleTimer = QTimer(self) + self.__idleTimer.setInterval( + Preferences.getWebBrowser("SyncFtpIdleTimeout") * 1000) + self.__idleTimer.timeout.connect(self.__idleTimeout) + + self.__ftp = E5Ftp() + + # do proxy setup + if not Preferences.getUI("UseProxy"): + proxyType = E5FtpProxyType.NoProxy + else: + proxyType = Preferences.getUI("ProxyType/Ftp") + if proxyType != E5FtpProxyType.NoProxy: + self.__ftp.setProxy( + proxyType, + Preferences.getUI("ProxyHost/Ftp"), + Preferences.getUI("ProxyPort/Ftp")) + if proxyType != E5FtpProxyType.NonAuthorizing: + self.__ftp.setProxyAuthentication( + Preferences.getUI("ProxyUser/Ftp"), + Preferences.getUI("ProxyPassword/Ftp"), + Preferences.getUI("ProxyAccount/Ftp")) + + QTimer.singleShot(0, self.__doFtpCommands) + + def __doFtpCommands(self): + """ + Private slot executing the sequence of FTP commands. + """ + try: + ok = self.__connectAndLogin() + if ok: + self.__changeToStore() + self.__ftp.retrlines("LIST", self.__dirListCallback) + self.__initialSync() + self.__state = "idle" + self.__idleTimer.start() + except (ftplib.all_errors + (E5FtpProxyError,)) as err: + self.syncError.emit(str(err)) + + def __connectAndLogin(self): + """ + Private method to connect to the FTP server and log in. + + @return flag indicating a successful log in (boolean) + """ + self.__ftp.connect( + Preferences.getWebBrowser("SyncFtpServer"), + Preferences.getWebBrowser("SyncFtpPort"), + timeout=5) + self.__ftp.login( + Preferences.getWebBrowser("SyncFtpUser"), + Preferences.getWebBrowser("SyncFtpPassword")) + self.__connected = True + return True + + def __changeToStore(self): + """ + Private slot to change to the storage directory. + + This action will create the storage path on the server, if it + does not exist. Upon return, the current directory of the server + is the sync directory. + """ + storePathList = \ + Preferences.getWebBrowser("SyncFtpPath")\ + .replace("\\", "/").split("/") + if storePathList[0] == "": + storePathList.pop(0) + while storePathList: + path = storePathList[0] + try: + self.__ftp.cwd(path) + except ftplib.error_perm as err: + code = err.args[0].strip()[:3] + if code == "550": + # path does not exist, create it + self.__ftp.mkd(path) + self.__ftp.cwd(path) + else: + raise + storePathList.pop(0) + + def __dirListCallback(self, line): + """ + Private slot handling the receipt of directory listing lines. + + @param line the received line of the directory listing (string) + """ + try: + urlInfo = self.__dirLineParser.parseLine(line) + except FtpDirLineParserError: + # silently ignore parser errors + urlInfo = None + + if urlInfo and urlInfo.isValid() and urlInfo.isFile(): + if urlInfo.name() in self._remoteFiles.values(): + self.__remoteFilesFound[urlInfo.name()] = \ + urlInfo.lastModified() + + QCoreApplication.processEvents() + + def __downloadFile(self, type_, fileName, timestamp): + """ + Private method to downlaod the given file. + + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param fileName name of the file to be downloaded (string) + @param timestamp time stamp in seconds of the file to be downloaded + (integer) + """ + self.syncStatus.emit(type_, self._messages[type_]["RemoteExists"]) + buffer = io.BytesIO() + try: + self.__ftp.retrbinary( + "RETR {0}".format(self._remoteFiles[type_]), + lambda x: self.__downloadFileCallback(buffer, x)) + ok, error = self.writeFile( + QByteArray(buffer.getvalue()), fileName, type_, timestamp) + if not ok: + self.syncStatus.emit(type_, error) + self.syncFinished.emit(type_, ok, True) + except ftplib.all_errors as err: + self.syncStatus.emit(type_, str(err)) + self.syncFinished.emit(type_, False, True) + + def __downloadFileCallback(self, buffer, data): + """ + Private method receiving the downloaded data. + + @param buffer reference to the buffer (io.BytesIO) + @param data byte string to store in the buffer (bytes) + @return number of bytes written to the buffer (integer) + """ + res = buffer.write(data) + QCoreApplication.processEvents() + return res + + def __uploadFile(self, type_, fileName): + """ + Private method to upload the given file. + + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param fileName name of the file to be uploaded (string) + @return flag indicating success (boolean) + """ + res = False + data = self.readFile(fileName, type_) + if data.isEmpty(): + self.syncStatus.emit(type_, self._messages[type_]["LocalMissing"]) + self.syncFinished.emit(type_, False, False) + else: + buffer = io.BytesIO(data.data()) + try: + self.__ftp.storbinary( + "STOR {0}".format(self._remoteFiles[type_]), + buffer, + callback=lambda x: QCoreApplication.processEvents()) + self.syncFinished.emit(type_, True, False) + res = True + except ftplib.all_errors as err: + self.syncStatus.emit(type_, str(err)) + self.syncFinished.emit(type_, False, False) + return res + + def __initialSyncFile(self, type_, fileName): + """ + Private method to do the initial synchronization of the given file. + + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param fileName name of the file to be synchronized (string) + """ + if not self.__forceUpload and \ + self._remoteFiles[type_] in self.__remoteFilesFound: + if QFileInfo(fileName).lastModified() < \ + self.__remoteFilesFound[self._remoteFiles[type_]]: + self.__downloadFile( + type_, fileName, + self.__remoteFilesFound[self._remoteFiles[type_]] + .toTime_t()) + else: + self.syncStatus.emit( + type_, self.tr("No synchronization required.")) + self.syncFinished.emit(type_, True, True) + else: + if self._remoteFiles[type_] not in self.__remoteFilesFound: + self.syncStatus.emit( + type_, self._messages[type_]["RemoteMissing"]) + else: + self.syncStatus.emit( + type_, self._messages[type_]["LocalNewer"]) + self.__uploadFile(type_, fileName) + + def __initialSync(self): + """ + Private slot to do the initial synchronization. + """ + # Bookmarks + if Preferences.getWebBrowser("SyncBookmarks"): + self.__initialSyncFile( + "bookmarks", + WebBrowserWindow.bookmarksManager().getFileName()) + + # History + if Preferences.getWebBrowser("SyncHistory"): + self.__initialSyncFile( + "history", + WebBrowserWindow.historyManager().getFileName()) + + # Passwords + if Preferences.getWebBrowser("SyncPasswords"): + self.__initialSyncFile( + "passwords", + WebBrowserWindow.passwordManager().getFileName()) + + # User Agent Settings + if Preferences.getWebBrowser("SyncUserAgents"): + self.__initialSyncFile( + "useragents", + WebBrowserWindow.userAgentsManager().getFileName()) + + # Speed Dial Settings + if Preferences.getWebBrowser("SyncSpeedDial"): + self.__initialSyncFile( + "speeddial", + WebBrowserWindow.speedDial().getFileName()) + + self.__forceUpload = False + + def __syncFile(self, type_, fileName): + """ + Private method to synchronize the given file. + + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param fileName name of the file to be synchronized (string) + """ + if self.__state == "initializing": + return + + # use idle timeout to check, if we are still connected + if self.__connected: + self.__idleTimeout() + if not self.__connected or self.__ftp.sock is None: + ok = self.__connectAndLogin() + if not ok: + self.syncStatus.emit( + type_, self.tr("Cannot log in to FTP host.")) + return + + # upload the changed file + self.__state = "uploading" + self.syncStatus.emit(type_, self._messages[type_]["Uploading"]) + if self.__uploadFile(type_, fileName): + self.syncStatus.emit( + type_, self.tr("Synchronization finished.")) + self.__state = "idle" + + def syncBookmarks(self): + """ + Public method to synchronize the bookmarks. + """ + self.__syncFile( + "bookmarks", + WebBrowserWindow.bookmarksManager().getFileName()) + + def syncHistory(self): + """ + Public method to synchronize the history. + """ + self.__syncFile( + "history", + WebBrowserWindow.historyManager().getFileName()) + + def syncPasswords(self): + """ + Public method to synchronize the passwords. + """ + self.__syncFile( + "passwords", + WebBrowserWindow.passwordManager().getFileName()) + + def syncUserAgents(self): + """ + Public method to synchronize the user agents. + """ + self.__syncFile( + "useragents", + WebBrowserWindow.userAgentsManager().getFileName()) + + def syncSpeedDial(self): + """ + Public method to synchronize the speed dial data. + """ + self.__syncFile( + "speeddial", + WebBrowserWindow.speedDial().getFileName()) + + def shutdown(self): + """ + Public method to shut down the handler. + """ + if self.__idleTimer.isActive(): + self.__idleTimer.stop() + + try: + if self.__connected: + self.__ftp.quit() + except ftplib.all_errors: + pass # ignore FTP errors because we are shutting down anyway + self.__connected = False + + def __idleTimeout(self): + """ + Private slot to prevent a disconnect from the server. + """ + if self.__state == "idle" and self.__connected: + try: + self.__ftp.voidcmd("NOOP") + except ftplib.Error as err: + code = err.args[0].strip()[:3] + if code == "421": + self.__connected = False + except IOError: + self.__connected = False
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncAssistantDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a wizard dialog to enter the synchronization data. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QWizard + +import UI.PixmapCache +import Globals + + +class SyncAssistantDialog(QWizard): + """ + Class implementing a wizard dialog to enter the synchronization data. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(SyncAssistantDialog, self).__init__(parent) + + from . import SyncGlobals + + from .SyncDataPage import SyncDataPage + from .SyncEncryptionPage import SyncEncryptionPage + from .SyncHostTypePage import SyncHostTypePage + from .SyncFtpSettingsPage import SyncFtpSettingsPage + from .SyncDirectorySettingsPage import SyncDirectorySettingsPage + from .SyncCheckPage import SyncCheckPage + + self.setPage(SyncGlobals.PageData, SyncDataPage(self)) + self.setPage(SyncGlobals.PageEncryption, SyncEncryptionPage(self)) + self.setPage(SyncGlobals.PageType, SyncHostTypePage(self)) + self.setPage(SyncGlobals.PageFTPSettings, SyncFtpSettingsPage(self)) + self.setPage(SyncGlobals.PageDirectorySettings, + SyncDirectorySettingsPage(self)) + self.setPage(SyncGlobals.PageCheck, SyncCheckPage(self)) + + self.setPixmap(QWizard.LogoPixmap, + UI.PixmapCache.getPixmap("ericWeb48.png")) + self.setPixmap(QWizard.WatermarkPixmap, + UI.PixmapCache.getPixmap("eric256.png")) + self.setPixmap(QWizard.BackgroundPixmap, + UI.PixmapCache.getPixmap("eric256.png")) + + self.setMinimumSize(650, 450) + if Globals.isWindowsPlatform(): + self.setWizardStyle(QWizard.ModernStyle)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncCheckPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the synchronization status wizard page. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import QByteArray, QTimer +from PyQt5.QtGui import QMovie +from PyQt5.QtWidgets import QWizardPage + +from . import SyncGlobals + +from .Ui_SyncCheckPage import Ui_SyncCheckPage + +import Preferences +import UI.PixmapCache + +from eric6config import getConfig + + +class SyncCheckPage(QWizardPage, Ui_SyncCheckPage): + """ + Class implementing the synchronization status wizard page. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(SyncCheckPage, self).__init__(parent) + self.setupUi(self) + + def initializePage(self): + """ + Public method to initialize the page. + """ + self.syncErrorLabel.hide() + + forceUpload = self.field("ReencryptData") + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + syncMgr = WebBrowserWindow.syncManager() + syncMgr.syncError.connect(self.__syncError) + syncMgr.syncStatus.connect(self.__updateMessages) + syncMgr.syncFinished.connect(self.__updateLabels) + + if Preferences.getWebBrowser("SyncType") == SyncGlobals.SyncTypeFtp: + self.handlerLabel.setText(self.tr("FTP")) + self.infoLabel.setText(self.tr("Host:")) + self.infoDataLabel.setText( + Preferences.getWebBrowser("SyncFtpServer")) + elif Preferences.getWebBrowser("SyncType") == \ + SyncGlobals.SyncTypeDirectory: + self.handlerLabel.setText(self.tr("Shared Directory")) + self.infoLabel.setText(self.tr("Directory:")) + self.infoDataLabel.setText( + Preferences.getWebBrowser("SyncDirectoryPath")) + else: + self.handlerLabel.setText(self.tr("No Synchronization")) + self.hostLabel.setText("") + + self.bookmarkMsgLabel.setText("") + self.historyMsgLabel.setText("") + self.passwordsMsgLabel.setText("") + self.userAgentsMsgLabel.setText("") + self.speedDialMsgLabel.setText("") + + if not syncMgr.syncEnabled(): + self.bookmarkLabel.setPixmap( + UI.PixmapCache.getPixmap("syncNo.png")) + self.historyLabel.setPixmap(UI.PixmapCache.getPixmap("syncNo.png")) + self.passwordsLabel.setPixmap( + UI.PixmapCache.getPixmap("syncNo.png")) + self.userAgentsLabel.setPixmap( + UI.PixmapCache.getPixmap("syncNo.png")) + self.speedDialLabel.setPixmap( + UI.PixmapCache.getPixmap("syncNo.png")) + return + + animationFile = os.path.join(getConfig("ericPixDir"), "loading.gif") + + # bookmarks + if Preferences.getWebBrowser("SyncBookmarks"): + self.__makeAnimatedLabel(animationFile, self.bookmarkLabel) + else: + self.bookmarkLabel.setPixmap( + UI.PixmapCache.getPixmap("syncNo.png")) + + # history + if Preferences.getWebBrowser("SyncHistory"): + self.__makeAnimatedLabel(animationFile, self.historyLabel) + else: + self.historyLabel.setPixmap(UI.PixmapCache.getPixmap("syncNo.png")) + + # Passwords + if Preferences.getWebBrowser("SyncPasswords"): + self.__makeAnimatedLabel(animationFile, self.passwordsLabel) + else: + self.passwordsLabel.setPixmap( + UI.PixmapCache.getPixmap("syncNo.png")) + + # user agent settings + if Preferences.getWebBrowser("SyncUserAgents"): + self.__makeAnimatedLabel(animationFile, self.userAgentsLabel) + else: + self.userAgentsLabel.setPixmap( + UI.PixmapCache.getPixmap("syncNo.png")) + + # speed dial settings + if Preferences.getWebBrowser("SyncSpeedDial"): + self.__makeAnimatedLabel(animationFile, self.speedDialLabel) + else: + self.speedDialLabel.setPixmap( + UI.PixmapCache.getPixmap("syncNo.png")) + + QTimer.singleShot( + 0, lambda: syncMgr.loadSettings(forceUpload=forceUpload)) + + def __makeAnimatedLabel(self, fileName, label): + """ + Private slot to create an animated label. + + @param fileName name of the file containing the animation (string) + @param label reference to the label to be animated (QLabel) + """ + movie = QMovie(fileName, QByteArray(), label) + movie.setSpeed(100) + label.setMovie(movie) + movie.start() + + def __updateMessages(self, type_, msg): + """ + Private slot to update the synchronization status info. + + @param type_ type of synchronization data (string) + @param msg synchronization message (string) + """ + if type_ == "bookmarks": + self.bookmarkMsgLabel.setText(msg) + elif type_ == "history": + self.historyMsgLabel.setText(msg) + elif type_ == "passwords": + self.passwordsMsgLabel.setText(msg) + elif type_ == "useragents": + self.userAgentsMsgLabel.setText(msg) + elif type_ == "speeddial": + self.speedDialMsgLabel.setText(msg) + + def __updateLabels(self, type_, status, download): + """ + Private slot to handle a finished synchronization event. + + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param status flag indicating success (boolean) + @param download flag indicating a download of a file (boolean) + """ + if type_ == "bookmarks": + if status: + self.bookmarkLabel.setPixmap( + UI.PixmapCache.getPixmap("syncCompleted.png")) + else: + self.bookmarkLabel.setPixmap( + UI.PixmapCache.getPixmap("syncFailed.png")) + elif type_ == "history": + if status: + self.historyLabel.setPixmap( + UI.PixmapCache.getPixmap("syncCompleted.png")) + else: + self.historyLabel.setPixmap( + UI.PixmapCache.getPixmap("syncFailed.png")) + elif type_ == "passwords": + if status: + self.passwordsLabel.setPixmap( + UI.PixmapCache.getPixmap("syncCompleted.png")) + else: + self.passwordsLabel.setPixmap( + UI.PixmapCache.getPixmap("syncFailed.png")) + elif type_ == "useragents": + if status: + self.userAgentsLabel.setPixmap( + UI.PixmapCache.getPixmap("syncCompleted.png")) + else: + self.userAgentsLabel.setPixmap( + UI.PixmapCache.getPixmap("syncFailed.png")) + elif type_ == "speeddial": + if status: + self.speedDialLabel.setPixmap( + UI.PixmapCache.getPixmap("syncCompleted.png")) + else: + self.speedDialLabel.setPixmap( + UI.PixmapCache.getPixmap("syncFailed.png")) + + def __syncError(self, message): + """ + Private slot to handle general synchronization issues. + + @param message error message (string) + """ + self.syncErrorLabel.show() + self.syncErrorLabel.setText(self.tr( + '<font color="#FF0000"><b>Error:</b> {0}</font>').format(message))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncCheckPage.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,217 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SyncCheckPage</class> + <widget class="QWizardPage" name="SyncCheckPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>400</height> + </rect> + </property> + <property name="title"> + <string>Synchronization status</string> + </property> + <property name="subTitle"> + <string>This page shows the status of the current synchronization process.</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Synchronization Data</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Sync Handler:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="handlerLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string notr="true">handler</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="infoLabel"> + <property name="text"> + <string notr="true">Host:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="infoDataLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string notr="true">host</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Synchronization Status</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Bookmarks:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="bookmarkLabel"/> + </item> + <item row="0" column="2" colspan="2"> + <widget class="QLabel" name="bookmarkMsgLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>History:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="historyLabel"/> + </item> + <item row="1" column="2" colspan="2"> + <widget class="QLabel" name="historyMsgLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Passwords:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="passwordsLabel"/> + </item> + <item row="2" column="2" colspan="2"> + <widget class="QLabel" name="passwordsMsgLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>User Agent Settings:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLabel" name="userAgentsLabel"/> + </item> + <item row="3" column="2" colspan="2"> + <widget class="QLabel" name="userAgentsMsgLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Speed Dial Settings:</string> + </property> + </widget> + </item> + <item row="4" column="1" colspan="2"> + <widget class="QLabel" name="speedDialLabel"/> + </item> + <item row="4" column="3"> + <widget class="QLabel" name="speedDialMsgLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="5" column="0" colspan="4"> + <widget class="QLabel" name="syncErrorLabel"> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>81</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncDataPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the synchronization data wizard page. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QWizardPage + +from .Ui_SyncDataPage import Ui_SyncDataPage + +import Preferences + + +class SyncDataPage(QWizardPage, Ui_SyncDataPage): + """ + Class implementing the synchronization data wizard page. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(SyncDataPage, self).__init__(parent) + self.setupUi(self) + + self.bookmarksCheckBox.setChecked( + Preferences.getWebBrowser("SyncBookmarks")) + self.historyCheckBox.setChecked( + Preferences.getWebBrowser("SyncHistory")) + self.passwordsCheckBox.setChecked( + Preferences.getWebBrowser("SyncPasswords")) + self.userAgentsCheckBox.setChecked( + Preferences.getWebBrowser("SyncUserAgents")) + self.speedDialCheckBox.setChecked( + Preferences.getWebBrowser("SyncSpeedDial")) + + self.activeCheckBox.setChecked( + Preferences.getWebBrowser("SyncEnabled")) + + def nextId(self): + """ + Public method returning the ID of the next wizard page. + + @return next wizard page ID (integer) + """ + # save the settings + Preferences.setWebBrowser( + "SyncEnabled", self.activeCheckBox.isChecked()) + + Preferences.setWebBrowser( + "SyncBookmarks", self.bookmarksCheckBox.isChecked()) + Preferences.setWebBrowser( + "SyncHistory", self.historyCheckBox.isChecked()) + Preferences.setWebBrowser( + "SyncPasswords", self.passwordsCheckBox.isChecked()) + Preferences.setWebBrowser( + "SyncUserAgents", self.userAgentsCheckBox.isChecked()) + Preferences.setWebBrowser( + "SyncSpeedDial", self.speedDialCheckBox.isChecked()) + + from . import SyncGlobals + if self.activeCheckBox.isChecked(): + return SyncGlobals.PageEncryption + else: + return SyncGlobals.PageCheck
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncDataPage.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SyncDataPage</class> + <widget class="QWizardPage" name="SyncDataPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>400</height> + </rect> + </property> + <property name="title"> + <string>Basic synchronization settings</string> + </property> + <property name="subTitle"> + <string>Please select, if synchronization should be enabled and which data should be synchronized.</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QCheckBox" name="activeCheckBox"> + <property name="toolTip"> + <string>Select to activate data synchronization</string> + </property> + <property name="text"> + <string>Activate synchronization</string> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="syncDataBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="title"> + <string>Data to be synchronized</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QCheckBox" name="bookmarksCheckBox"> + <property name="toolTip"> + <string>Select to synchronize bookmarks</string> + </property> + <property name="text"> + <string>Bookmarks</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="historyCheckBox"> + <property name="toolTip"> + <string>Select to synchronize history</string> + </property> + <property name="text"> + <string>History</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="passwordsCheckBox"> + <property name="toolTip"> + <string>Select to synchronize passwords</string> + </property> + <property name="text"> + <string>Passwords</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="userAgentsCheckBox"> + <property name="toolTip"> + <string>Select to synchronize user agent settings</string> + </property> + <property name="text"> + <string>User Agent Settings</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="speedDialCheckBox"> + <property name="toolTip"> + <string>Select to synchronize the speed dial data</string> + </property> + <property name="text"> + <string>Speed Dial Settings</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>150</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>activeCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>syncDataBox</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>63</x> + <y>15</y> + </hint> + <hint type="destinationlabel"> + <x>63</x> + <y>42</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncDirectorySettingsPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the synchronization shared directory settings wizard page. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QWizardPage + +from E5Gui.E5PathPicker import E5PathPickerModes + +from .Ui_SyncDirectorySettingsPage import Ui_SyncDirectorySettingsPage + +import Preferences + + +class SyncDirectorySettingsPage(QWizardPage, Ui_SyncDirectorySettingsPage): + """ + Class implementing the shared directory host settings wizard page. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(SyncDirectorySettingsPage, self).__init__(parent) + self.setupUi(self) + + self.directoryPicker.setMode(E5PathPickerModes.DirectoryMode) + self.directoryPicker.setText( + Preferences.getWebBrowser("SyncDirectoryPath")) + + self.directoryPicker.textChanged.connect(self.completeChanged) + + def nextId(self): + """ + Public method returning the ID of the next wizard page. + + @return next wizard page ID (integer) + """ + # save the settings + Preferences.setWebBrowser( + "SyncDirectoryPath", self.directoryPicker.text()) + + from . import SyncGlobals + return SyncGlobals.PageCheck + + def isComplete(self): + """ + Public method to check the completeness of the page. + + @return flag indicating completeness (boolean) + """ + return self.directoryPicker.text() != ""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncDirectorySettingsPage.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SyncDirectorySettingsPage</class> + <widget class="QWizardPage" name="SyncDirectorySettingsPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>400</height> + </rect> + </property> + <property name="windowTitle"> + <string/> + </property> + <property name="title"> + <string>Synchronize to a shared directory</string> + </property> + <property name="subTitle"> + <string>Please enter the data for synchronization via a shared directory. All fields must be filled.</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Shared Directory Settings</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Directory Name:</string> + </property> + </widget> + </item> + <item> + <widget class="E5PathPicker" name="directoryPicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the full path of the shared directory</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>317</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5PathPicker</class> + <extends>QWidget</extends> + <header>E5Gui/E5PathPicker.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncEncryptionPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing encryption settings wizard page. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QWizardPage + +from .Ui_SyncEncryptionPage import Ui_SyncEncryptionPage + +import Preferences + + +class SyncEncryptionPage(QWizardPage, Ui_SyncEncryptionPage): + """ + Class implementing encryption settings wizard page. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(SyncEncryptionPage, self).__init__(parent) + self.setupUi(self) + + self.keySizeComboBox.addItem(self.tr("128 Bits"), 16) + self.keySizeComboBox.addItem(self.tr("192 Bits"), 24) + self.keySizeComboBox.addItem(self.tr("256 Bits"), 32) + + self.registerField("ReencryptData", self.reencryptCheckBox) + + self.encryptionGroupBox.setChecked( + Preferences.getWebBrowser("SyncEncryptData")) + self.encryptionKeyEdit.setText( + Preferences.getWebBrowser("SyncEncryptionKey")) + self.encryptionKeyAgainEdit.setEnabled(False) + self.keySizeComboBox.setCurrentIndex(self.keySizeComboBox.findData( + Preferences.getWebBrowser("SyncEncryptionKeyLength"))) + self.loginsOnlyCheckBox.setChecked( + Preferences.getWebBrowser("SyncEncryptPasswordsOnly")) + + def nextId(self): + """ + Public method returning the ID of the next wizard page. + + @return next wizard page ID (integer) + """ + Preferences.setWebBrowser( + "SyncEncryptData", self.encryptionGroupBox.isChecked()) + Preferences.setWebBrowser( + "SyncEncryptionKey", self.encryptionKeyEdit.text()) + Preferences.setWebBrowser( + "SyncEncryptionKeyLength", self.keySizeComboBox.itemData( + self.keySizeComboBox.currentIndex())) + Preferences.setWebBrowser( + "SyncEncryptPasswordsOnly", self.loginsOnlyCheckBox.isChecked()) + + from . import SyncGlobals + return SyncGlobals.PageType + + def isComplete(self): + """ + Public method to check the completeness of the page. + + @return flag indicating completeness (boolean) + """ + if self.encryptionGroupBox.isChecked(): + if self.encryptionKeyEdit.text() == "": + complete = False + else: + if self.reencryptCheckBox.isChecked(): + complete = (self.encryptionKeyEdit.text() == + self.encryptionKeyAgainEdit.text()) + else: + complete = True + else: + complete = True + + return complete + + def __updateUI(self): + """ + Private slot to update the variable parts of the UI. + """ + error = "" + + if self.encryptionGroupBox.isChecked(): + self.encryptionKeyAgainEdit.setEnabled( + self.reencryptCheckBox.isChecked()) + + if self.encryptionKeyEdit.text() == "": + error = error or self.tr( + "Encryption key must not be empty.") + + if self.encryptionKeyEdit.text() != "" and \ + self.reencryptCheckBox.isChecked() and \ + (self.encryptionKeyEdit.text() != + self.encryptionKeyAgainEdit.text()): + error = error or self.tr( + "Repeated encryption key is wrong.") + + self.errorLabel.setText(error) + self.completeChanged.emit() + + @pyqtSlot(str) + def on_encryptionKeyEdit_textChanged(self, txt): + """ + Private slot to handle changes of the encryption key. + + @param txt content of the edit widget (string) + """ + self.passwordMeter.checkPasswordStrength(txt) + self.__updateUI() + + @pyqtSlot(str) + def on_encryptionKeyAgainEdit_textChanged(self, txt): + """ + Private slot to handle changes of the encryption key repetition. + + @param txt content of the edit widget (string) + """ + self.__updateUI() + + @pyqtSlot(bool) + def on_encryptionGroupBox_toggled(self, on): + """ + Private slot to handle changes of the encryption selection. + + @param on state of the group box (boolean) + """ + self.__updateUI() + + @pyqtSlot(bool) + def on_reencryptCheckBox_toggled(self, on): + """ + Private slot to handle changes of the re-encryption selection. + + @param on state of the check box (boolean) + """ + self.__updateUI()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncEncryptionPage.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,179 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SyncEncryptionPage</class> + <widget class="QWizardPage" name="SyncEncryptionPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>400</height> + </rect> + </property> + <property name="title"> + <string>Encryption Settings</string> + </property> + <property name="subTitle"> + <string>Please select, if the synchronized data should be encrypted and enter the encryption key</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="encryptionGroupBox"> + <property name="toolTip"> + <string>Select to encrypt the synchronzed data</string> + </property> + <property name="title"> + <string>Encrypt Data</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string><p>The encryption key will be used to encrypt and decrypt the synchronizde data. If the data should be re-encrypted, the respective selection should be done. The key must only be repeated, if a re-encryption is requested.<br/><br/><b>Note: If you forget the encryption key, the encrypted data cannot be recovered!</b><br/></p></string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QCheckBox" name="reencryptCheckBox"> + <property name="toolTip"> + <string>Select to re-encrypt the synchronized data</string> + </property> + <property name="text"> + <string>Re-encrypt synchronized data</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Encryption Key:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="encryptionKeyEdit"> + <property name="toolTip"> + <string>Enter the encryption key</string> + </property> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Encryption Key (again):</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLineEdit" name="encryptionKeyAgainEdit"> + <property name="toolTip"> + <string>Repeat the encryption key</string> + </property> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="4" column="0" colspan="2"> + <widget class="E5PasswordMeter" name="passwordMeter"> + <property name="toolTip"> + <string>Shows an indication for the encryption key strength</string> + </property> + </widget> + </item> + <item row="5" column="0" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Size of generated encryption key:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="keySizeComboBox"> + <property name="toolTip"> + <string>Select the size of the generated encryption key</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="6" column="0" colspan="2"> + <widget class="QLabel" name="errorLabel"> + <property name="styleSheet"> + <string notr="true">color : red;</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="7" column="0" colspan="2"> + <widget class="QCheckBox" name="loginsOnlyCheckBox"> + <property name="toolTip"> + <string>Select to encrypt only the passwords</string> + </property> + <property name="text"> + <string>Encrypt Passwords Only</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>191</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5PasswordMeter</class> + <extends>QProgressBar</extends> + <header>E5Gui/E5PasswordMeter</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>encryptionGroupBox</tabstop> + <tabstop>reencryptCheckBox</tabstop> + <tabstop>encryptionKeyEdit</tabstop> + <tabstop>encryptionKeyAgainEdit</tabstop> + <tabstop>keySizeComboBox</tabstop> + <tabstop>loginsOnlyCheckBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncFtpSettingsPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the synchronization FTP host settings wizard page. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QWizardPage + +from .Ui_SyncFtpSettingsPage import Ui_SyncFtpSettingsPage + +import Preferences + + +class SyncFtpSettingsPage(QWizardPage, Ui_SyncFtpSettingsPage): + """ + Class implementing the synchronization FTP host settings wizard page. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(SyncFtpSettingsPage, self).__init__(parent) + self.setupUi(self) + + self.serverEdit.setText(Preferences.getWebBrowser("SyncFtpServer")) + self.userNameEdit.setText(Preferences.getWebBrowser("SyncFtpUser")) + self.passwordEdit.setText(Preferences.getWebBrowser("SyncFtpPassword")) + self.pathEdit.setText(Preferences.getWebBrowser("SyncFtpPath")) + self.portSpinBox.setValue(Preferences.getWebBrowser("SyncFtpPort")) + self.idleSpinBox.setValue( + Preferences.getWebBrowser("SyncFtpIdleTimeout")) + + self.serverEdit.textChanged.connect(self.completeChanged) + self.userNameEdit.textChanged.connect(self.completeChanged) + self.passwordEdit.textChanged.connect(self.completeChanged) + self.pathEdit.textChanged.connect(self.completeChanged) + + def nextId(self): + """ + Public method returning the ID of the next wizard page. + + @return next wizard page ID (integer) + """ + # save the settings + Preferences.setWebBrowser("SyncFtpServer", self.serverEdit.text()) + Preferences.setWebBrowser("SyncFtpUser", self.userNameEdit.text()) + Preferences.setWebBrowser("SyncFtpPassword", self.passwordEdit.text()) + Preferences.setWebBrowser("SyncFtpPath", self.pathEdit.text()) + Preferences.setWebBrowser("SyncFtpPort", self.portSpinBox.value()) + Preferences.setWebBrowser("SyncFtpIdleTimeout", + self.idleSpinBox.value()) + + from . import SyncGlobals + return SyncGlobals.PageCheck + + def isComplete(self): + """ + Public method to check the completeness of the page. + + @return flag indicating completeness (boolean) + """ + return self.serverEdit.text() != "" and \ + self.userNameEdit.text() != "" and \ + self.passwordEdit.text() != "" and \ + self.pathEdit.text() != ""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncFtpSettingsPage.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,183 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SyncFtpSettingsPage</class> + <widget class="QWizardPage" name="SyncFtpSettingsPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>400</height> + </rect> + </property> + <property name="title"> + <string>Synchronize to an FTP host</string> + </property> + <property name="subTitle"> + <string>Please enter the data for synchronization via FTP. All fields must be filled.</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Remote FTP Host Settings</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Server:</string> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QLineEdit" name="serverEdit"> + <property name="toolTip"> + <string>Enter the FTP server name</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>User Name:</string> + </property> + </widget> + </item> + <item row="1" column="1" colspan="2"> + <widget class="QLineEdit" name="userNameEdit"> + <property name="toolTip"> + <string>Enter the user name</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Password:</string> + </property> + </widget> + </item> + <item row="2" column="1" colspan="2"> + <widget class="QLineEdit" name="passwordEdit"> + <property name="toolTip"> + <string>Enter the password</string> + </property> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Path:</string> + </property> + </widget> + </item> + <item row="3" column="1" colspan="2"> + <widget class="QLineEdit" name="pathEdit"> + <property name="toolTip"> + <string>Enter the remote path</string> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Port:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QSpinBox" name="portSpinBox"> + <property name="toolTip"> + <string>Enter the remote port</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>65635</number> + </property> + <property name="value"> + <number>21</number> + </property> + </widget> + </item> + <item row="4" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>218</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Idle Timeout:</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QSpinBox" name="idleSpinBox"> + <property name="toolTip"> + <string>Enter the idle timeout interval to prevent a server disconnect</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string> s</string> + </property> + <property name="minimum"> + <number>10</number> + </property> + <property name="maximum"> + <number>3600</number> + </property> + </widget> + </item> + <item row="5" column="2"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>419</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>101</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncGlobals.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing some global definitions. +""" + +# Page IDs for the sync wizard +PageData = 0 +PageEncryption = 1 +PageType = 2 +PageFTPSettings = 3 +PageDirectorySettings = 4 +PageCheck = 5 + +# Sync types +SyncTypeNone = -1 +SyncTypeFtp = 0 +SyncTypeDirectory = 1 + +# +# eflag: noqa = M702
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncHandler.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module containing a base class for synchronization handlers. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import QObject, pyqtSignal, QByteArray + +import Preferences + +from Utilities.crypto import dataEncrypt, dataDecrypt + + +class SyncHandler(QObject): + """ + Base class for synchronization handlers. + + @signal syncStatus(type_, message) emitted to indicate the synchronization + status (string one of "bookmarks", "history", "passwords", + "useragents" or "speeddial", string) + @signal syncError(message) emitted for a general error with the error + message (string) + @signal syncMessage(message) emitted to send a message about + synchronization (string) + @signal syncFinished(type_, done, download) emitted after a + synchronization has finished (string one of "bookmarks", "history", + "passwords", "useragents" or "speeddial", boolean, boolean) + """ + syncStatus = pyqtSignal(str, str) + syncError = pyqtSignal(str) + syncMessage = pyqtSignal(str) + syncFinished = pyqtSignal(str, bool, bool) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(SyncHandler, self).__init__(parent) + + self._firstTimeSynced = False + + self._remoteFiles = { + "bookmarks": "Bookmarks", + "history": "History", + "passwords": "Logins", + "useragents": "UserAgentSettings", + "speeddial": "SpeedDial", + } + + self._messages = { + "bookmarks": { + "RemoteExists": self.tr( + "Remote bookmarks file exists! Syncing local copy..."), + "RemoteMissing": self.tr( + "Remote bookmarks file does NOT exist. Exporting" + " local copy..."), + "LocalNewer": self.tr( + "Local bookmarks file is NEWER. Exporting local copy..."), + "LocalMissing": self.tr( + "Local bookmarks file does NOT exist. Skipping" + " synchronization!"), + "Uploading": self.tr("Uploading local bookmarks file..."), + }, + "history": { + "RemoteExists": self.tr( + "Remote history file exists! Syncing local copy..."), + "RemoteMissing": self.tr( + "Remote history file does NOT exist. Exporting" + " local copy..."), + "LocalNewer": self.tr( + "Local history file is NEWER. Exporting local copy..."), + "LocalMissing": self.tr( + "Local history file does NOT exist. Skipping" + " synchronization!"), + "Uploading": self.tr("Uploading local history file..."), + }, + "passwords": { + "RemoteExists": self.tr( + "Remote logins file exists! Syncing local copy..."), + "RemoteMissing": self.tr( + "Remote logins file does NOT exist. Exporting" + " local copy..."), + "LocalNewer": self.tr( + "Local logins file is NEWER. Exporting local copy..."), + "LocalMissing": self.tr( + "Local logins file does NOT exist. Skipping" + " synchronization!"), + "Uploading": self.tr("Uploading local logins file..."), + }, + "useragents": { + "RemoteExists": self.tr( + "Remote user agent settings file exists! Syncing local" + " copy..."), + "RemoteMissing": self.tr( + "Remote user agent settings file does NOT exist." + " Exporting local copy..."), + "LocalNewer": self.tr( + "Local user agent settings file is NEWER. Exporting" + " local copy..."), + "LocalMissing": self.tr( + "Local user agent settings file does NOT exist." + " Skipping synchronization!"), + "Uploading": self.tr( + "Uploading local user agent settings file..."), + }, + "speeddial": { + "RemoteExists": self.tr( + "Remote speed dial settings file exists! Syncing local" + " copy..."), + "RemoteMissing": self.tr( + "Remote speed dial settings file does NOT exist." + " Exporting local copy..."), + "LocalNewer": self.tr( + "Local speed dial settings file is NEWER. Exporting" + " local copy..."), + "LocalMissing": self.tr( + "Local speed dial settings file does NOT exist." + " Skipping synchronization!"), + "Uploading": self.tr( + "Uploading local speed dial settings file..."), + }, + } + + def syncBookmarks(self): + """ + Public method to synchronize the bookmarks. + + @exception NotImplementedError raised to indicate that this method + must be implemented by subclasses + """ + raise NotImplementedError + + def syncHistory(self): + """ + Public method to synchronize the history. + + @exception NotImplementedError raised to indicate that this method + must be implemented by subclasses + """ + raise NotImplementedError + + def syncPasswords(self): + """ + Public method to synchronize the passwords. + + @exception NotImplementedError raised to indicate that this method + must be implemented by subclasses + """ + raise NotImplementedError + + def syncUserAgents(self): + """ + Public method to synchronize the user agents. + + @exception NotImplementedError raised to indicate that this method + must be implemented by subclasses + """ + raise NotImplementedError + + def syncSpeedDial(self): + """ + Public method to synchronize the speed dial data. + + @exception NotImplementedError raised to indicate that this method + must be implemented by subclasses + """ + raise NotImplementedError + + def initialLoadAndCheck(self, forceUpload): + """ + Public method to do the initial check. + + @keyparam forceUpload flag indicating a forced upload of the files + (boolean) + @exception NotImplementedError raised to indicate that this method + must be implemented by subclasses + """ + raise NotImplementedError + + def shutdown(self): + """ + Public method to shut down the handler. + + @exception NotImplementedError raised to indicate that this method + must be implemented by subclasses + """ + raise NotImplementedError + + def readFile(self, fileName, type_): + """ + Public method to read a file. + + If encrypted synchronization is enabled, the data will be encrypted + using the relevant encryption key. + + @param fileName name of the file to be read (string) + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @return data of the file, optionally encrypted (QByteArray) + """ + if os.path.exists(fileName): + try: + inputFile = open(fileName, "rb") + data = inputFile.read() + inputFile.close() + except IOError: + return QByteArray() + + if Preferences.getWebBrowser("SyncEncryptData") and \ + (not Preferences.getWebBrowser("SyncEncryptPasswordsOnly") or + (Preferences.getWebBrowser("SyncEncryptPasswordsOnly") and + type_ == "passwords")): + key = Preferences.getWebBrowser("SyncEncryptionKey") + if not key: + return QByteArray() + + data, ok = dataEncrypt( + data, key, + keyLength=Preferences.getWebBrowser( + "SyncEncryptionKeyLength"), + hashIterations=100) + if not ok: + return QByteArray() + + return QByteArray(data) + + return QByteArray() + + def writeFile(self, data, fileName, type_, timestamp=0): + """ + Public method to write the data to a file. + + If encrypted synchronization is enabled, the data will be decrypted + using the relevant encryption key. + + @param data data to be written and optionally decrypted (QByteArray) + @param fileName name of the file the data is to be written to (string) + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param timestamp timestamp to be given to the file (int) + @return tuple giving a success flag and an error string (boolean, + string) + """ + data = bytes(data) + + if Preferences.getWebBrowser("SyncEncryptData") and \ + (not Preferences.getWebBrowser("SyncEncryptPasswordsOnly") or + (Preferences.getWebBrowser("SyncEncryptPasswordsOnly") and + type_ == "passwords")): + key = Preferences.getWebBrowser("SyncEncryptionKey") + if not key: + return False, self.tr("Invalid encryption key given.") + + data, ok = dataDecrypt( + data, key, + keyLength=Preferences.getWebBrowser("SyncEncryptionKeyLength")) + if not ok: + return False, self.tr("Data cannot be decrypted.") + + try: + outputFile = open(fileName, "wb") + outputFile.write(data) + outputFile.close() + if timestamp > 0: + os.utime(fileName, (timestamp, timestamp)) + return True, "" + except IOError as error: + return False, str(error)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncHostTypePage.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the synchronization host type wizard page. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QWizardPage + +from . import SyncGlobals + +from .Ui_SyncHostTypePage import Ui_SyncHostTypePage + +import Preferences + + +class SyncHostTypePage(QWizardPage, Ui_SyncHostTypePage): + """ + Class implementing the synchronization host type wizard page. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(SyncHostTypePage, self).__init__(parent) + self.setupUi(self) + + if Preferences.getWebBrowser("SyncType") == SyncGlobals.SyncTypeFtp: + self.ftpRadioButton.setChecked(True) + elif Preferences.getWebBrowser("SyncType") == \ + SyncGlobals.SyncTypeDirectory: + self.directoryRadioButton.setChecked(True) + else: + self.noneRadioButton.setChecked(True) + + def nextId(self): + """ + Public method returning the ID of the next wizard page. + + @return next wizard page ID (integer) + """ + # save the settings + if self.ftpRadioButton.isChecked(): + Preferences.setWebBrowser("SyncType", SyncGlobals.SyncTypeFtp) + return SyncGlobals.PageFTPSettings + elif self.directoryRadioButton.isChecked(): + Preferences.setWebBrowser( + "SyncType", SyncGlobals.SyncTypeDirectory) + return SyncGlobals.PageDirectorySettings + else: + Preferences.setWebBrowser("SyncType", SyncGlobals.SyncTypeNone) + return SyncGlobals.PageCheck
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncHostTypePage.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SyncHostTypePage</class> + <widget class="QWizardPage" name="SyncHostTypePage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>400</height> + </rect> + </property> + <property name="title"> + <string>Host Type Selection</string> + </property> + <property name="subTitle"> + <string>Please select the type of the host to be used for synchronization.</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Synchronization Host Type</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QRadioButton" name="ftpRadioButton"> + <property name="toolTip"> + <string>Select to use a FTP host</string> + </property> + <property name="text"> + <string>FTP</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="directoryRadioButton"> + <property name="toolTip"> + <string>Select to use a shared directory</string> + </property> + <property name="text"> + <string>Shared Directory</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="noneRadioButton"> + <property name="toolTip"> + <string>Select to use no particular host type</string> + </property> + <property name="text"> + <string>None</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>191</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <tabstops> + <tabstop>ftpRadioButton</tabstop> + <tabstop>directoryRadioButton</tabstop> + <tabstop>noneRadioButton</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/SyncManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the synchronization manager class. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QObject, pyqtSignal + +import Preferences + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + + +class SyncManager(QObject): + """ + Class implementing the synchronization manager. + + @signal syncError(message) emitted for a general error with the error + message (string) + @signal syncMessage(message) emitted to give status info about the sync + process (string) + @signal syncStatus(type_, message) emitted to indicate the synchronization + status (string one of "bookmarks", "history", "passwords", + "useragents" or "speeddial", string) + @signal syncFinished(type_, done, download) emitted after a + synchronization has finished (string one of "bookmarks", "history", + "passwords", "useragents" or "speeddial", boolean, boolean) + """ + syncError = pyqtSignal(str) + syncMessage = pyqtSignal(str) + syncStatus = pyqtSignal(str, str) + syncFinished = pyqtSignal(str, bool, bool) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(SyncManager, self).__init__(parent) + + self.__handler = None + + def handler(self): + """ + Public method to get a reference to the sync handler object. + + @return reference to the sync handler object (SyncHandler) + """ + return self.__handler + + def showSyncDialog(self): + """ + Public method to show the synchronization dialog. + """ + from .SyncAssistantDialog import SyncAssistantDialog + dlg = SyncAssistantDialog() + dlg.exec_() + + def loadSettings(self, forceUpload=False): + """ + Public method to load the settings. + + @keyparam forceUpload flag indicating a forced upload of the files + (boolean) + """ + if self.__handler is not None: + self.__handler.syncError.disconnect(self.__syncError) + self.__handler.syncFinished.disconnect(self.__syncFinished) + self.__handler.syncStatus.disconnect(self.__syncStatus) + self.__handler.syncMessage.disconnect(self.syncMessage) + self.__handler.shutdown() + + if self.syncEnabled(): + from . import SyncGlobals + if Preferences.getWebBrowser("SyncType") == \ + SyncGlobals.SyncTypeFtp: + from .FtpSyncHandler import FtpSyncHandler + self.__handler = FtpSyncHandler(self) + elif Preferences.getWebBrowser("SyncType") == \ + SyncGlobals.SyncTypeDirectory: + from .DirectorySyncHandler import DirectorySyncHandler + self.__handler = DirectorySyncHandler(self) + self.__handler.syncError.connect(self.__syncError) + self.__handler.syncFinished.connect(self.__syncFinished) + self.__handler.syncStatus.connect(self.__syncStatus) + self.__handler.syncMessage.connect(self.syncMessage) + + self.__handler.initialLoadAndCheck(forceUpload=forceUpload) + + # connect sync manager to bookmarks manager + if Preferences.getWebBrowser("SyncBookmarks"): + WebBrowserWindow.bookmarksManager()\ + .bookmarksSaved.connect(self.__syncBookmarks) + else: + try: + WebBrowserWindow.bookmarksManager()\ + .bookmarksSaved.disconnect(self.__syncBookmarks) + except TypeError: + pass + + # connect sync manager to history manager + if Preferences.getWebBrowser("SyncHistory"): + WebBrowserWindow.historyManager().historySaved\ + .connect(self.__syncHistory) + else: + try: + WebBrowserWindow.historyManager()\ + .historySaved.disconnect(self.__syncHistory) + except TypeError: + pass + + # connect sync manager to passwords manager + if Preferences.getWebBrowser("SyncPasswords"): + WebBrowserWindow.passwordManager()\ + .passwordsSaved.connect(self.__syncPasswords) + else: + try: + WebBrowserWindow.passwordManager()\ + .passwordsSaved.disconnect(self.__syncPasswords) + except TypeError: + pass + + # connect sync manager to user agent manager + if Preferences.getWebBrowser("SyncUserAgents"): + WebBrowserWindow.userAgentsManager()\ + .userAgentSettingsSaved.connect(self.__syncUserAgents) + else: + try: + WebBrowserWindow.userAgentsManager()\ + .userAgentSettingsSaved.disconnect( + self.__syncUserAgents) + except TypeError: + pass + + # connect sync manager to speed dial + if Preferences.getWebBrowser("SyncSpeedDial"): + WebBrowserWindow.speedDial()\ + .speedDialSaved.connect(self.__syncSpeedDial) + else: + try: + WebBrowserWindow.speedDial()\ + .speedDialSaved.disconnect(self.__syncSpeedDial) + except TypeError: + pass + else: + self.__handler = None + + try: + WebBrowserWindow.bookmarksManager()\ + .bookmarksSaved.disconnect(self.__syncBookmarks) + except TypeError: + pass + try: + WebBrowserWindow.historyManager().historySaved\ + .disconnect(self.__syncHistory) + except TypeError: + pass + try: + WebBrowserWindow.passwordManager()\ + .passwordsSaved.disconnect(self.__syncPasswords) + except TypeError: + pass + try: + WebBrowserWindow.userAgentsManager()\ + .userAgentSettingsSaved.disconnect(self.__syncUserAgents) + except TypeError: + pass + try: + WebBrowserWindow.speedDial()\ + .speedDialSaved.disconnect(self.__syncSpeedDial) + except TypeError: + pass + + def syncEnabled(self): + """ + Public method to check, if synchronization is enabled. + + @return flag indicating enabled synchronization + """ + from . import SyncGlobals + return Preferences.getWebBrowser("SyncEnabled") and \ + Preferences.getWebBrowser("SyncType") != SyncGlobals.SyncTypeNone + + def __syncBookmarks(self): + """ + Private slot to synchronize the bookmarks. + """ + if self.__handler is not None: + self.__handler.syncBookmarks() + + def __syncHistory(self): + """ + Private slot to synchronize the history. + """ + if self.__handler is not None: + self.__handler.syncHistory() + + def __syncPasswords(self): + """ + Private slot to synchronize the passwords. + """ + if self.__handler is not None: + self.__handler.syncPasswords() + + def __syncUserAgents(self): + """ + Private slot to synchronize the user agent settings. + """ + if self.__handler is not None: + self.__handler.syncUserAgents() + + def __syncSpeedDial(self): + """ + Private slot to synchronize the speed dial settings. + """ + if self.__handler is not None: + self.__handler.syncSpeedDial() + + def __syncError(self, message): + """ + Private slot to handle general synchronization issues. + + @param message error message (string) + """ + self.syncError.emit(message) + + def __syncFinished(self, type_, status, download): + """ + Private slot to handle a finished synchronization event. + + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param status flag indicating success (boolean) + @param download flag indicating a download of a file (boolean) + """ + if status and download: + if type_ == "bookmarks": + WebBrowserWindow.bookmarksManager().reload() + elif type_ == "history": + WebBrowserWindow.historyManager().reload() + elif type_ == "passwords": + WebBrowserWindow.passwordManager().reload() + elif type_ == "useragents": + WebBrowserWindow.userAgentsManager().reload() + elif type_ == "speeddial": + WebBrowserWindow.speedDial().reload() + self.syncFinished.emit(type_, status, download) + + def __syncStatus(self, type_, message): + """ + Private slot to handle a status update of a synchronization event. + + @param type_ type of the synchronization event (string one + of "bookmarks", "history", "passwords", "useragents" or + "speeddial") + @param message status message for the event (string) + """ + self.syncMessage.emit(message) + self.syncStatus.emit(type_, message) + + def close(self): + """ + Public slot to shut down the synchronization manager. + """ + if not self.syncEnabled(): + return + + if self.__handler is not None: + self.__handler.shutdown()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Sync/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing capabilities to sync some configuration data. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Tools/DelayedFileWatcher.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a file system watcher with a delay. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QFileSystemWatcher, QTimer + + +class DelayedFileWatcher(QFileSystemWatcher): + """ + Class implementing a file system watcher with a delay. + + @signal delayedDirectoryChanged(path) emitted to indicate a changed + directory + @signal delayedFileChanged(path) emitted to indicate a changed file + """ + delayedDirectoryChanged = pyqtSignal(str) + delayedFileChanged = pyqtSignal(str) + + def __init__(self, paths=None, parent=None): + """ + Constructor + + @param paths list of paths to be watched + @type list of str + @param parent reference to the parent object + @type QObject + """ + if paths: + super(DelayedFileWatcher, self).__init__(paths, parent) + else: + super(DelayedFileWatcher, self).__init__(parent) + + self.__dirQueue = [] + self.__fileQueue = [] + + self.directoryChanged.connect(self.__directoryChanged) + self.fileChanged.connect(self.__fileChanged) + + @pyqtSlot(str) + def __directoryChanged(self, path): + """ + Private slot handling a changed directory. + + @param path name of the changed directory + @type str + """ + self.__dirQueue.append(path) + QTimer.singleShot(500, self.__dequeueDirectory) + + @pyqtSlot(str) + def __fileChanged(self, path): + """ + Private slot handling a changed file. + + @param path name of the changed file + @type str + """ + self.__fileQueue.append(path) + QTimer.singleShot(500, self.__dequeueFile) + + @pyqtSlot() + def __dequeueDirectory(self): + """ + Private slot to signal a directory change. + """ + self.delayedDirectoryChanged.emit(self.__dirQueue.pop(0)) + + @pyqtSlot() + def __dequeueFile(self): + """ + Private slot to signal a file change. + """ + self.delayedFileChanged.emit(self.__fileQueue.pop(0))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Tools/Scripts.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,390 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module containing function to generate JavaScript code. +""" + +# +# This code was ported from QupZilla. +# Copyright (C) David Rosca <nowrep@gmail.com> +# + +from __future__ import unicode_literals + +from PyQt5.QtCore import QUrlQuery, QUrl + +from .WebBrowserTools import readAllFileContents + + +def setupWebChannel(): + """ + Function generating a script to setup the web channel. + + @return script to setup the web channel + @rtype str + """ + source = """ + (function() {{ + {0} + + function registerExternal(e) {{ + window.external = e; + if (window.external) {{ + var event = document.createEvent('Event'); + event.initEvent('_eric_external_created', true, true); + document.dispatchEvent(event); + }} + }} + + if (self !== top) {{ + if (top.external) + registerExternal(top.external); + else + top.document.addEventListener( + '_eric_external_created', function() {{ + registerExternal(top.external); + }}); + return; + }} + + new QWebChannel(qt.webChannelTransport, function(channel) {{ + registerExternal(channel.objects.eric_object); + }}); + + }})()""" + + return source.format(readAllFileContents(":/javascript/qwebchannel.js")) + + +def setStyleSheet(css): + """ + Function generating a script to set a user style sheet. + + @param css style sheet to be applied + @type str + @return script to set a user style sheet + @rtype str + """ + source = """ + (function() {{ + var css = document.createElement('style'); + css.setAttribute('type', 'text/css'); + css.appendChild(document.createTextNode('{0}')); + document.getElementsByTagName('head')[0].appendChild(css); + }})()""" + + style = css.replace("'", "\\'").replace("\n", "\\n") + return source.format(style) + + +def getFormData(pos): + """ + Function generating a script to extract data for a form element. + + @param pos position to extract data at + @type QPoint + @return script to extract form data + @rtype str + """ + source = """ + (function() {{ + var e = document.elementFromPoint({0}, {1}); + if (!e || e.tagName != 'INPUT') + return; + var fe = e.parentElement; + while (fe) {{ + if (fe.tagName == 'FORM') + break; + fe = fe.parentElement; + }} + if (!fe) + return; + var res = {{ + method: fe.method.toLowerCase(), + action: fe.action, + inputName: e.name, + inputs: [], + }}; + for (var i = 0; i < fe.length; ++i) {{ + var input = fe.elements[i]; + res.inputs.push([input.name, input.value]); + }} + return res; + }})()""" + return source.format(pos.x(), pos.y()) + + +def getAllImages(): + """ + Function generating a script to extract all image tags of a web page. + + @return script to extract image tags + @rtype str + """ + source = """ + (function() { + var out = []; + var imgs = document.getElementsByTagName('img'); + for (var i = 0; i < imgs.length; ++i) { + var e = imgs[i]; + out.push({ + src: e.src, + alt: e.alt + }); + } + return out; + })()""" + return source + + +def getAllMetaAttributes(): + """ + Function generating a script to extract all meta attributes of a web page. + + @return script to extract meta attributes + @rtype str + """ + source = """ + (function() { + var out = []; + var meta = document.getElementsByTagName('meta'); + for (var i = 0; i < meta.length; ++i) { + var e = meta[i]; + out.push({ + name: e.getAttribute('name'), + content: e.getAttribute('content'), + httpequiv: e.getAttribute('http-equiv'), + charset: e.getAttribute('charset') + }); + } + return out; + })()""" + return source + + +def getOpenSearchLinks(): + """ + Function generating a script to extract all open search links. + + @return script to extract all open serach links + @rtype str + """ + source = """ + (function() { + var out = []; + var links = document.getElementsByTagName('link'); + for (var i = 0; i < links.length; ++i) { + var e = links[i]; + if (e.type == 'application/opensearchdescription+xml') { + out.push({ + url: e.getAttribute('href'), + title: e.getAttribute('title') + }); + } + } + return out; + })()""" + return source + + +def sendPostData(url, data): + """ + Function generating a script to send Post data. + + @param url URL to send the data to + @type QUrl + @param data data to be sent + @type QByteArray + @return script to send Post data + @rtype str + """ + source = """ + (function() {{ + var form = document.createElement('form'); + form.setAttribute('method', 'POST'); + form.setAttribute('action', '{0}'); + var val; + {1} + form.submit(); + }})()""" + + valueSource = """ + val = document.createElement('input'); + val.setAttribute('type', 'hidden'); + val.setAttribute('name', '{0}'); + val.setAttribute('value', '{1}'); + form.appendChild(val);""" + + values = "" + query = QUrlQuery(data) + for name, value in query.queryItems(QUrl.FullyDecoded): + value = value.replace("'", "\\'") + name = name.replace("'", "\\'") + values += valueSource.format(name, value) + + return source.format(url.toString(), values) + + +def setupFormObserver(): + """ + Function generating a script to monitor a web form for user entries. + + @return script to monitor a web page + @rtype str + """ + source = """ + (function() { + function findUsername(inputs) { + for (var i = 0; i < inputs.length; ++i) + if (inputs[i].type == 'text' && inputs[i].value.length && + inputs[i].name.indexOf('user') != -1) + return inputs[i].value; + for (var i = 0; i < inputs.length; ++i) + if (inputs[i].type == 'text' && inputs[i].value.length && + inputs[i].name.indexOf('name') != -1) + return inputs[i].value; + for (var i = 0; i < inputs.length; ++i) + if (inputs[i].type == 'text' && inputs[i].value.length) + return inputs[i].value; + for (var i = 0; i < inputs.length; ++i) + if (inputs[i].type == 'email' && inputs[i].value.length) + return inputs[i].value; + return ''; + } + + function registerForm(form) { + form.addEventListener('submit', function() { + var form = this; + var data = ''; + var password = ''; + var inputs = form.getElementsByTagName('input'); + for (var i = 0; i < inputs.length; ++i) { + var input = inputs[i]; + var type = input.type.toLowerCase(); + if (type != 'text' && type != 'password' && + type != 'email') + continue; + if (!password && type == 'password') + password = input.value; + data += encodeURIComponent(input.name); + data += '='; + data += encodeURIComponent(input.value); + data += '&'; + } + if (!password) + return; + data = data.substring(0, data.length - 1); + var url = window.location.href; + var username = findUsername(inputs); + external.passwordManager.formSubmitted( + url, username, password, data); + }, true); + } + + for (var i = 0; i < document.forms.length; ++i) + registerForm(document.forms[i]); + + var observer = new MutationObserver(function(mutations) { + for (var i = 0; i < mutations.length; ++i) + for (var j = 0; j < mutations[i].addedNodes.length; ++j) + if (mutations[i].addedNodes[j].tagName == 'form') + registerForm(mutations[i].addedNodes[j]); + }); + observer.observe(document.documentElement, { childList: true }); + + })()""" + return source + + +def completeFormData(data): + """ + Function generating a script to fill in form data. + + @param data data to be filled into the form + @type QByteArray + @return script to fill a form + @rtype str + """ + source = """ + (function() {{ + var data = '{0}'.split('&'); + var inputs = document.getElementsByTagName('input'); + + for (var i = 0; i < data.length; ++i) {{ + var pair = data[i].split('='); + if (pair.length != 2) + continue; + var key = decodeURIComponent(pair[0]); + var val = decodeURIComponent(pair[1]); + for (var j = 0; j < inputs.length; ++j) {{ + var input = inputs[j]; + var type = input.type.toLowerCase(); + if (type != 'text' && type != 'password' && + type != 'email') + continue; + if (input.name == key) + input.value = val; + }} + }} + + }})()""" + + data = bytes(data).decode("utf-8") + data = data.replace("'", "\\'") + return source.format(data) + + +def setCss(css): + """ + Function generating a script to set a given CSS style sheet. + + @param css style sheet + @type str + @return script to set the style sheet + @rtype str + """ + source = """ + (function() {{ + var css = document.createElement('style'); + css.setAttribute('type', 'text/css'); + css.appendChild(document.createTextNode('{0}')); + document.getElementsByTagName('head')[0].appendChild(css); + }})()""" + style = css.replace("'", "\\'").replace("\n", "\\n") + return source.format(style) + +########################################################################### +## scripts below are specific for eric +########################################################################### + + +def getFeedLinks(): + """ + Function generating a script to extract all RSS and Atom feed links. + + @return script to extract all RSS and Atom feed links + @rtype str + """ + source = """ + (function() { + var out = []; + var links = document.getElementsByTagName('link'); + for (var i = 0; i < links.length; ++i) { + var e = links[i]; + if ((e.rel == 'alternate') && + ((e.type == 'application/atom+xml') || + (e.type == 'application/rss+xml') + ) + ) { + out.push({ + url: e.getAttribute('href'), + title: e.getAttribute('title') + }); + } + } + return out; + })()""" + return source
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Tools/WebBrowserTools.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing tool functions for the web browser. +""" + +from __future__ import unicode_literals +try: + str = unicode # __IGNORE_EXCEPTION__ +except NameError: + pass + +import os +import re + +from PyQt5.QtCore import QFile, QByteArray, QUrl, QCoreApplication, QBuffer, \ + QIODevice +from PyQt5.QtGui import QPixmap + + +def readAllFileContents(filename): + """ + Function to read the string contents of the given file. + + @param filename name of the file + @type str + @return contents of the file + @rtype str + """ + return str(readAllFileByteContents(filename), encoding="utf-8") + + +def readAllFileByteContents(filename): + """ + Function to read the bytes contents of the given file. + + @param filename name of the file + @type str + @return contents of the file + @rtype str + """ + dataFile = QFile(filename) + if filename and dataFile.open(QFile.ReadOnly): + contents = dataFile.readAll() + dataFile.close() + return contents + + return QByteArray() + + +def containsSpace(string): + """ + Function to check, if a string contains whitespace characters. + + @param string string to be checked + @type str + @return flag indicating the presence of at least one whitespace character + @rtype bool + """ + for ch in string: + if ch.isspace(): + return True + + return False + + +def ensureUniqueFilename(name, appendFormat="({0})"): + """ + Module function to generate an unique file name based on a pattern. + + @param name desired file name (string) + @param appendFormat format pattern to be used to make the unique name + (string) + @return unique file name + """ + if not os.path.exists(name): + return name + + tmpFileName = name + i = 1 + while os.path.exists(tmpFileName): + tmpFileName = name + index = tmpFileName.rfind(".") + + appendString = appendFormat.format(i) + if index == -1: + tmpFileName += appendString + else: + tmpFileName = tmpFileName[:index] + appendString + \ + tmpFileName[index:] + i += 1 + + return tmpFileName + + +def getFileNameFromUrl(url): + """ + Module function to generate a file name based on the given URL. + + @param url URL (QUrl) + @return file name (string) + """ + fileName = url.toString(QUrl.RemoveFragment | QUrl.RemoveQuery | + QUrl.RemoveScheme | QUrl.RemovePort) + if fileName.find("/") != -1: + pos = fileName.rfind("/") + fileName = fileName[pos:] + fileName = fileName.replace("/", "") + + fileName = filterCharsFromFilename(fileName) + + if not fileName: + fileName = filterCharsFromFilename(url.host().replace(".", "_")) + + return fileName + + +def filterCharsFromFilename(name): + """ + Module function to filter illegal characters. + + @param name name to be sanitized (string) + @return sanitized name (string) + """ + return name\ + .replace("/", "_")\ + .replace("\\", "")\ + .replace(":", "")\ + .replace("*", "")\ + .replace("?", "")\ + .replace('"', "")\ + .replace("<", "")\ + .replace(">", "")\ + .replace("|", "") + + +def pixmapFromByteArray(data): + """ + Module function to convert a byte array to a pixmap. + + @param data data for the pixmap + @type bytes or QByteArray + @return extracted pixmap + @rtype QPixmap + """ + pixmap = QPixmap() + barray = QByteArray.fromBase64(data) + pixmap.loadFromData(barray) + + return pixmap + + +def pixmapToByteArray(pixmap): + """ + Module function to convert a pixmap to a byte array containing the pixmap + as a PNG encoded as base64. + + @param pixmap pixmap to be converted + @type QPixmap + @return byte array containing the pixmap + @rtype QByteArray + """ + byteArray = QByteArray() + buffer = QBuffer(byteArray) + buffer.open(QIODevice.WriteOnly) + if pixmap.save(buffer, "PNG"): + return buffer.buffer().toBase64() + + return QByteArray() + + +def pixmapToDataUrl(pixmap): + """ + Module function to convert a pixmap to a data: URL. + + @param pixmap pixmap to be converted + @type QPixmap + @return data: URL + @rtype QUrl + """ + data = bytes(pixmapToByteArray(pixmap)).decode() + if data: + return QUrl("data:image/png;base64," + data) + else: + return QUrl() + + +def getWebEngineVersions(): + """ + Module function to extract the web engine version from the default user + agent string. + + @return tuple containing the Chrome version and the QtWebEngine version + @rtype tuple of str + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + useragent = WebBrowserWindow.webProfile().defaultUserAgent + match = re.search(r"""Chrome/([\d.]+)""", useragent) + if match: + chromeVersion = match.group(1) + else: + chromeVersion = QCoreApplication.translate( + "WebBrowserTools", "<unknown>") + match = re.search(r"""QtWebEngine/([\d.]+)""", useragent) + if match: + webengineVersion = match.group(1) + else: + webengineVersion = QCoreApplication.translate( + "WebBrowserTools", "<unknown>") + return (chromeVersion, webengineVersion)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Tools/WebHitTestResult.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an object for testing certain aspects of a web page. +""" + +# +# This code was ported from QupZilla. +# Copyright (C) David Rosca <nowrep@gmail.com> +# + +from __future__ import unicode_literals + +from PyQt5.QtCore import QPoint, QRect, QUrl + +class WebHitTestResult(object): + """ + Class implementing an object for testing certain aspects of a web page. + """ + def __init__(self, page, pos): + """ + Constructor + + @param page reference to the web page + @type WebBrowserPage + @param pos position to be tested + @type QPoint + """ + self.__isNull = True + self.__isContentEditable = False + self.__isContentSelected = False + self.__isMediaPaused = False + self.__isMediaMuted = False + self.__pos = QPoint(pos) + self.__alternateText = "" + self.__boundingRect = QRect() + self.__imageUrl = QUrl() + self.__linkTitle = "" + self.__linkUrl = QUrl() + self.__mediaUrl = QUrl() + self.__tagName = "" + + script = """ + (function() {{ + var e = document.elementFromPoint({0}, {1}); + if (!e) + return; + function isMediaElement(e) {{ + return e.tagName == 'AUDIO' || e.tagName == 'VIDEO'; + }} + function isEditableElement(e) {{ + if (e.isContentEditable) + return true; + if (e.tagName == 'INPUT' || e.tagName == 'TEXTAREA') + return e.getAttribute('readonly') != 'readonly'; + return false; + }} + function isSelected(e) {{ + var selection = window.getSelection(); + if (selection.type != 'Range') + return false; + return window.getSelection().containsNode(e, true); + }} + var res = {{ + alternateText: e.getAttribute('alt'), + boundingRect: '', + imageUrl: '', + contentEditable: isEditableElement(e), + contentSelected: isSelected(e), + linkTitle: '', + linkUrl: '', + mediaUrl: '', + mediaPaused: false, + mediaMuted: false, + tagName: e.tagName.toLowerCase() + }}; + var r = e.getBoundingClientRect(); + res.boundingRect = [r.top, r.left, r.width, r.height]; + if (e.tagName == 'IMG') + res.imageUrl = e.getAttribute('src'); + if (e.tagName == 'A') {{ + res.linkTitle = e.text; + res.linkUrl = e.getAttribute('href'); + }} + while (e) {{ + if (res.linkTitle == '' && e.tagName == 'A') + res.linkTitle = e.text; + if (res.linkUrl == '' && e.tagName == 'A') + res.linkUrl = e.getAttribute('href'); + if (res.mediaUrl == '' && isMediaElement(e)) {{ + res.mediaUrl = e.currentSrc; + res.mediaPaused = e.paused; + res.mediaMuted = e.muted; + }} + e = e.parentElement; + }} + return res; + }})() + """.format(pos.x(), pos.y()) + self.__populate(page.url(), page.execJavaScript(script)) + + def alternateText(self): + """ + Public method to get the alternate text. + + @return alternate text + @rtype str + """ + return self.__alternateText + + def boundingRect(self): + """ + Public method to get the bounding rectangle. + + @return bounding rectangle + @rtype QRect + """ + return QRect(self.__boundingRect) + + def imageUrl(self): + """ + Public method to get the URL of an image. + + @return image URL + @rtype QUrl + """ + return self.__imageUrl + + def isContentEditable(self): + """ + Public method to check for editable content. + + @return flag indicating editable content + @rtype bool + """ + return self.__isContentEditable + + def isContentSelected(self): + """ + Public method to check for selected content. + + @return flag indicating selected content + @rtype bool + """ + return self.__isContentSelected + + def isNull(self): + """ + Public method to test, if the hit test is empty. + + @return flag indicating an empty object + @rtype bool + """ + return self.__isNull + + def linkTitle(self): + """ + Public method to get the title for a link element. + + @return title for a link element + @rtype str + """ + return self.__linkTitle + + def linkUrl(self): + """ + Public method to get the URL for a link element. + + @return URL for a link element + @rtype QUrl + """ + return self.__linkUrl + + def mediaUrl(self): + """ + Public method to get the URL for a media element. + + @return URL for a media element + @rtype QUrl + """ + return self.__mediaUrl + + def mediaPaused(self): + """ + Public method to check, if a media element is paused. + + @return flag indicating a paused media element + @rtype bool + """ + return self.__isMediaPaused + + def mediaMuted(self): + """ + Public method to check, if a media element is muted. + + @return flag indicating a muted media element + @rtype bool + """ + return self.__isMediaMuted + + def pos(self): + """ + Public method to get the position of the hit test. + + @return position of hit test + @rtype QPoint + """ + return QPoint(self.__pos) + + def tagName(self): + """ + Public method to get the name of the tested tag. + + @return name of the tested tag + @rtype str + """ + return self.__tagName + + def __populate(self, url, res): + """ + Private method to populate the object. + + @param url URL of the tested page + @type QUrl + @param res dictionary with result data from JavaScript + @type dict + """ + if not res: + return + + self.__alternateText = res["alternateText"] + self.__imageUrl = QUrl(res["imageUrl"]) + self.__isContentEditable = res["contentEditable"] + self.__isContentSelected = res["contentSelected"] + self.__linkTitle = res["linkTitle"] + self.__linkUrl = QUrl(res["linkUrl"]) + self.__mediaUrl = QUrl(res["mediaUrl"]) + self.__isMediaPaused = res["mediaPaused"] + self.__isMediaMuted = res["mediaMuted"] + self.__tagName = res["tagName"] + + rect = res["boundingRect"] + if len(rect) == 4: + self.__boundingRect = QRect(int(rect[0]), int(rect[1]), + int(rect[2]), int(rect[3])) + + if not self.__imageUrl.isEmpty(): + self.__imageUrl = url.resolved(self.__imageUrl) + if not self.__linkUrl.isEmpty(): + self.__linkUrl = url.resolved(self.__linkUrl) + if not self.__mediaUrl.isEmpty(): + self.__mediaUrl = url.resolved(self.__mediaUrl)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Tools/WebIconDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to manage the Favicons. +""" + +from PyQt5.QtCore import pyqtSlot, Qt, QPoint +from PyQt5.QtWidgets import QDialog, QListWidgetItem, QMenu + +from .Ui_WebIconDialog import Ui_WebIconDialog + + +class WebIconDialog(QDialog, Ui_WebIconDialog): + """ + Class implementing a dialog to manage the Favicons. + """ + def __init__(self, iconsDB, parent=None): + """ + Constructor + + @param iconsDB icons database + @type dict + @param parent reference to the parent widget + @type QWidget + """ + super(WebIconDialog, self).__init__(parent) + self.setupUi(self) + + for url, icon in iconsDB.items(): + QListWidgetItem(icon, url, self.iconsList) + self.iconsList.sortItems(Qt.AscendingOrder) + + self.__setRemoveButtons() + + def __setRemoveButtons(self): + """ + Private method to set the state of the 'remove' buttons. + """ + self.removeAllButton.setEnabled(self.iconsList.count() > 0) + self.removeButton.setEnabled(len(self.iconsList.selectedItems()) > 0) + + @pyqtSlot(QPoint) + def on_iconsList_customContextMenuRequested(self, pos): + """ + Private slot to show the context menu. + + @param pos cursor position + @type QPoint + """ + menu = QMenu() + menu.addAction( + self.tr("Remove Selected"), + self.on_removeButton_clicked).setEnabled( + len(self.iconsList.selectedItems()) > 0) + menu.addAction( + self.tr("Remove All"), + self.on_removeAllButton_clicked).setEnabled( + self.iconsList.count() > 0) + + menu.exec_(self.iconsList.mapToGlobal(pos)) + + @pyqtSlot() + def on_iconsList_itemSelectionChanged(self): + """ + Private slot handling the selection of entries. + """ + self.__setRemoveButtons() + + @pyqtSlot() + def on_removeButton_clicked(self): + """ + Private slot to remove the selected items. + """ + for itm in self.iconsList.selectedItems(): + row = self.iconsList.row(itm) + self.iconsList.takeItem(row) + del itm + + @pyqtSlot() + def on_removeAllButton_clicked(self): + """ + Private slot to remove all entries. + """ + self.iconsList.clear() + + def getUrls(self): + """ + Public method to get the list of URLs. + + @return list of URLs + @rtype list of str + """ + urls = [] + for row in range(self.iconsList.count()): + urls.append(self.iconsList.item(row).text()) + + return urls
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Tools/WebIconDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>WebIconDialog</class> + <widget class="QDialog" name="WebIconDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>550</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>Favicons</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" rowspan="3"> + <widget class="QListWidget" name="iconsList"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="iconSize"> + <size> + <width>22</width> + <height>22</height> + </size> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected entries</string> + </property> + <property name="text"> + <string>&Remove</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all entries</string> + </property> + <property name="text"> + <string>Remove &All</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>WebIconDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>228</x> + <y>581</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>WebIconDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>296</x> + <y>587</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Tools/WebIconLoader.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an object to load web site icons. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject +from PyQt5.QtGui import QIcon, QPixmap, QImage +from PyQt5.QtNetwork import QNetworkRequest + +import WebBrowser.WebBrowserWindow + + +class WebIconLoader(QObject): + """ + Class implementing a loader for web site icons. + + @signal iconLoaded(icon) emitted when the con has been loaded + """ + iconLoaded = pyqtSignal(QIcon) + + def __init__(self, url, parent=None): + """ + Constructor + + @param url URL to fetch the icon from + @type QUrl + @param parent reference to the parent object + @type QObject + """ + super(WebIconLoader, self).__init__(parent) + + networkManager = \ + WebBrowser.WebBrowserWindow.WebBrowserWindow.networkManager() + self.__reply = networkManager.get(QNetworkRequest(url)) + self.__reply.finished.connect(self.__finished) + + @pyqtSlot() + def __finished(self): + """ + Private slot handling the downloaded icon. + """ + # ignore any errors and emit an empty icon in this case + data = self.__reply.readAll() + icon = QIcon(QPixmap.fromImage(QImage.fromData(data))) + self.iconLoaded.emit(icon) + + self.__reply.deleteLater() + self.__reply = None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Tools/WebIconProvider.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module containing a web site icon storage object. +""" + +from __future__ import unicode_literals + +import json +import os + +from PyQt5.QtCore import pyqtSignal, QObject, QByteArray, QBuffer, QIODevice, \ + QUrl +from PyQt5.QtGui import QIcon, QPixmap, QImage +from PyQt5.QtWidgets import QDialog + +from Utilities.AutoSaver import AutoSaver + +import UI.PixmapCache + + +class WebIconProvider(QObject): + """ + Class implementing a web site icon storage. + """ + changed = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(WebIconProvider, self).__init__(parent) + + self.__encoding = "iso-8859-1" + self.__iconsFileName = "web_site_icons.json" + self.__iconDatabasePath = "" # saving of icons disabled + + self.__iconsDB = {} + self.__loaded = False + + self.__saveTimer = AutoSaver(self, self.save) + + self.changed.connect(self.__saveTimer.changeOccurred) + + def setIconDatabasePath(self, path): + """ + Public method to set the path for the web site icons store. + + @param path path to store the icons file to + @type str + """ + if path != self.__iconDatabasePath: + self.close() + + self.__iconDatabasePath = path + + def iconDatabasePath(self): + """ + Public method o get the path for the web site icons store. + + @return path to store the icons file to + @rtype str + """ + return self.__iconDatabasePath + + def close(self): + """ + Public method to close the web icon provider. + """ + self.__saveTimer.saveIfNeccessary() + self.__loaded = False + self.__iconsDB = {} + + def load(self): + """ + Public method to load the bookmarks. + """ + if self.__loaded: + return + + if self.__iconDatabasePath: + filename = os.path.join(self.__iconDatabasePath, + self.__iconsFileName) + try: + f = open(filename, "r") + db = json.load(f) + f.close() + except (IOError, OSError): + # ignore silentyl + db = {} + + self.__iconsDB = {} + for url, data in db.items(): + self.__iconsDB[url] = QIcon(QPixmap.fromImage(QImage.fromData( + QByteArray(data.encode(self.__encoding))))) + + self.__loaded = True + + def save(self): + """ + Public method to save the zoom values. + """ + if not self.__loaded: + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if not WebBrowserWindow.isPrivate() and bool(self.__iconDatabasePath): + db = {} + for url, icon in self.__iconsDB.items(): + ba = QByteArray() + buffer = QBuffer(ba) + buffer.open(QIODevice.WriteOnly) + icon.pixmap(32).toImage().save(buffer, "PNG") + db[url] = bytes(buffer.data()).decode(self.__encoding) + + filename = os.path.join(self.__iconDatabasePath, + self.__iconsFileName) + try: + f = open(filename, "w") + json.dump(db, f) + f.close() + except (IOError, OSError): + # ignore silentyl + pass + + def saveIcon(self, view): + """ + Public method to save a web site icon. + + @param view reference to the view object + @type WebBrowserView + """ + scheme = view.url().scheme() + if scheme in ["eric", "about", "qthelp", "file", "abp", "ftp"]: + return + + self.load() + + if view.mainWindow().isPrivate(): + return + + urlStr = self.__urlToString(view.url()) + self.__iconsDB[urlStr] = view.icon() + + self.changed.emit() + + def __urlToString(self, url): + """ + Private method to convert an URL to a string. + + @param url URL to be converted + @type QUrl + @return string representation of the URL + @rtype str + """ + return url.toString(QUrl.PrettyDecoded | QUrl.RemoveUserInfo | + QUrl.RemoveFragment) + + def iconForUrl(self, url): + """ + Public method to get an icon for an URL. + + @param url URL to get icon for + @type QUrl + @return icon for the URL + @rtype QIcon + """ + scheme = url.scheme() + if scheme in ["eric", "about"]: + return UI.PixmapCache.getIcon("ericWeb.png") + elif scheme == "qthelp": + return UI.PixmapCache.getIcon("qthelp.png") + elif scheme == "file": + return UI.PixmapCache.getIcon("fileMisc.png") + elif scheme == "abp": + return UI.PixmapCache.getIcon("adBlockPlus.png") + elif scheme == "ftp": + return UI.PixmapCache.getIcon("network-server.png") + + self.load() + + urlStr = self.__urlToString(url) + for iconUrlStr in self.__iconsDB: + if iconUrlStr.startswith(urlStr): + return self.__iconsDB[iconUrlStr] + + # try replacing http scheme with https scheme + url = QUrl(url) + if url.scheme() == "http": + url.setScheme("https") + urlStr = self.__urlToString(url) + for iconUrlStr in self.__iconsDB: + if iconUrlStr.startswith(urlStr): + return self.__iconsDB[iconUrlStr] + + if scheme == "https": + return UI.PixmapCache.getIcon("securityHigh32.png") + else: + return UI.PixmapCache.getIcon("defaultIcon.png") + + def clear(self): + """ + Public method to clear the icons cache. + """ + self.load() + self.__iconsDB = {} + self.changed.emit() + self.__saveTimer.saveIfNeccessary() + + def showWebIconDialog(self): + """ + Public method to show a dialog to manage the Favicons. + """ + self.load() + + from .WebIconDialog import WebIconDialog + dlg = WebIconDialog(self.__iconsDB) + if dlg.exec_() == QDialog.Accepted: + changed = False + urls = dlg.getUrls() + for url in list(self.__iconsDB.keys())[:]: + if url not in urls: + del self.__iconsDB[url] + changed = True + if changed: + self.changed.emit() + + +__WebIconProvider = None + + +def instance(): + """ + Global function to get a reference to the web icon provider and create it, + if it hasn't been yet. + + @return reference to the web icon provider object + @rtype WebIconProvider + """ + global __WebIconProvider + + if __WebIconProvider is None: + __WebIconProvider = WebIconProvider() + + return __WebIconProvider
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Tools/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package containing tools for the QtWebEngine based browser. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UrlBar/BookmarkActionSelectionDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to select the action to be performed on the +bookmark. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QDialog + +from .Ui_BookmarkActionSelectionDialog import Ui_BookmarkActionSelectionDialog + +import UI.PixmapCache + + +class BookmarkActionSelectionDialog(QDialog, Ui_BookmarkActionSelectionDialog): + """ + Class implementing a dialog to select the action to be performed on + the bookmark. + """ + Undefined = -1 + AddBookmark = 0 + EditBookmark = 1 + AddSpeeddial = 2 + RemoveSpeeddial = 3 + + def __init__(self, url, parent=None): + """ + Constructor + + @param url URL to be worked on (QUrl) + @param parent reference to the parent widget (QWidget) + """ + super(BookmarkActionSelectionDialog, self).__init__(parent) + self.setupUi(self) + + self.__action = self.Undefined + + self.icon.setPixmap(UI.PixmapCache.getPixmap("bookmark32.png")) + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + + if WebBrowserWindow.bookmarksManager().bookmarkForUrl(url) is None: + self.__bmAction = self.AddBookmark + self.bookmarkPushButton.setText(self.tr("Add Bookmark")) + else: + self.__bmAction = self.EditBookmark + self.bookmarkPushButton.setText(self.tr("Edit Bookmark")) + + if WebBrowserWindow.speedDial().pageForUrl(url).url: + self.__sdAction = self.RemoveSpeeddial + self.speeddialPushButton.setText( + self.tr("Remove from Speed Dial")) + else: + self.__sdAction = self.AddSpeeddial + self.speeddialPushButton.setText(self.tr("Add to Speed Dial")) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + @pyqtSlot() + def on_bookmarkPushButton_clicked(self): + """ + Private slot handling selection of a bookmark action. + """ + self.__action = self.__bmAction + self.accept() + + @pyqtSlot() + def on_speeddialPushButton_clicked(self): + """ + Private slot handling selection of a speed dial action. + """ + self.__action = self.__sdAction + self.accept() + + def getAction(self): + """ + Public method to get the selected action. + + @return reference to the associated action + """ + return self.__action
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UrlBar/BookmarkActionSelectionDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BookmarkActionSelectionDialog</class> + <widget class="QDialog" name="BookmarkActionSelectionDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>291</width> + <height>153</height> + </rect> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="icon"> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string><b>Add/Edit Bookmark</b></string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="bookmarkPushButton"/> + </item> + <item> + <widget class="QPushButton" name="speeddialPushButton"/> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UrlBar/BookmarkInfoDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show some bookmark info. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtGui import QFont +from PyQt5.QtWidgets import QDialog + +from .Ui_BookmarkInfoDialog import Ui_BookmarkInfoDialog + +import UI.PixmapCache + + +class BookmarkInfoDialog(QDialog, Ui_BookmarkInfoDialog): + """ + Class implementing a dialog to show some bookmark info. + """ + def __init__(self, bookmark, parent=None): + """ + Constructor + + @param bookmark reference to the bookmark to be shown (Bookmark) + @param parent reference to the parent widget (QWidget) + """ + super(BookmarkInfoDialog, self).__init__(parent) + self.setupUi(self) + + self.__bookmark = bookmark + + self.icon.setPixmap(UI.PixmapCache.getPixmap("bookmark32.png")) + + font = QFont() + font.setPointSize(font.pointSize() + 2) + self.title.setFont(font) + + if bookmark is None: + self.titleEdit.setEnabled(False) + else: + self.titleEdit.setText(bookmark.title) + self.titleEdit.setFocus() + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + @pyqtSlot() + def on_removeButton_clicked(self): + """ + Private slot to remove the current bookmark. + """ + import WebBrowser.WebBrowserWindow + WebBrowser.WebBrowserWindow.WebBrowserWindow.bookmarksManager()\ + .removeBookmark(self.__bookmark) + self.close() + + def accept(self): + """ + Public slot handling the acceptance of the dialog. + """ + if self.__bookmark is not None and \ + self.titleEdit.text() != self.__bookmark.title: + import WebBrowser.WebBrowserWindow + WebBrowser.WebBrowserWindow.WebBrowserWindow.bookmarksManager()\ + .setTitle(self.__bookmark, self.titleEdit.text()) + self.close()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UrlBar/BookmarkInfoDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BookmarkInfoDialog</class> + <widget class="QDialog" name="BookmarkInfoDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>350</width> + <height>135</height> + </rect> + </property> + <property name="windowTitle"> + <string>Edit Bookmark</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>10</number> + </property> + <item> + <widget class="QLabel" name="icon"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="title"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Edit this Bookmark</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Press to remove this bookmark</string> + </property> + <property name="text"> + <string>Remove this Bookmark</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Title:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="titleEdit"/> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>removeButton</tabstop> + <tabstop>titleEdit</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>BookmarkInfoDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>227</x> + <y>114</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>134</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>BookmarkInfoDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>295</x> + <y>120</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>134</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UrlBar/FavIconLabel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the label to show the web site icon. +""" + +from __future__ import unicode_literals +try: + str = unicode +except NameError: + pass + +from PyQt5.QtCore import Qt, QPoint, QMimeData +from PyQt5.QtGui import QDrag, QPixmap +from PyQt5.QtWidgets import QLabel, QApplication + + +class FavIconLabel(QLabel): + """ + Class implementing the label to show the web site icon. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(FavIconLabel, self).__init__(parent) + + self.__browser = None + self.__dragStartPos = QPoint() + + self.setFocusPolicy(Qt.NoFocus) + self.setCursor(Qt.ArrowCursor) + self.setMinimumSize(16, 16) + self.resize(16, 16) + + self.__browserIconChanged() + + def __browserIconChanged(self): + """ + Private slot to set the icon. + """ + if self.__browser: + self.setPixmap( + self.__browser.icon().pixmap(16, 16)) + + def __clearIcon(self): + """ + Private slot to clear the icon. + """ + self.setPixmap(QPixmap()) + + def setBrowser(self, browser): + """ + Public method to set the browser connection. + + @param browser reference to the browser widegt (HelpBrowser) + """ + self.__browser = browser + self.__browser.loadFinished.connect(self.__browserIconChanged) + self.__browser.iconChanged.connect(self.__browserIconChanged) + self.__browser.loadStarted.connect(self.__clearIcon) + + def mousePressEvent(self, evt): + """ + Protected method to handle mouse press events. + + @param evt reference to the mouse event (QMouseEvent) + """ + if evt.button() == Qt.LeftButton: + self.__dragStartPos = evt.pos() + super(FavIconLabel, self).mousePressEvent(evt) + + def mouseMoveEvent(self, evt): + """ + Protected method to handle mouse move events. + + @param evt reference to the mouse event (QMouseEvent) + """ + if evt.button() == Qt.LeftButton and \ + (evt.pos() - self.__dragStartPos).manhattanLength() > \ + QApplication.startDragDistance() and \ + self.__browser is not None: + drag = QDrag(self) + mimeData = QMimeData() + title = self.__browser.title() + if title == "": + title = str(self.__browser.url().toEncoded(), encoding="utf-8") + mimeData.setText(title) + mimeData.setUrls([self.__browser.url()]) + p = self.pixmap() + if p: + drag.setPixmap(p) + drag.setMimeData(mimeData) + drag.exec_()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UrlBar/StackedUrlBar.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a widget to stack url bars. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QStackedWidget, QSizePolicy + + +class StackedUrlBar(QStackedWidget): + """ + Class implementing a widget to stack URL bars. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(StackedUrlBar, self).__init__(parent) + + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(6) + sizePolicy.setVerticalStretch(0) + self.setSizePolicy(sizePolicy) + self.setMinimumSize(200, 22) + + def currentUrlBar(self): + """ + Public method to get a reference to the current URL bar. + + @return reference to the current URL bar (UrlBar) + """ + return self.urlBar(self.currentIndex()) + + def urlBar(self, index): + """ + Public method to get a reference to the URL bar for a given index. + + @param index index of the url bar (integer) + @return reference to the URL bar for the given index (UrlBar) + """ + return self.widget(index) + + def moveBar(self, from_, to_): + """ + Public slot to move an URL bar. + + @param from_ index of URL bar to be moved (integer) + @param to_ index to move the URL bar to (integer) + """ + fromBar = self.widget(from_) + self.removeWidget(fromBar) + self.insertWidget(to_, fromBar) + + def urlBars(self): + """ + Public method to get a list of references to all URL bars. + + @return list of references to URL bars (list of UrlBar) + """ + urlBars = [] + for index in range(self.count()): + urlBars.append(self.widget(index)) + return urlBars
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UrlBar/UrlBar.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,381 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the URL bar widget. +""" + +from __future__ import unicode_literals +try: + str = unicode # __IGNORE_EXCEPTION__ +except NameError: + pass + +from PyQt5.QtCore import pyqtSlot, Qt, QPointF, QUrl, QDateTime, QTimer +from PyQt5.QtGui import QColor, QPalette, QLinearGradient, QIcon +from PyQt5.QtWidgets import QDialog, QApplication +from PyQt5.QtWebEngineWidgets import QWebEnginePage + +from E5Gui.E5LineEdit import E5LineEdit +from E5Gui.E5LineEditButton import E5LineEditButton + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + +from .FavIconLabel import FavIconLabel +import UI.PixmapCache +import Preferences + + +class UrlBar(E5LineEdit): + """ + Class implementing a line edit for entering URLs. + """ + def __init__(self, mainWindow, parent=None): + """ + Constructor + + @param mainWindow reference to the main window (HelpWindow) + @param parent reference to the parent widget (HelpBrowser) + """ + E5LineEdit.__init__(self, parent) + self.setInactiveText(self.tr("Enter the URL here.")) + self.setWhatsThis(self.tr("Enter the URL here.")) + + self.__mw = mainWindow + self.__browser = None + self.__privateMode = WebBrowserWindow.isPrivate() + + self.__bmActiveIcon = UI.PixmapCache.getIcon("bookmark16.png") + self.__bmInactiveIcon = QIcon( + self.__bmActiveIcon.pixmap(16, 16, QIcon.Disabled)) + + self.__favicon = FavIconLabel(self) + self.addWidget(self.__favicon, E5LineEdit.LeftSide) + + self.__rssButton = E5LineEditButton(self) + self.__rssButton.setIcon(UI.PixmapCache.getIcon("rss16.png")) + self.addWidget(self.__rssButton, E5LineEdit.RightSide) + self.__rssButton.setVisible(False) + + self.__bookmarkButton = E5LineEditButton(self) + self.addWidget(self.__bookmarkButton, E5LineEdit.RightSide) + self.__bookmarkButton.setVisible(False) + + self.__clearButton = E5LineEditButton(self) + self.__clearButton.setIcon(UI.PixmapCache.getIcon("clearLeft.png")) + self.addWidget(self.__clearButton, E5LineEdit.RightSide) + self.__clearButton.setVisible(False) + + self.__bookmarkButton.clicked.connect(self.__showBookmarkInfo) + self.__rssButton.clicked.connect(self.__rssClicked) + self.__clearButton.clicked.connect(self.clear) + self.textChanged.connect(self.__textChanged) + + self.__mw.bookmarksManager().entryChanged.connect( + self.__bookmarkChanged) + self.__mw.bookmarksManager().entryAdded.connect( + self.__bookmarkChanged) + self.__mw.bookmarksManager().entryRemoved.connect( + self.__bookmarkChanged) + self.__mw.speedDial().pagesChanged.connect( + self.__bookmarkChanged) + + def setBrowser(self, browser): + """ + Public method to set the browser connection. + + @param browser reference to the browser widget (WebBrowserView) + """ + self.__browser = browser + self.__favicon.setBrowser(browser) + + self.__browser.urlChanged.connect(self.__browserUrlChanged) + self.__browser.loadProgress.connect(self.update) + self.__browser.loadFinished.connect(self.__loadFinished) + self.__browser.loadStarted.connect(self.__loadStarted) + + def browser(self): + """ + Public method to get the associated browser. + + @return reference to the associated browser (HelpBrowser) + """ + return self.__browser + + def __browserUrlChanged(self, url): + """ + Private slot to handle a URL change of the associated browser. + + @param url new URL of the browser (QUrl) + """ + strUrl = url.toString() + if strUrl in ["eric:speeddial", "eric:home", + "about:blank", "about:config"]: + strUrl = "" + + if self.text() != strUrl: + self.setText(strUrl) + self.setCursorPosition(0) + + def __loadStarted(self): + """ + Private slot to perform actions before the page is loaded. + """ + self.__bookmarkButton.setVisible(False) + self.__rssButton.setVisible(False) + + def __checkBookmark(self): + """ + Private slot to check the current URL for the bookmarked state. + """ + manager = self.__mw.bookmarksManager() + if manager.bookmarkForUrl(self.__browser.url()) is not None: + self.__bookmarkButton.setIcon(self.__bmActiveIcon) + bookmarks = manager.bookmarksForUrl(self.__browser.url()) + from WebBrowser.Bookmarks.BookmarkNode import BookmarkNode + for bookmark in bookmarks: + manager.setTimestamp(bookmark, BookmarkNode.TsVisited, + QDateTime.currentDateTime()) + elif self.__mw.speedDial()\ + .pageForUrl(self.__browser.url()).url != "": + self.__bookmarkButton.setIcon(self.__bmActiveIcon) + else: + self.__bookmarkButton.setIcon(self.__bmInactiveIcon) + + def __loadFinished(self, ok): + """ + Private slot to set some data after the page was loaded. + + @param ok flag indicating a successful load (boolean) + """ + if self.__browser.url().scheme() in ["eric", "about"]: + self.__bookmarkButton.setVisible(False) + else: + self.__checkBookmark() + self.__bookmarkButton.setVisible(True) + + if ok: + QTimer.singleShot(0, self.__setRssButton) + + def __textChanged(self, txt): + """ + Private slot to handle changes of the text. + + @param txt current text (string) + """ + self.__clearButton.setVisible(txt != "") + + def preferencesChanged(self): + """ + Public slot to handle a change of preferences. + """ + self.update() + + def __showBookmarkInfo(self): + """ + Private slot to show a dialog with some bookmark info. + """ + from .BookmarkActionSelectionDialog import \ + BookmarkActionSelectionDialog + url = self.__browser.url() + dlg = BookmarkActionSelectionDialog(url) + if dlg.exec_() == QDialog.Accepted: + action = dlg.getAction() + if action == BookmarkActionSelectionDialog.AddBookmark: + self.__browser.addBookmark() + elif action == BookmarkActionSelectionDialog.EditBookmark: + bookmark = self.__mw.bookmarksManager()\ + .bookmarkForUrl(url) + from .BookmarkInfoDialog import BookmarkInfoDialog + dlg = BookmarkInfoDialog(bookmark, self.__browser) + dlg.exec_() + elif action == BookmarkActionSelectionDialog.AddSpeeddial: + self.__mw.speedDial().addPage( + url, self.__browser.title()) + elif action == BookmarkActionSelectionDialog.RemoveSpeeddial: + self.__mw.speedDial().removePage(url) + + @pyqtSlot() + def __bookmarkChanged(self): + """ + Private slot to handle bookmark or speed dial changes. + """ + self.__checkBookmark() + + def paintEvent(self, evt): + """ + Protected method handling a paint event. + + @param evt reference to the paint event (QPaintEvent) + """ + if self.__privateMode: + backgroundColor = QColor(220, 220, 220) # light gray + foregroundColor = Qt.black + else: + backgroundColor = QApplication.palette().color(QPalette.Base) + foregroundColor = QApplication.palette().color(QPalette.Text) + + if self.__browser is not None: + p = self.palette() + progress = self.__browser.progress() + if progress == 0 or progress == 100: + if self.__browser.url().scheme() == "https": + backgroundColor = Preferences.getWebBrowser( + "SaveUrlColor") + p.setBrush(QPalette.Base, backgroundColor) + p.setBrush(QPalette.Text, foregroundColor) + else: + if self.__browser.url().scheme() == "https": + backgroundColor = Preferences.getWebBrowser( + "SaveUrlColor") + highlight = QApplication.palette().color(QPalette.Highlight) + r = (highlight.red() + 2 * backgroundColor.red()) // 3 + g = (highlight.green() + 2 * backgroundColor.green()) // 3 + b = (highlight.blue() + 2 * backgroundColor.blue()) // 3 + + loadingColor = QColor(r, g, b) + if abs(loadingColor.lightness() - + backgroundColor.lightness()) < 20: + # special handling for special color schemes (e.g Gaia) + r = (2 * highlight.red() + backgroundColor.red()) // 3 + g = (2 * highlight.green() + backgroundColor.green()) // 3 + b = (2 * highlight.blue() + backgroundColor.blue()) // 3 + loadingColor = QColor(r, g, b) + + gradient = QLinearGradient( + QPointF(0, 0), QPointF(self.width(), 0)) + gradient.setColorAt(0, loadingColor) + gradient.setColorAt(progress / 100.0 - 0.000001, loadingColor) + gradient.setColorAt(progress / 100.0, backgroundColor) + p.setBrush(QPalette.Base, gradient) + + self.setPalette(p) + + E5LineEdit.paintEvent(self, evt) + + def focusOutEvent(self, evt): + """ + Protected method to handle focus out event. + + @param evt reference to the focus event (QFocusEvent) + """ + if self.text() == "" and self.__browser is not None: + self.__browserUrlChanged(self.__browser.url()) + E5LineEdit.focusOutEvent(self, evt) + + def mousePressEvent(self, evt): + """ + Protected method called by a mouse press event. + + @param evt reference to the mouse event (QMouseEvent) + """ + if evt.button() == Qt.XButton1: + self.__mw.currentBrowser().triggerPageAction( + QWebEnginePage.Back) + elif evt.button() == Qt.XButton2: + self.__mw.currentBrowser().triggerPageAction( + QWebEnginePage.Forward) + else: + super(UrlBar, self).mousePressEvent(evt) + + def mouseDoubleClickEvent(self, evt): + """ + Protected method to handle mouse double click events. + + @param evt reference to the mouse event (QMouseEvent) + """ + if evt.button() == Qt.LeftButton: + self.selectAll() + else: + E5LineEdit.mouseDoubleClickEvent(self, evt) + + def keyPressEvent(self, evt): + """ + Protected method to handle key presses. + + @param evt reference to the key press event (QKeyEvent) + """ + if evt.key() == Qt.Key_Escape: + if self.__browser is not None: + self.setText( + str(self.__browser.url().toEncoded(), encoding="utf-8")) + self.selectAll() + completer = self.completer() + if completer: + completer.popup().hide() + return + + currentText = self.text().strip() + if evt.key() in [Qt.Key_Enter, Qt.Key_Return] and \ + not currentText.lower().startswith("http://"): + append = "" + if evt.modifiers() == Qt.KeyboardModifiers(Qt.ControlModifier): + append = ".com" + elif evt.modifiers() == Qt.KeyboardModifiers( + Qt.ControlModifier | Qt.ShiftModifier): + append = ".org" + elif evt.modifiers() == Qt.KeyboardModifiers(Qt.ShiftModifier): + append = ".net" + + if append != "": + url = QUrl("http://www." + currentText) + host = url.host() + if not host.lower().endswith(append): + host += append + url.setHost(host) + self.setText(url.toString()) + + E5LineEdit.keyPressEvent(self, evt) + + def dragEnterEvent(self, evt): + """ + Protected method to handle drag enter events. + + @param evt reference to the drag enter event (QDragEnterEvent) + """ + mimeData = evt.mimeData() + if mimeData.hasUrls() or mimeData.hasText(): + evt.acceptProposedAction() + + E5LineEdit.dragEnterEvent(self, evt) + + def dropEvent(self, evt): + """ + Protected method to handle drop events. + + @param evt reference to the drop event (QDropEvent) + """ + mimeData = evt.mimeData() + + url = QUrl() + if mimeData.hasUrls(): + url = mimeData.urls()[0] + elif mimeData.hasText(): + url = QUrl.fromEncoded(mimeData.text().encode("utf-8"), + QUrl.TolerantMode) + + if url.isEmpty() or not url.isValid(): + E5LineEdit.dropEvent(self, evt) + return + + self.setText(str(url.toEncoded(), encoding="utf-8")) + self.selectAll() + + evt.acceptProposedAction() + + def __setRssButton(self): + """ + Private slot to show the RSS button. + """ + self.__rssButton.setVisible(self.__browser.checkRSS()) + + def __rssClicked(self): + """ + Private slot to handle clicking the RSS icon. + """ + from WebBrowser.Feeds.FeedsDialog import FeedsDialog + feeds = self.__browser.getRSS() + dlg = FeedsDialog(feeds, self.__browser) + dlg.exec_()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UrlBar/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the URL bar widget. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UserAgent/UserAgentDefaults.qrc Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,6 @@ +<!DOCTYPE RCC> +<RCC version="1.0"> +<qresource> + <file>UserAgentDefaults.xml</file> +</qresource> +</RCC>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UserAgent/UserAgentDefaults.xml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,53 @@ +<useragentswitcher> + <useragentmenu title="Firefox"> + <useragent description="Firefox 9.0.1 (Windows)" useragent="Mozilla/5.0 (Windows; U; Windows NT 6.2; rv:9.0.1) Gecko/20100101 Firefox/9.0.1"/> + <useragent description="Firefox 4.0.1 (Windows)" useragent="Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:2.0.1) Gecko/20100101 Firefox/4.0.1"/> + <useragent description="Firefox 3.5.3 (Windows)" useragent="Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3"/> + </useragentmenu> + + <useragentmenu title="Chrome"> + <useragent description="Chrome 18.0 (Windows)" useragent="Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.12 (KHTML, like Gecko) Chrome/18.6.872.0 Safari/535.12"/> + <useragent description="Chrome 17.0 (Windows)" useragent="Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.12 Safari/535.11"/> + <useragent description="Chrome 12.0 (Windows)" useragent="Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.0 Safari/534.30"/> + <useragent description="Chrome 11.0 (Windows)" useragent="Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24"/> + </useragentmenu> + + <useragentmenu title="Internet Explorer"> + <useragent description="Internet Explorer 10.0" useragent="Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1)"/> + <useragent description="Internet Explorer 9.0" useragent="Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)"/> + <useragent description="Internet Explorer 8.0" useragent="Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)"/> + <useragent description="Internet Explorer 7.0" useragent="Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)"/> + <useragent description="Internet Explorer 6.0" useragent="Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"/> + </useragentmenu> + + <useragentmenu title="Opera"> + <useragent description="Opera 12.0 (Linux)" useragent="Opera/9.80 (X11; Linux x86_64; U) Presto/2.9.181 Version/12.00"/> + <useragent description="Opera 12.0 (Mac)" useragent="Opera/9.80 (Macintosh; Intel Mac OS X 10.6.7; U) Presto/2.9.181 Version/12.00"/> + <useragent description="Opera 12.0 (Windows)" useragent="Opera/9.80 (Windows NT 6.1; U) Presto/2.9.181 Version/12.00"/> + <useragent description="Opera 11.1 (Linux)" useragent="Opera/9.80 (X11; Linux x86_64; U; en) Presto/2.8.131 Version/11.10"/> + <useragent description="Opera 11.1 (Mac)" useragent="Opera/9.80 (Macintosh; Intel Mac OS X 10.6.7; U; en) Presto/2.8.131 Version/11.10"/> + <useragent description="Opera 11.1 (Windows)" useragent="Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.10"/> + <useragent description="Opera 10.0 (Linux)" useragent="Opera/9.80 (X11; Linux x86_64; U; de) Presto/2.2.15 Version/10.00"/> + <useragent description="Opera 10.0 (Mac)" useragent="Opera/9.80 (Macintosh; Intel Mac OS X; U; en) Presto/2.2.15 Version/10.00"/> + <useragent description="Opera 10.0 (Windows)" useragent="Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.00"/> + </useragentmenu> + + <useragentmenu title="Safari"> + <useragent description="Safari 5.0.5 (Mac)" useragent="Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7_2; en-us) AppleWebKit/535.12 (KHTML, like Gecko) Version/5.0.5 Safari/535.12"/> + <useragent description="Safari 5.0.4 (Mac)" useragent="Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-us) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27"/> + <useragent description="Safari 4.0.4 (Mac)" useragent="Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-us) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10"/> + <separator/> + <useragent description="Mobile Safari 4.3.2 (iPad)" useragent="Mozilla/5.0 (iPad; U; CPU OS 4_3_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 Safari/6533.18.5"/> + <useragent description="Mobile Safari 4.3.2 (iPhone)" useragent="Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 Safari/6533.18.5"/> + <useragent description="Mobile Safari 4.3.2 (iPod touch)" useragent="Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 Safari/6533.18.5"/> + <useragent description="Mobile Safari 3.1.2 (iPhone)" useragent="Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_2 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7D11 Safari/528.16"/> + <useragent description="Mobile Safari 3.1.2 (iPod touch)" useragent="Mozilla/5.0 (iPod; U; CPU iPhone OS 3_1_2 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7D11 Safari/528.16"/> + </useragentmenu> + + <useragentmenu title="v_a_r_i_o_u_s"> + <useragent description="Googlebot 2.1" useragent="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"/> + <useragent description="Msnbot 1.1" useragent="msnbot/1.1 (+http://search.msn.com/msnbot.htm)" appcodename="" appname="" appversion="" platform="" vendor="" vendorsub=""/> + <useragent description="Yahoo Slurp" useragent="Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)" appcodename="" appname="" appversion="" platform="" vendor="" vendorsub=""/> + </useragentmenu> + +</useragentswitcher>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UserAgent/UserAgentDefaults_rc.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created: So. Juni 29 18:58:08 2014 +# by: The Resource Compiler for PyQt (Qt v5.3.1) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x03\xd6\ +\x00\ +\x00\x15\xe1\x78\x9c\xd5\x58\xdb\x6e\xdb\x38\x10\x7d\xef\x57\xcc\ +\xfa\x29\xc6\xee\x52\xa2\xee\xae\xd3\x02\x45\xef\x68\xbd\x0d\xe0\ +\x66\xdb\x3e\x09\xb2\xcd\x44\x42\x65\x51\xa0\xe4\x38\xdd\xaf\xdf\ +\x21\xe5\x8b\x6c\xc7\x0a\x65\xab\x05\x1a\xe4\x41\x24\x67\x74\xce\ +\x99\x19\x72\x28\x5f\x2e\x0a\x26\xa2\x5b\x96\x95\xc5\x32\x29\xa7\ +\x31\x13\xcf\x9f\x00\xfe\x5d\x6e\xe6\xe7\x2c\x5b\x40\x99\x94\x29\ +\x7b\xd6\x7b\x93\x08\x76\xc3\xef\x7b\x95\xcd\xae\x1d\xcc\x58\x31\ +\x15\x49\x5e\x26\x3c\xdb\x58\xc2\x80\x98\x84\xc2\xc5\x97\x24\x9b\ +\xf1\x65\xd1\xef\xc1\xc6\xfe\x59\x6f\xc4\xff\x4b\xd2\x34\x32\x5c\ +\x62\x6e\x2c\x86\x70\x3d\x84\xd5\x33\xfc\xf3\x19\x3c\x62\x0d\x41\ +\xdc\x3d\x55\xef\xe9\xc3\x5b\x36\xfd\xce\x0d\xcb\xa4\x26\xfe\x53\ +\x58\xa1\x18\x6a\xb5\x67\xe8\xd3\x72\xce\xa6\x45\x87\xc0\xb2\xbf\ +\xaf\xc7\x8a\x9d\xd5\xc8\xce\x69\xcb\xce\x26\x2e\xb1\xbb\x63\x47\ +\xc9\x80\x50\x62\x6f\xf9\x99\x03\x33\xb0\x9c\x0d\x3f\x05\xb7\xe6\ +\x77\x69\xec\x64\xfe\xf9\x93\x86\x72\x78\x19\x0b\x3e\x67\x1a\xd5\ +\x50\x19\x02\x0d\x6a\xa4\x1f\x97\xb5\x92\xd2\x87\x17\x79\x9e\xb2\ +\x2f\x6c\xf2\x21\x29\x0d\xd7\x76\x09\xb5\xe0\xe2\xc3\xbb\xcf\xa3\ +\x8f\x7f\x41\x9a\x7c\x67\x95\xae\x3e\x54\x28\x06\xa2\x78\x24\xf0\ +\x31\x27\x30\x8e\x6e\x22\x91\xac\x7c\x74\x32\xb0\x26\xea\x77\x44\ +\x94\x36\x12\x45\x14\x32\xf0\x6c\xa9\xa7\xce\x54\xab\x56\xd6\x4c\ +\xad\x2e\x98\x3a\xc4\x36\x1b\x99\xca\x02\xf7\x9d\x9d\x90\x4a\x9f\ +\x36\x44\x69\x37\x44\xb1\x6e\x9b\x88\x22\x0a\xf1\x06\x1e\x71\x77\ +\x98\x5a\xce\x29\xe5\xfd\x3e\x2b\x99\xc8\x58\x09\xaf\xef\xf3\x94\ +\x0b\x26\x34\x2a\xfd\xc0\x07\xa8\x49\xcc\xe3\x72\xa7\x7c\x9e\x47\ +\x65\x32\x49\xd9\x10\x46\xe3\xf7\xaf\x95\xf9\xfe\x66\xee\xeb\xc4\ +\xf9\x10\x79\x70\x04\xd8\x79\x10\x78\xd0\x19\x6e\xd0\x0a\x37\xe8\ +\x0c\xd7\x6f\x85\xeb\x1f\xe0\x9a\x27\xe2\x7a\xad\x70\xbd\x3d\x5c\ +\xb7\xa6\xb7\x4d\x75\x7e\xca\x71\x52\xa3\x22\x95\xdd\xea\x9c\xf8\ +\x98\x64\x8b\xfb\xdd\xcd\xa7\x96\xb1\x81\x06\xb8\xfc\x95\x62\xdf\ +\x50\x36\x70\x1f\x78\xa1\xe7\x60\x63\xe9\xc3\x95\x60\x45\x89\x6d\ +\x43\x76\x91\x80\xc2\xbf\x4c\x14\xf8\x62\x75\x26\x68\x1d\x00\x75\ +\x02\xa3\x68\x7a\x1c\x1e\x17\x93\xac\xe4\x45\x3c\x04\x19\xe4\x14\ +\x70\x02\x3e\x8d\xe1\xab\xdc\x14\x1e\xf1\x3b\x67\xf3\xe0\x69\x54\ +\x67\xb4\xdf\x53\xbb\xc3\xa7\xf2\xfa\x71\x42\x3a\x64\x5f\xaf\x91\ +\x08\x08\xb5\x6b\x24\xf0\xad\xad\x49\x9c\x9b\x92\xee\x19\xb5\x4f\ +\x4b\xa7\x1c\xcc\x13\x77\xca\x10\xdf\x56\x23\x61\x11\xea\x6e\x39\ +\x98\xed\xca\xc3\x3c\x7d\xb3\x1c\x86\xe3\x7c\x26\xed\x32\x62\xb6\ +\xa1\xd0\xe6\xc8\xab\x3a\xba\xc6\x99\x57\x19\xe2\xd1\x6a\x12\xf7\ +\x81\x38\xee\x74\xe0\x5a\x20\xaf\x1f\xa8\xf2\xd0\x0f\x2d\x75\x99\ +\x5e\x14\xda\x17\xd1\xb5\xd0\x0a\xbf\xf5\x25\xb4\xc6\xde\x39\x97\ +\xbd\x17\xfa\x47\xd8\xdb\xc4\x32\x89\xe5\x3e\x2a\xc0\xd9\x0a\xa8\ +\x5c\xfc\x16\x1a\x9c\x6e\x34\xd0\x23\x1a\x28\xb1\x28\x09\x1a\x25\ +\x38\xbb\x12\x94\xc7\xde\x71\x50\xb0\x3c\x12\x51\xc9\x85\x86\xae\ +\x11\x9f\x24\x29\x83\x8d\x3c\x0c\x09\x5c\x24\x57\xd1\xac\x41\x9f\ +\x5c\x56\xd2\x5e\x5e\x5d\x4b\x51\x4e\x68\x87\x56\x45\x76\xbb\x69\ +\x8f\xe4\x08\xbf\x11\x06\x8f\xa6\xc8\x82\x8a\x96\x11\xbc\xf3\xd7\ +\x52\x3d\xe5\x1d\x10\x57\x27\x5b\x47\x54\xc5\x3c\x63\x8d\xba\xa4\ +\xc1\x46\x59\x35\xfc\xad\x04\xf2\x19\x94\x7c\x31\x8d\x1b\x45\xf2\ +\xd9\x6f\x28\x11\xfd\xce\xcb\xa1\x1d\x52\x3d\x81\x16\x76\xdb\x47\ +\xb7\xe0\x5a\x9c\xff\x0a\x3f\x89\xd7\x9b\x51\x7a\x7a\x67\x48\x3b\ +\x3d\x7b\xbf\x5a\x5c\x9b\x26\x77\x17\x46\xa1\x08\x93\x90\x87\x8b\ +\xb0\xd0\xe8\x75\x6f\x39\xbf\x4d\xd9\x84\x97\x80\x4d\x56\xef\x43\ +\x73\xe3\x82\x9d\x19\x8f\xd6\x3f\xe3\xb2\xcc\x9f\x1a\xc6\x72\xb9\ +\x24\xb7\x6a\x89\xa0\xb5\x81\xeb\x24\x2e\xe7\xa9\xd6\x57\xd1\xa8\ +\xc8\x24\x05\xba\x47\x61\xae\xa6\x0d\x75\xad\x5b\xc3\x14\x2c\x12\ +\xd3\x98\xe0\x92\x82\xa9\x4c\x24\x12\xe6\x31\xca\xf3\x29\x9f\xb1\ +\x2c\x9a\x63\x24\xd4\xb0\xf6\x78\x57\x85\x5c\x8e\xf2\x34\x2a\x6f\ +\xb8\x98\xcb\xe7\x3b\x86\x77\x0f\xb1\x7d\x2a\x16\x13\x1c\x68\x70\ +\xfe\x16\xc5\x9c\xc3\x38\x5d\x88\x5c\x2f\x6e\xca\xe1\x8f\xca\x63\ +\x08\x2b\x39\x31\x4b\x73\xf2\x43\xae\x28\x39\x72\x88\xd9\x36\x7e\ +\x54\x32\x8d\x42\x1a\xff\x0c\x65\x87\x35\x55\x9b\xd9\xfe\x98\xfb\ +\x3f\x25\x8f\xae\x3e\ +" + +qt_resource_name = b"\ +\x00\x15\ +\x03\x80\x5a\x3c\ +\x00\x55\ +\x00\x73\x00\x65\x00\x72\x00\x41\x00\x67\x00\x65\x00\x6e\x00\x74\x00\x44\x00\x65\x00\x66\x00\x61\x00\x75\x00\x6c\x00\x74\x00\x73\ +\x00\x2e\x00\x78\x00\x6d\x00\x6c\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UserAgent/UserAgentManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a user agent manager. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, QObject, QXmlStreamReader + +from E5Gui import E5MessageBox + +from Utilities.AutoSaver import AutoSaver +import Utilities + + +class UserAgentManager(QObject): + """ + Class implementing a user agent manager. + + @signal changed() emitted to indicate a change + @signal userAgentSettingsSaved() emitted after the user agent settings + were saved + """ + changed = pyqtSignal() + userAgentSettingsSaved = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(UserAgentManager, self).__init__(parent) + + self.__agents = {} + # dictionary with agent strings indexed by host name + self.__loaded = False + self.__saveTimer = AutoSaver(self, self.save) + + self.changed.connect(self.__saveTimer.changeOccurred) + + def getFileName(self): + """ + Public method to get the file name of the user agents file. + + @return name of the user agents file (string) + """ + return os.path.join( + Utilities.getConfigDir(), "web_browser", "userAgentSettings.xml") + + def save(self): + """ + Public slot to save the user agent entries to disk. + """ + if not self.__loaded: + return + + from .UserAgentWriter import UserAgentWriter + agentFile = self.getFileName() + writer = UserAgentWriter() + if not writer.write(agentFile, self.__agents): + E5MessageBox.critical( + None, + self.tr("Saving user agent data"), + self.tr( + """<p>User agent data could not be saved to""" + """ <b>{0}</b></p>""").format(agentFile)) + else: + self.userAgentSettingsSaved.emit() + + def __load(self): + """ + Private method to load the saved user agent settings. + """ + agentFile = self.getFileName() + from .UserAgentReader import UserAgentReader + reader = UserAgentReader() + self.__agents = reader.read(agentFile) + if reader.error() != QXmlStreamReader.NoError: + E5MessageBox.warning( + None, + self.tr("Loading user agent data"), + self.tr("""Error when loading user agent data on""" + """ line {0}, column {1}:\n{2}""") + .format(reader.lineNumber(), + reader.columnNumber(), + reader.errorString())) + + self.__loaded = True + + def reload(self): + """ + Public method to reload the user agent settings. + """ + if not self.__loaded: + return + + self.__agents = {} + self.__load() + + def close(self): + """ + Public method to close the user agents manager. + """ + self.__saveTimer.saveIfNeccessary() + + def removeUserAgent(self, host): + """ + Public method to remove a user agent entry. + + @param host host name (string) + """ + if host in self.__agents: + del self.__agents[host] + self.changed.emit() + + def allHostNames(self): + """ + Public method to get a list of all host names we a user agent setting + for. + + @return sorted list of all host names (list of strings) + """ + if not self.__loaded: + self.__load() + + return sorted(self.__agents.keys()) + + def hostsCount(self): + """ + Public method to get the number of available user agent settings. + + @return number of user agent settings (integer) + """ + if not self.__loaded: + self.__load() + + return len(self.__agents) + + def userAgent(self, host): + """ + Public method to get the user agent setting for a host. + + @param host host name (string) + @return user agent string (string) + """ + if not self.__loaded: + self.__load() + + for agentHost in self.__agents: + if host.endswith(agentHost): + return self.__agents[agentHost] + + return "" + + def setUserAgent(self, host, agent): + """ + Public method to set the user agent string for a host. + + @param host host name (string) + @param agent user agent string (string) + """ + if host != "" and agent != "": + self.__agents[host] = agent + self.changed.emit() + + def userAgentForUrl(self, url): + """ + Public method to determine the user agent for the given URL. + + @param url URL to determine user agent for (QUrl) + @return user agent string (string) + """ + if url.isValid(): + host = url.host() + return self.userAgent(host) + + return "" + + def setUserAgentForUrl(self, url, agent): + """ + Public method to set the user agent string for an URL. + + @param url URL to register user agent setting for (QUrl) + @param agent new current user agent string (string) + """ + if url.isValid(): + host = url.host() + self.setUserAgent(host, agent)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UserAgent/UserAgentMenu.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a menu to select the user agent string. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamReader, QFile, QIODevice +from PyQt5.QtWidgets import QMenu, QAction, QActionGroup, QInputDialog, \ + QLineEdit + +from E5Gui import E5MessageBox + + +class UserAgentMenu(QMenu): + """ + Class implementing a menu to select the user agent string. + """ + def __init__(self, title, url=None, parent=None): + """ + Constructor + + @param title title of the menu (string) + @param url URL to set user agent for (QUrl) + @param parent reference to the parent widget (QWidget) + """ + super(UserAgentMenu, self).__init__(title, parent) + + self.__manager = None + self.__url = url + if self.__url: + if self.__url.isValid(): + from WebBrowser.WebBrowserWindow import WebBrowserWindow + self.__manager = WebBrowserWindow.userAgentsManager() + else: + self.__url = None + + self.aboutToShow.connect(self.__populateMenu) + + def __populateMenu(self): + """ + Private slot to populate the menu. + """ + self.aboutToShow.disconnect(self.__populateMenu) + + self.__actionGroup = QActionGroup(self) + + # add default action + self.__defaultUserAgent = QAction(self) + self.__defaultUserAgent.setText(self.tr("Default")) + self.__defaultUserAgent.setCheckable(True) + self.__defaultUserAgent.triggered.connect( + self.__switchToDefaultUserAgent) + if self.__url: + self.__defaultUserAgent.setChecked( + self.__manager.userAgentForUrl(self.__url) == "") + else: + from WebBrowser.WebBrowserPage import WebBrowserPage + self.__defaultUserAgent.setChecked( + WebBrowserPage.userAgent() == "") + self.addAction(self.__defaultUserAgent) + self.__actionGroup.addAction(self.__defaultUserAgent) + isChecked = self.__defaultUserAgent.isChecked() + + # add default extra user agents + isChecked = self.__addDefaultActions() or isChecked + + # add other action + self.addSeparator() + self.__otherUserAgent = QAction(self) + self.__otherUserAgent.setText(self.tr("Other...")) + self.__otherUserAgent.setCheckable(True) + self.__otherUserAgent.triggered.connect( + self.__switchToOtherUserAgent) + self.addAction(self.__otherUserAgent) + self.__actionGroup.addAction(self.__otherUserAgent) + self.__otherUserAgent.setChecked(not isChecked) + + def __switchToDefaultUserAgent(self): + """ + Private slot to set the default user agent. + """ + if self.__url: + self.__manager.removeUserAgent(self.__url.host()) + else: + from WebBrowser.WebBrowserPage import WebBrowserPage + WebBrowserPage.setUserAgent("") + + def __switchToOtherUserAgent(self): + """ + Private slot to set a custom user agent string. + """ + from WebBrowser.WebBrowserPage import WebBrowserPage + userAgent, ok = QInputDialog.getText( + self, + self.tr("Custom user agent"), + self.tr("User agent:"), + QLineEdit.Normal, + WebBrowserPage.userAgent(resolveEmpty=True)) + if ok: + if self.__url: + self.__manager.setUserAgentForUrl(self.__url, userAgent) + else: + WebBrowserPage.setUserAgent(userAgent) + + def __changeUserAgent(self): + """ + Private slot to change the user agent. + """ + act = self.sender() + if self.__url: + self.__manager.setUserAgentForUrl(self.__url, act.data()) + else: + from WebBrowser.WebBrowserPage import WebBrowserPage + WebBrowserPage.setUserAgent(act.data()) + + def __addDefaultActions(self): + """ + Private slot to add the default user agent entries. + + @return flag indicating that a user agent entry is checked (boolean) + """ + from . import UserAgentDefaults_rc # __IGNORE_WARNING__ + defaultUserAgents = QFile(":/UserAgentDefaults.xml") + defaultUserAgents.open(QIODevice.ReadOnly) + + menuStack = [] + isChecked = False + + if self.__url: + currentUserAgentString = self.__manager.userAgentForUrl(self.__url) + else: + from WebBrowser.WebBrowserPage import WebBrowserPage + currentUserAgentString = WebBrowserPage.userAgent() + xml = QXmlStreamReader(defaultUserAgents) + while not xml.atEnd(): + xml.readNext() + if xml.isStartElement() and xml.name() == "separator": + if menuStack: + menuStack[-1].addSeparator() + else: + self.addSeparator() + continue + + if xml.isStartElement() and xml.name() == "useragent": + attributes = xml.attributes() + title = attributes.value("description") + userAgent = attributes.value("useragent") + + act = QAction(self) + act.setText(title) + act.setData(userAgent) + act.setToolTip(userAgent) + act.setCheckable(True) + act.setChecked(userAgent == currentUserAgentString) + act.triggered.connect(self.__changeUserAgent) + if menuStack: + menuStack[-1].addAction(act) + else: + self.addAction(act) + self.__actionGroup.addAction(act) + isChecked = isChecked or act.isChecked() + + if xml.isStartElement() and xml.name() == "useragentmenu": + attributes = xml.attributes() + title = attributes.value("title") + if title == "v_a_r_i_o_u_s": + title = self.tr("Various") + + menu = QMenu(self) + menu.setTitle(title) + self.addMenu(menu) + menuStack.append(menu) + + if xml.isEndElement() and xml.name() == "useragentmenu": + menuStack.pop() + + if xml.hasError(): + E5MessageBox.critical( + self, + self.tr("Parsing default user agents"), + self.tr( + """<p>Error parsing default user agents.</p><p>{0}</p>""") + .format(xml.errorString())) + + return isChecked
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UserAgent/UserAgentModel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a model for user agent management. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QModelIndex, QAbstractTableModel + + +class UserAgentModel(QAbstractTableModel): + """ + Class implementing a model for user agent management. + """ + def __init__(self, manager, parent=None): + """ + Constructor + + @param manager reference to the user agent manager (UserAgentManager) + @param parent reference to the parent object (QObject) + """ + super(UserAgentModel, self).__init__(parent) + + self.__manager = manager + self.__manager.changed.connect(self.__userAgentsChanged) + + self.__headers = [ + self.tr("Host"), + self.tr("User Agent String"), + ] + + def __userAgentsChanged(self): + """ + Private slot handling a change of the registered user agent strings. + """ + self.beginResetModel() + self.endResetModel() + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove entries from the model. + + @param row start row (integer) + @param count number of rows to remove (integer) + @param parent parent index (QModelIndex) + @return flag indicating success (boolean) + """ + if parent.isValid(): + return False + + if count <= 0: + return False + + lastRow = row + count - 1 + + self.beginRemoveRows(parent, row, lastRow) + + hostsList = self.__manager.allHostNames() + for index in range(row, lastRow + 1): + self.__manager.removeUserAgent(hostsList[index]) + + return True + + def rowCount(self, parent=QModelIndex()): + """ + Public method to get the number of rows of the model. + + @param parent parent index (QModelIndex) + @return number of rows (integer) + """ + if parent.isValid(): + return 0 + else: + return self.__manager.hostsCount() + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns of the model. + + @param parent parent index (QModelIndex) + @return number of columns (integer) + """ + return len(self.__headers) + + def data(self, index, role): + """ + Public method to get data from the model. + + @param index index to get data for (QModelIndex) + @param role role of the data to retrieve (integer) + @return requested data + """ + if index.row() >= self.__manager.hostsCount() or index.row() < 0: + return None + + host = self.__manager.allHostNames()[index.row()] + userAgent = self.__manager.userAgent(host) + + if userAgent is None: + return None + + if role == Qt.DisplayRole: + if index.column() == 0: + return host + elif index.column() == 1: + return userAgent + + return None + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Public method to get the header data. + + @param section section number (integer) + @param orientation header orientation (Qt.Orientation) + @param role data role (integer) + @return header data + """ + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + try: + return self.__headers[section] + except IndexError: + pass + + return None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UserAgent/UserAgentReader.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + + +""" +Module implementing a class to read user agent data files. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamReader, QIODevice, QFile, QCoreApplication + + +class UserAgentReader(QXmlStreamReader): + """ + Class implementing a reader object for user agent data files. + """ + def __init__(self): + """ + Constructor + """ + super(UserAgentReader, self).__init__() + + def read(self, fileNameOrDevice): + """ + Public method to read a user agent file. + + @param fileNameOrDevice name of the file to read (string) + or reference to the device to read (QIODevice) + @return dictionary with user agent data (host as key, agent string as + value) + """ + self.__agents = {} + + if isinstance(fileNameOrDevice, QIODevice): + self.setDevice(fileNameOrDevice) + else: + f = QFile(fileNameOrDevice) + if not f.exists(): + return self.__agents + f.open(QFile.ReadOnly) + self.setDevice(f) + + while not self.atEnd(): + self.readNext() + if self.isStartElement(): + version = self.attributes().value("version") + if self.name() == "UserAgents" and \ + (not version or version == "1.0"): + self.__readUserAgents() + else: + self.raiseError(QCoreApplication.translate( + "UserAgentReader", + "The file is not a UserAgents version 1.0 file.")) + + return self.__agents + + def __readUserAgents(self): + """ + Private method to read the user agents data. + """ + if not self.isStartElement() and self.name() != "UserAgents": + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + if self.name() == "UserAgent": + continue + else: + break + + if self.isStartElement(): + if self.name() == "UserAgent": + attributes = self.attributes() + host = attributes.value("host") + agent = attributes.value("agent") + self.__agents[host] = agent + else: + self.__skipUnknownElement() + + def __skipUnknownElement(self): + """ + Private method to skip over all unknown elements. + """ + if not self.isStartElement(): + return + + while not self.atEnd(): + self.readNext() + if self.isEndElement(): + break + + if self.isStartElement(): + self.__skipUnknownElement()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UserAgent/UserAgentWriter.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a class to write user agent data files. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QXmlStreamWriter, QIODevice, QFile + + +class UserAgentWriter(QXmlStreamWriter): + """ + Class implementing a writer object to generate user agent data files. + """ + def __init__(self): + """ + Constructor + """ + super(UserAgentWriter, self).__init__() + + self.setAutoFormatting(True) + + def write(self, fileNameOrDevice, agents): + """ + Public method to write a user agent data file. + + @param fileNameOrDevice name of the file to write (string) + or device to write to (QIODevice) + @param agents dictionary with user agent data (host as key, agent + string as value) + @return flag indicating success (boolean) + """ + if isinstance(fileNameOrDevice, QIODevice): + f = fileNameOrDevice + else: + f = QFile(fileNameOrDevice) + if not f.open(QFile.WriteOnly): + return False + + self.setDevice(f) + return self.__write(agents) + + def __write(self, agents): + """ + Private method to write a user agent file. + + @param agents dictionary with user agent data (host as key, agent + string as value) + @return flag indicating success (boolean) + """ + self.writeStartDocument() + self.writeDTD("<!DOCTYPE useragents>") + self.writeStartElement("UserAgents") + self.writeAttribute("version", "1.0") + + for host, agent in agents.items(): + self.writeEmptyElement("UserAgent") + self.writeAttribute("host", host) + self.writeAttribute("agent", agent) + + self.writeEndDocument() + return True
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UserAgent/UserAgentsDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show all saved user agent settings. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QSortFilterProxyModel +from PyQt5.QtGui import QFont, QFontMetrics +from PyQt5.QtWidgets import QDialog + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + +from .UserAgentModel import UserAgentModel + +from .Ui_UserAgentsDialog import Ui_UserAgentsDialog + + +class UserAgentsDialog(QDialog, Ui_UserAgentsDialog): + """ + Class implementing a dialog to show all saved user agent settings. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(UserAgentsDialog, self).__init__(parent) + self.setupUi(self) + + self.removeButton.clicked.connect( + self.userAgentsTable.removeSelected) + self.removeAllButton.clicked.connect( + self.userAgentsTable.removeAll) + + self.userAgentsTable.verticalHeader().hide() + self.__userAgentModel = UserAgentModel( + WebBrowserWindow.userAgentsManager(), self) + self.__proxyModel = QSortFilterProxyModel(self) + self.__proxyModel.setSourceModel(self.__userAgentModel) + self.searchEdit.textChanged.connect( + self.__proxyModel.setFilterFixedString) + self.userAgentsTable.setModel(self.__proxyModel) + + fm = QFontMetrics(QFont()) + height = fm.height() + fm.height() // 3 + self.userAgentsTable.verticalHeader().setDefaultSectionSize(height) + self.userAgentsTable.verticalHeader().setMinimumSectionSize(-1) + + self.userAgentsTable.resizeColumnsToContents() + self.userAgentsTable.horizontalHeader().setStretchLastSection(True)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UserAgent/UserAgentsDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,179 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>UserAgentsDialog</class> + <widget class="QDialog" name="UserAgentsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>700</width> + <height>400</height> + </rect> + </property> + <property name="windowTitle"> + <string>User Agent Settings</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="E5ClearableLineEdit" name="searchEdit"> + <property name="toolTip"> + <string>Enter search term</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <widget class="E5TableView" name="userAgentsTable"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="textElideMode"> + <enum>Qt::ElideMiddle</enum> + </property> + <property name="showGrid"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected entries</string> + </property> + <property name="text"> + <string>&Remove</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all entries</string> + </property> + <property name="text"> + <string>Remove &All</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>208</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + <customwidget> + <class>E5TableView</class> + <extends>QTableView</extends> + <header>E5Gui/E5TableView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>userAgentsTable</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>removeAllButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>UserAgentsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>237</x> + <y>390</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>UserAgentsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>325</x> + <y>390</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/UserAgent/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the user agents manager. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/VirusTotal/VirusTotalApi.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,408 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the <a href="http://www.virustotal.com">VirusTotal</a> +API class. +""" + +from __future__ import unicode_literals +try: + str = unicode # __IGNORE_EXCEPTION__ +except NameError: + pass + +import json + +from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QUrlQuery, QByteArray +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply + +from E5Gui import E5MessageBox + +import Preferences + + +class VirusTotalAPI(QObject): + """ + Class implementing the <a href="http://www.virustotal.com">VirusTotal</a> + API. + + @signal checkServiceKeyFinished(bool, str) emitted after the service key + check has been performed. It gives a flag indicating validity + (boolean) and an error message in case of a network error (string). + @signal submitUrlError(str) emitted with the error string, if the URL scan + submission returned an error. + @signal urlScanReport(str) emitted with the URL of the URL scan report page + @signal fileScanReport(str) emitted with the URL of the file scan report + page + """ + checkServiceKeyFinished = pyqtSignal(bool, str) + submitUrlError = pyqtSignal(str) + urlScanReport = pyqtSignal(str) + fileScanReport = pyqtSignal(str) + + TestServiceKeyScanID = \ + "4feed2c2e352f105f6188efd1d5a558f24aee6971bdf96d5fdb19c197d6d3fad" + + ServiceResult_ItemQueued = -2 + ServiceResult_ItemNotPresent = 0 + ServiceResult_ItemPresent = 1 + + # HTTP Status Codes + ServiceCode_InvalidKey = 202 + ServiceCode_RateLimitExceeded = 204 + ServiceCode_InvalidPrivilege = 403 + + GetFileReportPattern = "{0}://www.virustotal.com/vtapi/v2/file/report" + ScanUrlPattern = "{0}://www.virustotal.com/vtapi/v2/url/scan" + GetUrlReportPattern = "{0}://www.virustotal.com/vtapi/v2/url/report" + GetIpAddressReportPattern = \ + "{0}://www.virustotal.com/vtapi/v2/ip-address/report" + GetDomainReportPattern = "{0}://www.virustotal.com/vtapi/v2/domain/report" + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(VirusTotalAPI, self).__init__(parent) + + self.__replies = [] + + self.__loadSettings() + + self.__lastIP = "" + self.__lastDomain = "" + self.__ipReportDlg = None + self.__domainReportDlg = None + + def __loadSettings(self): + """ + Private method to load the settings. + """ + if Preferences.getWebBrowser("VirusTotalSecure"): + protocol = "https" + else: + protocol = "http" + self.GetFileReportUrl = self.GetFileReportPattern.format(protocol) + self.ScanUrlUrl = self.ScanUrlPattern.format(protocol) + self.GetUrlReportUrl = self.GetUrlReportPattern.format(protocol) + self.GetIpAddressReportUrl = self.GetIpAddressReportPattern.format( + protocol) + self.GetDomainReportUrl = self.GetDomainReportPattern.format(protocol) + + self.errorMessages = { + 204: self.tr("Request limit has been reached."), + 0: self.tr("Requested item is not present."), + -2: self.tr("Requested item is still queued."), + } + + def preferencesChanged(self): + """ + Public slot to handle a change of preferences. + """ + self.__loadSettings() + + def checkServiceKeyValidity(self, key, protocol=""): + """ + Public method to check the validity of the given service key. + + @param key service key (string) + @param protocol protocol used to access VirusTotal (string) + """ + if protocol == "": + urlStr = self.GetFileReportUrl + else: + urlStr = self.GetFileReportPattern.format(protocol) + request = QNetworkRequest(QUrl(urlStr)) + request.setHeader(QNetworkRequest.ContentTypeHeader, + "application/x-www-form-urlencoded") + params = QByteArray("apikey={0}&resource={1}".format( + key, self.TestServiceKeyScanID).encode("utf-8")) + + import WebBrowser.WebBrowserWindow + nam = WebBrowser.WebBrowserWindow.WebBrowserWindow\ + .networkManager() + reply = nam.post(request, params) + reply.finished.connect(self.__checkServiceKeyValidityFinished) + self.__replies.append(reply) + + def __checkServiceKeyValidityFinished(self): + """ + Private slot to determine the result of the service key validity check. + """ + res = False + msg = "" + + reply = self.sender() + if reply.error() == QNetworkReply.NoError: + res = True + elif reply.error() == self.ServiceCode_InvalidKey: + res = False + else: + msg = reply.errorString() + self.__replies.remove(reply) + reply.deleteLater() + + self.checkServiceKeyFinished.emit(res, msg) + + def submitUrl(self, url): + """ + Public method to submit an URL to be scanned. + + @param url url to be scanned (QUrl) + """ + request = QNetworkRequest(QUrl(self.ScanUrlUrl)) + request.setHeader(QNetworkRequest.ContentTypeHeader, + "application/x-www-form-urlencoded") + params = QByteArray("apikey={0}&url=".format( + Preferences.getWebBrowser("VirusTotalServiceKey")) + .encode("utf-8")).append(QUrl.toPercentEncoding(url.toString())) + + import WebBrowser.WebBrowserWindow + nam = WebBrowser.WebBrowserWindow.WebBrowserWindow\ + .networkManager() + reply = nam.post(request, params) + reply.finished.connect(self.__submitUrlFinished) + self.__replies.append(reply) + + def __submitUrlFinished(self): + """ + Private slot to determine the result of the URL scan submission. + """ + reply = self.sender() + if reply.error() == QNetworkReply.NoError: + result = json.loads(str(reply.readAll(), "utf-8")) + if result["response_code"] == self.ServiceResult_ItemPresent: + self.urlScanReport.emit(result["permalink"]) + self.__getUrlScanReportUrl(result["scan_id"]) + else: + if result["response_code"] in self.errorMessages: + msg = self.errorMessages[result["response_code"]] + else: + msg = result["verbose_msg"] + self.submitUrlError.emit(msg) + elif reply.error() == self.ServiceCode_RateLimitExceeded: + self.submitUrlError.emit( + self.errorMessages[result[self.ServiceCode_RateLimitExceeded]]) + else: + self.submitUrlError.emit(reply.errorString()) + self.__replies.remove(reply) + reply.deleteLater() + + def __getUrlScanReportUrl(self, scanId): + """ + Private method to get the report URL for a URL scan. + + @param scanId ID of the scan to get the report URL for (string) + """ + request = QNetworkRequest(QUrl(self.GetUrlReportUrl)) + request.setHeader(QNetworkRequest.ContentTypeHeader, + "application/x-www-form-urlencoded") + params = QByteArray("apikey={0}&resource={1}".format( + Preferences.getWebBrowser("VirusTotalServiceKey"), scanId) + .encode("utf-8")) + + import WebBrowser.WebBrowserWindow + nam = WebBrowser.WebBrowserWindow.WebBrowserWindow\ + .networkManager() + reply = nam.post(request, params) + reply.finished.connect(self.__getUrlScanReportUrlFinished) + self.__replies.append(reply) + + def __getUrlScanReportUrlFinished(self): + """ + Private slot to determine the result of the URL scan report URL + request. + """ + reply = self.sender() + if reply.error() == QNetworkReply.NoError: + result = json.loads(str(reply.readAll(), "utf-8")) + if "filescan_id" in result and result["filescan_id"] is not None: + self.__getFileScanReportUrl(result["filescan_id"]) + self.__replies.remove(reply) + reply.deleteLater() + + def __getFileScanReportUrl(self, scanId): + """ + Private method to get the report URL for a file scan. + + @param scanId ID of the scan to get the report URL for (string) + """ + request = QNetworkRequest(QUrl(self.GetFileReportUrl)) + request.setHeader(QNetworkRequest.ContentTypeHeader, + "application/x-www-form-urlencoded") + params = QByteArray("apikey={0}&resource={1}".format( + Preferences.getWebBrowser("VirusTotalServiceKey"), scanId) + .encode("utf-8")) + + import WebBrowser.WebBrowserWindow + nam = WebBrowser.WebBrowserWindow.WebBrowserWindow\ + .networkManager() + reply = nam.post(request, params) + reply.finished.connect(self.__getFileScanReportUrlFinished) + self.__replies.append(reply) + + def __getFileScanReportUrlFinished(self): + """ + Private slot to determine the result of the file scan report URL + request. + """ + reply = self.sender() + if reply.error() == QNetworkReply.NoError: + result = json.loads(str(reply.readAll(), "utf-8")) + self.fileScanReport.emit(result["permalink"]) + self.__replies.remove(reply) + reply.deleteLater() + + def getIpAddressReport(self, ipAddress): + """ + Public method to retrieve a report for an IP address. + + @param ipAddress valid IPv4 address in dotted quad notation + @type str + """ + self.__lastIP = ipAddress + + queryItems = [ + ("apikey", Preferences.getWebBrowser("VirusTotalServiceKey")), + ("ip", ipAddress), + ] + url = QUrl(self.GetIpAddressReportUrl) + query = QUrlQuery() + query.setQueryItems(queryItems) + url.setQuery(query) + request = QNetworkRequest(url) + + import WebBrowser.WebBrowserWindow + nam = WebBrowser.WebBrowserWindow.WebBrowserWindow\ + .networkManager() + reply = nam.get(request) + reply.finished.connect(self.__getIpAddressReportFinished) + self.__replies.append(reply) + + def __getIpAddressReportFinished(self): + """ + Private slot to process the IP address report data. + """ + reply = self.sender() + if reply.error() == QNetworkReply.NoError: + result = json.loads(str(reply.readAll(), "utf-8")) + if result["response_code"] == 0: + E5MessageBox.information( + None, + self.tr("VirusTotal IP Address Report"), + self.tr("""VirusTotal does not have any information for""" + """ the given IP address.""")) + elif result["response_code"] == -1: + E5MessageBox.information( + None, + self.tr("VirusTotal IP Address Report"), + self.tr("""The submitted IP address is invalid.""")) + else: + owner = result["as_owner"] + resolutions = result["resolutions"] + try: + urls = result["detected_urls"] + except KeyError: + urls = [] + + from .VirusTotalIpReportDialog import VirusTotalIpReportDialog + self.__ipReportDlg = VirusTotalIpReportDialog( + self.__lastIP, owner, resolutions, urls) + self.__ipReportDlg.show() + self.__replies.remove(reply) + reply.deleteLater() + + def getDomainReport(self, domain): + """ + Public method to retrieve a report for a domain. + + @param domain domain name + @type str + """ + self.__lastDomain = domain + + queryItems = [ + ("apikey", Preferences.getWebBrowser("VirusTotalServiceKey")), + ("domain", domain), + ] + url = QUrl(self.GetDomainReportUrl) + query = QUrlQuery() + query.setQueryItems(queryItems) + url.setQuery(query) + request = QNetworkRequest(url) + + import WebBrowser.WebBrowserWindow + nam = WebBrowser.WebBrowserWindow.WebBrowserWindow\ + .networkManager() + reply = nam.get(request) + reply.finished.connect(self.__getDomainReportFinished) + self.__replies.append(reply) + + def __getDomainReportFinished(self): + """ + Private slot to process the IP address report data. + """ + reply = self.sender() + if reply.error() == QNetworkReply.NoError: + result = json.loads(str(reply.readAll(), "utf-8")) + if result["response_code"] == 0: + E5MessageBox.information( + None, + self.tr("VirusTotal Domain Report"), + self.tr("""VirusTotal does not have any information for""" + """ the given domain.""")) + elif result["response_code"] == -1: + E5MessageBox.information( + None, + self.tr("VirusTotal Domain Report"), + self.tr("""The submitted domain address is invalid.""")) + else: + resolutions = result["resolutions"] + try: + urls = result["detected_urls"] + except KeyError: + urls = [] + try: + subdomains = result["subdomains"] + except KeyError: + subdomains = [] + try: + bdCategory = result["BitDefender category"] + except KeyError: + bdCategory = self.tr("not available") + try: + tmCategory = result["TrendMicro category"] + except KeyError: + tmCategory = self.tr("not available") + try: + wtsCategory = result["Websense ThreatSeeker category"] + except KeyError: + wtsCategory = self.tr("not available") + try: + whois = result["whois"] + except KeyError: + whois = "" + + from .VirusTotalDomainReportDialog import \ + VirusTotalDomainReportDialog + self.__domainReportDlg = VirusTotalDomainReportDialog( + self.__lastDomain, resolutions, urls, subdomains, + bdCategory, tmCategory, wtsCategory, whois) + self.__domainReportDlg.show() + self.__replies.remove(reply) + reply.deleteLater() + + def close(self): + """ + Public slot to close the API. + """ + for reply in self.__replies: + reply.abort() + + self.__ipReportDlg and self.__ipReportDlg.close() + self.__domainReportDlg and self.__domainReportDlg.close()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/VirusTotal/VirusTotalDomainReportDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show the VirusTotal domain report. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, Qt +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem + +from .Ui_VirusTotalDomainReportDialog import Ui_VirusTotalDomainReportDialog + +import UI.PixmapCache + + +class VirusTotalDomainReportDialog(QDialog, Ui_VirusTotalDomainReportDialog): + """ + Class implementing a dialog to show the VirusTotal domain report. + """ + def __init__(self, domain, resolutions, urls, subdomains, + bdCategory, tmCategory, wtsCategory, whois, parent=None): + """ + Constructor + + @param domain domain name + @type str + @param resolutions list of resolved host names + @type list of dict + @param urls list of detected URLs + @type list of dict + @param subdomains list of subdomains + @type list of str + @param bdCategory BitDefender categorization + @type str + @param tmCategory TrendMicro categorization + @type str + @param wtsCategory Websense ThreatSeeker categorization + @type str + @param whois whois information + @type str + @param parent reference to the parent widget + @type QWidget + """ + super(VirusTotalDomainReportDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.headerLabel.setText( + self.tr("<b>Report for domain {0}</b>").format(domain)) + self.headerPixmap.setPixmap( + UI.PixmapCache.getPixmap("virustotal.png")) + + for resolution in resolutions: + QTreeWidgetItem( + self.resolutionsList, + [resolution["ip_address"], + resolution["last_resolved"].split()[0]] + ) + self.resolutionsList.resizeColumnToContents(0) + self.resolutionsList.resizeColumnToContents(1) + self.resolutionsList.sortByColumn(0, Qt.AscendingOrder) + + if not urls: + self.detectedUrlsGroup.setVisible(False) + for url in urls: + QTreeWidgetItem( + self.urlsList, + [url["url"], + self.tr("{0}/{1}", "positives / total").format( + url["positives"], url["total"]), + url["scan_date"].split()[0]] + ) + self.urlsList.resizeColumnToContents(0) + self.urlsList.resizeColumnToContents(1) + self.urlsList.resizeColumnToContents(2) + self.urlsList.sortByColumn(0, Qt.AscendingOrder) + + if not subdomains: + self.subdomainsGroup.setVisible(False) + else: + self.subdomainsList.addItems(subdomains) + self.subdomainsList.sortItems() + + self.bdLabel.setText(bdCategory) + self.tmLabel.setText(tmCategory) + self.wtsLabel.setText(wtsCategory) + + self.__whois = whois + self.__whoisDomain = domain + self.whoisButton.setEnabled(bool(whois)) + + @pyqtSlot() + def on_whoisButton_clicked(self): + """ + Private slot to show the whois information. + """ + from .VirusTotalWhoisDialog import VirusTotalWhoisDialog + dlg = VirusTotalWhoisDialog(self.__whoisDomain, self.__whois) + dlg.exec_()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/VirusTotal/VirusTotalDomainReportDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,304 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>VirusTotalDomainReportDialog</class> + <widget class="QDialog" name="VirusTotalDomainReportDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>900</width> + <height>700</height> + </rect> + </property> + <property name="windowTitle"> + <string>Domain Report</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="headerPixmap"/> + </item> + <item> + <widget class="QLabel" name="headerLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line9_3"> + <property name="frameShape"> + <enum>QFrame::HLine</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Sunken</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Categorizations</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string notr="true">BitDefender:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="bdLabel"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string notr="true">TrendMicro:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="tmLabel"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string notr="true">Websense ThreatSeeker:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="wtsLabel"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>690</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QPushButton" name="whoisButton"> + <property name="text"> + <string>Whois</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QGroupBox" name="resolutionsGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>4</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Resolutions</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QTreeWidget" name="resolutionsList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <property name="allColumnsShowFocus"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>IP-Address</string> + </property> + </column> + <column> + <property name="text"> + <string>Resolved Date</string> + </property> + </column> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="subdomainsGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>4</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Subdomains</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QListWidget" name="subdomainsList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QGroupBox" name="detectedUrlsGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>2</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Detected URLs</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTreeWidget" name="urlsList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <property name="allColumnsShowFocus"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>URL</string> + </property> + </column> + <column> + <property name="text"> + <string>Scan Result</string> + </property> + </column> + <column> + <property name="text"> + <string>Scan Date</string> + </property> + </column> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>whoisButton</tabstop> + <tabstop>resolutionsList</tabstop> + <tabstop>subdomainsList</tabstop> + <tabstop>urlsList</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>VirusTotalDomainReportDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>VirusTotalDomainReportDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/VirusTotal/VirusTotalIpReportDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show the VirusTotal IP address report. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem + +from .Ui_VirusTotalIpReportDialog import Ui_VirusTotalIpReportDialog + +import UI.PixmapCache + + +class VirusTotalIpReportDialog(QDialog, Ui_VirusTotalIpReportDialog): + """ + Class implementing a dialog to show the VirusTotal IP address report. + """ + def __init__(self, ip, owner, resolutions, urls, parent=None): + """ + Constructor + + @param ip IP address + @type str + @param owner owner of the IP address + @type str + @param resolutions list of resolved host names + @type list of dict + @param urls list of detected URLs + @type list of dict + @param parent reference to the parent widget + @type QWidget + """ + super(VirusTotalIpReportDialog, self).__init__(parent) + self.setupUi(self) + self.setWindowFlags(Qt.Window) + + self.headerLabel.setText( + self.tr("<b>Report for IP {0}</b>").format(ip)) + self.headerPixmap.setPixmap( + UI.PixmapCache.getPixmap("virustotal.png")) + self.ownerLabel.setText(owner) + + for resolution in resolutions: + QTreeWidgetItem( + self.resolutionsList, + [resolution["hostname"], + resolution["last_resolved"].split()[0]] + ) + self.resolutionsList.resizeColumnToContents(0) + self.resolutionsList.resizeColumnToContents(1) + self.resolutionsList.sortByColumn(0, Qt.AscendingOrder) + + if not urls: + self.detectedUrlsGroup.setVisible(False) + for url in urls: + QTreeWidgetItem( + self.urlsList, + [url["url"], + self.tr("{0}/{1}", "positives / total").format( + url["positives"], url["total"]), + url["scan_date"].split()[0]] + ) + self.urlsList.resizeColumnToContents(0) + self.urlsList.resizeColumnToContents(1) + self.urlsList.resizeColumnToContents(2) + self.urlsList.sortByColumn(0, Qt.AscendingOrder)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/VirusTotal/VirusTotalIpReportDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,208 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>VirusTotalIpReportDialog</class> + <widget class="QDialog" name="VirusTotalIpReportDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>IP Address Report</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="headerPixmap"/> + </item> + <item> + <widget class="QLabel" name="headerLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line9_3"> + <property name="frameShape"> + <enum>QFrame::HLine</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Sunken</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Owner:</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="ownerLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QGroupBox" name="resolutionsGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>4</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Resolutions</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QTreeWidget" name="resolutionsList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <property name="allColumnsShowFocus"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>Hostname</string> + </property> + </column> + <column> + <property name="text"> + <string>Resolved Date</string> + </property> + </column> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="detectedUrlsGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>2</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Detected URLs</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTreeWidget" name="urlsList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <property name="allColumnsShowFocus"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>URL</string> + </property> + </column> + <column> + <property name="text"> + <string>Scan Result</string> + </property> + </column> + <column> + <property name="text"> + <string>Scan Date</string> + </property> + </column> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>VirusTotalIpReportDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>VirusTotalIpReportDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/VirusTotal/VirusTotalWhoisDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show the 'whois' information. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QDialog + +from .Ui_VirusTotalWhoisDialog import Ui_VirusTotalWhoisDialog + +import UI.PixmapCache + + +class VirusTotalWhoisDialog(QDialog, Ui_VirusTotalWhoisDialog): + """ + Class implementing a dialog to show the 'whois' information. + """ + def __init__(self, domain, whois, parent=None): + """ + Constructor + + @param domain domain name + @type str + @param whois whois information + @type str + @param parent reference to the parent widget + @type QWidget + """ + super(VirusTotalWhoisDialog, self).__init__(parent) + self.setupUi(self) + + self.headerLabel.setText( + self.tr("<b>Whois information for domain {0}</b>").format(domain)) + self.headerPixmap.setPixmap( + UI.PixmapCache.getPixmap("virustotal.png")) + self.whoisEdit.setPlainText(whois)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/VirusTotal/VirusTotalWhoisDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,110 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>VirusTotalWhoisDialog</class> + <widget class="QDialog" name="VirusTotalWhoisDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>400</height> + </rect> + </property> + <property name="windowTitle"> + <string>Whois Information</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="headerPixmap"/> + </item> + <item> + <widget class="QLabel" name="headerLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line9_3"> + <property name="frameShape"> + <enum>QFrame::HLine</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Sunken</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QPlainTextEdit" name="whoisEdit"> + <property name="tabChangesFocus"> + <bool>true</bool> + </property> + <property name="lineWrapMode"> + <enum>QPlainTextEdit::NoWrap</enum> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>VirusTotalWhoisDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>VirusTotalWhoisDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/VirusTotal/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package containing the VirusTotal interface. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserClearPrivateDataDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to select which private data to clear. +""" + +from __future__ import unicode_literals + +from PyQt5.QtWidgets import QDialog + +from .Ui_WebBrowserClearPrivateDataDialog import \ + Ui_WebBrowserClearPrivateDataDialog + + +class WebBrowserClearPrivateDataDialog(QDialog, + Ui_WebBrowserClearPrivateDataDialog): + """ + Class implementing a dialog to select which private data to clear. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(WebBrowserClearPrivateDataDialog, self).__init__(parent) + self.setupUi(self) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + def getData(self): + """ + Public method to get the data from the dialog. + + @return tuple with flags indicating which data to clear + (browsing history, search history, favicons, disk cache, cookies, + passwords, web databases, downloads, flash, zoom values, SSL + certificate error exceptions) and the selected history period in + milliseconds (tuple of booleans and integer) + """ + index = self.historyCombo.currentIndex() + if index == 0: + # last hour + historyPeriod = 60 * 60 * 1000 + elif index == 1: + # last day + historyPeriod = 24 * 60 * 60 * 1000 + elif index == 2: + # last week + historyPeriod = 7 * 24 * 60 * 60 * 1000 + elif index == 3: + # last four weeks + historyPeriod = 4 * 7 * 24 * 60 * 60 * 1000 + elif index == 4: + # clear all + historyPeriod = 0 + + return (self.historyCheckBox.isChecked(), + self.searchCheckBox.isChecked(), + self.iconsCheckBox.isChecked(), + self.cacheCheckBox.isChecked(), + self.cookiesCheckBox.isChecked(), + self.passwordsCheckBox.isChecked(), + self.databasesCheckBox.isChecked(), + self.downloadsCheckBox.isChecked(), + self.flashCookiesCheckBox.isChecked(), + self.zoomCheckBox.isChecked(), + self.sslExceptionsCheckBox.isChecked(), + historyPeriod)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserClearPrivateDataDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,293 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>WebBrowserClearPrivateDataDialog</class> + <widget class="QDialog" name="WebBrowserClearPrivateDataDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>305</width> + <height>380</height> + </rect> + </property> + <property name="windowTitle"> + <string>Clear Private Data</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QCheckBox" name="historyCheckBox"> + <property name="toolTip"> + <string>Select to clear the browsing history</string> + </property> + <property name="text"> + <string>&Browsing History</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QComboBox" name="historyCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the history period to be deleted</string> + </property> + <item> + <property name="text"> + <string>Last Hour</string> + </property> + </item> + <item> + <property name="text"> + <string>Last Day</string> + </property> + </item> + <item> + <property name="text"> + <string>Last Week</string> + </property> + </item> + <item> + <property name="text"> + <string>Last 4 Weeks</string> + </property> + </item> + <item> + <property name="text"> + <string>Whole Period</string> + </property> + </item> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QCheckBox" name="searchCheckBox"> + <property name="toolTip"> + <string>Select to clear the search history</string> + </property> + <property name="text"> + <string>&Search History</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="downloadsCheckBox"> + <property name="toolTip"> + <string>Select to clear the download history</string> + </property> + <property name="text"> + <string>Download &History</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="cookiesCheckBox"> + <property name="toolTip"> + <string>Select to clear the cookies</string> + </property> + <property name="text"> + <string>&Cookies</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="cacheCheckBox"> + <property name="toolTip"> + <string>Select to clear the disk cache</string> + </property> + <property name="text"> + <string>Cached &Web Pages</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="iconsCheckBox"> + <property name="toolTip"> + <string>Select to clear the website icons</string> + </property> + <property name="text"> + <string>Website &Icons</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="passwordsCheckBox"> + <property name="toolTip"> + <string>Select to clear the saved passwords</string> + </property> + <property name="text"> + <string>Saved &Passwords</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="databasesCheckBox"> + <property name="toolTip"> + <string>Select to delete all web databases</string> + </property> + <property name="text"> + <string>Web &Databases</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="zoomCheckBox"> + <property name="toolTip"> + <string>Select to delete all remembered zoom settings</string> + </property> + <property name="text"> + <string>&Zoom Settings</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="sslExceptionsCheckBox"> + <property name="text"> + <string>SSL Certificate Error Exceptions</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="flashCookiesCheckBox"> + <property name="toolTip"> + <string>Select to clear cookies set by the Adobe Flash Player</string> + </property> + <property name="text"> + <string>Cookies from Adobe &Flash Player</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>historyCheckBox</tabstop> + <tabstop>historyCombo</tabstop> + <tabstop>searchCheckBox</tabstop> + <tabstop>downloadsCheckBox</tabstop> + <tabstop>cookiesCheckBox</tabstop> + <tabstop>cacheCheckBox</tabstop> + <tabstop>iconsCheckBox</tabstop> + <tabstop>passwordsCheckBox</tabstop> + <tabstop>databasesCheckBox</tabstop> + <tabstop>zoomCheckBox</tabstop> + <tabstop>sslExceptionsCheckBox</tabstop> + <tabstop>flashCookiesCheckBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>WebBrowserClearPrivateDataDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>267</x> + <y>342</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>252</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>WebBrowserClearPrivateDataDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>294</x> + <y>342</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>252</y> + </hint> + </hints> + </connection> + <connection> + <sender>historyCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>historyCombo</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>65</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>83</x> + <y>45</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserJavaScriptConsole.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a JavaScript console widget. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QTextCursor +from PyQt5.QtWidgets import QTextEdit, QMenu +from PyQt5.QtWebEngineWidgets import QWebEnginePage + + +class WebBrowserJavaScriptConsole(QTextEdit): + """ + Class implementing a JavaScript console widget. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(WebBrowserJavaScriptConsole, self).__init__(parent) + self.setAcceptRichText(False) + self.setLineWrapMode(QTextEdit.NoWrap) + self.setReadOnly(True) + + # create the context menu + self.__menu = QMenu(self) + self.__menu.addAction(self.tr('Clear'), self.clear) + self.__menu.addAction(self.tr('Copy'), self.copy) + self.__menu.addSeparator() + self.__menu.addAction(self.tr('Select All'), self.selectAll) + + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.__handleShowContextMenu) + + self.__levelStrings = { + QWebEnginePage.InfoMessageLevel: self.tr("Info"), + QWebEnginePage.WarningMessageLevel: self.tr("Warning"), + QWebEnginePage.ErrorMessageLevel: self.tr("Error"), + } + + def __handleShowContextMenu(self, coord): + """ + Private slot to show the context menu. + + @param coord the position of the mouse pointer (QPoint) + """ + coord = self.mapToGlobal(coord) + self.__menu.popup(coord) + + def __appendText(self, txt): + """ + Private method to append text to the end. + + @param txt text to insert (string) + """ + tc = self.textCursor() + tc.movePosition(QTextCursor.End) + self.setTextCursor(tc) + self.insertPlainText(txt) + self.ensureCursorVisible() + + def keyPressEvent(self, evt): + """ + Protected method handling key press events. + + @param evt key press event (QKeyEvent) + """ + if evt.modifiers() == Qt.ControlModifier: + if evt.key() == Qt.Key_C: + self.copy() + evt.accept() + return + elif evt.key() == Qt.Key_A: + self.selectAll() + evt.accept() + return + + def javaScriptConsoleMessage(self, level, message, lineNumber, sourceId): + """ + Public method to show a console message. + + @param level severity + @type QWebEnginePage.JavaScriptConsoleMessageLevel + @param message message to be shown + @type str + @param lineNumber line number of an error + @type int + @param sourceId source URL causing the error + @type str + """ + txt = self.tr("[{0}] {1}").format( + self.__levelStrings[level], message) + self.__appendText(txt) + + if lineNumber: + self.__appendText(self.tr(" at line {0}\n").format(lineNumber)) + else: + self.__appendText("\n") + + if sourceId: + self.__appendText(self.tr("URL: {0}\n").format(sourceId))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserLanguagesDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to configure the preferred languages. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot, QLocale, QStringListModel +from PyQt5.QtWidgets import QDialog + +from .Ui_WebBrowserLanguagesDialog import Ui_WebBrowserLanguagesDialog + +import Preferences + + +class WebBrowserLanguagesDialog(QDialog, Ui_WebBrowserLanguagesDialog): + """ + Class implementing a dialog to configure the preferred languages. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(WebBrowserLanguagesDialog, self).__init__(parent) + self.setupUi(self) + + self.__model = QStringListModel() + self.languagesList.setModel(self.__model) + self.languagesList.selectionModel().currentChanged.connect( + self.__currentChanged) + + languages = Preferences.toList(Preferences.Prefs.settings.value( + "WebBrowser/AcceptLanguages", self.defaultAcceptLanguages())) + self.__model.setStringList(languages) + + allLanguages = [] + for index in range(QLocale.C + 1, QLocale.LastLanguage + 1): + allLanguages += self.expand(QLocale.Language(index)) + self.__allLanguagesModel = QStringListModel() + self.__allLanguagesModel.setStringList(allLanguages) + self.addCombo.setModel(self.__allLanguagesModel) + + def __currentChanged(self, current, previous): + """ + Private slot to handle a change of the current selection. + + @param current index of the currently selected item (QModelIndex) + @param previous index of the previously selected item (QModelIndex) + """ + self.removeButton.setEnabled(current.isValid()) + row = current.row() + self.upButton.setEnabled(row > 0) + self.downButton.setEnabled( + row != -1 and row < self.__model.rowCount() - 1) + + @pyqtSlot() + def on_upButton_clicked(self): + """ + Private slot to move a language up. + """ + currentRow = self.languagesList.currentIndex().row() + data = self.languagesList.currentIndex().data() + self.__model.removeRow(currentRow) + self.__model.insertRow(currentRow - 1) + self.__model.setData(self.__model.index(currentRow - 1), data) + self.languagesList.setCurrentIndex(self.__model.index(currentRow - 1)) + + @pyqtSlot() + def on_downButton_clicked(self): + """ + Private slot to move a language down. + """ + currentRow = self.languagesList.currentIndex().row() + data = self.languagesList.currentIndex().data() + self.__model.removeRow(currentRow) + self.__model.insertRow(currentRow + 1) + self.__model.setData(self.__model.index(currentRow + 1), data) + self.languagesList.setCurrentIndex(self.__model.index(currentRow + 1)) + + @pyqtSlot() + def on_removeButton_clicked(self): + """ + Private slot to remove a language from the list of acceptable + languages. + """ + currentRow = self.languagesList.currentIndex().row() + self.__model.removeRow(currentRow) + + @pyqtSlot() + def on_addButton_clicked(self): + """ + Private slot to add a language to the list of acceptable languages. + """ + language = self.addCombo.currentText() + if language in self.__model.stringList(): + return + + self.__model.insertRow(self.__model.rowCount()) + self.__model.setData(self.__model.index(self.__model.rowCount() - 1), + language) + self.languagesList.setCurrentIndex( + self.__model.index(self.__model.rowCount() - 1)) + + def accept(self): + """ + Public method to accept the data entered. + """ + result = self.__model.stringList() + if result == self.defaultAcceptLanguages(): + Preferences.Prefs.settings.remove("WebBrowser/AcceptLanguages") + else: + Preferences.Prefs.settings.setValue( + "WebBrowser/AcceptLanguages", result) + super(WebBrowserLanguagesDialog, self).accept() + + @classmethod + def httpString(cls, languages): + """ + Class method to convert a list of acceptable languages into a + byte array. + + The byte array can be sent along with the Accept-Language http header + (see RFC 2616). + + @param languages list of acceptable languages (list of strings) + @return converted list (QByteArray) + """ + processed = [] + qvalue = 1.0 + for language in languages: + leftBracket = language.find('[') + rightBracket = language.find(']') + tag = language[leftBracket + 1:rightBracket] + if not processed: + processed.append(tag) + else: + processed.append("{0};q={1:.1f}".format(tag, qvalue)) + if qvalue > 0.1: + qvalue -= 0.1 + + return ", ".join(processed) + + @classmethod + def defaultAcceptLanguages(cls): + """ + Class method to get the list of default accept languages. + + @return list of acceptable languages (list of strings) + """ + language = QLocale.system().name() + if not language: + return [] + else: + return cls.expand(QLocale(language).language()) + + @classmethod + def expand(cls, language): + """ + Class method to expand a language enum to a readable languages + list. + + @param language language number (QLocale.Language) + @return list of expanded language names (list of strings) + """ + allLanguages = [] + countries = [l.country() for l in QLocale.matchingLocales( + language, QLocale.AnyScript, QLocale.AnyCountry)] + languageString = "{0} [{1}]"\ + .format(QLocale.languageToString(language), + QLocale(language).name().split('_')[0]) + allLanguages.append(languageString) + for country in countries: + languageString = "{0}/{1} [{2}]"\ + .format(QLocale.languageToString(language), + QLocale.countryToString(country), + '-'.join(QLocale(language, country).name() + .split('_')).lower()) + if languageString not in allLanguages: + allLanguages.append(languageString) + + return allLanguages
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserLanguagesDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>WebBrowserLanguagesDialog</class> + <widget class="QDialog" name="WebBrowserLanguagesDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Languages</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Languages in order of preference:</string> + </property> + </widget> + </item> + <item row="1" column="0" rowspan="4"> + <widget class="QListView" name="languagesList"/> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="upButton"> + <property name="text"> + <string>&Up</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QPushButton" name="downButton"> + <property name="text"> + <string>&Down</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QPushButton" name="removeButton"> + <property name="text"> + <string>&Remove</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>77</height> + </size> + </property> + </spacer> + </item> + <item row="5" column="0"> + <widget class="QComboBox" name="addCombo"/> + </item> + <item row="5" column="1"> + <widget class="QPushButton" name="addButton"> + <property name="text"> + <string>&Add</string> + </property> + </widget> + </item> + <item row="6" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>languagesList</tabstop> + <tabstop>upButton</tabstop> + <tabstop>downButton</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>addCombo</tabstop> + <tabstop>addButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>WebBrowserLanguagesDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>WebBrowserLanguagesDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserPage.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2008 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + + +""" +Module implementing the helpbrowser using QWebView. +""" + +from __future__ import unicode_literals +try: + str = unicode # __IGNORE_EXCEPTION__ +except NameError: + pass + +from PyQt5.QtCore import QUrl, QTimer, QEventLoop +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtWebEngineWidgets import QWebEnginePage +from PyQt5.QtWebChannel import QWebChannel + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + +from .JavaScript.ExternalJsObject import ExternalJsObject + +from .Tools.WebHitTestResult import WebHitTestResult + +import Preferences + + +class WebBrowserPage(QWebEnginePage): + """ + Class implementing an enhanced web page. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent parent widget of this window (QWidget) + """ + super(WebBrowserPage, self).__init__( + WebBrowserWindow.webProfile(), parent) + + self.__setupWebChannel() + + self.featurePermissionRequested.connect( + self.__featurePermissionRequested) + + self.authenticationRequired.connect( + WebBrowserWindow.networkManager().authentication) + + self.proxyAuthenticationRequired.connect( + WebBrowserWindow.networkManager().proxyAuthentication) + + self.fullScreenRequested.connect(self.__fullScreenRequested) + + def acceptNavigationRequest(self, url, type_, isMainFrame): + """ + Public method to determine, if a request may be accepted. + + @param url URL to navigate to + @type QUrl + @param type_ type of the navigation request + @type QWebEnginePage.NavigationType + @param isMainFrame flag indicating, that the request originated from + the main frame + @type bool + @return flag indicating acceptance + @rtype bool + """ + scheme = url.scheme() + if scheme == "mailto": + QDesktopServices.openUrl(url) + return False + + # AdBlock + if url.scheme() == "abp": + if WebBrowserWindow.adBlockManager().addSubscriptionFromUrl(url): + return False + + return QWebEnginePage.acceptNavigationRequest(self, url, type_, + isMainFrame) + + @classmethod + def userAgent(cls, resolveEmpty=False): + """ + Class method to get the global user agent setting. + + @param resolveEmpty flag indicating to resolve an empty + user agent (boolean) + @return user agent string (string) + """ + agent = Preferences.getWebBrowser("UserAgent") + if agent == "" and resolveEmpty: + agent = cls.userAgentForUrl(QUrl()) + return agent + + @classmethod + def setUserAgent(cls, agent): + """ + Class method to set the global user agent string. + + @param agent new current user agent string (string) + """ + Preferences.setWebBrowser("UserAgent", agent) + + @classmethod + def userAgentForUrl(cls, url): + """ + Class method to determine the user agent for the given URL. + + @param url URL to determine user agent for (QUrl) + @return user agent string (string) + """ + agent = WebBrowserWindow.userAgentsManager().userAgentForUrl(url) + if agent == "": + # no agent string specified for the given host -> use global one + agent = Preferences.getWebBrowser("UserAgent") + if agent == "": + # no global agent string specified -> use default one + agent = WebBrowserWindow.webProfile().httpUserAgent() + return agent + + def __featurePermissionRequested(self, url, feature): + """ + Private slot handling a feature permission request. + + @param url url requesting the feature + @type QUrl + @param feature requested feature + @type QWebEnginePage.Feature + """ + manager = WebBrowserWindow.featurePermissionManager() + manager.requestFeaturePermission(self, url, feature) + + def execJavaScript(self, script): + """ + Public method to execute a JavaScript function synchroneously. + + @param script JavaScript script source to be executed + @type str + @return result of the script + @rtype depending upon script result + """ + loop = QEventLoop() + resultDict = {"res": None} + QTimer.singleShot(500, loop.quit); + + def resultCallback(res, resDict=resultDict): + if loop and loop.isRunning(): + resDict["res"] = res + loop.quit() + + self.runJavaScript(script, resultCallback) + + loop.exec_() + return resultDict["res"] + + def scroll(self, x, y): + """ + Public method to scroll by the given amount of pixels. + + @param x horizontal scroll value + @type int + @param y vertical scroll value + @type int + """ + self.runJavaScript( + "window.scrollTo(window.scrollX + {0}, window.scrollY + {1})" + .format(x, y) + ) + + def hitTestContent(self, pos): + """ + Public method to test the content at a specified position. + + @param pos position to execute the test at + @type QPoint + @return test result object + @rtype WebHitTestResult + """ + return WebHitTestResult(self, pos) + + def __setupWebChannel(self): + """ + Private method to setup a web channel to our external object. + """ + oldChannel = self.webChannel() + newChannel = QWebChannel(self) + newChannel.registerObject("eric_object", ExternalJsObject(self)) + self.setWebChannel(newChannel) + + if oldChannel: + del oldChannel.registeredObjects["eric_object"] + del oldChannel + + def certificateError(self, error): + """ + Public method to handle SSL certificate errors. + + @param error object containing the certificate error information + @type QWebEngineCertificateError + @return flag indicating to ignore this error + @rtype bool + """ + return WebBrowserWindow.networkManager().certificateError( + error, self.view()) + + def __fullScreenRequested(self, request): + """ + Private slot handling a full screen request. + """ + self.view().requestFullScreen(request.toggleOn()) + + accepted = request.toggleOn() == self.view().isFullScreen() + + if accepted: + request.accept() + else: + request.reject() + + ############################################## + ## Methods below deal with JavaScript messages + ############################################## + + def javaScriptConsoleMessage(self, level, message, lineNumber, sourceId): + """ + Public method to show a console message. + + @param level severity + @type QWebEnginePage.JavaScriptConsoleMessageLevel + @param message message to be shown + @type str + @param lineNumber line number of an error + @type int + @param sourceId source URL causing the error + @type str + """ + self.view().mainWindow().javascriptConsole().javaScriptConsoleMessage( + level, message, lineNumber, sourceId)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserSnap.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing functions to generate page previews. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPixmap, QPainter + + +def renderTabPreview(view, w, h): + """ + Public function to render a pixmap of a page. + + @param view reference to the view to be previewed (QWebEngineView) + @param w width of the preview pixmap (integer) + @param h height of the preview pixmap (integer) + @return preview pixmap (QPixmap) + """ + pageImage = __render(view, view.width(), view.height()) + return pageImage.scaled( + w, h, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) + + +def __render(view, w, h): + """ + Private function to render a pixmap of given size for a web page. + + @param view reference to the view to be previewed (QWebEngineView) + @param w width of the pixmap (integer) + @param h height of the pixmap (integer) + @return rendered pixmap (QPixmap) + """ + # create the page image + pageImage = QPixmap(w, h) + pageImage.fill(Qt.transparent) + + # render it + p = QPainter(pageImage) + view.render(p) + p.end() + + return pageImage
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserTabBar.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a specialized tab bar for the web browser. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QPoint, QTimer, QEvent +from PyQt5.QtWidgets import QFrame, QLabel + +from E5Gui.E5TabWidget import E5WheelTabBar +from E5Gui.E5PassivePopup import E5PassivePopup + +import Preferences + + +class WebBrowserTabBar(E5WheelTabBar): + """ + Class implementing the tab bar of the web browser. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (WebBrowserTabWidget) + """ + super(WebBrowserTabBar, self).__init__(parent) + + self.__tabWidget = parent + + self.__previewPopup = None + self.__currentTabPreviewIndex = -1 + + self.setMouseTracking(True) + + def __showTabPreview(self): + """ + Private slot to show the tab preview. + """ + indexedBrowser = self.__tabWidget.browserAt( + self.__currentTabPreviewIndex) + currentBrowser = self.__tabWidget.currentBrowser() + + if indexedBrowser is None or currentBrowser is None: + return + + # no previews during load + if indexedBrowser.progress() != 0: + return + + w = self.tabSizeHint(self.__currentTabPreviewIndex).width() + h = int(w * currentBrowser.height() / currentBrowser.width()) + + self.__previewPopup = E5PassivePopup(self) + self.__previewPopup.setFrameShape(QFrame.StyledPanel) + self.__previewPopup.setFrameShadow(QFrame.Plain) + self.__previewPopup.setFixedSize(w, h) + + label = QLabel() + label.setPixmap(indexedBrowser.getPreview().scaled(w, h)) + + self.__previewPopup.setView(label) + self.__previewPopup.layout().setAlignment(Qt.AlignTop) + self.__previewPopup.layout().setContentsMargins(0, 0, 0, 0) + + tr = self.tabRect(self.__currentTabPreviewIndex) + pos = QPoint(tr.x(), tr.y() + tr.height()) + + self.__previewPopup.show(self.mapToGlobal(pos)) + + def mouseMoveEvent(self, evt): + """ + Protected method to handle mouse move events. + + @param evt reference to the mouse move event (QMouseEvent) + """ + if self.count() == 1: + return + + super(WebBrowserTabBar, self).mouseMoveEvent(evt) + + if Preferences.getWebBrowser("ShowPreview"): + # Find the tab under the mouse + i = 0 + tabIndex = -1 + while i < self.count() and tabIndex == -1: + if self.tabRect(i).contains(evt.pos()): + tabIndex = i + i += 1 + + # If found and not the current tab then show tab preview + if tabIndex != -1 and \ + tabIndex != self.currentIndex() and \ + self.__currentTabPreviewIndex != tabIndex and \ + evt.buttons() == Qt.NoButton: + self.__currentTabPreviewIndex = tabIndex + QTimer.singleShot(200, self.__showTabPreview) + + # If current tab or not found then hide previous tab preview + if tabIndex == self.currentIndex() or \ + tabIndex == -1: + if self.__previewPopup is not None: + self.__previewPopup.hide() + self.__currentTabPreviewIndex = -1 + + def leaveEvent(self, evt): + """ + Protected method to handle leave events. + + @param evt reference to the leave event (QEvent) + """ + if Preferences.getWebBrowser("ShowPreview"): + # If leave tabwidget then hide previous tab preview + if self.__previewPopup is not None: + self.__previewPopup.hide() + self.__currentTabPreviewIndex = -1 + + super(WebBrowserTabBar, self).leaveEvent(evt) + + def mousePressEvent(self, evt): + """ + Protected method to handle mouse press events. + + @param evt reference to the mouse press event (QMouseEvent) + """ + if Preferences.getWebBrowser("ShowPreview"): + if self.__previewPopup is not None: + self.__previewPopup.hide() + self.__currentTabPreviewIndex = -1 + + super(WebBrowserTabBar, self).mousePressEvent(evt) + + def event(self, evt): + """ + Public method to handle event. + + This event handler just handles the tooltip event and passes the + handling of all others to the superclass. + + @param evt reference to the event to be handled (QEvent) + @return flag indicating, if the event was handled (boolean) + """ + if evt.type() == QEvent.ToolTip and \ + Preferences.getWebBrowser("ShowPreview"): + # suppress tool tips if we are showing previews + evt.setAccepted(True) + return True + + return super(WebBrowserTabBar, self).event(evt) + + def tabRemoved(self, index): + """ + Public slot to handle the removal of a tab. + + @param index index of the removed tab (integer) + """ + if Preferences.getWebBrowser("ShowPreview"): + if self.__previewPopup is not None: + self.__previewPopup.hide() + self.__currentTabPreviewIndex = -1
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserTabWidget.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,957 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the central widget showing the web pages. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QMenu, QToolButton, QDialog +from PyQt5.QtPrintSupport import QPrinter, QPrintDialog + +from E5Gui.E5TabWidget import E5TabWidget +from E5Gui import E5MessageBox +from E5Gui.E5Application import e5App + +from .WebBrowserView import WebBrowserView +from .Tools import WebBrowserTools +from . import WebInspector + +import UI.PixmapCache + +import Utilities +import Preferences +import Globals + +from eric6config import getConfig + + +class WebBrowserTabWidget(E5TabWidget): + """ + Class implementing the central widget showing the web pages. + + @signal sourceChanged(WebBrowserView, QUrl) emitted after the URL of a + browser has changed + @signal titleChanged(WebBrowserView, str) emitted after the title of a + browser has changed + @signal showMessage(str) emitted to show a message in the main window + status bar + @signal browserClosed(QWidget) emitted after a browser was closed + @signal browserZoomValueChanged(int) emitted to signal a change of the + current browser's zoom level + """ + sourceChanged = pyqtSignal(WebBrowserView, QUrl) + titleChanged = pyqtSignal(WebBrowserView, str) + showMessage = pyqtSignal(str) + browserClosed = pyqtSignal(QWidget) + browserZoomValueChanged = pyqtSignal(int) + + def __init__(self, parent): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(WebBrowserTabWidget, self).__init__(parent, dnd=True) + + from .WebBrowserTabBar import WebBrowserTabBar + self.__tabBar = WebBrowserTabBar(self) + self.setCustomTabBar(True, self.__tabBar) + + self.__mainWindow = parent + + self.setUsesScrollButtons(True) + self.setDocumentMode(True) + self.setElideMode(Qt.ElideNone) + + from .ClosedTabsManager import ClosedTabsManager + self.__closedTabsManager = ClosedTabsManager(self) + self.__closedTabsManager.closedTabAvailable.connect( + self.__closedTabAvailable) + + from .UrlBar.StackedUrlBar import StackedUrlBar + self.__stackedUrlBar = StackedUrlBar(self) + self.__tabBar.tabMoved.connect(self.__stackedUrlBar.moveBar) + + self.__tabContextMenuIndex = -1 + self.currentChanged[int].connect(self.__currentChanged) + self.setTabContextMenuPolicy(Qt.CustomContextMenu) + self.customTabContextMenuRequested.connect(self.__showContextMenu) + + self.__rightCornerWidget = QWidget(self) + self.__rightCornerWidgetLayout = QHBoxLayout(self.__rightCornerWidget) + self.__rightCornerWidgetLayout.setContentsMargins(0, 0, 0, 0) + self.__rightCornerWidgetLayout.setSpacing(0) + + self.__navigationMenu = QMenu(self) + self.__navigationMenu.aboutToShow.connect(self.__showNavigationMenu) + self.__navigationMenu.triggered.connect(self.__navigationMenuTriggered) + + self.__navigationButton = QToolButton(self) + self.__navigationButton.setIcon( + UI.PixmapCache.getIcon("1downarrow.png")) + self.__navigationButton.setToolTip( + self.tr("Show a navigation menu")) + self.__navigationButton.setPopupMode(QToolButton.InstantPopup) + self.__navigationButton.setMenu(self.__navigationMenu) + self.__navigationButton.setEnabled(False) + self.__rightCornerWidgetLayout.addWidget(self.__navigationButton) + + self.__closedTabsMenu = QMenu(self) + self.__closedTabsMenu.aboutToShow.connect( + self.__aboutToShowClosedTabsMenu) + + self.__closedTabsButton = QToolButton(self) + self.__closedTabsButton.setIcon(UI.PixmapCache.getIcon("trash.png")) + self.__closedTabsButton.setToolTip( + self.tr("Show a navigation menu for closed tabs")) + self.__closedTabsButton.setPopupMode(QToolButton.InstantPopup) + self.__closedTabsButton.setMenu(self.__closedTabsMenu) + self.__closedTabsButton.setEnabled(False) + self.__rightCornerWidgetLayout.addWidget(self.__closedTabsButton) + + self.__closeButton = QToolButton(self) + self.__closeButton.setIcon(UI.PixmapCache.getIcon("close.png")) + self.__closeButton.setToolTip( + self.tr("Close the current web browser")) + self.__closeButton.setEnabled(False) + self.__closeButton.clicked.connect(self.closeBrowser) + self.__rightCornerWidgetLayout.addWidget(self.__closeButton) + if Preferences.getUI("SingleCloseButton") or \ + not hasattr(self, 'setTabsClosable'): + self.__closeButton.show() + else: + self.setTabsClosable(True) + self.tabCloseRequested.connect(self.closeBrowserAt) + self.__closeButton.hide() + + self.setCornerWidget(self.__rightCornerWidget, Qt.TopRightCorner) + + self.__newTabButton = QToolButton(self) + self.__newTabButton.setIcon(UI.PixmapCache.getIcon("plus.png")) + self.__newTabButton.setToolTip( + self.tr("Open a new web browser tab")) + self.setCornerWidget(self.__newTabButton, Qt.TopLeftCorner) + self.__newTabButton.clicked.connect(self.__newBrowser) + + self.__initTabContextMenu() + + self.__historyCompleter = None + + def __initTabContextMenu(self): + """ + Private method to create the tab context menu. + """ + self.__tabContextMenu = QMenu(self) + self.tabContextNewAct = self.__tabContextMenu.addAction( + UI.PixmapCache.getIcon("tabNew.png"), + self.tr('New Tab'), self.newBrowser) + self.__tabContextMenu.addSeparator() + self.leftMenuAct = self.__tabContextMenu.addAction( + UI.PixmapCache.getIcon("1leftarrow.png"), + self.tr('Move Left'), self.__tabContextMenuMoveLeft) + self.rightMenuAct = self.__tabContextMenu.addAction( + UI.PixmapCache.getIcon("1rightarrow.png"), + self.tr('Move Right'), self.__tabContextMenuMoveRight) + self.__tabContextMenu.addSeparator() + self.tabContextCloneAct = self.__tabContextMenu.addAction( + self.tr("Duplicate Page"), self.__tabContextMenuClone) + self.__tabContextMenu.addSeparator() + self.tabContextCloseAct = self.__tabContextMenu.addAction( + UI.PixmapCache.getIcon("tabClose.png"), + self.tr('Close'), self.__tabContextMenuClose) + self.tabContextCloseOthersAct = self.__tabContextMenu.addAction( + UI.PixmapCache.getIcon("tabCloseOther.png"), + self.tr("Close Others"), self.__tabContextMenuCloseOthers) + self.__tabContextMenu.addAction( + self.tr('Close All'), self.closeAllBrowsers) + self.__tabContextMenu.addSeparator() + self.__tabContextMenu.addAction( + UI.PixmapCache.getIcon("printPreview.png"), + self.tr('Print Preview'), self.__tabContextMenuPrintPreview) + self.__tabContextMenu.addAction( + UI.PixmapCache.getIcon("print.png"), + self.tr('Print'), self.__tabContextMenuPrint) + if Globals.isLinuxPlatform(): + self.__tabContextMenu.addAction( + UI.PixmapCache.getIcon("printPdf.png"), + self.tr('Print as PDF'), self.__tabContextMenuPrintPdf) + self.__tabContextMenu.addSeparator() + self.__tabContextMenu.addAction( + UI.PixmapCache.getIcon("reload.png"), + self.tr('Reload All'), self.reloadAllBrowsers) + self.__tabContextMenu.addSeparator() + self.__tabContextMenu.addAction( + UI.PixmapCache.getIcon("addBookmark.png"), + self.tr('Bookmark All Tabs'), self.__mainWindow.bookmarkAll) + + self.__tabBackContextMenu = QMenu(self) + self.__tabBackContextMenu.addAction( + self.tr('Close All'), self.closeAllBrowsers) + self.__tabBackContextMenu.addAction( + UI.PixmapCache.getIcon("reload.png"), + self.tr('Reload All'), self.reloadAllBrowsers) + self.__tabBackContextMenu.addAction( + UI.PixmapCache.getIcon("addBookmark.png"), + self.tr('Bookmark All Tabs'), self.__mainWindow.bookmarkAll) + self.__tabBackContextMenu.addSeparator() + self.__restoreClosedTabAct = self.__tabBackContextMenu.addAction( + UI.PixmapCache.getIcon("trash.png"), + self.tr('Restore Closed Tab'), self.restoreClosedTab) + self.__restoreClosedTabAct.setEnabled(False) + self.__restoreClosedTabAct.setData(0) + + def __showContextMenu(self, coord, index): + """ + Private slot to show the tab context menu. + + @param coord the position of the mouse pointer (QPoint) + @param index index of the tab the menu is requested for (integer) + """ + coord = self.mapToGlobal(coord) + if index == -1: + self.__tabBackContextMenu.popup(coord) + else: + self.__tabContextMenuIndex = index + self.leftMenuAct.setEnabled(index > 0) + self.rightMenuAct.setEnabled(index < self.count() - 1) + + self.tabContextCloseOthersAct.setEnabled(self.count() > 1) + + self.__tabContextMenu.popup(coord) + + def __tabContextMenuMoveLeft(self): + """ + Private method to move a tab one position to the left. + """ + self.moveTab(self.__tabContextMenuIndex, + self.__tabContextMenuIndex - 1) + + def __tabContextMenuMoveRight(self): + """ + Private method to move a tab one position to the right. + """ + self.moveTab(self.__tabContextMenuIndex, + self.__tabContextMenuIndex + 1) + + def __tabContextMenuClone(self): + """ + Private method to clone the selected tab. + """ + idx = self.__tabContextMenuIndex + if idx < 0: + idx = self.currentIndex() + if idx < 0 or idx > self.count(): + return + + url = self.widget(idx).url() + self.newBrowser(url) + + def __tabContextMenuClose(self): + """ + Private method to close the selected tab. + """ + self.closeBrowserAt(self.__tabContextMenuIndex) + + def __tabContextMenuCloseOthers(self): + """ + Private slot to close all other tabs. + """ + index = self.__tabContextMenuIndex + for i in list(range(self.count() - 1, index, -1)) + \ + list(range(index - 1, -1, -1)): + self.closeBrowserAt(i) + + def __tabContextMenuPrint(self): + """ + Private method to print the selected tab. + """ + browser = self.widget(self.__tabContextMenuIndex) + self.printBrowser(browser) + + def __tabContextMenuPrintPdf(self): + """ + Private method to print the selected tab as PDF. + """ + browser = self.widget(self.__tabContextMenuIndex) + self.printBrowserPdf(browser) + + def __tabContextMenuPrintPreview(self): + """ + Private method to show a print preview of the selected tab. + """ + browser = self.widget(self.__tabContextMenuIndex) + self.printPreviewBrowser(browser) + + @pyqtSlot() + def __newBrowser(self): + """ + Private slot to open a new browser tab. + """ + self.newBrowser() + + def newBrowser(self, link=None, position=-1): + """ + Public method to create a new web browser tab. + + @param link link to be shown (string or QUrl) + @keyparam position position to create the new tab at or -1 to add it + to the end (integer) + """ + if link is None: + linkName = "" + elif isinstance(link, QUrl): + linkName = link.toString() + else: + linkName = link + + from .UrlBar.UrlBar import UrlBar + urlbar = UrlBar(self.__mainWindow, self) + if self.__historyCompleter is None: + import WebBrowser.WebBrowserWindow + from .History.HistoryCompleter import HistoryCompletionModel, \ + HistoryCompleter + self.__historyCompletionModel = HistoryCompletionModel(self) + self.__historyCompletionModel.setSourceModel( + WebBrowser.WebBrowserWindow.WebBrowserWindow.historyManager() + .historyFilterModel()) + self.__historyCompleter = HistoryCompleter( + self.__historyCompletionModel, self) + self.__historyCompleter.activated[str].connect(self.__pathSelected) + urlbar.setCompleter(self.__historyCompleter) + urlbar.returnPressed.connect(self.__lineEditReturnPressed) + if position == -1: + self.__stackedUrlBar.addWidget(urlbar) + else: + self.__stackedUrlBar.insertWidget(position, urlbar) + + browser = WebBrowserView(self.__mainWindow, self) + urlbar.setBrowser(browser) + + browser.sourceChanged.connect(self.__sourceChanged) + browser.titleChanged.connect(self.__titleChanged) + browser.highlighted.connect(self.showMessage) + browser.backwardAvailable.connect( + self.__mainWindow.setBackwardAvailable) + browser.forwardAvailable.connect(self.__mainWindow.setForwardAvailable) + browser.loadStarted.connect(self.__loadStarted) + browser.loadFinished.connect(self.__loadFinished) + browser.iconChanged.connect(self.__iconChanged) + browser.search.connect(self.newBrowser) + browser.page().windowCloseRequested.connect( + self.__windowCloseRequested) + browser.zoomValueChanged.connect(self.browserZoomValueChanged) + + if position == -1: + index = self.addTab(browser, self.tr("...")) + else: + index = self.insertTab(position, browser, self.tr("...")) + self.setCurrentIndex(index) + + self.__mainWindow.closeAct.setEnabled(True) + self.__mainWindow.closeAllAct.setEnabled(True) + self.__closeButton.setEnabled(True) + self.__navigationButton.setEnabled(True) + + if not linkName: + if Preferences.getWebBrowser("StartupBehavior") == 0: + linkName = Preferences.getWebBrowser("HomePage") + elif Preferences.getWebBrowser("StartupBehavior") == 1: + linkName = "eric:speeddial" + + if linkName: + browser.setSource(QUrl(linkName)) + if not browser.documentTitle(): + self.setTabText(index, self.__elide(linkName, Qt.ElideMiddle)) + self.setTabToolTip(index, linkName) + else: + self.setTabText( + index, + self.__elide(browser.documentTitle().replace("&", "&&"))) + self.setTabToolTip(index, browser.documentTitle()) + + def newBrowserAfter(self, browser, link=None): + """ + Public method to create a new web browser tab after a given one. + + @param browser reference to the browser to add after (WebBrowserView) + @param link link to be shown (string or QUrl) + """ + if browser: + position = self.indexOf(browser) + 1 + else: + position = -1 + self.newBrowser(link, position) + + def __showNavigationMenu(self): + """ + Private slot to show the navigation button menu. + """ + self.__navigationMenu.clear() + for index in range(self.count()): + act = self.__navigationMenu.addAction( + self.tabIcon(index), self.tabText(index)) + act.setData(index) + + def __navigationMenuTriggered(self, act): + """ + Private slot called to handle the navigation button menu selection. + + @param act reference to the selected action (QAction) + """ + index = act.data() + if index is not None: + self.setCurrentIndex(index) + + def __windowCloseRequested(self): + """ + Private slot to handle the windowCloseRequested signal of a browser. + """ + page = self.sender() + if page is None: + return + + browser = page.view() + if browser is None: + return + + index = self.indexOf(browser) + self.closeBrowserAt(index) + + def reloadAllBrowsers(self): + """ + Public slot to reload all browsers. + """ + for index in range(self.count()): + browser = self.widget(index) + browser and browser.reload() + + @pyqtSlot() + def closeBrowser(self): + """ + Public slot called to handle the close action. + """ + self.closeBrowserAt(self.currentIndex()) + + def closeAllBrowsers(self, shutdown=False): + """ + Public slot called to handle the close all action. + + @param shutdown flag indicating a shutdown action + @type bool + """ + for index in range(self.count() - 1, -1, -1): + self.closeBrowserAt(index, shutdown=shutdown) + + def closeBrowserAt(self, index, shutdown=False): + """ + Public slot to close a browser based on its index. + + @param index index of browser to close + @type int + @param shutdown flag indicating a shutdown action + @type bool + """ + browser = self.widget(index) + if browser is None: + return + + urlbar = self.__stackedUrlBar.widget(index) + self.__stackedUrlBar.removeWidget(urlbar) + urlbar.deleteLater() + del urlbar + + self.__closedTabsManager.recordBrowser(browser, index) + + browser.closeWebInspector() + WebInspector.unregisterView(browser) + self.removeTab(index) + self.browserClosed.emit(browser) + browser.deleteLater() + del browser + + if self.count() == 0 and not shutdown: + self.newBrowser() + else: + self.currentChanged[int].emit(self.currentIndex()) + + def currentBrowser(self): + """ + Public method to get a reference to the current browser. + + @return reference to the current browser (WebBrowserView) + """ + return self.currentWidget() + + def browserAt(self, index): + """ + Public method to get a reference to the browser with the given index. + + @param index index of the browser to get (integer) + @return reference to the indexed browser (WebBrowserView) + """ + return self.widget(index) + + def browsers(self): + """ + Public method to get a list of references to all browsers. + + @return list of references to browsers (list of WebBrowserView) + """ + li = [] + for index in range(self.count()): + li.append(self.widget(index)) + return li + + @pyqtSlot() + def printBrowser(self, browser=None): + """ + Public slot called to print the displayed page. + + @param browser reference to the browser to be printed (WebBrowserView) + """ + if browser is None: + browser = self.currentBrowser() + + printer = QPrinter(mode=QPrinter.HighResolution) + if Preferences.getPrinter("ColorMode"): + printer.setColorMode(QPrinter.Color) + else: + printer.setColorMode(QPrinter.GrayScale) + if Preferences.getPrinter("FirstPageFirst"): + printer.setPageOrder(QPrinter.FirstPageFirst) + else: + printer.setPageOrder(QPrinter.LastPageFirst) + printer.setPageMargins( + Preferences.getPrinter("LeftMargin") * 10, + Preferences.getPrinter("TopMargin") * 10, + Preferences.getPrinter("RightMargin") * 10, + Preferences.getPrinter("BottomMargin") * 10, + QPrinter.Millimeter + ) + printerName = Preferences.getPrinter("PrinterName") + if printerName: + printer.setPrinterName(printerName) + printer.setResolution(Preferences.getPrinter("Resolution")) + + printDialog = QPrintDialog(printer, self) + if printDialog.exec_() == QDialog.Accepted: + browser.render(printer) + + @pyqtSlot() + def printBrowserPdf(self, browser=None): + """ + Public slot called to print the displayed page to PDF. + + @param browser reference to the browser to be printed (HelpBrowser) + """ + if browser is None: + browser = self.currentBrowser() + + printer = QPrinter(mode=QPrinter.HighResolution) + if Preferences.getPrinter("ColorMode"): + printer.setColorMode(QPrinter.Color) + else: + printer.setColorMode(QPrinter.GrayScale) + printerName = Preferences.getPrinter("PrinterName") + if printerName: + printer.setPrinterName(printerName) + printer.setOutputFormat(QPrinter.PdfFormat) + name = WebBrowserTools.getFileNameFromUrl(browser.url()) + if name: + name = name.rsplit('.', 1)[0] + name += '.pdf' + printer.setOutputFileName(name) + printer.setResolution(Preferences.getPrinter("Resolution")) + + printDialog = QPrintDialog(printer, self) + if printDialog.exec_() == QDialog.Accepted: + browser.render(printer) + + @pyqtSlot() + def printPreviewBrowser(self, browser=None): + """ + Public slot called to show a print preview of the displayed file. + + @param browser reference to the browser to be printed (HelpBrowserWV) + """ + from PyQt5.QtPrintSupport import QPrintPreviewDialog + + if browser is None: + browser = self.currentBrowser() + + printer = QPrinter(mode=QPrinter.HighResolution) + if Preferences.getPrinter("ColorMode"): + printer.setColorMode(QPrinter.Color) + else: + printer.setColorMode(QPrinter.GrayScale) + if Preferences.getPrinter("FirstPageFirst"): + printer.setPageOrder(QPrinter.FirstPageFirst) + else: + printer.setPageOrder(QPrinter.LastPageFirst) + printer.setPageMargins( + Preferences.getPrinter("LeftMargin") * 10, + Preferences.getPrinter("TopMargin") * 10, + Preferences.getPrinter("RightMargin") * 10, + Preferences.getPrinter("BottomMargin") * 10, + QPrinter.Millimeter + ) + printerName = Preferences.getPrinter("PrinterName") + if printerName: + printer.setPrinterName(printerName) + printer.setResolution(Preferences.getPrinter("Resolution")) + + preview = QPrintPreviewDialog(printer, self) + preview.paintRequested.connect(lambda p: browser.render(p)) + preview.exec_() + + def __sourceChanged(self, url): + """ + Private slot to handle a change of a browsers source. + + @param url URL of the new site (QUrl) + """ + browser = self.sender() + + if browser is not None: + self.sourceChanged.emit(browser, url) + + def __titleChanged(self, title): + """ + Private slot to handle a change of a browsers title. + + @param title new title (string) + """ + browser = self.sender() + + if browser is not None and isinstance(browser, QWidget): + index = self.indexOf(browser) + if title == "": + title = browser.url().toString() + + self.setTabText(index, self.__elide(title.replace("&", "&&"))) + self.setTabToolTip(index, title) + + self.titleChanged.emit(browser, title) + + def __elide(self, txt, mode=Qt.ElideRight, length=40): + """ + Private method to elide some text. + + @param txt text to be elided (string) + @keyparam mode elide mode (Qt.TextElideMode) + @keyparam length amount of characters to be used (integer) + @return the elided text (string) + """ + if mode == Qt.ElideNone or len(txt) < length: + return txt + elif mode == Qt.ElideLeft: + return "...{0}".format(txt[-length:]) + elif mode == Qt.ElideMiddle: + return "{0}...{1}".format(txt[:length // 2], txt[-(length // 2):]) + elif mode == Qt.ElideRight: + return "{0}...".format(txt[:length]) + else: + # just in case + return txt + + def preferencesChanged(self): + """ + Public slot to handle a change of preferences. + """ + for browser in self.browsers(): + browser.preferencesChanged() + + for urlbar in self.__stackedUrlBar.urlBars(): + urlbar.preferencesChanged() + + if Preferences.getUI("SingleCloseButton"): + self.setTabsClosable(False) + try: + self.tabCloseRequested.disconnect(self.closeBrowserAt) + except TypeError: + pass + self.__closeButton.show() + else: + self.setTabsClosable(True) + self.tabCloseRequested.connect(self.closeBrowserAt) + self.__closeButton.hide() + + def __loadStarted(self): + """ + Private method to handle the loadStarted signal. + """ + browser = self.sender() + + if browser is not None: + index = self.indexOf(browser) + anim = self.animationLabel( + index, os.path.join(getConfig("ericPixDir"), "loading.gif"), + 100) + if not anim: + loading = QIcon(os.path.join(getConfig("ericPixDir"), + "loading.gif")) + self.setTabIcon(index, loading) + else: + self.setTabIcon(index, QIcon()) + self.setTabText(index, self.tr("Loading...")) + self.setTabToolTip(index, self.tr("Loading...")) + self.showMessage.emit(self.tr("Loading...")) + + self.__mainWindow.setLoadingActions(True) + + def __loadFinished(self, ok): + """ + Private method to handle the loadFinished signal. + + @param ok flag indicating the result (boolean) + """ + browser = self.sender() + if not isinstance(browser, WebBrowserView): + return + + if browser is not None: + import WebBrowser.WebBrowserWindow + index = self.indexOf(browser) + self.resetAnimation(index) + self.setTabIcon( + index, WebBrowser.WebBrowserWindow.WebBrowserWindow.icon( + browser.url())) + if ok: + self.showMessage.emit(self.tr("Finished loading")) + else: + self.showMessage.emit(self.tr("Failed to load")) + + self.__mainWindow.setLoadingActions(False) + + def __iconChanged(self): + """ + Private slot to handle a change of the web site icon. + """ + browser = self.sender() + + if browser is not None and isinstance(browser, QWidget): + self.setTabIcon( + self.indexOf(browser), + browser.icon()) + self.__mainWindow.bookmarksManager().iconChanged(browser.url()) + + def getSourceFileList(self): + """ + Public method to get a list of all opened Qt help files. + + @return dictionary with tab id as key and host/namespace as value + """ + sourceList = {} + for i in range(self.count()): + browser = self.widget(i) + if browser is not None and \ + browser.source().isValid(): + sourceList[i] = browser.source().host() + + return sourceList + + def shallShutDown(self): + """ + Public method to check, if the application should be shut down. + + @return flag indicating a shut down (boolean) + """ + if self.count() > 1 and Preferences.getWebBrowser( + "WarnOnMultipleClose"): + mb = E5MessageBox.E5MessageBox( + E5MessageBox.Information, + self.tr("Are you sure you want to close the window?"), + self.tr("""Are you sure you want to close the window?\n""" + """You have %n tab(s) open.""", "", self.count()), + modal=True, + parent=self) + if self.__mainWindow.fromEric: + quitButton = mb.addButton( + self.tr("&Close"), E5MessageBox.AcceptRole) + quitButton.setIcon(UI.PixmapCache.getIcon("close.png")) + else: + quitButton = mb.addButton( + self.tr("&Quit"), E5MessageBox.AcceptRole) + quitButton.setIcon(UI.PixmapCache.getIcon("exit.png")) + closeTabButton = mb.addButton( + self.tr("C&lose Current Tab"), E5MessageBox.AcceptRole) + closeTabButton.setIcon(UI.PixmapCache.getIcon("tabClose.png")) + mb.addButton(E5MessageBox.Cancel) + mb.exec_() + if mb.clickedButton() == quitButton: + return True + else: + if mb.clickedButton() == closeTabButton: + self.closeBrowser() + return False + + return True + + def stackedUrlBar(self): + """ + Public method to get a reference to the stacked url bar. + + @return reference to the stacked url bar (StackedUrlBar) + """ + return self.__stackedUrlBar + + def currentUrlBar(self): + """ + Public method to get a reference to the current url bar. + + @return reference to the current url bar (UrlBar) + """ + return self.__stackedUrlBar.currentWidget() + + def __lineEditReturnPressed(self): + """ + Private slot to handle the entering of an URL. + """ + edit = self.sender() + url = self.__guessUrlFromPath(edit.text()) + if e5App().keyboardModifiers() == Qt.AltModifier: + self.newBrowser(url) + else: + self.currentBrowser().setSource(url) + self.currentBrowser().setFocus() + + def __pathSelected(self, path): + """ + Private slot called when a URL is selected from the completer. + + @param path path to be shown (string) + """ + url = self.__guessUrlFromPath(path) + self.currentBrowser().setSource(url) + + def __guessUrlFromPath(self, path): + """ + Private method to guess an URL given a path string. + + @param path path string to guess an URL for (string) + @return guessed URL (QUrl) + """ + manager = self.__mainWindow.openSearchManager() + path = Utilities.fromNativeSeparators(path) + url = manager.convertKeywordSearchToUrl(path) + if url.isValid(): + return url + + try: + url = QUrl.fromUserInput(path) + except AttributeError: + url = QUrl(path) + + if url.scheme() == "about" and \ + url.path() == "home": + url = QUrl("eric:home") + + if url.scheme() in ["s", "search"]: + url = manager.currentEngine().searchUrl(url.path().strip()) + + if url.scheme() != "" and \ + (url.host() != "" or url.path() != ""): + return url + + urlString = Preferences.getWebBrowser("DefaultScheme") + path.strip() + url = QUrl.fromEncoded(urlString.encode("utf-8"), QUrl.TolerantMode) + + return url + + def __currentChanged(self, index): + """ + Private slot to handle an index change. + + @param index new index (integer) + """ + self.__stackedUrlBar.setCurrentIndex(index) + + browser = self.browserAt(index) + if browser is not None: + if browser.url() == "" and browser.hasFocus(): + self.__stackedUrlBar.currentWidget.setFocus() + elif browser.url() != "": + browser.setFocus() + + def restoreClosedTab(self): + """ + Public slot to restore the most recently closed tab. + """ + if not self.canRestoreClosedTab(): + return + + act = self.sender() + tab = self.__closedTabsManager.getClosedTabAt(act.data()) + + self.newBrowser(tab.url.toString(), position=tab.position) + + def canRestoreClosedTab(self): + """ + Public method to check, if closed tabs can be restored. + + @return flag indicating that closed tabs can be restored (boolean) + """ + return self.__closedTabsManager.isClosedTabAvailable() + + def restoreAllClosedTabs(self): + """ + Public slot to restore all closed tabs. + """ + if not self.canRestoreClosedTab(): + return + + for tab in self.__closedTabsManager.allClosedTabs(): + self.newBrowser(tab.url.toString(), position=tab.position) + self.__closedTabsManager.clearList() + + def clearClosedTabsList(self): + """ + Public slot to clear the list of closed tabs. + """ + self.__closedTabsManager.clearList() + + def __aboutToShowClosedTabsMenu(self): + """ + Private slot to populate the closed tabs menu. + """ + fm = self.__closedTabsMenu.fontMetrics() + maxWidth = fm.width('m') * 40 + + self.__closedTabsMenu.clear() + index = 0 + for tab in self.__closedTabsManager.allClosedTabs(): + title = fm.elidedText(tab.title, Qt.ElideRight, maxWidth) + self.__closedTabsMenu.addAction( + self.__mainWindow.icon(tab.url), title, + self.restoreClosedTab).setData(index) + index += 1 + self.__closedTabsMenu.addSeparator() + self.__closedTabsMenu.addAction( + self.tr("Restore All Closed Tabs"), self.restoreAllClosedTabs) + self.__closedTabsMenu.addAction( + self.tr("Clear List"), self.clearClosedTabsList) + + def closedTabsManager(self): + """ + Public slot to get a reference to the closed tabs manager. + + @return reference to the closed tabs manager (ClosedTabsManager) + """ + return self.__closedTabsManager + + def __closedTabAvailable(self, avail): + """ + Private slot to handle changes of the availability of closed tabs. + + @param avail flag indicating the availability of closed tabs (boolean) + """ + self.__closedTabsButton.setEnabled(avail) + self.__restoreClosedTabAct.setEnabled(avail)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserView.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,1602 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2008 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + + +""" +Module implementing the web browser using QWebEngineView. +""" + +from __future__ import unicode_literals +try: + str = unicode # __IGNORE_EXCEPTION__ +except NameError: + pass + +from PyQt5.QtCore import pyqtSignal, QUrl, QFileInfo, Qt, QTimer, QEvent, \ + QPoint +from PyQt5.QtGui import QDesktopServices, QClipboard, QIcon, \ + QContextMenuEvent, QPixmap +from PyQt5.QtWidgets import qApp, QStyle, QMenu, QApplication +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage + +from E5Gui import E5MessageBox + +from .WebBrowserPage import WebBrowserPage + +from .Tools.WebIconLoader import WebIconLoader +from .Tools import Scripts + +from . import WebInspector +from .Tools.WebBrowserTools import readAllFileContents, pixmapToDataUrl + +import Preferences +import UI.PixmapCache + + +class WebBrowserView(QWebEngineView): + """ + Class implementing the web browser view widget. + + @signal sourceChanged(QUrl) emitted after the current URL has changed + @signal forwardAvailable(bool) emitted after the current URL has changed + @signal backwardAvailable(bool) emitted after the current URL has changed + @signal highlighted(str) emitted, when the mouse hovers over a link + @signal search(QUrl) emitted, when a search is requested + @signal zoomValueChanged(int) emitted to signal a change of the zoom value + @signal iconChanged() emitted to signal a changed web site icon + """ + sourceChanged = pyqtSignal(QUrl) + forwardAvailable = pyqtSignal(bool) + backwardAvailable = pyqtSignal(bool) + highlighted = pyqtSignal(str) + search = pyqtSignal(QUrl) + zoomValueChanged = pyqtSignal(int) + iconChanged = pyqtSignal() + + ZoomLevels = [ + 30, 40, 50, 67, 80, 90, + 100, + 110, 120, 133, 150, 170, 200, 220, 233, 250, 270, 285, 300, + ] + ZoomLevelDefault = 100 + + def __init__(self, mainWindow, parent=None, name=""): + """ + Constructor + + @param mainWindow reference to the main window (WebBrowserWindow) + @param parent parent widget of this window (QWidget) + @param name name of this window (string) + """ + super(WebBrowserView, self).__init__(parent) + self.setObjectName(name) + + self.__rwhvqt = None + self.installEventFilter(self) + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + self.__speedDial = WebBrowserWindow.speedDial() + + self.__page = WebBrowserPage(self) + self.setPage(self.__page) + + self.__mw = mainWindow + self.__isLoading = False + self.__progress = 0 + self.__siteIconLoader = None + self.__siteIcon = QIcon() + self.__menu = QMenu(self) + self.__clickedPos = QPoint() + self.__firstLoad = False + self.__preview = QPixmap() + + self.__currentZoom = 100 + self.__zoomLevels = WebBrowserView.ZoomLevels[:] + + self.iconUrlChanged.connect(self.__iconUrlChanged) + self.urlChanged.connect(self.__urlChanged) + self.page().linkHovered.connect(self.__linkHovered) + + self.loadStarted.connect(self.__loadStarted) + self.loadProgress.connect(self.__loadProgress) + self.loadFinished.connect(self.__loadFinished) + self.renderProcessTerminated.connect(self.__renderProcessTerminated) + + self.__mw.openSearchManager().currentEngineChanged.connect( + self.__currentEngineChanged) + + self.setAcceptDrops(True) + + self.__rss = [] + + self.__clickedFrame = None + + self.__mw.personalInformationManager().connectPage(self.page()) + + self.__inspector = None + WebInspector.registerView(self) + + self.grabGesture(Qt.PinchGesture) + + def __currentEngineChanged(self): + """ + Private slot to track a change of the current search engine. + """ + if self.url().toString() == "eric:home": + self.reload() + + def mainWindow(self): + """ + Public method to get a reference to the main window. + + @return reference to the main window + @rtype WebBrowserWindow + """ + return self.__mw + + def load(self, url): + """ + Public method to load a web site. + + @param url URL to be loaded + @type QUrl + """ + super(WebBrowserView, self).load(url) + + if not self.__firstLoad: + self.__firstLoad = True + WebInspector.pushView(self) + + def setSource(self, name, newTab=False): + """ + Public method used to set the source to be displayed. + + @param name filename to be shown (QUrl) + @param newTab flag indicating to open the URL in a new tab (bool) + """ + if name is None or not name.isValid(): + return + + if newTab: + # open in a new tab + self.__mw.newTab(name) + return + + if not name.scheme(): + name.setUrl(Preferences.getWebBrowser("DefaultScheme") + + name.toString()) + + if len(name.scheme()) == 1 or \ + name.scheme() == "file": + # name is a local file + if name.scheme() and len(name.scheme()) == 1: + # it is a local path on win os + name = QUrl.fromLocalFile(name.toString()) + + if not QFileInfo(name.toLocalFile()).exists(): + E5MessageBox.critical( + self, + self.tr("eric6 Web Browser"), + self.tr( + """<p>The file <b>{0}</b> does not exist.</p>""") + .format(name.toLocalFile())) + return + + if name.toLocalFile().endswith(".pdf") or \ + name.toLocalFile().endswith(".PDF") or \ + name.toLocalFile().endswith(".chm") or \ + name.toLocalFile().endswith(".CHM"): + started = QDesktopServices.openUrl(name) + if not started: + E5MessageBox.critical( + self, + self.tr("eric6 Web Browser"), + self.tr( + """<p>Could not start a viewer""" + """ for file <b>{0}</b>.</p>""") + .format(name.path())) + return + elif name.scheme() in ["mailto"]: + started = QDesktopServices.openUrl(name) + if not started: + E5MessageBox.critical( + self, + self.tr("eric6 Web Browser"), + self.tr( + """<p>Could not start an application""" + """ for URL <b>{0}</b>.</p>""") + .format(name.toString())) + return + else: + if name.toString().endswith(".pdf") or \ + name.toString().endswith(".PDF") or \ + name.toString().endswith(".chm") or \ + name.toString().endswith(".CHM"): + started = QDesktopServices.openUrl(name) + if not started: + E5MessageBox.critical( + self, + self.tr("eric6 Web Browser"), + self.tr( + """<p>Could not start a viewer""" + """ for file <b>{0}</b>.</p>""") + .format(name.path())) + return + + self.load(name) + + def source(self): + """ + Public method to return the URL of the loaded page. + + @return URL loaded in the help browser (QUrl) + """ + return self.url() + + def documentTitle(self): + """ + Public method to return the title of the loaded page. + + @return title (string) + """ + return self.title() + + def backward(self): + """ + Public slot to move backwards in history. + """ + self.triggerPageAction(QWebEnginePage.Back) + self.__urlChanged(self.history().currentItem().url()) + + def forward(self): + """ + Public slot to move forward in history. + """ + self.triggerPageAction(QWebEnginePage.Forward) + self.__urlChanged(self.history().currentItem().url()) + + def home(self): + """ + Public slot to move to the first page loaded. + """ + homeUrl = QUrl(Preferences.getWebBrowser("HomePage")) + self.setSource(homeUrl) + self.__urlChanged(self.history().currentItem().url()) + + def reload(self): + """ + Public slot to reload the current page. + """ + self.triggerPageAction(QWebEnginePage.Reload) + + def reloadBypassingCache(self): + """ + Public slot to reload the current page bypassing the cache. + """ + self.triggerPageAction(QWebEnginePage.ReloadAndBypassCache) + + def copy(self): + """ + Public slot to copy the selected text. + """ + self.triggerPageAction(QWebEnginePage.Copy) + + def cut(self): + """ + Public slot to cut the selected text. + """ + self.triggerPageAction(QWebEnginePage.Cut) + + def paste(self): + """ + Public slot to paste text from the clipboard. + """ + self.triggerPageAction(QWebEnginePage.Paste) + + def undo(self): + """ + Public slot to undo the last edit action. + """ + self.triggerPageAction(QWebEnginePage.Undo) + + def redo(self): + """ + Public slot to redo the last edit action. + """ + self.triggerPageAction(QWebEnginePage.Redo) + + def selectAll(self): + """ + Public slot to select all text. + """ + self.triggerPageAction(QWebEnginePage.SelectAll) + + def isForwardAvailable(self): + """ + Public method to determine, if a forward move in history is possible. + + @return flag indicating move forward is possible (boolean) + """ + return self.history().canGoForward() + + def isBackwardAvailable(self): + """ + Public method to determine, if a backwards move in history is possible. + + @return flag indicating move backwards is possible (boolean) + """ + return self.history().canGoBack() + + def __levelForZoom(self, zoom): + """ + Private method determining the zoom level index given a zoom factor. + + @param zoom zoom factor (integer) + @return index of zoom factor (integer) + """ + try: + index = self.__zoomLevels.index(zoom) + except ValueError: + for index in range(len(self.__zoomLevels)): + if zoom <= self.__zoomLevels[index]: + break + return index + + def setZoomValue(self, value, saveValue=True): + """ + Public method to set the zoom value. + + @param value zoom value (integer) + @keyparam saveValue flag indicating to save the zoom value with the + zoom manager + @type bool + """ + if value != self.__currentZoom: + self.setZoomFactor(value / 100.0) + self.__currentZoom = value + if saveValue and not self.__mw.isPrivate(): + from .ZoomManager import ZoomManager + ZoomManager.instance().setZoomValue(self.url(), value) + self.zoomValueChanged.emit(value) + + def zoomValue(self): + """ + Public method to get the current zoom value. + + @return zoom value (integer) + """ + val = self.zoomFactor() * 100 + return int(val) + + def zoomIn(self): + """ + Public slot to zoom into the page. + """ + index = self.__levelForZoom(self.__currentZoom) + if index < len(self.__zoomLevels) - 1: + self.setZoomValue(self.__zoomLevels[index + 1]) + + def zoomOut(self): + """ + Public slot to zoom out of the page. + """ + index = self.__levelForZoom(self.__currentZoom) + if index > 0: + self.setZoomValue(self.__zoomLevels[index - 1]) + + def zoomReset(self): + """ + Public method to reset the zoom factor. + """ + index = self.__levelForZoom(WebBrowserView.ZoomLevelDefault) + self.setZoomValue(self.__zoomLevels[index]) + + def hasSelection(self): + """ + Public method to determine, if there is some text selected. + + @return flag indicating text has been selected (boolean) + """ + return self.selectedText() != "" + + def findNextPrev(self, txt, case, backwards, callback): + """ + Public slot to find the next occurrence of a text. + + @param txt text to search for (string) + @param case flag indicating a case sensitive search (boolean) + @param backwards flag indicating a backwards search (boolean) + @param callback reference to a function with a bool parameter + @type function(bool) or None + """ + findFlags = QWebEnginePage.FindFlags() + if case: + findFlags |= QWebEnginePage.FindCaseSensitively + if backwards: + findFlags |= QWebEnginePage.FindBackward + + if callback is None: + self.findText(txt, findFlags) + else: + self.findText(txt, findFlags, callback) + + def contextMenuEvent(self, evt): + """ + Public method called to create a context menu. + + This method is overridden from QWebEngineView. + + @param evt reference to the context menu event object + (QContextMenuEvent) + """ + pos = evt.pos() + reason = evt.reason() + QTimer.singleShot( + 0, + lambda: self._contextMenuEvent(QContextMenuEvent(reason, pos))) + # needs to be done this way because contextMenuEvent is blocking + # the main loop + + def _contextMenuEvent(self, evt): + """ + Protected method called to create a context menu. + + This method is overridden from QWebEngineView. + + @param evt reference to the context menu event object + (QContextMenuEvent) + """ + self.__menu.clear() + + hitTest = self.page().hitTestContent(evt.pos()) + + self.__createContextMenu(self.__menu, hitTest) + + if not hitTest.isContentEditable() and not hitTest.isContentSelected(): + self.__menu.addSeparator() + self.__menu.addAction(self.__mw.adBlockIcon().menuAction()) + + if Preferences.getWebBrowser("WebInspectorEnabled"): + self.__menu.addSeparator() + self.__menu.addAction( + UI.PixmapCache.getIcon("webInspector.png"), + self.tr("Inspect Element..."), self.__webInspector) + + if not self.__menu.isEmpty(): + pos = evt.globalPos() + self.__menu.popup(QPoint(pos.x(), pos.y() + 1)) + + def __createContextMenu(self, menu, hitTest): + """ + Private method to populate the context menu. + + @param menu reference to the menu to be populated + @type QMenu + @param hitTest reference to the hit test object + @type WebHitTestResult + """ + if not hitTest.linkUrl().isEmpty() and \ + hitTest.linkUrl().scheme() != "javascript": + self.__createLinkContextMenu(menu, hitTest) + + if not hitTest.imageUrl().isEmpty(): + self.__createImageContextMenu(menu, hitTest) + + if not hitTest.mediaUrl().isEmpty(): + self.__createMediaContextMenu(menu, hitTest) + + if hitTest.isContentEditable(): + menu.addAction(self.__mw.undoAct) + menu.addAction(self.__mw.redoAct) + menu.addSeparator() + menu.addAction(self.__mw.cutAct) + menu.addAction(self.__mw.copyAct) + menu.addAction(self.__mw.pasteAct) + menu.addSeparator() + self.__mw.personalInformationManager().createSubMenu( + menu, self, hitTest) + + if hitTest.tagName() == "input": + menu.addSeparator() + act = menu.addAction("") + act.setVisible(False) + self.__checkForForm(act, hitTest.pos()) + + if self.selectedText(): + self.__createSelectedTextContextMenu(menu, hitTest) + + if self.__menu.isEmpty(): + self.__createPageContextMenu(menu) + + def __createLinkContextMenu(self, menu, hitTest): + """ + Private method to populate the context menu for URLs. + + @param menu reference to the menu to be populated + @type QMenu + @param hitTest reference to the hit test object + @type WebHitTestResult + """ + if not menu.isEmpty(): + menu.addSeparator() + + menu.addAction( + UI.PixmapCache.getIcon("openNewTab.png"), + self.tr("Open Link in New Tab\tCtrl+LMB"), + self.__openLinkInNewTab).setData(hitTest.linkUrl()) + menu.addAction( + UI.PixmapCache.getIcon("newWindow.png"), + self.tr("Open Link in New Window"), + self.__openLinkInNewWindow).setData(hitTest.linkUrl()) + menu.addAction( + UI.PixmapCache.getIcon("privateMode.png"), + self.tr("Open Link in New Private Window"), + self.__openLinkInNewPrivateWindow).setData(hitTest.linkUrl()) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("download.png"), + self.tr("Save Lin&k"), self.__downloadLink) + menu.addAction( + UI.PixmapCache.getIcon("bookmark22.png"), + self.tr("Bookmark this Link"), self.__bookmarkLink)\ + .setData(hitTest.linkUrl()) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("editCopy.png"), + self.tr("Copy Link to Clipboard"), self.__copyLink)\ + .setData(hitTest.linkUrl()) + menu.addAction( + UI.PixmapCache.getIcon("mailSend.png"), + self.tr("Send Link"), + self.__sendLink).setData(hitTest.linkUrl()) + if Preferences.getWebBrowser("VirusTotalEnabled") and \ + Preferences.getWebBrowser("VirusTotalServiceKey") != "": + menu.addAction( + UI.PixmapCache.getIcon("virustotal.png"), + self.tr("Scan Link with VirusTotal"), + self.__virusTotal).setData(hitTest.linkUrl()) + + def __createImageContextMenu(self, menu, hitTest): + """ + Private method to populate the context menu for images. + + @param menu reference to the menu to be populated + @type QMenu + @param hitTest reference to the hit test object + @type WebHitTestResult + """ + if not menu.isEmpty(): + menu.addSeparator() + + menu.addAction( + UI.PixmapCache.getIcon("openNewTab.png"), + self.tr("Open Image in New Tab"), + self.__openLinkInNewTab).setData(hitTest.imageUrl()) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("download.png"), + self.tr("Save Image"), self.__downloadImage) + menu.addAction( + self.tr("Copy Image to Clipboard"), self.__copyImage) + menu.addAction( + UI.PixmapCache.getIcon("editCopy.png"), + self.tr("Copy Image Location to Clipboard"), + self.__copyLink).setData(hitTest.imageUrl()) + menu.addAction( + UI.PixmapCache.getIcon("mailSend.png"), + self.tr("Send Image Link"), + self.__sendLink).setData(hitTest.imageUrl()) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("adBlockPlus.png"), + self.tr("Block Image"), self.__blockImage)\ + .setData(hitTest.imageUrl().toString()) + if Preferences.getWebBrowser("VirusTotalEnabled") and \ + Preferences.getWebBrowser("VirusTotalServiceKey") != "": + menu.addAction( + UI.PixmapCache.getIcon("virustotal.png"), + self.tr("Scan Image with VirusTotal"), + self.__virusTotal).setData(hitTest.imageUrl()) + + def __createMediaContextMenu(self, menu, hitTest): + """ + Private method to populate the context menu for media elements. + + @param menu reference to the menu to be populated + @type QMenu + @param hitTest reference to the hit test object + @type WebHitTestResult + """ + self.__clickedPos = hitTest.pos() + + if not menu.isEmpty(): + menu.addSeparator() + + if hitTest.mediaPaused(): + menu.addAction( + UI.PixmapCache.getIcon("mediaPlaybackStart.png"), + self.tr("Play"), self.__pauseMedia) + else: + menu.addAction( + UI.PixmapCache.getIcon("mediaPlaybackPause.png"), + self.tr("Pause"), self.__pauseMedia) + if hitTest.mediaMuted(): + menu.addAction( + UI.PixmapCache.getIcon("audioVolumeHigh.png"), + self.tr("Unmute"), self.__muteMedia) + else: + menu.addAction( + UI.PixmapCache.getIcon("audioVolumeMuted.png"), + self.tr("Mute"), self.__muteMedia) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("editCopy.png"), + self.tr("Copy Media Address to Clipboard"), + self.__copyLink).setData(hitTest.mediaUrl()) + menu.addAction( + UI.PixmapCache.getIcon("mailSend.png"), + self.tr("Send Media Address"), self.__sendLink)\ + .setData(hitTest.mediaUrl()) + menu.addAction( + UI.PixmapCache.getIcon("download.png"), + self.tr("Save Media"), self.__downloadMedia) + + def __createSelectedTextContextMenu(self, menu, hitTest): + """ + Private method to populate the context menu for selected text. + + @param menu reference to the menu to be populated + @type QMenu + @param hitTest reference to the hit test object + @type WebHitTestResult + """ + if not menu.isEmpty(): + menu.addSeparator() + + menu.addAction(self.__mw.copyAct) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("mailSend.png"), + self.tr("Send Text"), + self.__sendLink).setData(self.selectedText()) + + engineName = self.__mw.openSearchManager().currentEngineName() + if engineName: + menu.addAction(self.tr("Search with '{0}'").format(engineName), + self.__searchDefaultRequested) + + from .OpenSearch.OpenSearchEngineAction import \ + OpenSearchEngineAction + + self.__searchMenu = menu.addMenu(self.tr("Search with...")) + engineNames = self.__mw.openSearchManager().allEnginesNames() + for engineName in engineNames: + engine = self.__mw.openSearchManager().engine(engineName) + act = OpenSearchEngineAction(engine, self.__searchMenu) + act.setData(engineName) + self.__searchMenu.addAction(act) + self.__searchMenu.triggered.connect(self.__searchRequested) + + menu.addSeparator() + + from .WebBrowserLanguagesDialog import WebBrowserLanguagesDialog + languages = Preferences.toList( + Preferences.Prefs.settings.value( + "WebBrowser/AcceptLanguages", + WebBrowserLanguagesDialog.defaultAcceptLanguages())) + if languages: + language = languages[0] + langCode = language.split("[")[1][:2] + googleTranslatorUrl = QUrl( + "http://translate.google.com/#auto|{0}|{1}".format( + langCode, self.selectedText())) + menu.addAction( + UI.PixmapCache.getIcon("translate.png"), + self.tr("Google Translate"), self.__openLinkInNewTab)\ + .setData(googleTranslatorUrl) + wiktionaryUrl = QUrl( + "http://{0}.wiktionary.org/wiki/Special:Search?search={1}" + .format(langCode, self.selectedText())) + menu.addAction( + UI.PixmapCache.getIcon("wikipedia.png"), + self.tr("Dictionary"), self.__openLinkInNewTab)\ + .setData(wiktionaryUrl) + menu.addSeparator() + + guessedUrl = QUrl.fromUserInput(self.selectedText().strip()) + if self.__isUrlValid(guessedUrl): + menu.addAction( + self.tr("Go to web address"), + self.__openLinkInNewTab).setData(guessedUrl) + + def __createPageContextMenu(self, menu): + """ + Private method to populate the basic context menu. + + @param menu reference to the menu to be populated + @type QMenu + """ + + menu.addAction(self.__mw.newTabAct) + menu.addAction(self.__mw.newAct) + menu.addSeparator() + # TODO: Qt 5.7: Save +## menu.addAction(self.__mw.saveAsAct) +## menu.addSeparator() + + if self.url().toString() == "eric:speeddial": + # special menu for the spedd dial page + menu.addAction(self.__mw.backAct) + menu.addAction(self.__mw.forwardAct) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("plus.png"), + self.tr("Add New Page"), self.__addSpeedDial) + menu.addAction( + UI.PixmapCache.getIcon("preferences-general.png"), + self.tr("Configure Speed Dial"), self.__configureSpeedDial) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("reload.png"), + self.tr("Reload All Dials"), self.__reloadAllSpeedDials) + return + + menu.addAction( + UI.PixmapCache.getIcon("bookmark22.png"), + self.tr("Bookmark this Page"), self.addBookmark) + menu.addAction( + UI.PixmapCache.getIcon("editCopy.png"), + self.tr("Copy Page Link"), self.__copyLink).setData(self.url()) + menu.addAction( + UI.PixmapCache.getIcon("mailSend.png"), + self.tr("Send Page Link"), self.__sendLink).setData(self.url()) + menu.addSeparator() + + from .UserAgent.UserAgentMenu import UserAgentMenu + self.__userAgentMenu = UserAgentMenu(self.tr("User Agent"), + url=self.url()) + menu.addMenu(self.__userAgentMenu) + menu.addSeparator() + menu.addAction(self.__mw.backAct) + menu.addAction(self.__mw.forwardAct) + menu.addAction(self.__mw.homeAct) + menu.addAction(self.__mw.reloadAct) + menu.addAction(self.__mw.stopAct) + menu.addSeparator() + menu.addAction(self.__mw.zoomInAct) + menu.addAction(self.__mw.zoomResetAct) + menu.addAction(self.__mw.zoomOutAct) + menu.addSeparator() + menu.addAction(self.__mw.selectAllAct) + menu.addSeparator() + menu.addAction(self.__mw.findAct) + menu.addSeparator() + menu.addAction(self.__mw.pageSourceAct) + menu.addSeparator() + menu.addAction(self.__mw.siteInfoAct) + if self.url().scheme() in ["http", "https"]: + menu.addSeparator() + + w3url = QUrl.fromEncoded( + b"http://validator.w3.org/check?uri=" + + QUrl.toPercentEncoding(bytes(self.url().toEncoded()).decode())) + menu.addAction( + UI.PixmapCache.getIcon("w3.png"), + self.tr("Validate Page"), self.__openLinkInNewTab)\ + .setData(w3url) + + from .WebBrowserLanguagesDialog import WebBrowserLanguagesDialog + languages = Preferences.toList( + Preferences.Prefs.settings.value( + "WebBrowser/AcceptLanguages", + WebBrowserLanguagesDialog.defaultAcceptLanguages())) + if languages: + language = languages[0] + langCode = language.split("[")[1][:2] + googleTranslatorUrl = QUrl.fromEncoded( + b"http://translate.google.com/translate?sl=auto&tl=" + + langCode.encode() + + b"&u=" + + QUrl.toPercentEncoding( + bytes(self.url().toEncoded()).decode())) + menu.addAction( + UI.PixmapCache.getIcon("translate.png"), + self.tr("Google Translate"), self.__openLinkInNewTab)\ + .setData(googleTranslatorUrl) + + def __checkForForm(self, act, pos): + """ + Private method to check the given position for an open search form. + + @param act reference to the action to be populated upon success + @type QAction + @param pos position to be tested + @type QPoint + """ + self.__clickedPos = pos + + from .Tools import Scripts + script = Scripts.getFormData(pos) + self.page().runJavaScript( + script, lambda res: self.__checkForFormCallback(res, act)) + + def __checkForFormCallback(self, res, act): + """ + Private method handling the __checkForForm result. + + @param res result dictionary generated by JavaScript + @type dict + @param act reference to the action to be populated upon success + @type QAction + """ + if act is None or not bool(res): + return + + url = QUrl(res["action"]) + method = res["method"] + + if not url.isEmpty() and method in ["get", "post"]: + act.setVisible(True) + act.setText(self.tr("Add to web search toolbar")) + act.triggered.connect(self.__addSearchEngine) + + def __isUrlValid(self, url): + """ + Private method to check a URL for validity. + + @param url URL to be checked (QUrl) + @return flag indicating a valid URL (boolean) + """ + return url.isValid() and \ + bool(url.host()) and \ + bool(url.scheme()) and \ + "." in url.host() + + def __openLinkInNewTab(self): + """ + Private method called by the context menu to open a link in a new + tab. + """ + act = self.sender() + url = act.data() + if url.isEmpty(): + return + + self.setSource(url, newTab=True) + + def __openLinkInNewWindow(self): + """ + Private slot called by the context menu to open a link in a new + window. + """ + act = self.sender() + url = act.data() + if url.isEmpty(): + return + + self.__mw.newWindow(url) + + def __openLinkInNewPrivateWindow(self): + """ + Private slot called by the context menu to open a link in a new + private window. + """ + act = self.sender() + url = act.data() + if url.isEmpty(): + return + + self.__mw.newPrivateWindow(url) + + def __bookmarkLink(self): + """ + Private slot to bookmark a link via the context menu. + """ + act = self.sender() + url = act.data() + if url.isEmpty(): + return + + from .Bookmarks.AddBookmarkDialog import AddBookmarkDialog + dlg = AddBookmarkDialog() + dlg.setUrl(bytes(url.toEncoded()).decode()) + dlg.exec_() + + def __sendLink(self): + """ + Private slot to send a link via email. + """ + act = self.sender() + data = act.data() + if isinstance(data, QUrl) and data.isEmpty(): + return + + if isinstance(data, QUrl): + data = data.toString() + QDesktopServices.openUrl(QUrl("mailto:?body=" + data)) + + def __copyLink(self): + """ + Private slot to copy a link to the clipboard. + """ + act = self.sender() + data = act.data() + if isinstance(data, QUrl) and data.isEmpty(): + return + + if isinstance(data, QUrl): + data = data.toString() + QApplication.clipboard().setText(data) + + def __downloadLink(self): + """ + Private slot to download a link and save it to disk. + """ + self.triggerPageAction(QWebEnginePage.DownloadLinkToDisk) + + def __downloadImage(self): + """ + Private slot to download an image and save it to disk. + """ + self.triggerPageAction(QWebEnginePage.DownloadImageToDisk) + + def __copyImage(self): + """ + Private slot to copy an image to the clipboard. + """ + self.triggerPageAction(QWebEnginePage.CopyImageToClipboard) + + def __blockImage(self): + """ + Private slot to add a block rule for an image URL. + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + act = self.sender() + url = act.data() + dlg = WebBrowserWindow.adBlockManager().showDialog() + dlg.addCustomRule(url) + + def __downloadMedia(self): + """ + Private slot to download a media and save it to disk. + """ + self.triggerPageAction(QWebEnginePage.DownloadMediaToDisk) + + def __pauseMedia(self): + """ + Private slot to pause or play the selected media. + """ + self.triggerPageAction(QWebEnginePage.ToggleMediaPlayPause) + + def __muteMedia(self): + """ + Private slot to (un)mute the selected media. + """ + self.triggerPageAction(QWebEnginePage.ToggleMediaMute) + + def __virusTotal(self): + """ + Private slot to scan the selected URL with VirusTotal. + """ + act = self.sender() + url = act.data() + self.__mw.requestVirusTotalScan(url) + + def __searchDefaultRequested(self): + """ + Private slot to search for some text with the current search engine. + """ + searchText = self.selectedText() + + if not searchText: + return + + engine = self.__mw.openSearchManager().currentEngine() + if engine: + self.search.emit(engine.searchUrl(searchText)) + + def __searchRequested(self, act): + """ + Private slot to search for some text with a selected search engine. + + @param act reference to the action that triggered this slot (QAction) + """ + searchText = self.selectedText() + + if not searchText: + return + + engineName = act.data() + if engineName: + engine = self.__mw.openSearchManager().engine(engineName) + else: + engine = self.__mw.openSearchManager().currentEngine() + if engine: + self.search.emit(engine.searchUrl(searchText)) + + def __addSearchEngine(self): + """ + Private slot to add a new search engine. + """ + from .Tools import Scripts + script = Scripts.getFormData(self.__clickedPos) + self.page().runJavaScript( + script, + lambda res: self.__mw.openSearchManager().addEngineFromForm( + res, self)) + + def __webInspector(self): + """ + Private slot to show the web inspector window. + """ + if self.__inspector is None: + from .WebInspector import WebInspector + self.__inspector = WebInspector() + self.__inspector.setView(self, True) + self.__inspector.show() + else: + self.closeWebInspector() + + def closeWebInspector(self): + """ + Public slot to close the web inspector. + """ + if self.__inspector is not None: + if self.__inspector.isVisible(): + self.__inspector.hide() + WebInspector.unregisterView(self.__inspector) + self.__inspector.deleteLater() + self.__inspector = None + + def addBookmark(self): + """ + Public slot to bookmark the current page. + """ + from .Tools import Scripts + script = Scripts.getAllMetaAttributes() + self.page().runJavaScript( + script, self.__addBookmarkCallback) + + def __addBookmarkCallback(self, res): + """ + Private callback method of __addBookmark(). + + @param url URL for the bookmark + @type str + @param title title for the bookmark + @type str + @param res result of the JavaScript + @type list + """ + description = "" + for meta in res: + if meta["name"] == "description": + description = meta["content"] + + from .Bookmarks.AddBookmarkDialog import AddBookmarkDialog + dlg = AddBookmarkDialog() + dlg.setUrl(bytes(self.url().toEncoded()).decode()) + dlg.setTitle(self.title()) + dlg.setDescription(description) + dlg.exec_() + + def dragEnterEvent(self, evt): + """ + Protected method called by a drag enter event. + + @param evt reference to the drag enter event (QDragEnterEvent) + """ + evt.acceptProposedAction() + + def dragMoveEvent(self, evt): + """ + Protected method called by a drag move event. + + @param evt reference to the drag move event (QDragMoveEvent) + """ + evt.ignore() + if evt.source() != self: + if len(evt.mimeData().urls()) > 0: + evt.acceptProposedAction() + else: + url = QUrl(evt.mimeData().text()) + if url.isValid(): + evt.acceptProposedAction() + + if not evt.isAccepted(): + super(WebBrowserView, self).dragMoveEvent(evt) + + def dropEvent(self, evt): + """ + Protected method called by a drop event. + + @param evt reference to the drop event (QDropEvent) + """ + super(WebBrowserView, self).dropEvent(evt) + if not evt.isAccepted() and \ + evt.source() != self and \ + evt.possibleActions() & Qt.CopyAction: + url = QUrl() + if len(evt.mimeData().urls()) > 0: + url = evt.mimeData().urls()[0] + if not url.isValid(): + url = QUrl(evt.mimeData().text()) + if url.isValid(): + self.setSource(url) + evt.acceptProposedAction() + + def _mousePressEvent(self, evt): + """ + Protected method called by a mouse press event. + + @param evt reference to the mouse event (QMouseEvent) + """ + self.__mw.setEventMouseButtons(evt.buttons()) + self.__mw.setEventKeyboardModifiers(evt.modifiers()) + + if evt.button() == Qt.XButton1: + self.pageAction(QWebEnginePage.Back).trigger() + elif evt.button() == Qt.XButton2: + self.pageAction(QWebEnginePage.Forward).trigger() + else: + super(WebBrowserView, self).mousePressEvent(evt) + + def _mouseReleaseEvent(self, evt): + """ + Protected method called by a mouse release event. + + @param evt reference to the mouse event (QMouseEvent) + """ + accepted = evt.isAccepted() + self.__page.event(evt) + if not evt.isAccepted() and \ + self.__mw.eventMouseButtons() & Qt.MidButton: + url = QUrl(QApplication.clipboard().text(QClipboard.Selection)) + if not url.isEmpty() and \ + url.isValid() and \ + url.scheme() != "": + self.__mw.setEventMouseButtons(Qt.NoButton) + self.__mw.setEventKeyboardModifiers(Qt.NoModifier) + self.setSource(url) + evt.setAccepted(accepted) + + def _wheelEvent(self, evt): + """ + Protected method to handle wheel events. + + @param evt reference to the wheel event (QWheelEvent) + """ + delta = evt.angleDelta().y() + if evt.modifiers() & Qt.ControlModifier: + if delta < 0: + self.zoomOut() + else: + self.zoomIn() + evt.accept() + return + + if evt.modifiers() & Qt.ShiftModifier: + if delta < 0: + self.backward() + else: + self.forward() + evt.accept() + return + + super(WebBrowserView, self).wheelEvent(evt) + + def _keyPressEvent(self, evt): + """ + Protected method called by a key press. + + @param evt reference to the key event (QKeyEvent) + """ + if self.__mw.personalInformationManager().viewKeyPressEvent(self, evt): + evt.accept() + return + + if evt.key() == Qt.Key_Escape: + if self.isFullScreen(): + self.triggerPageAction(QWebEnginePage.ExitFullScreen) + evt.accept() + return + + super(WebBrowserView, self).keyPressEvent(evt) + + def _keyReleaseEvent(self, evt): + """ + Protected method called by a key release. + + @param evt reference to the key event (QKeyEvent) + """ + super(WebBrowserView, self).keyReleaseEvent(evt) + + def focusOutEvent(self, evt): + """ + Protected method called by a focus out event. + + @param evt reference to the focus event (QFocusEvent) + """ + super(WebBrowserView, self).focusOutEvent(evt) + + # TODO: Gestures: Obsoleted by eventFilter() (?) + def event(self, evt): + """ + Public method handling events. + + @param evt reference to the event (QEvent) + @return flag indicating, if the event was handled (boolean) + """ + if evt.type() == QEvent.Gesture: + self._gestureEvent(evt) + return True + + return super(WebBrowserView, self).event(evt) + + def _gestureEvent(self, evt): + """ + Protected method handling gesture events. + + @param evt reference to the gesture event (QGestureEvent + """ + pinch = evt.gesture(Qt.PinchGesture) + if pinch: + if pinch.state() == Qt.GestureStarted: + pinch.setScaleFactor(self.__currentZoom / 100.0) + else: + scaleFactor = pinch.scaleFactor() + self.setZoomValue(int(scaleFactor * 100)) + evt.accept() + + def eventFilter(self, obj, evt): + """ + Public method to process event for other objects. + + @param obj reference to object to process events for + @type QObject + @param evt reference to event to be processed + @type QEvent + @return flag indicating that the event should be filtered out + @rtype bool + """ + # find the render widget receiving events for the web page + if obj is self and evt.type() == QEvent.ChildAdded: + child = evt.child() + if child and child.inherits( + "QtWebEngineCore::RenderWidgetHostViewQtDelegateWidget"): + self.__rwhvqt = child + self.grabGesture(Qt.PinchGesture) + self.__rwhvqt.grabGesture(Qt.PinchGesture) + self.__rwhvqt.installEventFilter(self) + + # forward events to WebBrowserView + if obj is self.__rwhvqt: + wasAccepted = evt.isAccepted() + evt.setAccepted(False) + if evt.type() == QEvent.KeyPress: + self._keyPressEvent(evt) + elif evt.type() == QEvent.KeyRelease: + self._keyReleaseEvent(evt) + elif evt.type() == QEvent.MouseButtonPress: + self._mousePressEvent(evt) + elif evt.type() == QEvent.MouseButtonRelease: + self._mouseReleaseEvent(evt) + elif evt.type() == QEvent.Wheel: + self._wheelEvent(evt) + elif evt.type() == QEvent.Gesture: + self._gestureEvent(evt) + ret = evt.isAccepted() + evt.setAccepted(wasAccepted) + return ret + + # block already handled events + if obj is self: + if evt.type() in [QEvent.KeyPress, QEvent.KeyRelease, + QEvent.MouseButtonPress, + QEvent.MouseButtonRelease, + QEvent.Wheel, QEvent.Gesture]: + return True + + return super(WebBrowserView, self).eventFilter(obj, evt) + + def clearHistory(self): + """ + Public slot to clear the history. + """ + self.history().clear() + self.__urlChanged(self.history().currentItem().url()) + + ########################################################################### + ## Signal converters below + ########################################################################### + + def __urlChanged(self, url): + """ + Private slot to handle the urlChanged signal. + + @param url the new url (QUrl) + """ + self.sourceChanged.emit(url) + + self.forwardAvailable.emit(self.isForwardAvailable()) + self.backwardAvailable.emit(self.isBackwardAvailable()) + + def __iconUrlChanged(self, url): + """ + Private slot to handle the iconUrlChanged signal. + + @param url URL to get web site icon from + @type QUrl + """ + self.__siteIcon = QIcon() + if self.__siteIconLoader is not None: + self.__siteIconLoader.deleteLater() + self.__siteIconLoader = WebIconLoader(url, self) + self.__siteIconLoader.iconLoaded.connect(self.__iconLoaded) + + def __iconLoaded(self, icon): + """ + Private slot handling the loaded web site icon. + + @param icon web site icon + @type QIcon + """ + self.__siteIcon = icon + + from .Tools import WebIconProvider + WebIconProvider.instance().saveIcon(self) + + self.iconChanged.emit() + + def icon(self): + """ + Public method to get the web site icon. + + @return web site icon + @rtype QIcon + """ + if not self.__siteIcon.isNull(): + return QIcon(self.__siteIcon) + + from .Tools import WebIconProvider + return WebIconProvider.instance().iconForUrl(self.url()) + + def __linkHovered(self, link): + """ + Private slot to handle the linkHovered signal. + + @param link the URL of the link (string) + """ + self.highlighted.emit(link) + + ########################################################################### + ## Signal handlers below + ########################################################################### + + def __renderProcessTerminated(self, status, exitCode): + """ + Private slot handling a crash of the web page render process. + + @param status termination status + @type QWebEnginePage.RenderProcessTerminationStatus + @param exitCode exit code of the process + @type int + """ + if status == QWebEnginePage.NormalTerminationStatus: + return + + QTimer.singleShot(0, self.__showTabCrashPage) + + def __showTabCrashPage(self): + """ + Private slot to show the tab crash page. + """ + html = readAllFileContents(":/html/tabCrashPage.html") + html = html.replace("@IMAGE@", pixmapToDataUrl( + qApp.style().standardIcon(QStyle.SP_MessageBoxWarning).pixmap( + 48, 48)).toString()) + html = html.replace("@FAVICON@", pixmapToDataUrl( + qApp.style() .standardIcon(QStyle.SP_MessageBoxWarning).pixmap( + 16, 16)).toString()) + html = html.replace("@TITLE@", self.tr("Failed loading page")) + html = html.replace("@H1@", self.tr("Failed loading page")) + html = html.replace( + "@LI-1@", + self.tr("Something went wrong while loading this page.")) + html = html.replace( + "@LI-2@", + self.tr( + "Try reloading the page or closing some tabs to make more" + " memory available.")) + html = html.replace( + "@BUTTON@", self.tr("Reload Page")) + self.page().setHtml(html, self.url()) + + def __loadStarted(self): + """ + Private method to handle the loadStarted signal. + """ + self.__isLoading = True + self.__progress = 0 + + def __loadProgress(self, progress): + """ + Private method to handle the loadProgress signal. + + @param progress progress value (integer) + """ + self.__progress = progress + + def __loadFinished(self, ok): + """ + Private method to handle the loadFinished signal. + + @param ok flag indicating the result (boolean) + """ + self.__isLoading = False + self.__progress = 0 + + QTimer.singleShot(200, self.__renderPreview) + + from .ZoomManager import ZoomManager + zoomValue = ZoomManager.instance().zoomValue(self.url()) + self.setZoomValue(zoomValue) + + if ok: + self.__mw.historyManager().addHistoryEntry(self) + self.__mw.adBlockManager().page().hideBlockedPageEntries( + self.page()) + self.__mw.passwordManager().completePage(self.page()) + + def isLoading(self): + """ + Public method to get the loading state. + + @return flag indicating the loading state (boolean) + """ + return self.__isLoading + + def progress(self): + """ + Public method to get the load progress. + + @return load progress (integer) + """ + return self.__progress + + def __renderPreview(self): + """ + Private slot to render a preview pixmap after the page was loaded. + """ + from .WebBrowserSnap import renderTabPreview + w = 600 # some default width, the preview gets scaled when shown + h = int(w * self.height() / self.width()) + self.__preview = renderTabPreview(self, w, h) + + def getPreview(self): + """ + Public method to get the preview pixmap. + + @return preview pixmap + @rtype QPixmap + """ + return self.__preview + + # TODO: Qt 5.7: Save +## def saveAs(self): +## """ +## Public method to save the current page to a file. +## """ +## url = self.url() +## if url.isEmpty(): +## return +## +## self.__mw.downloadManager().download(url, True, mainWindow=self.__mw) + + ########################################################################### + ## Miscellaneous methods below + ########################################################################### + + def createWindow(self, windowType): + """ + Public method called, when a new window should be created. + + @param windowType type of the requested window + (QWebEnginePage.WebWindowType) + @return reference to the created browser window (WebBrowserView) + """ + self.__mw.newTab(addNextTo=self) + return self.__mw.currentBrowser() + + def preferencesChanged(self): + """ + Public method to indicate a change of the settings. + """ + self.reload() + + ########################################################################### + ## RSS related methods below + ########################################################################### + + def checkRSS(self): + """ + Public method to check, if the loaded page contains feed links. + + @return flag indicating the existence of feed links (boolean) + """ + self.__rss = [] + + script = Scripts.getFeedLinks() + feeds = self.page().execJavaScript(script) + + if feeds is not None: + for feed in feeds: + if feed["url"] and feed["title"]: + self.__rss.append((feed["title"], feed["url"])) + + return len(self.__rss) > 0 + + def getRSS(self): + """ + Public method to get the extracted RSS feeds. + + @return list of RSS feeds (list of tuples of two strings) + """ + return self.__rss + + def hasRSS(self): + """ + Public method to check, if the loaded page has RSS links. + + @return flag indicating the presence of RSS links (boolean) + """ + return len(self.__rss) > 0 + + ########################################################################### + ## Full Screen handling below + ########################################################################### + + def isFullScreen(self): + """ + Public method to check, if full screen mode is active. + + @return flag indicating full screen mode + @rtype bool + """ + return self.__mw.isFullScreen() + + def requestFullScreen(self, enable): + """ + Public method to request full screen mode. + + @param enable flag indicating full screen mode on or off + @type bool + """ + if enable: + self.__mw.enterHtmlFullScreen() + else: + self.__mw.showNormal() + + ########################################################################### + ## Speed Dial slots below + ########################################################################### + + def __addSpeedDial(self): + """ + Private slot to add a new speed dial. + """ + self.__page.runJavaScript("addSpeedDial();") + + def __configureSpeedDial(self): + """ + Private slot to configure the speed dial. + """ + self.page().runJavaScript("configureSpeedDial();") + + def __reloadAllSpeedDials(self): + """ + Private slot to reload all speed dials. + """ + self.page().runJavaScript("reloadAll();")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserWebSearchWidget.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a web search widget for the web browser. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, QUrl, QModelIndex, QTimer, Qt +from PyQt5.QtGui import QStandardItem, QStandardItemModel, QFont, QIcon, \ + QPixmap +from PyQt5.QtWidgets import QMenu, QCompleter +from PyQt5.QtWebEngineWidgets import QWebEnginePage + +import UI.PixmapCache + +import Preferences + +from E5Gui.E5LineEdit import E5ClearableLineEdit + + +class WebBrowserWebSearchWidget(E5ClearableLineEdit): + """ + Class implementing a web search widget for the web browser. + + @signal search(QUrl) emitted when the search should be done + """ + search = pyqtSignal(QUrl) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(WebBrowserWebSearchWidget, self).__init__(parent) + + from E5Gui.E5LineEdit import E5LineEdit + from E5Gui.E5LineEditButton import E5LineEditButton + from .OpenSearch.OpenSearchManager import OpenSearchManager + + self.__mw = parent + + self.__openSearchManager = OpenSearchManager(self) + self.__openSearchManager.currentEngineChanged.connect( + self.__currentEngineChanged) + self.__currentEngine = "" + + self.__enginesMenu = QMenu(self) + + self.__engineButton = E5LineEditButton(self) + self.__engineButton.setMenu(self.__enginesMenu) + self.addWidget(self.__engineButton, E5LineEdit.LeftSide) + + self.__searchButton = E5LineEditButton(self) + self.__searchButton.setIcon(UI.PixmapCache.getIcon("webSearch.png")) + self.addWidget(self.__searchButton, E5LineEdit.LeftSide) + + self.__model = QStandardItemModel(self) + self.__completer = QCompleter() + self.__completer.setModel(self.__model) + self.__completer.setCompletionMode( + QCompleter.UnfilteredPopupCompletion) + self.__completer.setWidget(self) + + self.__searchButton.clicked.connect(self.__searchButtonClicked) + self.textEdited.connect(self.__textEdited) + self.returnPressed.connect(self.__searchNow) + self.__completer.activated[QModelIndex].connect( + self.__completerActivated) + self.__completer.highlighted[QModelIndex].connect( + self.__completerHighlighted) + self.__enginesMenu.aboutToShow.connect(self.__showEnginesMenu) + + self.__suggestionsItem = None + self.__suggestions = [] + self.__suggestTimer = None + self.__suggestionsEnabled = Preferences.getWebBrowser( + "WebSearchSuggestions") + + self.__recentSearchesItem = None + self.__recentSearches = [] + self.__maxSavedSearches = 10 + + self.__engine = None + self.__loadSearches() + self.__setupCompleterMenu() + self.__currentEngineChanged() + + def __searchNow(self): + """ + Private slot to perform the web search. + """ + searchText = self.text() + if not searchText: + return + + import WebBrowser.WebBrowserWindow + if WebBrowser.WebBrowserWindow.WebBrowserWindow.isPrivate(): + return + + if searchText in self.__recentSearches: + self.__recentSearches.remove(searchText) + self.__recentSearches.insert(0, searchText) + if len(self.__recentSearches) > self.__maxSavedSearches: + self.__recentSearches = \ + self.__recentSearches[:self.__maxSavedSearches] + self.__setupCompleterMenu() + + self.__mw.currentBrowser().setFocus() + self.__mw.currentBrowser().load( + self.__openSearchManager.currentEngine().searchUrl(searchText)) + + def __setupCompleterMenu(self): + """ + Private method to create the completer menu. + """ + if not self.__suggestions or \ + (self.__model.rowCount() > 0 and + self.__model.item(0) != self.__suggestionsItem): + self.__model.clear() + self.__suggestionsItem = None + else: + self.__model.removeRows(1, self.__model.rowCount() - 1) + + boldFont = QFont() + boldFont.setBold(True) + + if self.__suggestions: + if self.__model.rowCount() == 0: + if not self.__suggestionsItem: + self.__suggestionsItem = QStandardItem( + self.tr("Suggestions")) + self.__suggestionsItem.setFont(boldFont) + self.__model.appendRow(self.__suggestionsItem) + + for suggestion in self.__suggestions: + self.__model.appendRow(QStandardItem(suggestion)) + + if not self.__recentSearches: + self.__recentSearchesItem = QStandardItem( + self.tr("No Recent Searches")) + self.__recentSearchesItem.setFont(boldFont) + self.__model.appendRow(self.__recentSearchesItem) + else: + self.__recentSearchesItem = QStandardItem( + self.tr("Recent Searches")) + self.__recentSearchesItem.setFont(boldFont) + self.__model.appendRow(self.__recentSearchesItem) + for recentSearch in self.__recentSearches: + self.__model.appendRow(QStandardItem(recentSearch)) + + view = self.__completer.popup() + view.setFixedHeight(view.sizeHintForRow(0) * self.__model.rowCount() + + view.frameWidth() * 2) + + self.__searchButton.setEnabled( + bool(self.__recentSearches or self.__suggestions)) + + def __completerActivated(self, index): + """ + Private slot handling the selection of an entry from the completer. + + @param index index of the item (QModelIndex) + """ + if self.__suggestionsItem and \ + self.__suggestionsItem.index().row() == index.row(): + return + + if self.__recentSearchesItem and \ + self.__recentSearchesItem.index().row() == index.row(): + return + + self.__searchNow() + + def __completerHighlighted(self, index): + """ + Private slot handling the highlighting of an entry of the completer. + + @param index index of the item (QModelIndex) + @return flah indicating a successful highlighting (boolean) + """ + if self.__suggestionsItem and \ + self.__suggestionsItem.index().row() == index.row(): + return False + + if self.__recentSearchesItem and \ + self.__recentSearchesItem.index().row() == index.row(): + return False + + self.setText(index.data()) + return True + + def __textEdited(self, txt): + """ + Private slot to handle changes of the search text. + + @param txt search text (string) + """ + if self.__suggestionsEnabled: + if self.__suggestTimer is None: + self.__suggestTimer = QTimer(self) + self.__suggestTimer.setSingleShot(True) + self.__suggestTimer.setInterval(200) + self.__suggestTimer.timeout.connect(self.__getSuggestions) + self.__suggestTimer.start() + else: + self.__completer.setCompletionPrefix(txt) + self.__completer.complete() + + def __getSuggestions(self): + """ + Private slot to get search suggestions from the configured search + engine. + """ + searchText = self.text() + if searchText: + self.__openSearchManager.currentEngine()\ + .requestSuggestions(searchText) + + def __newSuggestions(self, suggestions): + """ + Private slot to receive a new list of suggestions. + + @param suggestions list of suggestions (list of strings) + """ + self.__suggestions = suggestions + self.__setupCompleterMenu() + self.__completer.complete() + + def __showEnginesMenu(self): + """ + Private slot to handle the display of the engines menu. + """ + self.__enginesMenu.clear() + + from .OpenSearch.OpenSearchEngineAction import OpenSearchEngineAction + engineNames = self.__openSearchManager.allEnginesNames() + for engineName in engineNames: + engine = self.__openSearchManager.engine(engineName) + action = OpenSearchEngineAction(engine, self.__enginesMenu) + action.setData(engineName) + action.triggered.connect(self.__changeCurrentEngine) + self.__enginesMenu.addAction(action) + + if self.__openSearchManager.currentEngineName() == engineName: + action.setCheckable(True) + action.setChecked(True) + + cb = self.__mw.currentBrowser() + from .Tools import Scripts + script = Scripts.getOpenSearchLinks() + cb.page().runJavaScript(script, self.__showEnginesMenuCallback) + + def __showEnginesMenuCallback(self, res): + """ + Private method handling the open search links callback. + + @param res result of the JavaScript + @type list of dict + """ + cb = self.__mw.currentBrowser() + if res: + self.__enginesMenu.addSeparator() + for entry in res: + url = cb.url().resolved(QUrl(entry["url"])) + title = entry["title"] + if url.isEmpty(): + continue + if not title: + title = cb.title() + + action = self.__enginesMenu.addAction( + self.tr("Add '{0}'").format(title), + self.__addEngineFromUrl) + action.setData(url) + action.setIcon(cb.icon()) + + self.__enginesMenu.addSeparator() + self.__enginesMenu.addAction(self.__mw.searchEnginesAction()) + + if self.__recentSearches: + self.__enginesMenu.addAction(self.tr("Clear Recent Searches"), + self.clear) + + def __changeCurrentEngine(self): + """ + Private slot to handle the selection of a search engine. + """ + action = self.sender() + if action is not None: + name = action.data() + self.__openSearchManager.setCurrentEngineName(name) + + def __addEngineFromUrl(self): + """ + Private slot to add a search engine given its URL. + """ + action = self.sender() + if action is not None: + url = action.data() + if not isinstance(url, QUrl): + return + + self.__openSearchManager.addEngine(url) + + def __searchButtonClicked(self): + """ + Private slot to show the search menu via the search button. + """ + self.__setupCompleterMenu() + self.__completer.complete() + + def clear(self): + """ + Public method to clear all private data. + """ + self.__recentSearches = [] + self.__setupCompleterMenu() + super(WebBrowserWebSearchWidget, self).clear() + self.clearFocus() + + def preferencesChanged(self): + """ + Public method to handle the change of preferences. + """ + self.__suggestionsEnabled = Preferences.getWebBrowser( + "WebSearchSuggestions") + if not self.__suggestionsEnabled: + self.__suggestions = [] + self.__setupCompleterMenu() + + def saveSearches(self): + """ + Public method to save the recently performed web searches. + """ + Preferences.Prefs.settings.setValue( + 'WebBrowser/WebSearches', self.__recentSearches) + + def __loadSearches(self): + """ + Private method to load the recently performed web searches. + """ + searches = Preferences.Prefs.settings.value('WebBrowser/WebSearches') + if searches is not None: + self.__recentSearches = searches + + def openSearchManager(self): + """ + Public method to get a reference to the opensearch manager object. + + @return reference to the opensearch manager object (OpenSearchManager) + """ + return self.__openSearchManager + + def __currentEngineChanged(self): + """ + Private slot to track a change of the current search engine. + """ + if self.__openSearchManager.engineExists(self.__currentEngine): + oldEngine = self.__openSearchManager.engine(self.__currentEngine) + oldEngine.imageChanged.disconnect(self.__engineImageChanged) + if self.__suggestionsEnabled: + oldEngine.suggestions.disconnect(self.__newSuggestions) + + newEngine = self.__openSearchManager.currentEngine() + if newEngine.networkAccessManager() is None: + newEngine.setNetworkAccessManager(self.__mw.networkManager()) + newEngine.imageChanged.connect(self.__engineImageChanged) + if self.__suggestionsEnabled: + newEngine.suggestions.connect(self.__newSuggestions) + + self.setInactiveText(self.__openSearchManager.currentEngineName()) + self.__currentEngine = self.__openSearchManager.currentEngineName() + self.__engineButton.setIcon(QIcon(QPixmap.fromImage( + self.__openSearchManager.currentEngine().image()))) + self.__suggestions = [] + self.__setupCompleterMenu() + + def __engineImageChanged(self): + """ + Private slot to handle a change of the current search engine icon. + """ + self.__engineButton.setIcon(QIcon(QPixmap.fromImage( + self.__openSearchManager.currentEngine().image()))) + + def mousePressEvent(self, evt): + """ + Protected method called by a mouse press event. + + @param evt reference to the mouse event (QMouseEvent) + """ + if evt.button() == Qt.XButton1: + self.__mw.currentBrowser().triggerPageAction( + QWebEnginePage.Back) + elif evt.button() == Qt.XButton2: + self.__mw.currentBrowser().triggerPageAction( + QWebEnginePage.Forward) + else: + super(WebBrowserWebSearchWidget, self).mousePressEvent(evt)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebBrowserWindow.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,4008 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2002 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the web browser main window. +""" + +from __future__ import unicode_literals +try: + str = unicode # __IGNORE_EXCEPTION__ +except NameError: + pass + +import os +import shutil +import sys + +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QByteArray, QSize, QTimer, \ + QUrl, QTextCodec, QProcess, QEvent +from PyQt5.QtGui import QDesktopServices, QKeySequence, QFont, QFontMetrics +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QSizePolicy, QDockWidget, \ + QComboBox, QLabel, QSplitter, QMenu, QToolButton, QLineEdit, \ + QApplication, QWhatsThis, QDialog, QHBoxLayout, QProgressBar, QAction, \ + QInputDialog +from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEnginePage, \ + QWebEngineProfile, QWebEngineScript +try: + from PyQt5.QtHelp import QHelpEngine, QHelpEngineCore, QHelpSearchQuery + QTHELP_AVAILABLE = True +except ImportError: + QTHELP_AVAILABLE = False + +from E5Gui.E5Action import E5Action +from E5Gui import E5MessageBox, E5FileDialog, E5ErrorMessage +from E5Gui.E5MainWindow import E5MainWindow +from E5Gui.E5Application import e5App +from E5Gui.E5ZoomWidget import E5ZoomWidget + +from E5Network.E5NetworkIcon import E5NetworkIcon + +import Preferences +from Preferences import Shortcuts + +import Utilities +import Globals + +import UI.PixmapCache +import UI.Config +from UI.Info import Version + +from .data import icons_rc # __IGNORE_WARNING__ +from .data import html_rc # __IGNORE_WARNING__ +from .data import javascript_rc # __IGNORE_WARNING__ +from .data import qml_rc # __IGNORE_WARNING__ + + +from .Tools import Scripts, WebBrowserTools, WebIconProvider + +from .ZoomManager import ZoomManager + +from eric6config import getConfig + + +class WebBrowserWindow(E5MainWindow): + """ + Class implementing the web browser main window. + + @signal webBrowserClosed() emitted after the window was requested to close + @signal zoomTextOnlyChanged(bool) emitted after the zoom text only setting + was changed + """ + webBrowserClosed = pyqtSignal() + + BrowserWindows = [] + + _fromEric = False + UseQtHelp = QTHELP_AVAILABLE + _isPrivate = False + + _webProfile = None + _networkManager = None + _cookieJar = None + _helpEngine = None + _bookmarksManager = None + _historyManager = None + _passwordManager = None + _adblockManager = None + _downloadManager = None + _feedsManager = None + _userAgentsManager = None + _syncManager = None + _speedDial = None + _personalInformationManager = None + _greaseMonkeyManager = None + _notification = None + _featurePermissionManager = None + _flashCookieManager = None + + def __init__(self, home, path, parent, name, fromEric=False, + initShortcutsOnly=False, searchWord=None, + private=False, qthelp=False, settingsDir=""): + """ + Constructor + + @param home the URL to be shown (string) + @param path the path of the working dir (usually '.') (string) + @param parent parent widget of this window (QWidget) + @param name name of this window (string) + @param fromEric flag indicating whether it was called from within + eric6 (boolean) + @keyparam initShortcutsOnly flag indicating to just initialize the + keyboard shortcuts (boolean) + @keyparam searchWord word to search for (string) + @keyparam private flag indicating a private browsing window (bool) + @keyparam qthelp flag indicating to enable the QtHelp support (bool) + @keyparam settingsDir directory to be used for the settings files (str) + """ + super(WebBrowserWindow, self).__init__(parent) + self.setObjectName(name) + self.setWindowTitle(self.tr("eric6 Web Browser")) + + self.__settingsDir = settingsDir + self.__fromEric = fromEric + WebBrowserWindow._fromEric = fromEric + self.__initShortcutsOnly = initShortcutsOnly + self.setWindowIcon(UI.PixmapCache.getIcon("ericWeb.png")) + + self.__mHistory = [] + self.__lastConfigurationPageName = "" + + WebBrowserWindow._isPrivate = private + + self.__eventMouseButtons = Qt.NoButton + self.__eventKeyboardModifiers = Qt.NoModifier + + if self.__initShortcutsOnly: + WebBrowserWindow.setUseQtHelp( + self.__fromEric or qthelp or bool(searchWord)) + self.__initActions() + else: + if Preferences.getWebBrowser("WebInspectorEnabled"): + os.putenv( + "QTWEBENGINE_REMOTE_DEBUGGING", + str(Preferences.getWebBrowser("WebInspectorPort"))) + + WebBrowserWindow.setUseQtHelp( + self.__fromEric or qthelp or bool(searchWord)) + + self.webProfile(private) + self.networkManager() + + self.__htmlFullScreen = False + self.__windowStates = 0 + + from .SearchWidget import SearchWidget + from .QtHelp.HelpTocWidget import HelpTocWidget + from .QtHelp.HelpIndexWidget import HelpIndexWidget + from .QtHelp.HelpSearchWidget import HelpSearchWidget + from .WebBrowserView import WebBrowserView + from .WebBrowserTabWidget import WebBrowserTabWidget + from .AdBlock.AdBlockIcon import AdBlockIcon + from .VirusTotal.VirusTotalApi import VirusTotalAPI + + if not self.__fromEric: + self.setStyle(Preferences.getUI("Style"), + Preferences.getUI("StyleSheet")) + + # initialize some SSL stuff + from E5Network.E5SslUtilities import initSSL + initSSL() + + if WebBrowserWindow.useQtHelp: + self.__helpEngine = QHelpEngine( + os.path.join(Utilities.getConfigDir(), + "web_browser", "eric6help.qhc"), + self) + self.__removeOldDocumentation() + self.__helpEngine.warning.connect(self.__warning) + else: + self.__helpEngine = None + self.__helpInstaller = None + + self.__zoomWidget = E5ZoomWidget( + UI.PixmapCache.getPixmap("zoomOut.png"), + UI.PixmapCache.getPixmap("zoomIn.png"), + UI.PixmapCache.getPixmap("zoomReset.png"), self) + self.statusBar().addPermanentWidget(self.__zoomWidget) + self.__zoomWidget.setMapping( + WebBrowserView.ZoomLevels, WebBrowserView.ZoomLevelDefault) + self.__zoomWidget.valueChanged.connect(self.__zoomValueChanged) + + self.__tabWidget = WebBrowserTabWidget(self) + self.__tabWidget.currentChanged[int].connect(self.__currentChanged) + self.__tabWidget.titleChanged.connect(self.__titleChanged) + self.__tabWidget.showMessage.connect(self.statusBar().showMessage) + self.__tabWidget.browserZoomValueChanged.connect( + self.__zoomWidget.setValue) + + self.__searchWidget = SearchWidget(self, self) + centralWidget = QWidget() + layout = QVBoxLayout() + layout.setContentsMargins(1, 1, 1, 1) + layout.addWidget(self.__tabWidget) + layout.addWidget(self.__searchWidget) + self.__tabWidget.setSizePolicy( + QSizePolicy.Preferred, QSizePolicy.Expanding) + centralWidget.setLayout(layout) + self.setCentralWidget(centralWidget) + self.__searchWidget.hide() + + if WebBrowserWindow.useQtHelp: + # setup the TOC widget + self.__tocWindow = HelpTocWidget(self.__helpEngine, self) + self.__tocDock = QDockWidget(self.tr("Contents"), self) + self.__tocDock.setObjectName("TocWindow") + self.__tocDock.setWidget(self.__tocWindow) + self.addDockWidget(Qt.LeftDockWidgetArea, self.__tocDock) + + # setup the index widget + self.__indexWindow = HelpIndexWidget(self.__helpEngine, self) + self.__indexDock = QDockWidget(self.tr("Index"), self) + self.__indexDock.setObjectName("IndexWindow") + self.__indexDock.setWidget(self.__indexWindow) + self.addDockWidget(Qt.LeftDockWidgetArea, self.__indexDock) + + # setup the search widget + self.__searchWord = searchWord + self.__indexing = False + self.__indexingProgress = None + self.__searchEngine = self.__helpEngine.searchEngine() + self.__searchEngine.indexingStarted.connect( + self.__indexingStarted) + self.__searchEngine.indexingFinished.connect( + self.__indexingFinished) + self.__searchWindow = HelpSearchWidget( + self.__searchEngine, self) + self.__searchDock = QDockWidget(self.tr("Search"), self) + self.__searchDock.setObjectName("SearchWindow") + self.__searchDock.setWidget(self.__searchWindow) + self.addDockWidget(Qt.LeftDockWidgetArea, self.__searchDock) + + # JavaScript Console window + from .WebBrowserJavaScriptConsole import \ + WebBrowserJavaScriptConsole + self.__javascriptConsole = WebBrowserJavaScriptConsole(self) + self.__javascriptConsoleDock = QDockWidget( + self.tr("JavaScript Console")) + self.__javascriptConsoleDock.setObjectName("JavascriptConsole") + self.__javascriptConsoleDock.setAllowedAreas( + Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea) + self.__javascriptConsoleDock.setWidget(self.__javascriptConsole) + self.addDockWidget(Qt.BottomDockWidgetArea, + self.__javascriptConsoleDock) + + if Preferences.getWebBrowser("SaveGeometry"): + g = Preferences.getGeometry("WebBrowserGeometry") + else: + g = QByteArray() + if g.isEmpty(): + s = QSize(800, 800) + self.resize(s) + else: + self.restoreGeometry(g) + + WebBrowserWindow.BrowserWindows.append(self) + + self.__setIconDatabasePath() + self.__initWebEngineSettings() + + # initialize some of our class objects + self.passwordManager() + self.historyManager() + self.greaseMonkeyManager() + + self.__initActions() + self.__initMenus() + self.__initToolbars() + + syncMgr = self.syncManager() + syncMgr.syncMessage.connect(self.statusBar().showMessage) + syncMgr.syncError.connect(self.statusBar().showMessage) + + self.__tabWidget.newBrowser(home) + self.__tabWidget.currentBrowser().setFocus() + + self.__adBlockIcon = AdBlockIcon(self) + self.statusBar().addPermanentWidget(self.__adBlockIcon) + self.__adBlockIcon.setEnabled( + Preferences.getWebBrowser("AdBlockEnabled")) + self.__tabWidget.currentChanged[int].connect( + self.__adBlockIcon.currentChanged) + self.__tabWidget.sourceChanged.connect( + self.__adBlockIcon.sourceChanged) + + self.networkIcon = E5NetworkIcon(self) + self.statusBar().addPermanentWidget(self.networkIcon) + + QDesktopServices.setUrlHandler("http", self.__linkActivated) + QDesktopServices.setUrlHandler("https", self.__linkActivated) + + # setup connections + self.__activating = False + if WebBrowserWindow.useQtHelp: + # TOC window + self.__tocWindow.linkActivated.connect(self.__linkActivated) + self.__tocWindow.escapePressed.connect( + self.__activateCurrentBrowser) + # index window + self.__indexWindow.linkActivated.connect(self.__linkActivated) + self.__indexWindow.linksActivated.connect( + self.__linksActivated) + self.__indexWindow.escapePressed.connect( + self.__activateCurrentBrowser) + # search window + self.__searchWindow.linkActivated.connect( + self.__linkActivated) + self.__searchWindow.escapePressed.connect( + self.__activateCurrentBrowser) + + state = Preferences.getWebBrowser("WebBrowserState") + self.restoreState(state) + + self.__initHelpDb() + + self.__virusTotal = VirusTotalAPI(self) + self.__virusTotal.submitUrlError.connect( + self.__virusTotalSubmitUrlError) + self.__virusTotal.urlScanReport.connect( + self.__virusTotalUrlScanReport) + self.__virusTotal.fileScanReport.connect( + self.__virusTotalFileScanReport) + + self.__shutdownCalled = False + + self.flashCookieManager() + + if WebBrowserWindow.useQtHelp: + QTimer.singleShot(0, self.__lookForNewDocumentation) + if self.__searchWord is not None: + QTimer.singleShot(0, self.__searchForWord) + + self.__lastActiveWindow = None + e5App().focusChanged[QWidget, QWidget].connect( + self.__appFocusChanged) + + QTimer.singleShot(0, syncMgr.loadSettings) + + def __del__(self): + """ + Special method called during object destruction. + + Note: This empty variant seems to get rid of the Qt message + 'Warning: QBasicTimer::start: QBasicTimer can only be used with + threads started with QThread' + """ + pass + + def fromEric(self): + """ + Public method to check, if the web browser was called from within the + eric IDE. + + @return flag indicating that the browserw as opened from within eric + @rtype bool + """ + return self.__fromEric + + def __setIconDatabasePath(self, enable=True): + """ + Private method to set the favicons path. + + @param enable flag indicating to enabled icon storage (boolean) + """ + if enable: + iconDatabasePath = os.path.join(Utilities.getConfigDir(), + "web_browser", "favicons") + if not os.path.exists(iconDatabasePath): + os.makedirs(iconDatabasePath) + else: + iconDatabasePath = "" # setting an empty path disables it + + WebIconProvider.instance().setIconDatabasePath(iconDatabasePath) + + def __initWebEngineSettings(self): + """ + Private method to set the global web settings. + """ + settings = QWebEngineSettings.globalSettings() + + settings.setFontFamily( + QWebEngineSettings.StandardFont, + Preferences.getWebBrowser("StandardFontFamily")) + settings.setFontFamily( + QWebEngineSettings.FixedFont, + Preferences.getWebBrowser("FixedFontFamily")) + settings.setFontFamily( + QWebEngineSettings.SerifFont, + Preferences.getWebBrowser("SerifFontFamily")) + settings.setFontFamily( + QWebEngineSettings.SansSerifFont, + Preferences.getWebBrowser("SansSerifFontFamily")) + settings.setFontFamily( + QWebEngineSettings.CursiveFont, + Preferences.getWebBrowser("CursiveFontFamily")) + settings.setFontFamily( + QWebEngineSettings.FantasyFont, + Preferences.getWebBrowser("FantasyFontFamily")) + + settings.setFontSize( + QWebEngineSettings.DefaultFontSize, + Preferences.getWebBrowser("DefaultFontSize")) + settings.setFontSize( + QWebEngineSettings.DefaultFixedFontSize, + Preferences.getWebBrowser("DefaultFixedFontSize")) + settings.setFontSize( + QWebEngineSettings.MinimumFontSize, + Preferences.getWebBrowser("MinimumFontSize")) + settings.setFontSize( + QWebEngineSettings.MinimumLogicalFontSize, + Preferences.getWebBrowser("MinimumLogicalFontSize")) + + styleSheet = Preferences.getWebBrowser("UserStyleSheet") + self.__setUserStyleSheet(styleSheet) + + settings.setAttribute( + QWebEngineSettings.AutoLoadImages, + Preferences.getWebBrowser("AutoLoadImages")) + settings.setAttribute( + QWebEngineSettings.JavascriptEnabled, + Preferences.getWebBrowser("JavaScriptEnabled")) + settings.setAttribute( + QWebEngineSettings.JavascriptCanOpenWindows, + Preferences.getWebBrowser("JavaScriptCanOpenWindows")) + settings.setAttribute( + QWebEngineSettings.JavascriptCanAccessClipboard, + Preferences.getWebBrowser("JavaScriptCanAccessClipboard")) + settings.setAttribute( + QWebEngineSettings.PluginsEnabled, + Preferences.getWebBrowser("PluginsEnabled")) + + if self.isPrivate(): + settings.setAttribute( + QWebEngineSettings.LocalStorageEnabled, False) + else: + settings.setAttribute( + QWebEngineSettings.LocalStorageEnabled, + Preferences.getWebBrowser("LocalStorageEnabled")) + settings.setDefaultTextEncoding( + Preferences.getWebBrowser("DefaultTextEncoding")) + + settings.setAttribute( + QWebEngineSettings.SpatialNavigationEnabled, + Preferences.getWebBrowser("SpatialNavigationEnabled")) + settings.setAttribute( + QWebEngineSettings.LinksIncludedInFocusChain, + Preferences.getWebBrowser("LinksIncludedInFocusChain")) + settings.setAttribute( + QWebEngineSettings.LocalContentCanAccessRemoteUrls, + Preferences.getWebBrowser("LocalContentCanAccessRemoteUrls")) + settings.setAttribute( + QWebEngineSettings.LocalContentCanAccessFileUrls, + Preferences.getWebBrowser("LocalContentCanAccessFileUrls")) + settings.setAttribute( + QWebEngineSettings.XSSAuditingEnabled, + Preferences.getWebBrowser("XSSAuditingEnabled")) + settings.setAttribute( + QWebEngineSettings.ScrollAnimatorEnabled, + Preferences.getWebBrowser("ScrollAnimatorEnabled")) + settings.setAttribute( + QWebEngineSettings.ErrorPageEnabled, + Preferences.getWebBrowser("ErrorPageEnabled")) + settings.setAttribute( + QWebEngineSettings.FullScreenSupportEnabled, + Preferences.getWebBrowser("FullScreenSupportEnabled")) + + def __initActions(self): + """ + Private method to define the user interface actions. + """ + # list of all actions + self.__actions = [] + + self.newTabAct = E5Action( + self.tr('New Tab'), + UI.PixmapCache.getIcon("tabNew.png"), + self.tr('&New Tab'), + QKeySequence(self.tr("Ctrl+T", "File|New Tab")), + 0, self, 'webbrowser_file_new_tab') + self.newTabAct.setStatusTip(self.tr('Open a new web browser tab')) + self.newTabAct.setWhatsThis(self.tr( + """<b>New Tab</b>""" + """<p>This opens a new web browser tab.</p>""" + )) + if not self.__initShortcutsOnly: + self.newTabAct.triggered.connect(self.newTab) + self.__actions.append(self.newTabAct) + + self.newAct = E5Action( + self.tr('New Window'), + UI.PixmapCache.getIcon("newWindow.png"), + self.tr('New &Window'), + QKeySequence(self.tr("Ctrl+N", "File|New Window")), + 0, self, 'webbrowser_file_new_window') + self.newAct.setStatusTip(self.tr('Open a new web browser window')) + self.newAct.setWhatsThis(self.tr( + """<b>New Window</b>""" + """<p>This opens a new web browser window in the current""" + """ privacy mode.</p>""" + )) + if not self.__initShortcutsOnly: + self.newAct.triggered.connect(self.newWindow) + self.__actions.append(self.newAct) + + self.newPrivateAct = E5Action( + self.tr('New Private Window'), + UI.PixmapCache.getIcon("privateMode.png"), + self.tr('New &Private Window'), + QKeySequence(self.tr("Ctrl+Shift+P", "File|New Private Window")), + 0, self, 'webbrowser_file_new_private_window') + self.newPrivateAct.setStatusTip(self.tr( + 'Open a new private web browser window')) + self.newPrivateAct.setWhatsThis(self.tr( + """<b>New Private Window</b>""" + """<p>This opens a new private web browser window by starting""" + """ a new web browser instance in private mode.</p>""" + )) + if not self.__initShortcutsOnly: + self.newPrivateAct.triggered.connect(self.newPrivateWindow) + self.__actions.append(self.newPrivateAct) + + self.openAct = E5Action( + self.tr('Open File'), + UI.PixmapCache.getIcon("open.png"), + self.tr('&Open File'), + QKeySequence(self.tr("Ctrl+O", "File|Open")), + 0, self, 'webbrowser_file_open') + self.openAct.setStatusTip(self.tr('Open a file for display')) + self.openAct.setWhatsThis(self.tr( + """<b>Open File</b>""" + """<p>This opens a new file for display.""" + """ It pops up a file selection dialog.</p>""" + )) + if not self.__initShortcutsOnly: + self.openAct.triggered.connect(self.__openFile) + self.__actions.append(self.openAct) + + self.openTabAct = E5Action( + self.tr('Open File in New Tab'), + UI.PixmapCache.getIcon("openNewTab.png"), + self.tr('Open File in New &Tab'), + QKeySequence(self.tr("Shift+Ctrl+O", "File|Open in new tab")), + 0, self, 'webbrowser_file_open_tab') + self.openTabAct.setStatusTip( + self.tr('Open a file for display in a new tab')) + self.openTabAct.setWhatsThis(self.tr( + """<b>Open File in New Tab</b>""" + """<p>This opens a new file for display in a new tab.""" + """ It pops up a file selection dialog.</p>""" + )) + if not self.__initShortcutsOnly: + self.openTabAct.triggered.connect(self.__openFileNewTab) + self.__actions.append(self.openTabAct) + # TODO: Qt 5.7: Save +## +## self.saveAsAct = E5Action( +## self.tr('Save As'), +## UI.PixmapCache.getIcon("fileSaveAs.png"), +## self.tr('&Save As...'), +## QKeySequence(self.tr("Shift+Ctrl+S", "File|Save As")), +## 0, self, 'webbrowser_file_save_as') +## self.saveAsAct.setStatusTip( +## self.tr('Save the current page to disk')) +## self.saveAsAct.setWhatsThis(self.tr( +## """<b>Save As...</b>""" +## """<p>Saves the current page to disk.</p>""" +## )) +## if not self.__initShortcutsOnly: +## self.saveAsAct.triggered.connect(self.__savePageAs) +## self.__actions.append(self.saveAsAct) +## + self.savePageScreenAct = E5Action( + self.tr('Save Page Screen'), + UI.PixmapCache.getIcon("fileSavePixmap.png"), + self.tr('Save Page Screen...'), + 0, 0, self, 'webbrowser_file_save_page_screen') + self.savePageScreenAct.setStatusTip( + self.tr('Save the current page as a screen shot')) + self.savePageScreenAct.setWhatsThis(self.tr( + """<b>Save Page Screen...</b>""" + """<p>Saves the current page as a screen shot.</p>""" + )) + if not self.__initShortcutsOnly: + self.savePageScreenAct.triggered.connect(self.__savePageScreen) + self.__actions.append(self.savePageScreenAct) + + self.saveVisiblePageScreenAct = E5Action( + self.tr('Save Visible Page Screen'), + UI.PixmapCache.getIcon("fileSaveVisiblePixmap.png"), + self.tr('Save Visible Page Screen...'), + 0, 0, self, 'webbrowser_file_save_visible_page_screen') + self.saveVisiblePageScreenAct.setStatusTip( + self.tr('Save the visible part of the current page as a' + ' screen shot')) + self.saveVisiblePageScreenAct.setWhatsThis(self.tr( + """<b>Save Visible Page Screen...</b>""" + """<p>Saves the visible part of the current page as a""" + """ screen shot.</p>""" + )) + if not self.__initShortcutsOnly: + self.saveVisiblePageScreenAct.triggered.connect( + self.__saveVisiblePageScreen) + self.__actions.append(self.saveVisiblePageScreenAct) + + bookmarksManager = self.bookmarksManager() + self.importBookmarksAct = E5Action( + self.tr('Import Bookmarks'), + self.tr('&Import Bookmarks...'), + 0, 0, self, 'webbrowser_file_import_bookmarks') + self.importBookmarksAct.setStatusTip( + self.tr('Import bookmarks from other browsers')) + self.importBookmarksAct.setWhatsThis(self.tr( + """<b>Import Bookmarks</b>""" + """<p>Import bookmarks from other browsers.</p>""" + )) + if not self.__initShortcutsOnly: + self.importBookmarksAct.triggered.connect( + bookmarksManager.importBookmarks) + self.__actions.append(self.importBookmarksAct) + + self.exportBookmarksAct = E5Action( + self.tr('Export Bookmarks'), + self.tr('&Export Bookmarks...'), + 0, 0, self, 'webbrowser_file_export_bookmarks') + self.exportBookmarksAct.setStatusTip( + self.tr('Export the bookmarks into a file')) + self.exportBookmarksAct.setWhatsThis(self.tr( + """<b>Export Bookmarks</b>""" + """<p>Export the bookmarks into a file.</p>""" + )) + if not self.__initShortcutsOnly: + self.exportBookmarksAct.triggered.connect( + bookmarksManager.exportBookmarks) + self.__actions.append(self.exportBookmarksAct) + + self.printAct = E5Action( + self.tr('Print'), + UI.PixmapCache.getIcon("print.png"), + self.tr('&Print'), + QKeySequence(self.tr("Ctrl+P", "File|Print")), + 0, self, 'webbrowser_file_print') + self.printAct.setStatusTip(self.tr('Print the displayed help')) + self.printAct.setWhatsThis(self.tr( + """<b>Print</b>""" + """<p>Print the displayed help text.</p>""" + )) + if not self.__initShortcutsOnly: + self.printAct.triggered.connect(self.__tabWidget.printBrowser) + self.__actions.append(self.printAct) + + if Globals.isLinuxPlatform(): + self.printPdfAct = E5Action( + self.tr('Print as PDF'), + UI.PixmapCache.getIcon("printPdf.png"), + self.tr('Print as PDF'), + 0, 0, self, 'webbrowser_file_print_pdf') + self.printPdfAct.setStatusTip(self.tr( + 'Print the displayed help as PDF')) + self.printPdfAct.setWhatsThis(self.tr( + """<b>Print as PDF</b>""" + """<p>Print the displayed help text as a PDF file.</p>""" + )) + if not self.__initShortcutsOnly: + self.printPdfAct.triggered.connect( + self.__tabWidget.printBrowserPdf) + self.__actions.append(self.printPdfAct) + else: + self.printPdfAct = None + + self.printPreviewAct = E5Action( + self.tr('Print Preview'), + UI.PixmapCache.getIcon("printPreview.png"), + self.tr('Print Preview'), + 0, 0, self, 'webbrowser_file_print_preview') + self.printPreviewAct.setStatusTip(self.tr( + 'Print preview of the displayed help')) + self.printPreviewAct.setWhatsThis(self.tr( + """<b>Print Preview</b>""" + """<p>Print preview of the displayed help text.</p>""" + )) + if not self.__initShortcutsOnly: + self.printPreviewAct.triggered.connect( + self.__tabWidget.printPreviewBrowser) + self.__actions.append(self.printPreviewAct) + + self.closeAct = E5Action( + self.tr('Close'), + UI.PixmapCache.getIcon("close.png"), + self.tr('&Close'), + QKeySequence(self.tr("Ctrl+W", "File|Close")), + 0, self, 'webbrowser_file_close') + self.closeAct.setStatusTip(self.tr( + 'Close the current help window')) + self.closeAct.setWhatsThis(self.tr( + """<b>Close</b>""" + """<p>Closes the current web browser window.</p>""" + )) + if not self.__initShortcutsOnly: + self.closeAct.triggered.connect(self.__tabWidget.closeBrowser) + self.__actions.append(self.closeAct) + + self.closeAllAct = E5Action( + self.tr('Close All'), + self.tr('Close &All'), + 0, 0, self, 'webbrowser_file_close_all') + self.closeAllAct.setStatusTip(self.tr('Close all help windows')) + self.closeAllAct.setWhatsThis(self.tr( + """<b>Close All</b>""" + """<p>Closes all web browser windows except the first one.</p>""" + )) + if not self.__initShortcutsOnly: + self.closeAllAct.triggered.connect( + self.__tabWidget.closeAllBrowsers) + self.__actions.append(self.closeAllAct) + + self.exitAct = E5Action( + self.tr('Quit'), + UI.PixmapCache.getIcon("exit.png"), + self.tr('&Quit'), + QKeySequence(self.tr("Ctrl+Q", "File|Quit")), + 0, self, 'webbrowser_file_quit') + self.exitAct.setStatusTip(self.tr('Quit the eric6 Web Browser')) + self.exitAct.setWhatsThis(self.tr( + """<b>Quit</b>""" + """<p>Quit the eric6 Web Browser.</p>""" + )) + if not self.__initShortcutsOnly: + if self.__fromEric: + self.exitAct.triggered.connect(self.close) + else: + self.exitAct.triggered.connect(self.__closeAllWindows) + self.__actions.append(self.exitAct) + + self.backAct = E5Action( + self.tr('Backward'), + UI.PixmapCache.getIcon("back.png"), + self.tr('&Backward'), + QKeySequence(self.tr("Alt+Left", "Go|Backward")), + 0, self, 'webbrowser_go_backward') + self.backAct.setStatusTip(self.tr('Move one screen backward')) + self.backAct.setWhatsThis(self.tr( + """<b>Backward</b>""" + """<p>Moves one screen backward. If none is""" + """ available, this action is disabled.</p>""" + )) + if not self.__initShortcutsOnly: + self.backAct.triggered.connect(self.__backward) + self.__actions.append(self.backAct) + + self.forwardAct = E5Action( + self.tr('Forward'), + UI.PixmapCache.getIcon("forward.png"), + self.tr('&Forward'), + QKeySequence(self.tr("Alt+Right", "Go|Forward")), + 0, self, 'webbrowser_go_foreward') + self.forwardAct.setStatusTip(self.tr( + 'Move one screen forward')) + self.forwardAct.setWhatsThis(self.tr( + """<b>Forward</b>""" + """<p>Moves one screen forward. If none is""" + """ available, this action is disabled.</p>""" + )) + if not self.__initShortcutsOnly: + self.forwardAct.triggered.connect(self.__forward) + self.__actions.append(self.forwardAct) + + self.homeAct = E5Action( + self.tr('Home'), + UI.PixmapCache.getIcon("home.png"), + self.tr('&Home'), + QKeySequence(self.tr("Ctrl+Home", "Go|Home")), + 0, self, 'webbrowser_go_home') + self.homeAct.setStatusTip(self.tr( + 'Move to the initial help screen')) + self.homeAct.setWhatsThis(self.tr( + """<b>Home</b>""" + """<p>Moves to the initial screen.</p>""" + )) + if not self.__initShortcutsOnly: + self.homeAct.triggered.connect(self.__home) + self.__actions.append(self.homeAct) + + self.reloadAct = E5Action( + self.tr('Reload'), + UI.PixmapCache.getIcon("reload.png"), + self.tr('&Reload'), + QKeySequence(self.tr("Ctrl+R", "Go|Reload")), + QKeySequence(self.tr("F5", "Go|Reload")), + self, 'webbrowser_go_reload') + self.reloadAct.setStatusTip(self.tr( + 'Reload the current screen')) + self.reloadAct.setWhatsThis(self.tr( + """<b>Reload</b>""" + """<p>Reloads the current screen.</p>""" + )) + if not self.__initShortcutsOnly: + self.reloadAct.triggered.connect(self.__reload) + self.__actions.append(self.reloadAct) + + self.stopAct = E5Action( + self.tr('Stop'), + UI.PixmapCache.getIcon("stopLoading.png"), + self.tr('&Stop'), + QKeySequence(self.tr("Ctrl+.", "Go|Stop")), + QKeySequence(self.tr("Esc", "Go|Stop")), + self, 'webbrowser_go_stop') + self.stopAct.setStatusTip(self.tr('Stop loading')) + self.stopAct.setWhatsThis(self.tr( + """<b>Stop</b>""" + """<p>Stops loading of the current tab.</p>""" + )) + if not self.__initShortcutsOnly: + self.stopAct.triggered.connect(self.__stopLoading) + self.__actions.append(self.stopAct) + + self.copyAct = E5Action( + self.tr('Copy'), + UI.PixmapCache.getIcon("editCopy.png"), + self.tr('&Copy'), + QKeySequence(self.tr("Ctrl+C", "Edit|Copy")), + 0, self, 'webbrowser_edit_copy') + self.copyAct.setStatusTip(self.tr('Copy the selected text')) + self.copyAct.setWhatsThis(self.tr( + """<b>Copy</b>""" + """<p>Copy the selected text to the clipboard.</p>""" + )) + if not self.__initShortcutsOnly: + self.copyAct.triggered.connect(self.__copy) + self.__actions.append(self.copyAct) + + self.cutAct = E5Action( + self.tr('Cut'), + UI.PixmapCache.getIcon("editCut.png"), + self.tr('Cu&t'), + QKeySequence(self.tr("Ctrl+X", "Edit|Cut")), + 0, self, 'webbrowser_edit_cut') + self.cutAct.setStatusTip(self.tr('Cut the selected text')) + self.cutAct.setWhatsThis(self.tr( + """<b>Cut</b>""" + """<p>Cut the selected text to the clipboard.</p>""" + )) + if not self.__initShortcutsOnly: + self.cutAct.triggered.connect(self.__cut) + self.__actions.append(self.cutAct) + + self.pasteAct = E5Action( + self.tr('Paste'), + UI.PixmapCache.getIcon("editPaste.png"), + self.tr('&Paste'), + QKeySequence(self.tr("Ctrl+V", "Edit|Paste")), + 0, self, 'webbrowser_edit_paste') + self.pasteAct.setStatusTip(self.tr('Paste text from the clipboard')) + self.pasteAct.setWhatsThis(self.tr( + """<b>Paste</b>""" + """<p>Paste some text from the clipboard.</p>""" + )) + if not self.__initShortcutsOnly: + self.pasteAct.triggered.connect(self.__paste) + self.__actions.append(self.pasteAct) + + self.undoAct = E5Action( + self.tr('Undo'), + UI.PixmapCache.getIcon("editUndo.png"), + self.tr('&Undo'), + QKeySequence(self.tr("Ctrl+Z", "Edit|Undo")), + 0, self, 'webbrowser_edit_undo') + self.undoAct.setStatusTip(self.tr('Undo the last edit action')) + self.undoAct.setWhatsThis(self.tr( + """<b>Undo</b>""" + """<p>Undo the last edit action.</p>""" + )) + if not self.__initShortcutsOnly: + self.undoAct.triggered.connect(self.__undo) + self.__actions.append(self.undoAct) + + self.redoAct = E5Action( + self.tr('Redo'), + UI.PixmapCache.getIcon("editRedo.png"), + self.tr('&Redo'), + QKeySequence(self.tr("Ctrl+Shift+Z", "Edit|Redo")), + 0, self, 'webbrowser_edit_redo') + self.redoAct.setStatusTip(self.tr('Redo the last edit action')) + self.redoAct.setWhatsThis(self.tr( + """<b>Redo</b>""" + """<p>Redo the last edit action.</p>""" + )) + if not self.__initShortcutsOnly: + self.redoAct.triggered.connect(self.__redo) + self.__actions.append(self.redoAct) + + self.selectAllAct = E5Action( + self.tr('Select All'), + UI.PixmapCache.getIcon("editSelectAll.png"), + self.tr('&Select All'), + QKeySequence(self.tr("Ctrl+A", "Edit|Select All")), + 0, self, 'webbrowser_edit_select_all') + self.selectAllAct.setStatusTip(self.tr('Select all text')) + self.selectAllAct.setWhatsThis(self.tr( + """<b>Select All</b>""" + """<p>Select all text of the current browser.</p>""" + )) + if not self.__initShortcutsOnly: + self.selectAllAct.triggered.connect(self.__selectAll) + self.__actions.append(self.selectAllAct) + + self.findAct = E5Action( + self.tr('Find...'), + UI.PixmapCache.getIcon("find.png"), + self.tr('&Find...'), + QKeySequence(self.tr("Ctrl+F", "Edit|Find")), + 0, self, 'webbrowser_edit_find') + self.findAct.setStatusTip(self.tr('Find text in page')) + self.findAct.setWhatsThis(self.tr( + """<b>Find</b>""" + """<p>Find text in the current page.</p>""" + )) + if not self.__initShortcutsOnly: + self.findAct.triggered.connect(self.__find) + self.__actions.append(self.findAct) + + self.findNextAct = E5Action( + self.tr('Find next'), + UI.PixmapCache.getIcon("findNext.png"), + self.tr('Find &next'), + QKeySequence(self.tr("F3", "Edit|Find next")), + 0, self, 'webbrowser_edit_find_next') + self.findNextAct.setStatusTip(self.tr( + 'Find next occurrence of text in page')) + self.findNextAct.setWhatsThis(self.tr( + """<b>Find next</b>""" + """<p>Find the next occurrence of text in the current page.</p>""" + )) + if not self.__initShortcutsOnly: + self.findNextAct.triggered.connect(self.__searchWidget.findNext) + self.__actions.append(self.findNextAct) + + self.findPrevAct = E5Action( + self.tr('Find previous'), + UI.PixmapCache.getIcon("findPrev.png"), + self.tr('Find &previous'), + QKeySequence(self.tr("Shift+F3", "Edit|Find previous")), + 0, self, 'webbrowser_edit_find_previous') + self.findPrevAct.setStatusTip( + self.tr('Find previous occurrence of text in page')) + self.findPrevAct.setWhatsThis(self.tr( + """<b>Find previous</b>""" + """<p>Find the previous occurrence of text in the current""" + """ page.</p>""" + )) + if not self.__initShortcutsOnly: + self.findPrevAct.triggered.connect( + self.__searchWidget.findPrevious) + self.__actions.append(self.findPrevAct) + + self.bookmarksManageAct = E5Action( + self.tr('Manage Bookmarks'), + self.tr('&Manage Bookmarks...'), + QKeySequence(self.tr("Ctrl+Shift+B", "Help|Manage bookmarks")), + 0, self, 'webbrowser_bookmarks_manage') + self.bookmarksManageAct.setStatusTip(self.tr( + 'Open a dialog to manage the bookmarks.')) + self.bookmarksManageAct.setWhatsThis(self.tr( + """<b>Manage Bookmarks...</b>""" + """<p>Open a dialog to manage the bookmarks.</p>""" + )) + if not self.__initShortcutsOnly: + self.bookmarksManageAct.triggered.connect( + self.__showBookmarksDialog) + self.__actions.append(self.bookmarksManageAct) + + self.bookmarksAddAct = E5Action( + self.tr('Add Bookmark'), + UI.PixmapCache.getIcon("addBookmark.png"), + self.tr('Add &Bookmark...'), + QKeySequence(self.tr("Ctrl+D", "Help|Add bookmark")), + 0, self, 'webbrowser_bookmark_add') + self.bookmarksAddAct.setIconVisibleInMenu(False) + self.bookmarksAddAct.setStatusTip(self.tr( + 'Open a dialog to add a bookmark.')) + self.bookmarksAddAct.setWhatsThis(self.tr( + """<b>Add Bookmark</b>""" + """<p>Open a dialog to add the current URL as a bookmark.</p>""" + )) + if not self.__initShortcutsOnly: + self.bookmarksAddAct.triggered.connect(self.__addBookmark) + self.__actions.append(self.bookmarksAddAct) + + self.bookmarksAddFolderAct = E5Action( + self.tr('Add Folder'), + self.tr('Add &Folder...'), + 0, 0, self, 'webbrowser_bookmark_show_all') + self.bookmarksAddFolderAct.setStatusTip(self.tr( + 'Open a dialog to add a new bookmarks folder.')) + self.bookmarksAddFolderAct.setWhatsThis(self.tr( + """<b>Add Folder...</b>""" + """<p>Open a dialog to add a new bookmarks folder.</p>""" + )) + if not self.__initShortcutsOnly: + self.bookmarksAddFolderAct.triggered.connect( + self.__addBookmarkFolder) + self.__actions.append(self.bookmarksAddFolderAct) + + self.bookmarksAllTabsAct = E5Action( + self.tr('Bookmark All Tabs'), + self.tr('Bookmark All Tabs...'), + 0, 0, self, 'webbrowser_bookmark_all_tabs') + self.bookmarksAllTabsAct.setStatusTip(self.tr( + 'Bookmark all open tabs.')) + self.bookmarksAllTabsAct.setWhatsThis(self.tr( + """<b>Bookmark All Tabs...</b>""" + """<p>Open a dialog to add a new bookmarks folder for""" + """ all open tabs.</p>""" + )) + if not self.__initShortcutsOnly: + self.bookmarksAllTabsAct.triggered.connect(self.bookmarkAll) + self.__actions.append(self.bookmarksAllTabsAct) + + self.whatsThisAct = E5Action( + self.tr('What\'s This?'), + UI.PixmapCache.getIcon("whatsThis.png"), + self.tr('&What\'s This?'), + QKeySequence(self.tr("Shift+F1", "Help|What's This?'")), + 0, self, 'webbrowser_help_whats_this') + self.whatsThisAct.setStatusTip(self.tr('Context sensitive help')) + self.whatsThisAct.setWhatsThis(self.tr( + """<b>Display context sensitive help</b>""" + """<p>In What's This? mode, the mouse cursor shows an arrow""" + """ with a question mark, and you can click on the interface""" + """ elements to get a short description of what they do and how""" + """ to use them. In dialogs, this feature can be accessed using""" + """ the context help button in the titlebar.</p>""" + )) + if not self.__initShortcutsOnly: + self.whatsThisAct.triggered.connect(self.__whatsThis) + self.__actions.append(self.whatsThisAct) + + self.aboutAct = E5Action( + self.tr('About'), + self.tr('&About'), + 0, 0, self, 'webbrowser_help_about') + self.aboutAct.setStatusTip(self.tr( + 'Display information about this software')) + self.aboutAct.setWhatsThis(self.tr( + """<b>About</b>""" + """<p>Display some information about this software.</p>""" + )) + if not self.__initShortcutsOnly: + self.aboutAct.triggered.connect(self.__about) + self.__actions.append(self.aboutAct) + + self.aboutQtAct = E5Action( + self.tr('About Qt'), + self.tr('About &Qt'), + 0, 0, self, 'webbrowser_help_about_qt') + self.aboutQtAct.setStatusTip( + self.tr('Display information about the Qt toolkit')) + self.aboutQtAct.setWhatsThis(self.tr( + """<b>About Qt</b>""" + """<p>Display some information about the Qt toolkit.</p>""" + )) + if not self.__initShortcutsOnly: + self.aboutQtAct.triggered.connect(self.__aboutQt) + self.__actions.append(self.aboutQtAct) + + self.zoomInAct = E5Action( + self.tr('Zoom in'), + UI.PixmapCache.getIcon("zoomIn.png"), + self.tr('Zoom &in'), + QKeySequence(self.tr("Ctrl++", "View|Zoom in")), + QKeySequence(self.tr("Zoom In", "View|Zoom in")), + self, 'webbrowser_view_zoom_in') + self.zoomInAct.setStatusTip(self.tr('Zoom in on the web page')) + self.zoomInAct.setWhatsThis(self.tr( + """<b>Zoom in</b>""" + """<p>Zoom in on the web page.""" + """ This makes the web page bigger.</p>""" + )) + if not self.__initShortcutsOnly: + self.zoomInAct.triggered.connect(self.__zoomIn) + self.__actions.append(self.zoomInAct) + + self.zoomOutAct = E5Action( + self.tr('Zoom out'), + UI.PixmapCache.getIcon("zoomOut.png"), + self.tr('Zoom &out'), + QKeySequence(self.tr("Ctrl+-", "View|Zoom out")), + QKeySequence(self.tr("Zoom Out", "View|Zoom out")), + self, 'webbrowser_view_zoom_out') + self.zoomOutAct.setStatusTip(self.tr('Zoom out on the web page')) + self.zoomOutAct.setWhatsThis(self.tr( + """<b>Zoom out</b>""" + """<p>Zoom out on the web page.""" + """ This makes the web page smaller.</p>""" + )) + if not self.__initShortcutsOnly: + self.zoomOutAct.triggered.connect(self.__zoomOut) + self.__actions.append(self.zoomOutAct) + + self.zoomResetAct = E5Action( + self.tr('Zoom reset'), + UI.PixmapCache.getIcon("zoomReset.png"), + self.tr('Zoom &reset'), + QKeySequence(self.tr("Ctrl+0", "View|Zoom reset")), + 0, self, 'webbrowser_view_zoom_reset') + self.zoomResetAct.setStatusTip(self.tr( + 'Reset the zoom of the web page')) + self.zoomResetAct.setWhatsThis(self.tr( + """<b>Zoom reset</b>""" + """<p>Reset the zoom of the web page. """ + """This sets the zoom factor to 100%.</p>""" + )) + if not self.__initShortcutsOnly: + self.zoomResetAct.triggered.connect(self.__zoomReset) + self.__actions.append(self.zoomResetAct) + + self.pageSourceAct = E5Action( + self.tr('Show page source'), + self.tr('Show page source'), + QKeySequence(self.tr('Ctrl+U')), 0, + self, 'webbrowser_show_page_source') + self.pageSourceAct.setStatusTip(self.tr( + 'Show the page source in an editor')) + self.pageSourceAct.setWhatsThis(self.tr( + """<b>Show page source</b>""" + """<p>Show the page source in an editor.</p>""" + )) + if not self.__initShortcutsOnly: + self.pageSourceAct.triggered.connect(self.__showPageSource) + self.__actions.append(self.pageSourceAct) + self.addAction(self.pageSourceAct) + + self.fullScreenAct = E5Action( + self.tr('Full Screen'), + UI.PixmapCache.getIcon("windowFullscreen.png"), + self.tr('&Full Screen'), + QKeySequence(self.tr('F11')), 0, + self, 'webbrowser_view_full_scree') + if not self.__initShortcutsOnly: + self.fullScreenAct.triggered.connect(self.__viewFullScreen) + self.__actions.append(self.fullScreenAct) + self.addAction(self.fullScreenAct) + + self.nextTabAct = E5Action( + self.tr('Show next tab'), + self.tr('Show next tab'), + QKeySequence(self.tr('Ctrl+Alt+Tab')), 0, + self, 'webbrowser_view_next_tab') + if not self.__initShortcutsOnly: + self.nextTabAct.triggered.connect(self.__nextTab) + self.__actions.append(self.nextTabAct) + self.addAction(self.nextTabAct) + + self.prevTabAct = E5Action( + self.tr('Show previous tab'), + self.tr('Show previous tab'), + QKeySequence(self.tr('Shift+Ctrl+Alt+Tab')), 0, + self, 'webbrowser_view_previous_tab') + if not self.__initShortcutsOnly: + self.prevTabAct.triggered.connect(self.__prevTab) + self.__actions.append(self.prevTabAct) + self.addAction(self.prevTabAct) + + self.switchTabAct = E5Action( + self.tr('Switch between tabs'), + self.tr('Switch between tabs'), + QKeySequence(self.tr('Ctrl+1')), 0, + self, 'webbrowser_switch_tabs') + if not self.__initShortcutsOnly: + self.switchTabAct.triggered.connect(self.__switchTab) + self.__actions.append(self.switchTabAct) + self.addAction(self.switchTabAct) + + self.prefAct = E5Action( + self.tr('Preferences'), + UI.PixmapCache.getIcon("configure.png"), + self.tr('&Preferences...'), 0, 0, self, 'webbrowser_preferences') + self.prefAct.setStatusTip(self.tr( + 'Set the prefered configuration')) + self.prefAct.setWhatsThis(self.tr( + """<b>Preferences</b>""" + """<p>Set the configuration items of the application""" + """ with your prefered values.</p>""" + )) + if not self.__initShortcutsOnly: + self.prefAct.triggered.connect(self.__showPreferences) + self.__actions.append(self.prefAct) + + self.acceptedLanguagesAct = E5Action( + self.tr('Languages'), + UI.PixmapCache.getIcon("flag.png"), + self.tr('&Languages...'), 0, 0, + self, 'webbrowser_accepted_languages') + self.acceptedLanguagesAct.setStatusTip(self.tr( + 'Configure the accepted languages for web pages')) + self.acceptedLanguagesAct.setWhatsThis(self.tr( + """<b>Languages</b>""" + """<p>Configure the accepted languages for web pages.</p>""" + )) + if not self.__initShortcutsOnly: + self.acceptedLanguagesAct.triggered.connect( + self.__showAcceptedLanguages) + self.__actions.append(self.acceptedLanguagesAct) + + self.cookiesAct = E5Action( + self.tr('Cookies'), + UI.PixmapCache.getIcon("cookie.png"), + self.tr('C&ookies...'), 0, 0, self, 'webbrowser_cookies') + self.cookiesAct.setStatusTip(self.tr( + 'Configure cookies handling')) + self.cookiesAct.setWhatsThis(self.tr( + """<b>Cookies</b>""" + """<p>Configure cookies handling.</p>""" + )) + if not self.__initShortcutsOnly: + self.cookiesAct.triggered.connect( + self.__showCookiesConfiguration) + self.__actions.append(self.cookiesAct) + + self.flashCookiesAct = E5Action( + self.tr('Flash Cookies'), + UI.PixmapCache.getIcon("flashCookie.png"), + self.tr('&Flash Cookies...'), 0, 0, self, + 'webbrowser_flash_cookies') + self.flashCookiesAct.setStatusTip(self.tr( + 'Manage flash cookies')) + self.flashCookiesAct.setWhatsThis(self.tr( + """<b>Flash Cookies</b>""" + """<p>Show a dialog to manage the flash cookies.</p>""" + )) + if not self.__initShortcutsOnly: + self.flashCookiesAct.triggered.connect( + self.__showFlashCookiesManagement) + self.__actions.append(self.flashCookiesAct) + + self.personalDataAct = E5Action( + self.tr('Personal Information'), + UI.PixmapCache.getIcon("pim.png"), + self.tr('Personal Information...'), + 0, 0, + self, 'webbrowser_personal_information') + self.personalDataAct.setStatusTip(self.tr( + 'Configure personal information for completing form fields')) + self.personalDataAct.setWhatsThis(self.tr( + """<b>Personal Information...</b>""" + """<p>Opens a dialog to configure the personal information""" + """ used for completing form fields.</p>""" + )) + if not self.__initShortcutsOnly: + self.personalDataAct.triggered.connect( + self.__showPersonalInformationDialog) + self.__actions.append(self.personalDataAct) + + self.greaseMonkeyAct = E5Action( + self.tr('GreaseMonkey Scripts'), + UI.PixmapCache.getIcon("greaseMonkey.png"), + self.tr('GreaseMonkey Scripts...'), + 0, 0, + self, 'webbrowser_greasemonkey') + self.greaseMonkeyAct.setStatusTip(self.tr( + 'Configure the GreaseMonkey Scripts')) + self.greaseMonkeyAct.setWhatsThis(self.tr( + """<b>GreaseMonkey Scripts...</b>""" + """<p>Opens a dialog to configure the available GreaseMonkey""" + """ Scripts.</p>""" + )) + if not self.__initShortcutsOnly: + self.greaseMonkeyAct.triggered.connect( + self.__showGreaseMonkeyConfigDialog) + self.__actions.append(self.greaseMonkeyAct) + + self.editMessageFilterAct = E5Action( + self.tr('Edit Message Filters'), + UI.PixmapCache.getIcon("warning.png"), + self.tr('Edit Message Filters...'), 0, 0, self, + 'webbrowser_manage_message_filters') + self.editMessageFilterAct.setStatusTip(self.tr( + 'Edit the message filters used to suppress unwanted messages')) + self.editMessageFilterAct.setWhatsThis(self.tr( + """<b>Edit Message Filters</b>""" + """<p>Opens a dialog to edit the message filters used to""" + """ suppress unwanted messages been shown in an error""" + """ window.</p>""" + )) + if not self.__initShortcutsOnly: + self.editMessageFilterAct.triggered.connect( + E5ErrorMessage.editMessageFilters) + self.__actions.append(self.editMessageFilterAct) + + self.featurePermissionAct = E5Action( + self.tr('Edit HTML5 Feature Permissions'), + UI.PixmapCache.getIcon("featurePermission.png"), + self.tr('Edit HTML5 Feature Permissions...'), 0, 0, self, + 'webbrowser_edit_feature_permissions') + self.featurePermissionAct.setStatusTip(self.tr( + 'Edit the remembered HTML5 feature permissions')) + self.featurePermissionAct.setWhatsThis(self.tr( + """<b>Edit HTML5 Feature Permissions</b>""" + """<p>Opens a dialog to edit the remembered HTML5""" + """ feature permissions.</p>""" + )) + if not self.__initShortcutsOnly: + self.featurePermissionAct.triggered.connect( + self.__showFeaturePermissionDialog) + self.__actions.append(self.featurePermissionAct) + + if WebBrowserWindow.useQtHelp or self.__initShortcutsOnly: + self.syncTocAct = E5Action( + self.tr('Sync with Table of Contents'), + UI.PixmapCache.getIcon("syncToc.png"), + self.tr('Sync with Table of Contents'), + 0, 0, self, 'webbrowser_sync_toc') + self.syncTocAct.setStatusTip(self.tr( + 'Synchronizes the table of contents with current page')) + self.syncTocAct.setWhatsThis(self.tr( + """<b>Sync with Table of Contents</b>""" + """<p>Synchronizes the table of contents with current""" + """ page.</p>""" + )) + if not self.__initShortcutsOnly: + self.syncTocAct.triggered.connect(self.__syncTOC) + self.__actions.append(self.syncTocAct) + + self.showTocAct = E5Action( + self.tr('Table of Contents'), + self.tr('Table of Contents'), + 0, 0, self, 'webbrowser_show_toc') + self.showTocAct.setStatusTip(self.tr( + 'Shows the table of contents window')) + self.showTocAct.setWhatsThis(self.tr( + """<b>Table of Contents</b>""" + """<p>Shows the table of contents window.</p>""" + )) + if not self.__initShortcutsOnly: + self.showTocAct.triggered.connect(self.__showTocWindow) + self.__actions.append(self.showTocAct) + + self.showIndexAct = E5Action( + self.tr('Index'), + self.tr('Index'), + 0, 0, self, 'webbrowser_show_index') + self.showIndexAct.setStatusTip(self.tr( + 'Shows the index window')) + self.showIndexAct.setWhatsThis(self.tr( + """<b>Index</b>""" + """<p>Shows the index window.</p>""" + )) + if not self.__initShortcutsOnly: + self.showIndexAct.triggered.connect(self.__showIndexWindow) + self.__actions.append(self.showIndexAct) + + self.showSearchAct = E5Action( + self.tr('Search'), + self.tr('Search'), + 0, 0, self, 'webbrowser_show_search') + self.showSearchAct.setStatusTip(self.tr( + 'Shows the search window')) + self.showSearchAct.setWhatsThis(self.tr( + """<b>Search</b>""" + """<p>Shows the search window.</p>""" + )) + if not self.__initShortcutsOnly: + self.showSearchAct.triggered.connect( + self.__showSearchWindow) + self.__actions.append(self.showSearchAct) + + self.manageQtHelpDocsAct = E5Action( + self.tr('Manage QtHelp Documents'), + self.tr('Manage QtHelp &Documents'), + 0, 0, self, 'webbrowser_qthelp_documents') + self.manageQtHelpDocsAct.setStatusTip(self.tr( + 'Shows a dialog to manage the QtHelp documentation set')) + self.manageQtHelpDocsAct.setWhatsThis(self.tr( + """<b>Manage QtHelp Documents</b>""" + """<p>Shows a dialog to manage the QtHelp documentation""" + """ set.</p>""" + )) + if not self.__initShortcutsOnly: + self.manageQtHelpDocsAct.triggered.connect( + self.__manageQtHelpDocumentation) + self.__actions.append(self.manageQtHelpDocsAct) + + self.manageQtHelpFiltersAct = E5Action( + self.tr('Manage QtHelp Filters'), + self.tr('Manage QtHelp &Filters'), + 0, 0, self, 'webbrowser_qthelp_filters') + self.manageQtHelpFiltersAct.setStatusTip(self.tr( + 'Shows a dialog to manage the QtHelp filters')) + self.manageQtHelpFiltersAct.setWhatsThis(self.tr( + """<b>Manage QtHelp Filters</b>""" + """<p>Shows a dialog to manage the QtHelp filters.</p>""" + )) + if not self.__initShortcutsOnly: + self.manageQtHelpFiltersAct.triggered.connect( + self.__manageQtHelpFilters) + self.__actions.append(self.manageQtHelpFiltersAct) + + self.reindexDocumentationAct = E5Action( + self.tr('Reindex Documentation'), + self.tr('&Reindex Documentation'), + 0, 0, self, 'webbrowser_qthelp_reindex') + self.reindexDocumentationAct.setStatusTip(self.tr( + 'Reindexes the documentation set')) + self.reindexDocumentationAct.setWhatsThis(self.tr( + """<b>Reindex Documentation</b>""" + """<p>Reindexes the documentation set.</p>""" + )) + if not self.__initShortcutsOnly: + self.reindexDocumentationAct.triggered.connect( + self.__searchEngine.reindexDocumentation) + self.__actions.append(self.reindexDocumentationAct) + + self.clearPrivateDataAct = E5Action( + self.tr('Clear private data'), + self.tr('&Clear private data'), + 0, 0, + self, 'webbrowser_clear_private_data') + self.clearPrivateDataAct.setStatusTip(self.tr( + 'Clear private data')) + self.clearPrivateDataAct.setWhatsThis(self.tr( + """<b>Clear private data</b>""" + """<p>Clears the private data like browsing history, search""" + """ history or the favicons database.</p>""" + )) + if not self.__initShortcutsOnly: + self.clearPrivateDataAct.triggered.connect( + self.__clearPrivateData) + self.__actions.append(self.clearPrivateDataAct) + + self.clearIconsAct = E5Action( + self.tr('Clear icons database'), + self.tr('Clear &icons database'), + 0, 0, + self, 'webbrowser_clear_icons_db') + self.clearIconsAct.setStatusTip(self.tr( + 'Clear the database of favicons')) + self.clearIconsAct.setWhatsThis(self.tr( + """<b>Clear icons database</b>""" + """<p>Clears the database of favicons of previously visited""" + """ URLs.</p>""" + )) + if not self.__initShortcutsOnly: + self.clearIconsAct.triggered.connect(self.__clearIconsDatabase) + self.__actions.append(self.clearIconsAct) + + self.manageIconsAct = E5Action( + self.tr('Manage saved Favicons'), + UI.PixmapCache.getIcon("icons.png"), + self.tr('Manage saved Favicons'), + 0, 0, + self, 'webbrowser_manage_icons_db') + self.manageIconsAct.setStatusTip(self.tr( + 'Show a dialog to manage the saved favicons')) + self.manageIconsAct.setWhatsThis(self.tr( + """<b>Manage saved Favicons</b>""" + """<p>This shows a dialog to manage the saved favicons of""" + """ previously visited URLs.</p>""" + )) + if not self.__initShortcutsOnly: + self.manageIconsAct.triggered.connect(self.__showWebIconsDialog) + self.__actions.append(self.manageIconsAct) + + self.searchEnginesAct = E5Action( + self.tr('Configure Search Engines'), + self.tr('Configure Search &Engines...'), + 0, 0, + self, 'webbrowser_search_engines') + self.searchEnginesAct.setStatusTip(self.tr( + 'Configure the available search engines')) + self.searchEnginesAct.setWhatsThis(self.tr( + """<b>Configure Search Engines...</b>""" + """<p>Opens a dialog to configure the available search""" + """ engines.</p>""" + )) + if not self.__initShortcutsOnly: + self.searchEnginesAct.triggered.connect( + self.__showEnginesConfigurationDialog) + self.__actions.append(self.searchEnginesAct) + + self.passwordsAct = E5Action( + self.tr('Manage Saved Passwords'), + UI.PixmapCache.getIcon("passwords.png"), + self.tr('Manage Saved Passwords...'), + 0, 0, + self, 'webbrowser_manage_passwords') + self.passwordsAct.setStatusTip(self.tr( + 'Manage the saved passwords')) + self.passwordsAct.setWhatsThis(self.tr( + """<b>Manage Saved Passwords...</b>""" + """<p>Opens a dialog to manage the saved passwords.</p>""" + )) + if not self.__initShortcutsOnly: + self.passwordsAct.triggered.connect(self.__showPasswordsDialog) + self.__actions.append(self.passwordsAct) + + self.adblockAct = E5Action( + self.tr('Ad Block'), + UI.PixmapCache.getIcon("adBlockPlus.png"), + self.tr('&Ad Block...'), + 0, 0, + self, 'webbrowser_adblock') + self.adblockAct.setStatusTip(self.tr( + 'Configure AdBlock subscriptions and rules')) + self.adblockAct.setWhatsThis(self.tr( + """<b>Ad Block...</b>""" + """<p>Opens a dialog to configure AdBlock subscriptions and""" + """ rules.</p>""" + )) + if not self.__initShortcutsOnly: + self.adblockAct.triggered.connect(self.__showAdBlockDialog) + self.__actions.append(self.adblockAct) + + self.certificateErrorsAct = E5Action( + self.tr('Manage SSL Certificate Errors'), + UI.PixmapCache.getIcon("certificates.png"), + self.tr('Manage SSL Certificate Errors...'), + 0, 0, + self, 'webbrowser_manage_certificate_errors') + self.certificateErrorsAct.setStatusTip(self.tr( + 'Manage the accepted SSL certificate Errors')) + self.certificateErrorsAct.setWhatsThis(self.tr( + """<b>Manage SSL Certificate Errors...</b>""" + """<p>Opens a dialog to manage the accepted SSL""" + """ certificate errors.</p>""" + )) + if not self.__initShortcutsOnly: + self.certificateErrorsAct.triggered.connect( + self.__showCertificateErrorsDialog) + self.__actions.append(self.certificateErrorsAct) + + self.showDownloadManagerAct = E5Action( + self.tr('Downloads'), + self.tr('Downloads'), + 0, 0, self, 'webbrowser_show_downloads') + self.showDownloadManagerAct.setStatusTip(self.tr( + 'Shows the downloads window')) + self.showDownloadManagerAct.setWhatsThis(self.tr( + """<b>Downloads</b>""" + """<p>Shows the downloads window.</p>""" + )) + if not self.__initShortcutsOnly: + self.showDownloadManagerAct.triggered.connect( + self.__showDownloadsWindow) + self.__actions.append(self.showDownloadManagerAct) + + self.feedsManagerAct = E5Action( + self.tr('RSS Feeds Dialog'), + UI.PixmapCache.getIcon("rss22.png"), + self.tr('&RSS Feeds Dialog...'), + QKeySequence(self.tr("Ctrl+Shift+F", "Help|RSS Feeds Dialog")), + 0, self, 'webbrowser_rss_feeds') + self.feedsManagerAct.setStatusTip(self.tr( + 'Open a dialog showing the configured RSS feeds.')) + self.feedsManagerAct.setWhatsThis(self.tr( + """<b>RSS Feeds Dialog...</b>""" + """<p>Open a dialog to show the configured RSS feeds.""" + """ It can be used to mange the feeds and to show their""" + """ contents.</p>""" + )) + if not self.__initShortcutsOnly: + self.feedsManagerAct.triggered.connect(self.__showFeedsManager) + self.__actions.append(self.feedsManagerAct) + + self.siteInfoAct = E5Action( + self.tr('Siteinfo Dialog'), + UI.PixmapCache.getIcon("helpAbout.png"), + self.tr('&Siteinfo Dialog...'), + QKeySequence(self.tr("Ctrl+Shift+I", "Help|Siteinfo Dialog")), + 0, self, 'webbrowser_siteinfo') + self.siteInfoAct.setStatusTip(self.tr( + 'Open a dialog showing some information about the current site.')) + self.siteInfoAct.setWhatsThis(self.tr( + """<b>Siteinfo Dialog...</b>""" + """<p>Opens a dialog showing some information about the current""" + """ site.</p>""" + )) + if not self.__initShortcutsOnly: + self.siteInfoAct.triggered.connect(self.__showSiteinfoDialog) + self.__actions.append(self.siteInfoAct) + + self.userAgentManagerAct = E5Action( + self.tr('Manage User Agent Settings'), + self.tr('Manage &User Agent Settings'), + 0, 0, self, 'webbrowser_user_agent_settings') + self.userAgentManagerAct.setStatusTip(self.tr( + 'Shows a dialog to manage the User Agent settings')) + self.userAgentManagerAct.setWhatsThis(self.tr( + """<b>Manage User Agent Settings</b>""" + """<p>Shows a dialog to manage the User Agent settings.</p>""" + )) + if not self.__initShortcutsOnly: + self.userAgentManagerAct.triggered.connect( + self.__showUserAgentsDialog) + self.__actions.append(self.userAgentManagerAct) + + self.synchronizationAct = E5Action( + self.tr('Synchronize data'), + UI.PixmapCache.getIcon("sync.png"), + self.tr('&Synchronize Data...'), + 0, 0, self, 'webbrowser_synchronize_data') + self.synchronizationAct.setStatusTip(self.tr( + 'Shows a dialog to synchronize data via the network')) + self.synchronizationAct.setWhatsThis(self.tr( + """<b>Synchronize Data...</b>""" + """<p>This shows a dialog to synchronize data via the""" + """ network.</p>""" + )) + if not self.__initShortcutsOnly: + self.synchronizationAct.triggered.connect( + self.__showSyncDialog) + self.__actions.append(self.synchronizationAct) + + self.zoomValuesAct = E5Action( + self.tr('Manage Saved Zoom Values'), + UI.PixmapCache.getIcon("zoomReset.png"), + self.tr('Manage Saved Zoom Values...'), + 0, 0, + self, 'webbrowser_manage_zoom_values') + self.zoomValuesAct.setStatusTip(self.tr( + 'Manage the saved zoom values')) + self.zoomValuesAct.setWhatsThis(self.tr( + """<b>Manage Saved Zoom Values...</b>""" + """<p>Opens a dialog to manage the saved zoom values.</p>""" + )) + if not self.__initShortcutsOnly: + self.zoomValuesAct.triggered.connect(self.__showZoomValuesDialog) + self.__actions.append(self.zoomValuesAct) + + self.showJavaScriptConsoleAct = E5Action( + self.tr('JavaScript Console'), + self.tr('JavaScript Console'), + 0, 0, self, 'webbrowser_show_javascript_console') + self.showJavaScriptConsoleAct.setStatusTip(self.tr( + 'Toggle the JavaScript console window')) + self.showJavaScriptConsoleAct.setWhatsThis(self.tr( + """<b>JavaScript Console</b>""" + """<p>This toggles the JavaScript console window.</p>""" + )) + if not self.__initShortcutsOnly: + self.showJavaScriptConsoleAct.triggered.connect( + self.__toggleJavaScriptConsole) + self.__actions.append(self.showJavaScriptConsoleAct) + + self.backAct.setEnabled(False) + self.forwardAct.setEnabled(False) + + # now read the keyboard shortcuts for the actions + Shortcuts.readShortcuts( + helpViewer=self, helpViewerCategory="webBrowser") + + def getActions(self): + """ + Public method to get a list of all actions. + + @return list of all actions (list of E5Action) + """ + return self.__actions[:] + + def __initMenus(self): + """ + Private method to create the menus. + """ + mb = self.menuBar() + + menu = mb.addMenu(self.tr('&File')) + menu.setTearOffEnabled(True) + menu.addAction(self.newTabAct) + menu.addAction(self.newAct) + menu.addAction(self.newPrivateAct) + menu.addAction(self.openAct) + menu.addAction(self.openTabAct) + menu.addSeparator() + # TODO: Qt 5.7: Save +## menu.addAction(self.saveAsAct) + menu.addAction(self.savePageScreenAct) + menu.addAction(self.saveVisiblePageScreenAct) + menu.addSeparator() + menu.addAction(self.printPreviewAct) + menu.addAction(self.printAct) + if self.printPdfAct: + menu.addAction(self.printPdfAct) + menu.addSeparator() + menu.addAction(self.closeAct) + menu.addAction(self.closeAllAct) + menu.addSeparator() + menu.addAction(self.exitAct) + + menu = mb.addMenu(self.tr('&Edit')) + menu.setTearOffEnabled(True) + menu.addAction(self.undoAct) + menu.addAction(self.redoAct) + menu.addSeparator() + menu.addAction(self.copyAct) + menu.addAction(self.cutAct) + menu.addAction(self.pasteAct) + menu.addSeparator() + menu.addAction(self.selectAllAct) + menu.addSeparator() + menu.addAction(self.findAct) + menu.addAction(self.findNextAct) + menu.addAction(self.findPrevAct) + + menu = mb.addMenu(self.tr('&View')) + menu.setTearOffEnabled(True) + menu.addAction(self.zoomInAct) + menu.addAction(self.zoomResetAct) + menu.addAction(self.zoomOutAct) + menu.addSeparator() + menu.addAction(self.pageSourceAct) + menu.addAction(self.fullScreenAct) + self.__textEncodingMenu = menu.addMenu( + self.tr("Text Encoding")) + self.__textEncodingMenu.aboutToShow.connect( + self.__aboutToShowTextEncodingMenu) + self.__textEncodingMenu.triggered.connect(self.__setTextEncoding) + + menu = mb.addMenu(self.tr('&Go')) + menu.setTearOffEnabled(True) + menu.addAction(self.backAct) + menu.addAction(self.forwardAct) + menu.addAction(self.homeAct) + menu.addSeparator() + menu.addAction(self.stopAct) + menu.addAction(self.reloadAct) + if WebBrowserWindow.useQtHelp: + menu.addSeparator() + menu.addAction(self.syncTocAct) + + from .History.HistoryMenu import HistoryMenu + self.historyMenu = HistoryMenu(self, self.__tabWidget) + self.historyMenu.setTearOffEnabled(True) + self.historyMenu.setTitle(self.tr('H&istory')) + self.historyMenu.openUrl.connect(self.openUrl) + self.historyMenu.newUrl.connect(self.openUrlNewTab) + mb.addMenu(self.historyMenu) + + from .Bookmarks.BookmarksMenu import BookmarksMenuBarMenu + self.bookmarksMenu = BookmarksMenuBarMenu(self) + self.bookmarksMenu.setTearOffEnabled(True) + self.bookmarksMenu.setTitle(self.tr('&Bookmarks')) + self.bookmarksMenu.openUrl.connect(self.openUrl) + self.bookmarksMenu.newUrl.connect(self.openUrlNewTab) + mb.addMenu(self.bookmarksMenu) + + bookmarksActions = [] + bookmarksActions.append(self.bookmarksManageAct) + bookmarksActions.append(self.bookmarksAddAct) + bookmarksActions.append(self.bookmarksAllTabsAct) + bookmarksActions.append(self.bookmarksAddFolderAct) + bookmarksActions.append("--SEPARATOR--") + bookmarksActions.append(self.importBookmarksAct) + bookmarksActions.append(self.exportBookmarksAct) + self.bookmarksMenu.setInitialActions(bookmarksActions) + + menu = mb.addMenu(self.tr('&Settings')) + menu.setTearOffEnabled(True) + menu.addAction(self.prefAct) + menu.addAction(self.acceptedLanguagesAct) + menu.addAction(self.cookiesAct) + menu.addAction(self.flashCookiesAct) + menu.addAction(self.personalDataAct) + menu.addAction(self.greaseMonkeyAct) + menu.addAction(self.featurePermissionAct) + menu.addSeparator() + menu.addAction(self.editMessageFilterAct) + menu.addSeparator() + menu.addAction(self.searchEnginesAct) + menu.addSeparator() + menu.addAction(self.passwordsAct) + menu.addAction(self.certificateErrorsAct) + menu.addSeparator() + menu.addAction(self.zoomValuesAct) + menu.addAction(self.manageIconsAct) + menu.addSeparator() + menu.addAction(self.adblockAct) + menu.addSeparator() + self.__settingsMenu = menu + self.__settingsMenu.aboutToShow.connect( + self.__aboutToShowSettingsMenu) + + from .UserAgent.UserAgentMenu import UserAgentMenu + self.__userAgentMenu = UserAgentMenu(self.tr("Global User Agent")) + menu.addMenu(self.__userAgentMenu) + menu.addAction(self.userAgentManagerAct) + menu.addSeparator() + + if WebBrowserWindow.useQtHelp: + menu.addAction(self.manageQtHelpDocsAct) + menu.addAction(self.manageQtHelpFiltersAct) + menu.addAction(self.reindexDocumentationAct) + menu.addSeparator() + menu.addAction(self.clearPrivateDataAct) + menu.addAction(self.clearIconsAct) + + menu = mb.addMenu(self.tr("&Tools")) + menu.setTearOffEnabled(True) + menu.addAction(self.feedsManagerAct) + menu.addAction(self.siteInfoAct) + menu.addSeparator() + menu.addAction(self.synchronizationAct) + + menu = mb.addMenu(self.tr("&Window")) + menu.setTearOffEnabled(True) + menu.addAction(self.showDownloadManagerAct) + menu.addAction(self.showJavaScriptConsoleAct) + if WebBrowserWindow.useQtHelp: + menu.addSeparator() + menu.addAction(self.showTocAct) + menu.addAction(self.showIndexAct) + menu.addAction(self.showSearchAct) + + mb.addSeparator() + + menu = mb.addMenu(self.tr('&Help')) + menu.setTearOffEnabled(True) + menu.addAction(self.aboutAct) + menu.addAction(self.aboutQtAct) + menu.addSeparator() + menu.addAction(self.whatsThisAct) + + def __initToolbars(self): + """ + Private method to create the toolbars. + """ + filetb = self.addToolBar(self.tr("File")) + filetb.setObjectName("FileToolBar") + filetb.setIconSize(UI.Config.ToolBarIconSize) + filetb.addAction(self.newTabAct) + filetb.addAction(self.newAct) + filetb.addAction(self.newPrivateAct) + filetb.addAction(self.openAct) + filetb.addAction(self.openTabAct) + filetb.addSeparator() + # TODO: Qt 5.7: Save +## filetb.addAction(self.saveAsAct) + filetb.addAction(self.savePageScreenAct) + filetb.addSeparator() + filetb.addAction(self.printPreviewAct) + filetb.addAction(self.printAct) + if self.printPdfAct: + filetb.addAction(self.printPdfAct) + filetb.addSeparator() + filetb.addAction(self.closeAct) + filetb.addAction(self.exitAct) + + self.savePageScreenMenu = QMenu(self) + self.savePageScreenMenu.addAction(self.savePageScreenAct) + self.savePageScreenMenu.addAction(self.saveVisiblePageScreenAct) + savePageScreenButton = filetb.widgetForAction(self.savePageScreenAct) + savePageScreenButton.setMenu(self.savePageScreenMenu) + savePageScreenButton.setPopupMode(QToolButton.MenuButtonPopup) + + edittb = self.addToolBar(self.tr("Edit")) + edittb.setObjectName("EditToolBar") + edittb.setIconSize(UI.Config.ToolBarIconSize) + edittb.addAction(self.undoAct) + edittb.addAction(self.redoAct) + edittb.addSeparator() + edittb.addAction(self.copyAct) + edittb.addAction(self.cutAct) + edittb.addAction(self.pasteAct) + edittb.addSeparator() + edittb.addAction(self.selectAllAct) + + viewtb = self.addToolBar(self.tr("View")) + viewtb.setObjectName("ViewToolBar") + viewtb.setIconSize(UI.Config.ToolBarIconSize) + viewtb.addAction(self.zoomInAct) + viewtb.addAction(self.zoomResetAct) + viewtb.addAction(self.zoomOutAct) + viewtb.addSeparator() + viewtb.addAction(self.fullScreenAct) + + findtb = self.addToolBar(self.tr("Find")) + findtb.setObjectName("FindToolBar") + findtb.setIconSize(UI.Config.ToolBarIconSize) + findtb.addAction(self.findAct) + findtb.addAction(self.findNextAct) + findtb.addAction(self.findPrevAct) + + if WebBrowserWindow.useQtHelp: + filtertb = self.addToolBar(self.tr("Filter")) + filtertb.setObjectName("FilterToolBar") + self.filterCombo = QComboBox() + self.filterCombo.setMinimumWidth( + QFontMetrics(QFont()).width("ComboBoxWithEnoughWidth")) + filtertb.addWidget(QLabel(self.tr("Filtered by: "))) + filtertb.addWidget(self.filterCombo) + self.__helpEngine.setupFinished.connect(self.__setupFilterCombo) + self.filterCombo.activated[str].connect( + self.__filterQtHelpDocumentation) + self.__setupFilterCombo() + + settingstb = self.addToolBar(self.tr("Settings")) + settingstb.setObjectName("SettingsToolBar") + settingstb.setIconSize(UI.Config.ToolBarIconSize) + settingstb.addAction(self.prefAct) + settingstb.addAction(self.acceptedLanguagesAct) + settingstb.addAction(self.cookiesAct) + settingstb.addAction(self.flashCookiesAct) + settingstb.addAction(self.personalDataAct) + settingstb.addAction(self.greaseMonkeyAct) + settingstb.addAction(self.featurePermissionAct) + + toolstb = self.addToolBar(self.tr("Tools")) + toolstb.setObjectName("ToolsToolBar") + toolstb.setIconSize(UI.Config.ToolBarIconSize) + toolstb.addAction(self.feedsManagerAct) + toolstb.addAction(self.siteInfoAct) + toolstb.addSeparator() + toolstb.addAction(self.synchronizationAct) + + helptb = self.addToolBar(self.tr("Help")) + helptb.setObjectName("HelpToolBar") + helptb.setIconSize(UI.Config.ToolBarIconSize) + helptb.addAction(self.whatsThisAct) + + self.addToolBarBreak() + + gotb = self.addToolBar(self.tr("Go")) + gotb.setObjectName("GoToolBar") + gotb.setIconSize(UI.Config.ToolBarIconSize) + gotb.addAction(self.backAct) + gotb.addAction(self.forwardAct) + gotb.addAction(self.reloadAct) + gotb.addAction(self.stopAct) + gotb.addAction(self.homeAct) + gotb.addSeparator() + + self.__navigationSplitter = QSplitter(gotb) + self.__navigationSplitter.addWidget(self.__tabWidget.stackedUrlBar()) + + from .WebBrowserWebSearchWidget import WebBrowserWebSearchWidget + self.searchEdit = WebBrowserWebSearchWidget(self) + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(2) + sizePolicy.setVerticalStretch(0) + self.searchEdit.setSizePolicy(sizePolicy) + self.searchEdit.search.connect(self.__linkActivated) + self.__navigationSplitter.addWidget(self.searchEdit) + gotb.addWidget(self.__navigationSplitter) + + self.__navigationSplitter.setSizePolicy( + QSizePolicy.Expanding, QSizePolicy.Maximum) + self.__navigationSplitter.setCollapsible(0, False) + + self.backMenu = QMenu(self) + self.backMenu.aboutToShow.connect(self.__showBackMenu) + self.backMenu.triggered.connect(self.__navigationMenuActionTriggered) + backButton = gotb.widgetForAction(self.backAct) + backButton.setMenu(self.backMenu) + backButton.setPopupMode(QToolButton.MenuButtonPopup) + + self.forwardMenu = QMenu(self) + self.forwardMenu.aboutToShow.connect(self.__showForwardMenu) + self.forwardMenu.triggered.connect( + self.__navigationMenuActionTriggered) + forwardButton = gotb.widgetForAction(self.forwardAct) + forwardButton.setMenu(self.forwardMenu) + forwardButton.setPopupMode(QToolButton.MenuButtonPopup) + + from .Bookmarks.BookmarksToolBar import BookmarksToolBar + bookmarksModel = self.bookmarksManager().bookmarksModel() + self.bookmarksToolBar = BookmarksToolBar(self, bookmarksModel, self) + self.bookmarksToolBar.setObjectName("BookmarksToolBar") + self.bookmarksToolBar.setIconSize(UI.Config.ToolBarIconSize) + self.bookmarksToolBar.openUrl.connect(self.openUrl) + self.bookmarksToolBar.newUrl.connect(self.openUrlNewTab) + self.addToolBarBreak() + self.addToolBar(self.bookmarksToolBar) + + self.addToolBarBreak() + vttb = self.addToolBar(self.tr("VirusTotal")) + vttb.setObjectName("VirusTotalToolBar") + vttb.setIconSize(UI.Config.ToolBarIconSize) + vttb.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + self.virustotalScanCurrentAct = vttb.addAction( + UI.PixmapCache.getIcon("virustotal.png"), + self.tr("Scan current site"), + self.__virusTotalScanCurrentSite) + self.virustotalIpReportAct = vttb.addAction( + UI.PixmapCache.getIcon("virustotal.png"), + self.tr("IP Address Report"), + self.__virusTotalIpAddressReport) + self.virustotalDomainReportAct = vttb.addAction( + UI.PixmapCache.getIcon("virustotal.png"), + self.tr("Domain Report"), + self.__virusTotalDomainReport) + if not Preferences.getWebBrowser("VirusTotalEnabled") or \ + Preferences.getWebBrowser("VirusTotalServiceKey") == "": + self.virustotalScanCurrentAct.setEnabled(False) + self.virustotalIpReportAct.setEnabled(False) + self.virustotalDomainReportAct.setEnabled(False) + + def __nextTab(self): + """ + Private slot used to show the next tab. + """ + fwidget = QApplication.focusWidget() + while fwidget and not hasattr(fwidget, 'nextTab'): + fwidget = fwidget.parent() + if fwidget: + fwidget.nextTab() + + def __prevTab(self): + """ + Private slot used to show the previous tab. + """ + fwidget = QApplication.focusWidget() + while fwidget and not hasattr(fwidget, 'prevTab'): + fwidget = fwidget.parent() + if fwidget: + fwidget.prevTab() + + def __switchTab(self): + """ + Private slot used to switch between the current and the previous + current tab. + """ + fwidget = QApplication.focusWidget() + while fwidget and not hasattr(fwidget, 'switchTab'): + fwidget = fwidget.parent() + if fwidget: + fwidget.switchTab() + + def __whatsThis(self): + """ + Private slot called in to enter Whats This mode. + """ + QWhatsThis.enterWhatsThisMode() + + def __titleChanged(self, browser, title): + """ + Private slot called to handle a change of a browser's title. + + @param browser reference to the browser (WebBrowserView) + @param title new title (string) + """ + self.historyManager().updateHistoryEntry( + browser.url().toString(), title) + + @pyqtSlot() + def newTab(self, link=None, addNextTo=None): + """ + Public slot called to open a new web browser tab. + + @param link file to be displayed in the new window (string or QUrl) + @param addNextTo reference to the browser to open the tab after + (HelpBrowser) + """ + if addNextTo: + self.__tabWidget.newBrowserAfter(addNextTo, link) + else: + self.__tabWidget.newBrowser(link) + + @pyqtSlot() + def newWindow(self, link=None): + """ + Public slot called to open a new web browser window. + + @param link URL to be displayed in the new window + @type str or QUrl + """ + if link is None: + linkName = "" + elif isinstance(link, QUrl): + linkName = link.toString() + else: + linkName = link + h = WebBrowserWindow(linkName, ".", self.parent(), "webbrowser", + self.__fromEric, private=self.isPrivate()) + h.show() + + @pyqtSlot() + def newPrivateWindow(self, link=None): + """ + Public slot called to open a new private web browser window. + + @param link URL to be displayed in the new window + @type str or QUrl + """ + if link is None: + linkName = "" + elif isinstance(link, QUrl): + linkName = link.toString() + else: + linkName = link + + applPath = os.path.join(getConfig("ericDir"), "eric6_browser.py") + args = [] + args.append(applPath) + args.append("--config={0}".format(Utilities.getConfigDir())) + if self.__settingsDir: + args.append("--settings={0}".format(self.__settingsDir)) + args.append("--private") + if linkName: + args.append(linkName) + + if not os.path.isfile(applPath) or \ + not QProcess.startDetached(sys.executable, args): + E5MessageBox.critical( + self, + self.tr('New Private Window'), + self.tr( + '<p>Could not start the process.<br>' + 'Ensure that it is available as <b>{0}</b>.</p>' + ).format(applPath), + self.tr('OK')) + + def __openFile(self): + """ + Private slot called to open a file. + """ + fn = E5FileDialog.getOpenFileName( + self, + self.tr("Open File"), + "", + self.tr("Help Files (*.html *.htm);;" + "PDF Files (*.pdf);;" + "CHM Files (*.chm);;" + "All Files (*)" + )) + if fn: + if Utilities.isWindowsPlatform(): + url = "file:///" + Utilities.fromNativeSeparators(fn) + else: + url = "file://" + fn + self.currentBrowser().setSource(QUrl(url)) + + def __openFileNewTab(self): + """ + Private slot called to open a file in a new tab. + """ + fn = E5FileDialog.getOpenFileName( + self, + self.tr("Open File"), + "", + self.tr("Help Files (*.html *.htm);;" + "PDF Files (*.pdf);;" + "CHM Files (*.chm);;" + "All Files (*)" + )) + if fn: + if Utilities.isWindowsPlatform(): + url = "file:///" + Utilities.fromNativeSeparators(fn) + else: + url = "file://" + fn + self.newTab(url) + + # TODO: Qt 5.7: Save +## def __savePageAs(self): +## """ +## Private slot to save the current page. +## """ +## browser = self.currentBrowser() +## if browser is not None: +## browser.saveAs() +## + @pyqtSlot() + def __savePageScreen(self, visibleOnly=False): + """ + Private slot to save the current page as a screen shot. + + @param visibleOnly flag indicating to just save the visible part + of the page (boolean) + """ + from .PageScreenDialog import PageScreenDialog + self.__pageScreen = PageScreenDialog( + self.currentBrowser(), visibleOnly=visibleOnly) + self.__pageScreen.show() + + @pyqtSlot() + def __saveVisiblePageScreen(self): + """ + Private slot to save the visible part of the current page as a screen + shot. + """ + self.__savePageScreen(visibleOnly=True) + + def __about(self): + """ + Private slot to show the about information. + """ + chromeVersion, webengineVersion = \ + WebBrowserTools.getWebEngineVersions() + E5MessageBox.about( + self, + self.tr("eric6 Web Browser"), + self.tr( + """<b>eric6 Web Browser - {0}</b>""" + """<p>The eric6 Web Browser is a combined help file and HTML""" + """ browser. It is part of the eric6 development""" + """ toolset.</p>""" + """<p>It is based on QtWebEngine {1} and Chrome {2}.</p>""" + ).format(Version, webengineVersion, chromeVersion)) + + def __aboutQt(self): + """ + Private slot to show info about Qt. + """ + E5MessageBox.aboutQt(self, self.tr("eric6 Web Browser")) + + def setBackwardAvailable(self, b): + """ + Public slot called when backward references are available. + + @param b flag indicating availability of the backwards action (boolean) + """ + self.backAct.setEnabled(b) + + def setForwardAvailable(self, b): + """ + Public slot called when forward references are available. + + @param b flag indicating the availability of the forwards action + (boolean) + """ + self.forwardAct.setEnabled(b) + + def setLoadingActions(self, b): + """ + Public slot to set the loading dependent actions. + + @param b flag indicating the loading state to consider (boolean) + """ + self.reloadAct.setEnabled(not b) + self.stopAct.setEnabled(b) + + def __addBookmark(self): + """ + Private slot called to add the displayed file to the bookmarks. + """ + view = self.currentBrowser() + view.addBookmark() + urlStr = bytes(view.url().toEncoded()).decode() + title = view.title() + + script = Scripts.getAllMetaAttributes() + view.page().runJavaScript( + script, + lambda res: self.__addBookmarkCallback(urlStr, title, res)) + + def __addBookmarkCallback(self, url, title, res): + """ + Private callback method of __addBookmark(). + + @param url URL for the bookmark + @type str + @param title title for the bookmark + @type str + @param res result of the JavaScript + @type list + """ + description = "" + for meta in res: + if meta["name"] == "description": + description = meta["content"] + + from .Bookmarks.AddBookmarkDialog import AddBookmarkDialog + dlg = AddBookmarkDialog() + dlg.setUrl(url) + dlg.setTitle(title) + dlg.setDescription(description) + menu = self.bookmarksManager().menu() + idx = self.bookmarksManager().bookmarksModel().nodeIndex(menu) + dlg.setCurrentIndex(idx) + dlg.exec_() + + def __addBookmarkFolder(self): + """ + Private slot to add a new bookmarks folder. + """ + from .Bookmarks.AddBookmarkDialog import AddBookmarkDialog + dlg = AddBookmarkDialog() + menu = self.bookmarksManager().menu() + idx = self.bookmarksManager().bookmarksModel().nodeIndex(menu) + dlg.setCurrentIndex(idx) + dlg.setFolder(True) + dlg.exec_() + + def __showBookmarksDialog(self): + """ + Private slot to show the bookmarks dialog. + """ + from .Bookmarks.BookmarksDialog import BookmarksDialog + self.__bookmarksDialog = BookmarksDialog(self) + self.__bookmarksDialog.openUrl.connect(self.openUrl) + self.__bookmarksDialog.newUrl.connect(self.openUrlNewTab) + self.__bookmarksDialog.show() + + def bookmarkAll(self): + """ + Public slot to bookmark all open tabs. + """ + from .Bookmarks.AddBookmarkDialog import AddBookmarkDialog + dlg = AddBookmarkDialog() + dlg.setFolder(True) + dlg.setTitle(self.tr("Saved Tabs")) + dlg.exec_() + + folder = dlg.addedNode() + if folder is None: + return + + for view in self.__tabWidget.browsers(): + urlStr = bytes(view.url().toEncoded()).decode() + title = view.title() + + script = Scripts.getAllMetaAttributes() + view.page().runJavaScript( + script, + lambda res: self.__bookmarkAllCallback(folder, urlStr, + title, res)) + + def __bookmarkAllCallback(self, folder, url, title, res): + """ + Private callback method of __addBookmark(). + + @param folder reference to the bookmarks folder + @type BookmarkNode + @param url URL for the bookmark + @type str + @param title title for the bookmark + @type str + @param res result of the JavaScript + @type list + """ + description = "" + for meta in res: + if meta["name"] == "description": + description = meta["content"] + + from .Bookmarks.BookmarkNode import BookmarkNode + bookmark = BookmarkNode(BookmarkNode.Bookmark) + bookmark.url = url + bookmark.title = title + bookmark.desc = description + + self.bookmarksManager().addBookmark(folder, bookmark) + + def __find(self): + """ + Private slot to handle the find action. + + It opens the search dialog in order to perform the various + search actions and to collect the various search info. + """ + self.__searchWidget.showFind() + + def __closeAllWindows(self): + """ + Private slot to close all windows. + """ + for browser in WebBrowserWindow.BrowserWindows: + if browser != self: + browser.close() + self.close() + + def closeEvent(self, e): + """ + Protected event handler for the close event. + + @param e the close event (QCloseEvent) + <br />This event is simply accepted after the history has been + saved and all window references have been deleted. + """ + if not self.__shutdownCalled: + res = self.shutdown() + + if res: + e.accept() + self.webBrowserClosed.emit() + else: + e.ignore() + else: + e.accept() + + def shutdown(self): + """ + Public method to shut down the web browser. + + @return flag indicating successful shutdown (boolean) + """ + if not self.__tabWidget.shallShutDown(): + return False + + if not self.downloadManager().allowQuit(): + return False + + self.downloadManager().shutdown() + + self.cookieJar().close() + + self.bookmarksToolBar.setModel(None) + self.bookmarksManager().close() + + self.historyManager().close() + + self.passwordManager().close() + + self.adBlockManager().close() + + self.userAgentsManager().close() + + self.speedDial().close() + + self.syncManager().close() + + ZoomManager.instance().close() + + WebIconProvider.instance().close() + + self.__virusTotal.close() + + self.flashCookieManager().shutdown() + + self.searchEdit.openSearchManager().close() + + if WebBrowserWindow.useQtHelp: + self.__searchEngine.cancelIndexing() + self.__searchEngine.cancelSearching() + + if self.__helpInstaller: + self.__helpInstaller.stop() + + self.searchEdit.saveSearches() + + self.__tabWidget.closeAllBrowsers(shutdown=True) + + state = self.saveState() + Preferences.setWebBrowser("WebBrowserState", state) + + if Preferences.getWebBrowser("SaveGeometry"): + if not self.isFullScreen(): + Preferences.setGeometry("WebBrowserGeometry", + self.saveGeometry()) + else: + Preferences.setGeometry("WebBrowserGeometry", QByteArray()) + + try: + if self.__fromEric or len(WebBrowserWindow.BrowserWindows) > 1: + del WebBrowserWindow.BrowserWindows[ + WebBrowserWindow.BrowserWindows.index(self)] + except ValueError: + pass + + self.networkManager().shutdown() + + if not self.__fromEric: + Preferences.syncPreferences() + + self.__shutdownCalled = True + return True + + def __backward(self): + """ + Private slot called to handle the backward action. + """ + self.currentBrowser().backward() + + def __forward(self): + """ + Private slot called to handle the forward action. + """ + self.currentBrowser().forward() + + def __home(self): + """ + Private slot called to handle the home action. + """ + self.currentBrowser().home() + + def __reload(self): + """ + Private slot called to handle the reload action. + """ + self.currentBrowser().reloadBypassingCache() + + def __stopLoading(self): + """ + Private slot called to handle loading of the current page. + """ + self.currentBrowser().stop() + + def __zoomValueChanged(self, value): + """ + Private slot to handle value changes of the zoom widget. + + @param value zoom value (integer) + """ + self.currentBrowser().setZoomValue(value) + + def __zoomIn(self): + """ + Private slot called to handle the zoom in action. + """ + self.currentBrowser().zoomIn() + self.__zoomWidget.setValue(self.currentBrowser().zoomValue()) + + def __zoomOut(self): + """ + Private slot called to handle the zoom out action. + """ + self.currentBrowser().zoomOut() + self.__zoomWidget.setValue(self.currentBrowser().zoomValue()) + + def __zoomReset(self): + """ + Private slot called to handle the zoom reset action. + """ + self.currentBrowser().zoomReset() + self.__zoomWidget.setValue(self.currentBrowser().zoomValue()) + + def __viewFullScreen(self): + """ + Private slot called to toggle fullscreen mode. + """ + if self.__htmlFullScreen: + self.currentBrowser().triggerPageAction( + QWebEnginePage.ExitFullScreen) + return + + if self.isFullScreen(): + # switch back to normal + self.showNormal() + self.menuBar().show() + self.fullScreenAct.setIcon( + UI.PixmapCache.getIcon("windowFullscreen.png")) + self.fullScreenAct.setIconText(self.tr('Full Screen')) + else: + # switch to full screen + self.showFullScreen() + self.menuBar().hide() + self.fullScreenAct.setIcon( + UI.PixmapCache.getIcon("windowRestore.png")) + self.fullScreenAct.setIconText(self.tr('Restore Window')) + + def enterHtmlFullScreen(self): + """ + Public method to switch to full screen initiated by the + HTML page. + """ + self.showFullScreen() + self.__htmlFullScreen = True + + def __copy(self): + """ + Private slot called to handle the copy action. + """ + self.currentBrowser().copy() + + def __cut(self): + """ + Private slot called to handle the cut action. + """ + self.currentBrowser().cut() + + def __paste(self): + """ + Private slot called to handle the paste action. + """ + self.currentBrowser().paste() + + def __undo(self): + """ + Private slot to handle the undo action. + """ + self.currentBrowser().undo() + + def __redo(self): + """ + Private slot to handle the redo action. + """ + self.currentBrowser().redo() + + def __selectAll(self): + """ + Private slot to handle the select all action. + """ + self.currentBrowser().selectAll() + + @classmethod + def isPrivate(cls): + """ + Public method to check the private browsing mode. + + @return flag indicating private browsing mode + @rtype bool + """ + return cls._isPrivate + + def currentBrowser(self): + """ + Public method to get a reference to the current web browser. + + @return reference to the current help browser (WebBrowserView) + """ + return self.__tabWidget.currentBrowser() + + def browserAt(self, index): + """ + Public method to get a reference to the web browser with the given + index. + + @param index index of the browser to get (integer) + @return reference to the indexed web browser (WebBrowserView) + """ + return self.__tabWidget.browserAt(index) + + def browsers(self): + """ + Public method to get a list of references to all web browsers. + + @return list of references to web browsers (list of WebBrowserView) + """ + return self.__tabWidget.browsers() + + def __currentChanged(self, index): + """ + Private slot to handle the currentChanged signal. + + @param index index of the current tab (integer) + """ + if index > -1: + cb = self.currentBrowser() + if cb is not None: + self.setForwardAvailable(cb.isForwardAvailable()) + self.setBackwardAvailable(cb.isBackwardAvailable()) + self.setLoadingActions(cb.isLoading()) + + # set value of zoom widget + self.__zoomWidget.setValue(cb.zoomValue()) + + def __showPreferences(self): + """ + Private slot to set the preferences. + """ + from Preferences.ConfigurationDialog import ConfigurationDialog + dlg = ConfigurationDialog( + self, 'Configuration', True, fromEric=self.__fromEric, + displayMode=ConfigurationDialog.WebBrowserMode) + dlg.preferencesChanged.connect(self.preferencesChanged) + dlg.masterPasswordChanged.connect(self.masterPasswordChanged) + dlg.show() + if self.__lastConfigurationPageName: + dlg.showConfigurationPageByName(self.__lastConfigurationPageName) + else: + dlg.showConfigurationPageByName("empty") + dlg.exec_() + QApplication.processEvents() + if dlg.result() == QDialog.Accepted: + dlg.setPreferences() + Preferences.syncPreferences() + self.preferencesChanged() + self.__lastConfigurationPageName = dlg.getConfigurationPageName() + + def preferencesChanged(self): + """ + Public slot to handle a change of preferences. + """ + if not self.__fromEric: + self.setStyle(Preferences.getUI("Style"), + Preferences.getUI("StyleSheet")) + + self.__initWebEngineSettings() + + self.networkManager().preferencesChanged() + + self.historyManager().preferencesChanged() + + self.__tabWidget.preferencesChanged() + + self.searchEdit.preferencesChanged() + + if not self.isPrivate(): + profile = self.webProfile() + if Preferences.getWebBrowser("DiskCacheEnabled"): + profile.setHttpCacheType(QWebEngineProfile.DiskHttpCache) + profile.setHttpCacheMaximumSize( + Preferences.getWebBrowser("DiskCacheSize") * 1024 * 1024) + else: + profile.setHttpCacheType(QWebEngineProfile.MemoryHttpCache) + profile.setHttpCacheMaximumSize(0) + + self.__virusTotal.preferencesChanged() + if not Preferences.getWebBrowser("VirusTotalEnabled") or \ + Preferences.getWebBrowser("VirusTotalServiceKey") == "": + self.virustotalScanCurrentAct.setEnabled(False) + self.virustotalIpReportAct.setEnabled(False) + self.virustotalDomainReportAct.setEnabled(False) + else: + self.virustotalScanCurrentAct.setEnabled(True) + self.virustotalIpReportAct.setEnabled(True) + self.virustotalDomainReportAct.setEnabled(True) + + def masterPasswordChanged(self, oldPassword, newPassword): + """ + Public slot to handle the change of the master password. + + @param oldPassword current master password (string) + @param newPassword new master password (string) + """ + from Preferences.ConfigurationDialog import ConfigurationDialog + self.passwordManager().masterPasswordChanged(oldPassword, newPassword) + if self.__fromEric and isinstance(self.sender(), ConfigurationDialog): + # we were called from our local configuration dialog + Preferences.convertPasswords(oldPassword, newPassword) + Utilities.crypto.changeRememberedMaster(newPassword) + + def __showAcceptedLanguages(self): + """ + Private slot to configure the accepted languages for web pages. + """ + from .WebBrowserLanguagesDialog import WebBrowserLanguagesDialog + dlg = WebBrowserLanguagesDialog(self) + dlg.exec_() + self.networkManager().languagesChanged() + + def __showCookiesConfiguration(self): + """ + Private slot to configure the cookies handling. + """ + from .CookieJar.CookiesConfigurationDialog import \ + CookiesConfigurationDialog + dlg = CookiesConfigurationDialog(self) + dlg.exec_() + + def __showFlashCookiesManagement(self): + """ + Private slot to show the flash cookies management dialog. + """ + self.flashCookieManager().showFlashCookieManagerDialog() + + @classmethod + def setUseQtHelp(cls, use): + """ + Class method to set the QtHelp usage. + + @param use flag indicating usage (boolean) + """ + if use: + cls.useQtHelp = use and QTHELP_AVAILABLE + else: + cls.useQtHelp = False + + @classmethod + def helpEngine(cls): + """ + Class method to get a reference to the help engine. + + @return reference to the help engine (QHelpEngine) + """ + if cls.useQtHelp: + if cls._helpEngine is None: + cls._helpEngine = \ + QHelpEngine(os.path.join(Utilities.getConfigDir(), + "web_browser", "eric6help.qhc")) + return cls._helpEngine + else: + return None + + @classmethod + def networkManager(cls): + """ + Class method to get a reference to the network manager object. + + @return reference to the network access manager (NetworkManager) + """ + if cls._networkManager is None: + from .Network.NetworkManager import NetworkManager + cls._networkManager = NetworkManager(cls.helpEngine()) + + return cls._networkManager + + @classmethod + def cookieJar(cls): + """ + Class method to get a reference to the cookie jar. + + @return reference to the cookie jar (CookieJar) + """ + if cls._cookieJar is None: + from .CookieJar.CookieJar import CookieJar + cls._cookieJar = CookieJar() + + return cls._cookieJar + + def __clearIconsDatabase(self): + """ + Private slot to clear the favicons databse. + """ + WebIconProvider.instance().clear() + + def __showWebIconsDialog(self): + """ + Private slot to show a dialog to manage the favicons database. + """ + WebIconProvider.instance().showWebIconDialog() + + @pyqtSlot(QUrl) + def __linkActivated(self, url): + """ + Private slot to handle the selection of a link. + + @param url URL to be shown (QUrl) + """ + if not self.__activating: + self.__activating = True + self.currentBrowser().setUrl(url) + self.__activating = False + + def __linksActivated(self, links, keyword): + """ + Private slot to select a topic to be shown. + + @param links dictionary with help topic as key (string) and + URL as value (QUrl) + @param keyword keyword for the link set (string) + """ + if not self.__activating: + from .QtHelp.HelpTopicDialog import HelpTopicDialog + self.__activating = True + dlg = HelpTopicDialog(self, keyword, links) + if dlg.exec_() == QDialog.Accepted: + self.currentBrowser().setSource(dlg.link()) + self.__activating = False + + def __activateCurrentBrowser(self): + """ + Private slot to activate the current browser. + """ + self.currentBrowser().setFocus() + + def __syncTOC(self): + """ + Private slot to synchronize the TOC with the currently shown page. + """ + if WebBrowserWindow.UseQtHelp: + QApplication.setOverrideCursor(Qt.WaitCursor) + url = self.currentBrowser().source() + self.__showTocWindow() + if not self.__tocWindow.syncToContent(url): + self.statusBar().showMessage( + self.tr("Could not find an associated content."), 5000) + QApplication.restoreOverrideCursor() + + def __showTocWindow(self): + """ + Private method to show the table of contents window. + """ + if WebBrowserWindow.useQtHelp: + self.__activateDock(self.__tocWindow) + + def __showIndexWindow(self): + """ + Private method to show the index window. + """ + if WebBrowserWindow.useQtHelp: + self.__activateDock(self.__indexWindow) + + def __showSearchWindow(self): + """ + Private method to show the search window. + """ + if WebBrowserWindow.useQtHelp: + self.__activateDock(self.__searchWindow) + + def __activateDock(self, widget): + """ + Private method to activate the dock widget of the given widget. + + @param widget reference to the widget to be activated (QWidget) + """ + widget.parent().show() + widget.parent().raise_() + widget.setFocus() + + def __setupFilterCombo(self): + """ + Private slot to setup the filter combo box. + """ + if WebBrowserWindow.useQtHelp: + curFilter = self.filterCombo.currentText() + if not curFilter: + curFilter = self.__helpEngine.currentFilter() + self.filterCombo.clear() + self.filterCombo.addItems(self.__helpEngine.customFilters()) + idx = self.filterCombo.findText(curFilter) + if idx < 0: + idx = 0 + self.filterCombo.setCurrentIndex(idx) + + def __filterQtHelpDocumentation(self, customFilter): + """ + Private slot to filter the QtHelp documentation. + + @param customFilter name of filter to be applied (string) + """ + if self.__helpEngine: + self.__helpEngine.setCurrentFilter(customFilter) + + def __manageQtHelpDocumentation(self): + """ + Private slot to manage the QtHelp documentation database. + """ + if WebBrowserWindow.useQtHelp: + from .QtHelp.QtHelpDocumentationDialog import \ + QtHelpDocumentationDialog + dlg = QtHelpDocumentationDialog(self.__helpEngine, self) + dlg.exec_() + if dlg.hasChanges(): + for i in sorted(dlg.getTabsToClose(), reverse=True): + self.__tabWidget.closeBrowserAt(i) + self.__helpEngine.setupData() + + def getSourceFileList(self): + """ + Public method to get a list of all opened source files. + + @return dictionary with tab id as key and host/namespace as value + """ + return self.__tabWidget.getSourceFileList() + + def __manageQtHelpFilters(self): + """ + Private slot to manage the QtHelp filters. + """ + if WebBrowserWindow.useQtHelp: + from .QtHelp.QtHelpFiltersDialog import QtHelpFiltersDialog + dlg = QtHelpFiltersDialog(self.__helpEngine, self) + dlg.exec_() + + def __indexingStarted(self): + """ + Private slot to handle the start of the indexing process. + """ + if WebBrowserWindow.useQtHelp: + self.__indexing = True + if self.__indexingProgress is None: + self.__indexingProgress = QWidget() + layout = QHBoxLayout(self.__indexingProgress) + layout.setContentsMargins(0, 0, 0, 0) + sizePolicy = QSizePolicy(QSizePolicy.Preferred, + QSizePolicy.Maximum) + + label = QLabel(self.tr("Updating search index")) + label.setSizePolicy(sizePolicy) + layout.addWidget(label) + + progressBar = QProgressBar() + progressBar.setRange(0, 0) + progressBar.setTextVisible(False) + progressBar.setFixedHeight(16) + progressBar.setSizePolicy(sizePolicy) + layout.addWidget(progressBar) + + self.statusBar().insertPermanentWidget( + 0, self.__indexingProgress) + + def __indexingFinished(self): + """ + Private slot to handle the start of the indexing process. + """ + if WebBrowserWindow.useQtHelp: + self.statusBar().removeWidget(self.__indexingProgress) + self.__indexingProgress = None + self.__indexing = False + if self.__searchWord is not None: + self.__searchForWord() + + def __searchForWord(self): + """ + Private slot to search for a word. + """ + if WebBrowserWindow.useQtHelp and not self.__indexing and \ + self.__searchWord is not None: + self.__searchDock.show() + self.__searchDock.raise_() + query = QHelpSearchQuery(QHelpSearchQuery.DEFAULT, + [self.__searchWord]) + self.__searchEngine.search([query]) + self.__searchWord = None + + def search(self, word): + """ + Public method to search for a word. + + @param word word to search for (string) + """ + if WebBrowserWindow.useQtHelp: + self.__searchWord = word + self.__searchForWord() + + def __removeOldDocumentation(self): + """ + Private slot to remove non-existing documentation from the help engine. + """ + for namespace in self.__helpEngine.registeredDocumentations(): + docFile = self.__helpEngine.documentationFileName(namespace) + if not os.path.exists(docFile): + self.__helpEngine.unregisterDocumentation(namespace) + + def __lookForNewDocumentation(self): + """ + Private slot to look for new documentation to be loaded into the + help database. + """ + if WebBrowserWindow.useQtHelp: + from .QtHelp.HelpDocsInstaller import HelpDocsInstaller + self.__helpInstaller = HelpDocsInstaller( + self.__helpEngine.collectionFile()) + self.__helpInstaller.errorMessage.connect( + self.__showInstallationError) + self.__helpInstaller.docsInstalled.connect(self.__docsInstalled) + + self.statusBar().showMessage( + self.tr("Looking for Documentation...")) + self.__helpInstaller.installDocs() + + def __showInstallationError(self, message): + """ + Private slot to show installation errors. + + @param message message to be shown (string) + """ + E5MessageBox.warning( + self, + self.tr("eric6 Web Browser"), + message) + + def __docsInstalled(self, installed): + """ + Private slot handling the end of documentation installation. + + @param installed flag indicating that documents were installed + (boolean) + """ + if WebBrowserWindow.useQtHelp: + if installed: + self.__helpEngine.setupData() + self.statusBar().clearMessage() + + def __initHelpDb(self): + """ + Private slot to initialize the documentation database. + """ + if WebBrowserWindow.useQtHelp: + if not self.__helpEngine.setupData(): + return + + unfiltered = self.tr("Unfiltered") + if unfiltered not in self.__helpEngine.customFilters(): + hc = QHelpEngineCore(self.__helpEngine.collectionFile()) + hc.setupData() + hc.addCustomFilter(unfiltered, []) + hc = None + del hc + + self.__helpEngine.blockSignals(True) + self.__helpEngine.setCurrentFilter(unfiltered) + self.__helpEngine.blockSignals(False) + self.__helpEngine.setupData() + + def __warning(self, msg): + """ + Private slot handling warnings from the help engine. + + @param msg message sent by the help engine (string) + """ + E5MessageBox.warning( + self, + self.tr("Help Engine"), msg) + + def __aboutToShowSettingsMenu(self): + """ + Private slot to show the Settings menu. + """ + self.editMessageFilterAct.setEnabled( + E5ErrorMessage.messageHandlerInstalled()) + + def __showBackMenu(self): + """ + Private slot showing the backwards navigation menu. + """ + self.backMenu.clear() + history = self.currentBrowser().history() + historyCount = history.count() + backItems = history.backItems(historyCount) + for index in range(len(backItems) - 1, -1, -1): + item = backItems[index] + act = QAction(self) + act.setData(-1 * (index + 1)) + icon = WebBrowserWindow.icon(item.url()) + act.setIcon(icon) + act.setText(item.title()) + self.backMenu.addAction(act) + + def __showForwardMenu(self): + """ + Private slot showing the forwards navigation menu. + """ + self.forwardMenu.clear() + history = self.currentBrowser().history() + historyCount = history.count() + forwardItems = history.forwardItems(historyCount) + for index in range(len(forwardItems)): + item = forwardItems[index] + act = QAction(self) + act.setData(index + 1) + icon = WebBrowserWindow.icon(item.url()) + act.setIcon(icon) + act.setText(item.title()) + self.forwardMenu.addAction(act) + + def __navigationMenuActionTriggered(self, act): + """ + Private slot to go to the selected page. + + @param act reference to the action selected in the navigation menu + (QAction) + """ + offset = act.data() + history = self.currentBrowser().history() + historyCount = history.count() + if offset < 0: + # go back + history.goToItem(history.backItems(historyCount)[-1 * offset - 1]) + else: + # go forward + history.goToItem(history.forwardItems(historyCount)[offset - 1]) + + def __clearPrivateData(self): + """ + Private slot to clear the private data. + """ + from .WebBrowserClearPrivateDataDialog import \ + WebBrowserClearPrivateDataDialog + dlg = WebBrowserClearPrivateDataDialog(self) + if dlg.exec_() == QDialog.Accepted: + # browsing history, search history, favicons, disk cache, cookies, + # passwords, web databases, downloads, Flash cookies + (history, searches, favicons, cache, cookies, + passwords, databases, downloads, flashCookies, zoomValues, + sslExceptions, historyPeriod) = dlg.getData() + if history: + self.historyManager().clear(historyPeriod) + self.__tabWidget.clearClosedTabsList() + self.webProfile().clearAllVisitedLinks() + if searches: + self.searchEdit.clear() + if downloads: + self.downloadManager().cleanup() + self.downloadManager().hide() + if favicons: + self.__clearIconsDatabase() + if cache: + cachePath = self.webProfile().cachePath() + if cachePath: + shutil.rmtree(cachePath) + if cookies: + self.cookieJar().clear() + self.webProfile().cookieStore().deleteAllCookies() + if passwords: + self.passwordManager().clear() + if flashCookies: + self.flashCookieManager().removeAllCookies() + if zoomValues: + ZoomManager.instance().clear() + if sslExceptions: + self.networkManager().clearSslExceptions() + + def __showEnginesConfigurationDialog(self): + """ + Private slot to show the search engines configuration dialog. + """ + from .OpenSearch.OpenSearchDialog import OpenSearchDialog + + dlg = OpenSearchDialog(self) + dlg.exec_() + + def searchEnginesAction(self): + """ + Public method to get a reference to the search engines configuration + action. + + @return reference to the search engines configuration action (QAction) + """ + return self.searchEnginesAct + + def __showPasswordsDialog(self): + """ + Private slot to show the passwords management dialog. + """ + from .Passwords.PasswordsDialog import PasswordsDialog + + dlg = PasswordsDialog(self) + dlg.exec_() + + def __showCertificateErrorsDialog(self): + """ + Private slot to show the certificate errors management dialog. + """ + self.networkManager().showSslErrorExceptionsDialog() + + def __showAdBlockDialog(self): + """ + Private slot to show the AdBlock configuration dialog. + """ + self.adBlockManager().showDialog() + + def __showPersonalInformationDialog(self): + """ + Private slot to show the Personal Information configuration dialog. + """ + self.personalInformationManager().showConfigurationDialog() + + def __showGreaseMonkeyConfigDialog(self): + """ + Private slot to show the GreaseMonkey scripts configuration dialog. + """ + self.greaseMonkeyManager().showConfigurationDialog() + + def __showFeaturePermissionDialog(self): + """ + Private slot to show the feature permission dialog. + """ + self.featurePermissionManager().showFeaturePermissionsDialog() + + def __showZoomValuesDialog(self): + """ + Private slot to show the zoom values management dialog. + """ + from .ZoomManager.ZoomValuesDialog import ZoomValuesDialog + + dlg = ZoomValuesDialog(self) + dlg.exec_() + + def __showDownloadsWindow(self): + """ + Private slot to show the downloads dialog. + """ + self.downloadManager().show() + + def __showPageSource(self): + """ + Private slot to show the source of the current page in an editor. + """ + self.currentBrowser().page().toHtml(self.__showPageSourceCallback) + + def __showPageSourceCallback(self, src): + """ + Private method to show the source of the current page in an editor. + + @param src source of the web page + @type str + """ + from QScintilla.MiniEditor import MiniEditor + editor = MiniEditor(parent=self) + editor.setText(src, "Html") + editor.setLanguage("dummy.html") + editor.show() + + def __toggleJavaScriptConsole(self): + """ + Private slot to toggle the JavaScript console. + """ + if self.__javascriptConsoleDock.isVisible(): + self.__javascriptConsoleDock.hide() + else: + self.__javascriptConsoleDock.show() + + def javascriptConsole(self): + """ + Public method to get a reference to the JavaScript console widget. + + @return reference to the JavaScript console + @rtype WebBrowserJavaScriptConsole + """ + return self.__javascriptConsole + + @classmethod + def icon(cls, url): + """ + Class method to get the icon for an URL. + + @param url URL to get icon for (QUrl) + @return icon for the URL (QIcon) + """ + return WebIconProvider.instance().iconForUrl(url) + + @classmethod + def bookmarksManager(cls): + """ + Class method to get a reference to the bookmarks manager. + + @return reference to the bookmarks manager (BookmarksManager) + """ + if cls._bookmarksManager is None: + from .Bookmarks.BookmarksManager import BookmarksManager + cls._bookmarksManager = BookmarksManager() + + return cls._bookmarksManager + + def openUrl(self, url, title): + """ + Public slot to load a URL in the current tab. + + @param url URL to be opened (QUrl) + @param title title of the bookmark (string) + """ + self.__linkActivated(url) + + def openUrlNewTab(self, url, title): + """ + Public slot to load a URL in a new tab. + + @param url URL to be opened (QUrl) + @param title title of the bookmark (string) + """ + self.newTab(url) + + @classmethod + def historyManager(cls): + """ + Class method to get a reference to the history manager. + + @return reference to the history manager (HistoryManager) + """ + if cls._historyManager is None: + from .History.HistoryManager import HistoryManager + cls._historyManager = HistoryManager() + + return cls._historyManager + + @classmethod + def passwordManager(cls): + """ + Class method to get a reference to the password manager. + + @return reference to the password manager (PasswordManager) + """ + if cls._passwordManager is None: + from .Passwords.PasswordManager import PasswordManager + cls._passwordManager = PasswordManager() + + return cls._passwordManager + + @classmethod + def adBlockManager(cls): + """ + Class method to get a reference to the AdBlock manager. + + @return reference to the AdBlock manager (AdBlockManager) + """ + if cls._adblockManager is None: + from .AdBlock.AdBlockManager import AdBlockManager + cls._adblockManager = AdBlockManager() + + return cls._adblockManager + + def adBlockIcon(self): + """ + Public method to get a reference to the AdBlock icon. + + @return reference to the AdBlock icon (AdBlockIcon) + """ + return self.__adBlockIcon + + @classmethod + def downloadManager(cls): + """ + Class method to get a reference to the download manager. + + @return reference to the download manager (DownloadManager) + """ + if cls._downloadManager is None: + from .Download.DownloadManager import DownloadManager + cls._downloadManager = DownloadManager() + + return cls._downloadManager + + @classmethod + def personalInformationManager(cls): + """ + Class method to get a reference to the personal information manager. + + @return reference to the personal information manager + (PersonalInformationManager) + """ + if cls._personalInformationManager is None: + from .PersonalInformationManager.PersonalInformationManager \ + import PersonalInformationManager + cls._personalInformationManager = PersonalInformationManager() + + return cls._personalInformationManager + + @classmethod + def greaseMonkeyManager(cls): + """ + Class method to get a reference to the GreaseMonkey manager. + + @return reference to the GreaseMonkey manager (GreaseMonkeyManager) + """ + if cls._greaseMonkeyManager is None: + from .GreaseMonkey.GreaseMonkeyManager import GreaseMonkeyManager + cls._greaseMonkeyManager = GreaseMonkeyManager() + + return cls._greaseMonkeyManager + + @classmethod + def featurePermissionManager(cls): + """ + Class method to get a reference to the feature permission manager. + + @return reference to the feature permission manager + @rtype FeaturePermissionManager + """ + if cls._featurePermissionManager is None: + from .FeaturePermissions.FeaturePermissionManager import \ + FeaturePermissionManager + cls._featurePermissionManager = FeaturePermissionManager() + + return cls._featurePermissionManager + + @classmethod + def flashCookieManager(cls): + """ + Class method to get a reference to the flash cookies manager. + + @return reference to the flash cookies manager + @rtype FlashCookieManager + """ + if cls._flashCookieManager is None: + from .FlashCookieManager.FlashCookieManager import \ + FlashCookieManager + cls._flashCookieManager = FlashCookieManager() + + return cls._flashCookieManager + + @classmethod + def mainWindow(cls): + """ + Class method to get a reference to the main window. + + @return reference to the main window (WebBrowserWindow) + """ + if cls.BrowserWindows: + return cls.BrowserWindows[0] + else: + return None + + @classmethod + def mainWindows(cls): + """ + Class method to get references to all main windows. + + @return references to all main window (list of WebBrowserWindow) + """ + return cls.BrowserWindows + + def __appFocusChanged(self, old, now): + """ + Private slot to handle a change of the focus. + + @param old reference to the widget, that lost focus (QWidget or None) + @param now reference to the widget having the focus (QWidget or None) + """ + if isinstance(now, WebBrowserWindow): + self.__lastActiveWindow = now + + def getWindow(self): + """ + Public method to get a reference to the most recent active + web browser window. + + @return reference to most recent web browser window + @rtype WebBrowserWindow + """ + if self.__lastActiveWindow: + return self.__lastActiveWindow + + return self.mainWindow() + + def openSearchManager(self): + """ + Public method to get a reference to the opensearch manager object. + + @return reference to the opensearch manager object (OpenSearchManager) + """ + return self.searchEdit.openSearchManager() + + def __aboutToShowTextEncodingMenu(self): + """ + Private slot to populate the text encoding menu. + """ + self.__textEncodingMenu.clear() + + codecs = [] + for codec in QTextCodec.availableCodecs(): + codecs.append(str(codec, encoding="utf-8").lower()) + codecs.sort() + + defaultTextEncoding = \ + QWebEngineSettings.globalSettings().defaultTextEncoding().lower() + if defaultTextEncoding in codecs: + currentCodec = defaultTextEncoding + else: + currentCodec = "" + + isDefaultEncodingUsed = True + isoMenu = QMenu(self.tr("ISO"), self.__textEncodingMenu) + winMenu = QMenu(self.tr("Windows"), self.__textEncodingMenu) + isciiMenu = QMenu(self.tr("ISCII"), self.__textEncodingMenu) + uniMenu = QMenu(self.tr("Unicode"), self.__textEncodingMenu) + otherMenu = QMenu(self.tr("Other"), self.__textEncodingMenu) + ibmMenu = QMenu(self.tr("IBM"), self.__textEncodingMenu) + + for codec in codecs: + if codec.startswith(("iso", "latin", "csisolatin")): + act = isoMenu.addAction(codec) + elif codec.startswith(("windows", "cp1")): + act = winMenu.addAction(codec) + elif codec.startswith("iscii"): + act = isciiMenu.addAction(codec) + elif codec.startswith("utf"): + act = uniMenu.addAction(codec) + elif codec.startswith(("ibm", "csibm", "cp")): + act = ibmMenu.addAction(codec) + else: + act = otherMenu.addAction(codec) + + act.setData(codec) + act.setCheckable(True) + if currentCodec == codec: + act.setChecked(True) + isDefaultEncodingUsed = False + + act = self.__textEncodingMenu.addAction( + self.tr("Default Encoding")) + act.setData("") + act.setCheckable(True) + act.setChecked(isDefaultEncodingUsed) + self.__textEncodingMenu.addMenu(uniMenu) + self.__textEncodingMenu.addMenu(isoMenu) + self.__textEncodingMenu.addMenu(winMenu) + self.__textEncodingMenu.addMenu(ibmMenu) + self.__textEncodingMenu.addMenu(isciiMenu) + self.__textEncodingMenu.addMenu(otherMenu) + + def __setTextEncoding(self, act): + """ + Private slot to set the selected text encoding as the default for + this session. + + @param act reference to the selected action (QAction) + """ + codec = act.data() + if codec == "": + QWebEngineSettings.globalSettings().setDefaultTextEncoding("") + else: + QWebEngineSettings.globalSettings().setDefaultTextEncoding(codec) + + def eventMouseButtons(self): + """ + Public method to get the last recorded mouse buttons. + + @return mouse buttons (Qt.MouseButtons) + """ + return self.__eventMouseButtons + + def eventKeyboardModifiers(self): + """ + Public method to get the last recorded keyboard modifiers. + + @return keyboard modifiers (Qt.KeyboardModifiers) + """ + return self.__eventKeyboardModifiers + + def setEventMouseButtons(self, buttons): + """ + Public method to record mouse buttons. + + @param buttons mouse buttons to record (Qt.MouseButtons) + """ + self.__eventMouseButtons = buttons + + def setEventKeyboardModifiers(self, modifiers): + """ + Public method to record keyboard modifiers. + + @param modifiers keyboard modifiers to record (Qt.KeyboardModifiers) + """ + self.__eventKeyboardModifiers = modifiers + + def mousePressEvent(self, evt): + """ + Protected method called by a mouse press event. + + @param evt reference to the mouse event (QMouseEvent) + """ + if evt.button() == Qt.XButton1: + self.currentBrowser().triggerPageAction(QWebEnginePage.Back) + elif evt.button() == Qt.XButton2: + self.currentBrowser().triggerPageAction(QWebEnginePage.Forward) + else: + super(WebBrowserWindow, self).mousePressEvent(evt) + + @classmethod + def feedsManager(cls): + """ + Class method to get a reference to the RSS feeds manager. + + @return reference to the RSS feeds manager (FeedsManager) + """ + if cls._feedsManager is None: + from .Feeds.FeedsManager import FeedsManager + cls._feedsManager = FeedsManager() + + return cls._feedsManager + + def __showFeedsManager(self): + """ + Private slot to show the feeds manager dialog. + """ + feedsManager = self.feedsManager() + feedsManager.openUrl.connect(self.openUrl) + feedsManager.newUrl.connect(self.openUrlNewTab) + feedsManager.rejected.connect(self.__feedsManagerClosed) + feedsManager.show() + + def __feedsManagerClosed(self): + """ + Private slot to handle closing the feeds manager dialog. + """ + feedsManager = self.sender() + feedsManager.openUrl.disconnect(self.openUrl) + feedsManager.newUrl.disconnect(self.openUrlNewTab) + feedsManager.rejected.disconnect(self.__feedsManagerClosed) + + def __showSiteinfoDialog(self): + """ + Private slot to show the site info dialog. + """ + from .SiteInfo.SiteInfoDialog import SiteInfoDialog + self.__siteinfoDialog = SiteInfoDialog(self.currentBrowser(), self) + self.__siteinfoDialog.show() + + @classmethod + def userAgentsManager(cls): + """ + Class method to get a reference to the user agents manager. + + @return reference to the user agents manager (UserAgentManager) + """ + if cls._userAgentsManager is None: + from .UserAgent.UserAgentManager import UserAgentManager + cls._userAgentsManager = UserAgentManager() + + return cls._userAgentsManager + + def __showUserAgentsDialog(self): + """ + Private slot to show the user agents management dialog. + """ + from .UserAgent.UserAgentsDialog import UserAgentsDialog + + dlg = UserAgentsDialog(self) + dlg.exec_() + + @classmethod + def syncManager(cls): + """ + Class method to get a reference to the data synchronization manager. + + @return reference to the data synchronization manager (SyncManager) + """ + if cls._syncManager is None: + from .Sync.SyncManager import SyncManager + cls._syncManager = SyncManager() + + return cls._syncManager + + def __showSyncDialog(self): + """ + Private slot to show the synchronization dialog. + """ + self.syncManager().showSyncDialog() + + @classmethod + def speedDial(cls): + """ + Class methdo to get a reference to the speed dial. + + @return reference to the speed dial (SpeedDial) + """ + if cls._speedDial is None: + from .SpeedDial.SpeedDial import SpeedDial + cls._speedDial = SpeedDial() + + return cls._speedDial + + def keyPressEvent(self, evt): + """ + Protected method to handle key presses. + + @param evt reference to the key press event (QKeyEvent) + """ + number = -1 + key = evt.key() + + if key == Qt.Key_1: + number = 1 + elif key == Qt.Key_2: + number = 2 + elif key == Qt.Key_3: + number = 3 + elif key == Qt.Key_4: + number = 4 + elif key == Qt.Key_5: + number = 5 + elif key == Qt.Key_6: + number = 6 + elif key == Qt.Key_7: + number = 7 + elif key == Qt.Key_8: + number = 8 + elif key == Qt.Key_9: + number = 9 + elif key == Qt.Key_0: + number = 10 + + if number != -1: + if evt.modifiers() == Qt.KeyboardModifiers(Qt.AltModifier): + if number == 10: + number = self.__tabWidget.count() + self.__tabWidget.setCurrentIndex(number - 1) + return + + if evt.modifiers() == Qt.KeyboardModifiers(Qt.MetaModifier): + url = self.speedDial().urlForShortcut(number - 1) + if url.isValid(): + self.__linkActivated(url) + return + + super(WebBrowserWindow, self).keyPressEvent(evt) + + def event(self, evt): + """ + Public method handling events. + + @param evt reference to the event + @type QEvent + @return flag indicating a handled event + @rtype bool + """ + if evt.type() == QEvent.WindowStateChange: + if not bool(evt.oldState() & Qt.WindowFullScreen) and \ + bool(self.windowState() & Qt.WindowFullScreen): + # enter full screen mode + self.__windowStates = evt.oldState() + elif bool(evt.oldState() & Qt.WindowFullScreen) and \ + not bool(self.windowState() & Qt.WindowFullScreen): + # leave full screen mode + self.setWindowState(self.__windowStates) + self.__htmlFullScreen = False + + return super(WebBrowserWindow, self).event(evt) + + ########################################################################### + ## Interface to VirusTotal below ## + ########################################################################### + + def __virusTotalScanCurrentSite(self): + """ + Private slot to ask VirusTotal for a scan of the URL of the current + browser. + """ + cb = self.currentBrowser() + if cb is not None: + url = cb.url() + if url.scheme() in ["http", "https", "ftp"]: + self.requestVirusTotalScan(url) + + def requestVirusTotalScan(self, url): + """ + Public method to submit a request to scan an URL by VirusTotal. + + @param url URL to be scanned (QUrl) + """ + self.__virusTotal.submitUrl(url) + + def __virusTotalSubmitUrlError(self, msg): + """ + Private slot to handle an URL scan submission error. + + @param msg error message (str) + """ + E5MessageBox.critical( + self, + self.tr("VirusTotal Scan"), + self.tr("""<p>The VirusTotal scan could not be""" + """ scheduled.<p>\n<p>Reason: {0}</p>""").format(msg)) + + def __virusTotalUrlScanReport(self, url): + """ + Private slot to initiate the display of the URL scan report page. + + @param url URL of the URL scan report page (string) + """ + self.newTab(url) + + def __virusTotalFileScanReport(self, url): + """ + Private slot to initiate the display of the file scan report page. + + @param url URL of the file scan report page (string) + """ + self.newTab(url) + + def __virusTotalIpAddressReport(self): + """ + Private slot to retrieve an IP address report. + """ + ip, ok = QInputDialog.getText( + self, + self.tr("IP Address Report"), + self.tr("Enter a valid IPv4 address in dotted quad notation:"), + QLineEdit.Normal) + if ok and ip: + if ip.count(".") == 3: + self.__virusTotal.getIpAddressReport(ip) + else: + E5MessageBox.information( + self, + self.tr("IP Address Report"), + self.tr("""The given IP address is not in dotted quad""" + """ notation.""")) + + def __virusTotalDomainReport(self): + """ + Private slot to retrieve a domain report. + """ + domain, ok = QInputDialog.getText( + self, + self.tr("Domain Report"), + self.tr("Enter a valid domain name:"), + QLineEdit.Normal) + if ok and domain: + self.__virusTotal.getDomainReport(domain) + + ########################################################################### + ## Style sheet handling below ## + ########################################################################### + + def reloadUserStyleSheet(self): + """ + Public method to reload the user style sheet. + """ + styleSheet = Preferences.getWebBrowser("UserStyleSheet") + self.__setUserStyleSheet(styleSheet) + + def __setUserStyleSheet(self, styleSheetFile): + """ + Private method to set a user style sheet. + + @param styleSheetFile name of the user style sheet file (string) + """ + userStyle = \ + self.adBlockManager().elementHidingRules().replace('"', '\\"') + + userStyle += WebBrowserTools.readAllFileContents(styleSheetFile)\ + .replace("\n", "") + name = "_eric_userstylesheet" + + oldScript = self.webProfile().scripts().findScript(name) + if not oldScript.isNull(): + self.webProfile().scripts().remove(oldScript) + + if userStyle: + script = QWebEngineScript() + script.setName(name) + script.setInjectionPoint(QWebEngineScript.DocumentCreation) + script.setWorldId(QWebEngineScript.ApplicationWorld) + script.setRunsOnSubFrames(True) + script.setSourceCode(Scripts.setStyleSheet(userStyle)) + self.webProfile().scripts().insert(script) + + ########################################## + ## Support for desktop notifications below + ########################################## + + @classmethod + def showNotification(cls, icon, heading, text): + """ + Class method to show a desktop notification. + + @param icon icon to be shown in the notification (QPixmap) + @param heading heading of the notification (string) + @param text text of the notification (string) + """ + if cls._fromEric: + e5App().getObject("UserInterface").showNotification( + icon, heading, text) + else: + if Preferences.getUI("NotificationsEnabled"): + if cls._notification is None: + from UI.NotificationWidget import NotificationWidget + cls._notification = NotificationWidget() + cls._notification.setPixmap(icon) + cls._notification.setHeading(heading) + cls._notification.setText(text) + cls._notification.setTimeout( + Preferences.getUI("NotificationTimeout")) + cls._notification.move( + Preferences.getUI("NotificationPosition")) + cls._notification.show() + + @classmethod + def notificationsEnabled(cls): + """ + Class method to check, if notifications are enabled. + + @return flag indicating, if notifications are enabled (boolean) + """ + if cls._fromEric: + return e5App().getObject("UserInterface").notificationsEnabled() + else: + return Preferences.getUI("NotificationsEnabled") + + ################################### + ## Support for download files below + ################################### + + @classmethod + def downloadRequested(self, download): + """ + Class method to handle a download request. + + @param download reference to the download data + @type QWebEngineDownloadItem + """ + self.downloadManager().download(download) + + ######################################## + ## Support for web engine profiles below + ######################################## + + @classmethod + def webProfile(cls, private=False): + """ + Class method handling the web engine profile. + + @param private flag indicating the privacy mode + @type bool + @return reference to the web profile object + @rtype QWebEngineProfile + """ + if cls._webProfile is None: + if private: + cls._webProfile = QWebEngineProfile() + else: + cls._webProfile = QWebEngineProfile.defaultProfile() + cls._webProfile.downloadRequested.connect( + cls.downloadRequested) + + # add the default user agent string + userAgent = cls._webProfile.httpUserAgent() + cls._webProfile.defaultUserAgent = userAgent + + if not private: + if Preferences.getWebBrowser("DiskCacheEnabled"): + cls._webProfile.setHttpCacheType( + QWebEngineProfile.DiskHttpCache) + cls._webProfile.setHttpCacheMaximumSize( + Preferences.getWebBrowser("DiskCacheSize") + * 1024 * 1024) + cls._webProfile.setCachePath(os.path.join( + Utilities.getConfigDir(), "web_browser")) + else: + cls._webProfile.setHttpCacheType( + QWebEngineProfile.MemoryHttpCache) + cls._webProfile.setHttpCacheMaximumSize(0) + cls._webProfile.setPersistentStoragePath(os.path.join( + Utilities.getConfigDir(), "web_browser", + "persistentstorage")) + cls._webProfile.setPersistentCookiesPolicy( + QWebEngineProfile.AllowPersistentCookies) + + # Setup QWebChannel user script + script = QWebEngineScript() + script.setName("_eric_webchannel") + script.setInjectionPoint(QWebEngineScript.DocumentCreation) + script.setWorldId(QWebEngineScript.MainWorld) + script.setRunsOnSubFrames(True) + script.setSourceCode(Scripts.setupWebChannel()) + cls._webProfile.scripts().insert(script) + + return cls._webProfile
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/WebInspector.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a QWebEngineView to load the web inspector in. +""" + +from __future__ import unicode_literals +try: + str = unicode # __IGNORE_EXCEPTION__ +except NameError: + pass + +import json + +from PyQt5.QtCore import QSize, QUrl +from PyQt5.QtNetwork import QNetworkRequest +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage + +import Preferences + +_VIEWS = [] + + +class WebInspector(QWebEngineView): + """ + Class implementing a QWebEngineView to load the web inspector in. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget + @type QWidget + """ + super(WebInspector, self).__init__(parent) + + self.__view = None + self.__inspectElement = False + + self.__reloadGeometry() + + registerView(self) + + self.page().windowCloseRequested.connect(self.close) + self.page().loadFinished.connect(self.__loadFinished) + + def closeEvent(self, evt): + """ + Protected method to save the geometry when closed. + + @param evt event object + @type QCloseEvent + """ + Preferences.setGeometry("WebInspectorGeometry", self.saveGeometry()) + super(WebInspector, self).closeEvent(evt) + + def __reloadGeometry(self): + """ + Private method to restore the geometry. + """ + geom = Preferences.getGeometry("WebInspectorGeometry") + if geom.isEmpty(): + s = QSize(600, 600) + self.resize(s) + else: + self.restoreGeometry(geom) + + def setView(self, view, inspectElement=False): + """ + Public method to connect a view to this inspector. + + @param view reference to the view object + @type WebBrowserView + @param inspectElement flag indicating to start a web inspection + @type bool + """ + self.__view = view + if not self.isEnabled(): + return + + self.__inspectElement = inspectElement + + port = Preferences.getWebBrowser("WebInspectorPort") + inspectorUrl = QUrl("http://localhost:{0}".format(port)) + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + self.__reply = WebBrowserWindow.networkManager().get( + QNetworkRequest(inspectorUrl.resolved(QUrl("json/list")))) + self.__reply.finished.connect(self.__inspectorReplyFinished) + + def __inspectorReplyFinished(self): + """ + Private slot handling the reply. + """ + result = str(self.__reply.readAll(), encoding="utf8") + clients = json.loads(result) + + self.__reply.deleteLater() + self.__replay = None + + pageUrl = QUrl() + try: + index = _VIEWS.index(self.__view) + except ValueError: + index = -1 + if len(clients) > index: + port = Preferences.getWebBrowser("WebInspectorPort") + inspectorUrl = QUrl("http://localhost:{0}".format(port)) + + client = clients[index] + pageUrl = inspectorUrl.resolved( + QUrl(client["devtoolsFrontendUrl"])) + self.load(pageUrl) + pushView(self) + self.show() + + def inspectElement(self): + """ + Public method to inspect an element. + """ + self.__inspectElement = True + + def isEnabled(self): + """ + Public method to check, if the web inspector is enabled. + + @return flag indicating the enabled state + @rtype bool + """ + return Preferences.getWebBrowser("WebInspectorEnabled") + + def __loadFinished(self): + """ + Private slot handling the finished signal. + """ + if self.__inspectElement: + self.__view.triggerPageAction(QWebEnginePage.InspectElement) + self.__inspectElement = False + + +def registerView(view): + """ + Function to register a view. + + @param view reference to the view + @type WebBrowserView + """ + if _VIEWS is None: + return + + _VIEWS.insert(0, view) + + +def unregisterView(view): + """ + Function to unregister a view. + + @param view reference to the view + @type WebBrowserView + """ + if _VIEWS is None: + return + + if view in _VIEWS: + _VIEWS.remove(view) + + +def pushView(view): + """ + Function to push a view to the front of the list. + + @param view reference to the view + @type WebBrowserView + """ + if _VIEWS is None: + return + + if view in _VIEWS: + _VIEWS.remove(view) + _VIEWS.insert(0, view)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/ZoomManager/ZoomManager.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a manager for site specific zoom level settings. +""" + +from __future__ import unicode_literals + +import json + +from PyQt5.QtCore import pyqtSignal, QObject + +from Utilities.AutoSaver import AutoSaver +import Preferences + + +class ZoomManager(QObject): + """ + Class implementing a manager for site specific zoom level settings. + """ + changed = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(ZoomManager, self).__init__(parent) + + self.__zoomDB = {} + + self.__saveTimer = AutoSaver(self, self.save) + + self.changed.connect(self.__saveTimer.changeOccurred) + + self.__loaded = False + + def close(self): + """ + Public method to close the zoom manager. + """ + self.__saveTimer.saveIfNeccessary() + + def load(self): + """ + Public method to load the bookmarks. + """ + if self.__loaded: + return + + dbString = Preferences.getWebBrowser("ZoomValuesDB") + if dbString: + try: + db = json.loads(dbString) + self.__zoomDB = db + except ValueError: + # ignore silently + pass + + self.__loaded = True + + def save(self): + """ + Public method to save the zoom values. + """ + if not self.__loaded: + return + + from WebBrowser.WebBrowserWindow import WebBrowserWindow + if not WebBrowserWindow.isPrivate(): + dbString = json.dumps(self.__zoomDB) + Preferences.setWebBrowser("ZoomValuesDB", dbString) + + def __keyFromUrl(self, url): + """ + Private method to generate a DB key for an URL. + + @param url URL to generate a key for + @type QUrl + @return key for the given URL + @rtype str + """ + if url.isEmpty(): + key = "" + else: + scheme = url.scheme() + host = url.host() + if host: + key = host + elif scheme == "file": + path = url.path() + key = path.rsplit("/", 1)[0] + else: + key = "" + + return key + + def setZoomValue(self, url, zoomValue): + """ + Public method to record the zoom value for the given URL. + + Note: Only zoom values not equal 100% are recorded. + + @param url URL of the page to remember the zoom value for + @type QUrl + @param zoomValue zoom value for the URL + @type int + """ + self.load() + + key = self.__keyFromUrl(url) + if not key: + return + + if ((zoomValue == 100 and key not in self.__zoomDB) or + (key in self.__zoomDB and self.__zoomDB[key] == zoomValue)): + return + + if zoomValue == 100: + del self.__zoomDB[key] + else: + self.__zoomDB[key] = zoomValue + + self.changed.emit() + + def zoomValue(self, url): + """ + Public method to get the zoom value for an URL. + + @param url URL of the page to get the zoom value for + @type QUrl + @return zoomValue zoom value for the URL + @rtype int + """ + self.load() + + key = self.__keyFromUrl(url) + if not key: + zoom = 100 + + if key in self.__zoomDB: + zoom = self.__zoomDB[key] + else: + # default zoom value (i.e. no zoom) + zoom = 100 + + return zoom + + def clear(self): + """ + Public method to clear the saved zoom values. + """ + self.__zoomDB = {} + self.__loaded = True + + self.changed.emit() + + def removeZoomValue(self, site): + """ + Public method to remove a zoom value entry. + + @param site web site name + @type str + """ + self.load() + + if site in self.__zoomDB: + del self.__zoomDB[site] + self.changed.emit() + + def allSiteNames(self): + """ + Public method to get a list of all site names. + + @return sorted list of all site names + @rtype list of str + """ + self.load() + + return sorted(self.__zoomDB.keys()) + + def sitesCount(self): + """ + Public method to get the number of available sites. + + @return number of sites + @rtype int + """ + self.load() + + return len(self.__zoomDB) + + def siteInfo(self, site): + """ + Public method to get the zoom value for the site. + + @param site web site name + @type str + @return zoom value for the site + @rtype int + """ + self.load() + + if site not in self.__zoomDB: + return None + + return self.__zoomDB[site] + + +__ZoomManager = None + + +def instance(): + """ + Global function to get a reference to the zoom manager and create it, if + it hasn't been yet. + + @return reference to the zoom manager object + @rtype ZoomManager + """ + global __ZoomManager + + if __ZoomManager is None: + __ZoomManager = ZoomManager() + + return __ZoomManager
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/ZoomManager/ZoomValuesDialog.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show all saved zoom values. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QSortFilterProxyModel +from PyQt5.QtGui import QFont, QFontMetrics +from PyQt5.QtWidgets import QDialog + +from .Ui_ZoomValuesDialog import Ui_ZoomValuesDialog + + +class ZoomValuesDialog(QDialog, Ui_ZoomValuesDialog): + """ + Class implementing a dialog to show all saved zoom values. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(ZoomValuesDialog, self).__init__(parent) + self.setupUi(self) + + self.removeButton.clicked.connect( + self.zoomValuesTable.removeSelected) + self.removeAllButton.clicked.connect(self.zoomValuesTable.removeAll) + + from . import ZoomManager + from .ZoomValuesModel import ZoomValuesModel + + self.zoomValuesTable.verticalHeader().hide() + self.__zoomValuesModel = ZoomValuesModel( + ZoomManager.instance(), self) + self.__proxyModel = QSortFilterProxyModel(self) + self.__proxyModel.setSourceModel(self.__zoomValuesModel) + self.searchEdit.textChanged.connect( + self.__proxyModel.setFilterFixedString) + self.zoomValuesTable.setModel(self.__proxyModel) + + fm = QFontMetrics(QFont()) + height = fm.height() + fm.height() // 3 + self.zoomValuesTable.verticalHeader().setDefaultSectionSize(height) + self.zoomValuesTable.verticalHeader().setMinimumSectionSize(-1) + + self.__calculateHeaderSizes() + + def __calculateHeaderSizes(self): + """ + Private method to calculate the section sizes of the horizontal header. + """ + fm = QFontMetrics(QFont()) + for section in range(self.__zoomValuesModel.columnCount()): + header = self.zoomValuesTable.horizontalHeader()\ + .sectionSizeHint(section) + if section == 0: + header = fm.width("extraveryveryverylongsitename") + elif section == 1: + header = fm.width("averagelongzoomvalue") + buffer = fm.width("mm") + header += buffer + self.zoomValuesTable.horizontalHeader()\ + .resizeSection(section, header) + self.zoomValuesTable.horizontalHeader().setStretchLastSection(True)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/ZoomManager/ZoomValuesDialog.ui Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,191 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ZoomValuesDialog</class> + <widget class="QDialog" name="ZoomValuesDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>350</height> + </rect> + </property> + <property name="windowTitle"> + <string>Saved Zoom Values</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="E5ClearableLineEdit" name="searchEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + <property name="toolTip"> + <string>Enter search term</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <widget class="E5TableView" name="zoomValuesTable"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="textElideMode"> + <enum>Qt::ElideMiddle</enum> + </property> + <property name="showGrid"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected entries</string> + </property> + <property name="text"> + <string>&Remove</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeAllButton"> + <property name="toolTip"> + <string>Press to remove all entries</string> + </property> + <property name="text"> + <string>Remove &All</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>208</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + <customwidget> + <class>E5TableView</class> + <extends>QTableView</extends> + <header>E5Gui/E5TableView.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>searchEdit</tabstop> + <tabstop>zoomValuesTable</tabstop> + <tabstop>removeButton</tabstop> + <tabstop>removeAllButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ZoomValuesDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>237</x> + <y>340</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ZoomValuesDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>325</x> + <y>340</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/ZoomManager/ZoomValuesModel.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a model for zoom values management. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QModelIndex, QAbstractTableModel + + +class ZoomValuesModel(QAbstractTableModel): + """ + Class implementing a model for zoom values management. + """ + def __init__(self, manager, parent=None): + """ + Constructor + + @param manager reference to the zoom values manager (ZoomManager) + @param parent reference to the parent object (QObject) + """ + super(ZoomValuesModel, self).__init__(parent) + + self.__manager = manager + manager.changed.connect(self.__zoomValuesChanged) + + self.__headers = [ + self.tr("Website"), + self.tr("Zoom Value [%]"), + ] + + def __zoomValuesChanged(self): + """ + Private slot handling a change of the registered zoom values. + """ + self.beginResetModel() + self.endResetModel() + + def removeRows(self, row, count, parent=QModelIndex()): + """ + Public method to remove entries from the model. + + @param row start row (integer) + @param count number of rows to remove (integer) + @param parent parent index (QModelIndex) + @return flag indicating success (boolean) + """ + if parent.isValid(): + return False + + if count <= 0: + return False + + lastRow = row + count - 1 + + self.beginRemoveRows(parent, row, lastRow) + + siteList = self.__manager.allSiteNames() + for index in range(row, lastRow + 1): + self.__manager.removeZoomValue(siteList[index]) + + return True + + def rowCount(self, parent=QModelIndex()): + """ + Public method to get the number of rows of the model. + + @param parent parent index (QModelIndex) + @return number of rows (integer) + """ + if parent.isValid(): + return 0 + else: + return self.__manager.sitesCount() + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns of the model. + + @param parent parent index (QModelIndex) + @return number of columns (integer) + """ + return len(self.__headers) + + def data(self, index, role): + """ + Public method to get data from the model. + + @param index index to get data for (QModelIndex) + @param role role of the data to retrieve (integer) + @return requested data + """ + if index.row() >= self.__manager.sitesCount() or index.row() < 0: + return None + + site = self.__manager.allSiteNames()[index.row()] + siteInfo = self.__manager.siteInfo(site) + + if siteInfo is None: + return None + + if role == Qt.DisplayRole: + if index.column() == 0: + return site + elif index.column() == 1: + return siteInfo + + return None + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Public method to get the header data. + + @param section section number (integer) + @param orientation header orientation (Qt.Orientation) + @param role data role (integer) + @return header data + """ + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + try: + return self.__headers[section] + except IndexError: + pass + + return None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/ZoomManager/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing a manager for site specific zoom level settings. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/__init__.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2002 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing a little web browser. + +The web browser is a HTML browser for the display of HTML help files like the +Qt Online Documentation and for browsing the internet. It may be used as a +standalone version as well by using the eric6_browser.py script found in the +main eric6 installation directory. + +This package requires at least Qt 5.6.0. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/html.qrc Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,9 @@ +<!DOCTYPE RCC> +<RCC version="1.0"> +<qresource> + <file>html/adblockPage.html</file> + <file>html/startPage.html</file> + <file>html/speeddialPage.html</file> + <file>html/tabCrashPage.html</file> +</qresource> +</RCC>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/html/adblockPage.html Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +<title>@TITLE@</title> +<link rel="icon" href="@FAVICON@" type="image/x-icon" /> +<style> +body { + padding: 3em 0em; + background: -webkit-gradient(linear, left top, left bottom, from(#85784A), to(#FDFDFD), color-stop(0.5, #FDFDFD)); + background-repeat: repeat-x; +} +#box { + background: white; + border: 1px solid #85784A; + max-width: 600px; + height: 50%; + padding: 40px; + padding-bottom: 10px; + margin: auto; + border-radius: 0.8em; + text-align: center; + vertical-align: middle; + margin: auto; +} +h1 { + font-size: 130%; + font-weight: bold; + border-bottom: 1px solid #85784A; + margin-bottom: 0px; +} +</style> +</head> +<body> + <div id="box"> + <img src="@IMAGE@" width="64" height="64"/> + <h1>AdBlock Plus</h1> + <p>@MESSAGE@</p> + </div> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/html/speeddialPage.html Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,636 @@ +<!DOCTYPE html> +<html> +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +<title>@SITE-TITLE@</title> +<link rel="icon" href="@FAVICON@" type="image/x-icon" /> +<style type="text/css" media="screen"> +body { + background: -webkit-gradient(linear, left top, left bottom, from(#85784A), to(#FDFDFD), color-stop(0.5, #FDFDFD)); + background-repeat: repeat-x; + font: 13px/22px "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #525c66; +} +body * { + -webkit-user-select: none; + font-size: 100%; + line-height: 1.6; + margin: 0px; +} +.add { + position: absolute; + right:10px;top:10px; + width: 24px; + height: 24px; + background: url(@IMG_PLUS@); + cursor: pointer; +} + + +#quickdial { + margin: auto; + text-align: center; + font-weight: bold; +} +#quickdial div.entry { + position: relative; + float: left; + border-width: 10px; + -webkit-border-image: url(@BOX-BORDER@) 10; + margin: 5px; +} +#quickdial img { + display: block; + margin: auto; +} +#quickdial a { + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: 87%; +} + + +div.entry:hover .edit, +div.entry:hover .close, +div.entry:hover .reload { + display: inline; +} +span.boxTitle { + width:100%; + max-height: 20px; + position: absolute; + top: 88%; + left: 0px; + text-align: center; + overflow:hidden; +} +span.close { + width: 14px; + height: 14px; + position: absolute; + left: 92%; + top: 90%; + background: url(@IMG_CLOSE@) no-repeat; + background-position: center; + border: 1px solid transparent; + display: none; +} +span.close:hover { + border-color: grey; + border-radius: 3px; +} +span.edit { + width: 14px; + height: 14px; + position: absolute; + left: 0px; + top: 90%; + background: url(@IMG_EDIT@) no-repeat; + background-position: center; + border: 1px solid transparent; + display: none; +} +span.edit:hover { + border-color: grey; + border-radius: 3px; +} +span.reload { + width: 16px; + height: 16px; + position: absolute; + left: 92%; + top: 0px; + background: url(@IMG_RELOAD@) no-repeat; + background-position: center; + border: 1px solid transparent; + display: none; +} +span.reload:hover { + border-color: grey; + border-radius: 4px; +} + + +#overlay-edit { + width: 380px; + max-height: 265px; + margin-left: auto; + margin-right: auto; + margin-top: 100px; + border-width: 20px; + -webkit-border-image: url(@BOX-BORDER@) 25; +} +#overlay-edit img { + display: block; + margin-left: auto; + margin-right: auto; +} +#overlay-edit .buttonbox input { + margin-right: 3px; + margin-left: 3px; +} + + +.formTable { + width: 350px; + margin-left: auto; + margin-right: auto; + margin-top: 15px; +} +.formTable input[type="text"] { + width: 100%; + -webkit-user-select: auto; +} + + +.sett { + position: absolute; + right:36px; + top:10px; + width: 24px; + height: 24px; + background: url(@IMG_SETTINGS@); + cursor: pointer; +} +#settingsBox { + position: absolute; + right: 58px; + top: 25px; + min-width: 250px; + width: auto; + height: auto; + background: #EDECE6; + margin: 5px; + border-radius: 15px; + padding: 8px 15px; + border: 1px solid transparent; + opacity: 1; + z-index: 200; +} +#settingsBox .content { + float: right; + margin-left: 115px; +} +#settingsBox p label { + margin: 2px; + padding: 1px; + text-align: center; +} +#settingsBox .togop { + margin-bottom: 1px; + padding-bottom: 2px; +} +#settingsBox .button { + margin: 2px; + padding: 1px; + text-align:center; + width: 98%; +} +#settingsBox .rowsel { + margin: 2px; + padding: 3px 0; + border-bottom: 1px solid #888; +} +#settingsBox .rowsel input { + text-align: center; + width: 80%; + height: 12px; + margin: 0px; + padding-bottom: 0; +} +#settingsBox .rowsel span { + font-weight: bold; + text-align: center; + margin: 2px; + margin-right: 7px; + display: inline-block; + width: 25px; +} + +.buttonbox { + margin-top: 5px; + margin-bottom: -5px; + text-align: right; +} +</style> + +<script type="text/javascript" src="@JQUERY@"></script> +<script type="text/javascript" src="@JQUERY-UI@"></script> +<script type="text/javascript"> + var LOADING_IMAGE = '@LOADING-IMG@'; + var URL = '@URL@'; + var TITLE = '@TITLE@'; + var EDIT = '@APPLY@'; + var NEW_PAGE = '@NEW-PAGE@'; + var TITLE_EDIT = '@TITLE-EDIT@'; + var TITLE_REMOVE = '@TITLE-REMOVE@'; + var TITLE_RELOAD = '@TITLE-RELOAD@'; + var TITLE_FETCHTITLE = '@TITLE-FETCHTITLE@'; + var MAX_PAGES_ROW = @ROW-PAGES@; + var DIAL_WIDTH = @SD-SIZE@; + + var editingId = -1; + var ignoreNextChanged = false; + + function escapeTitle(title) { + title = title.replace(/"/g, '"'); + title = title.replace(/'/g, '''); + return title; + } + + function unescapeTitle(title) { + title = title.replace(/"/g, '"'); + title = title.replace(/'/g, '\''); + return title; + } + + function escapeUrl(url) { + url = url.replace(/"/g, ''); + url = url.replace(/'/g, ''); + return url; + } + + function onRemoveClick(box) { + removeBox($(box).index()); + } + + function onEditKeyPress(e) { + if (e.keyCode == 13) { + boxEdited(); + return false; + } + else if (e.keyCode == 27) { + $('#fadeOverlay').click(); + return false; + } + return true; + } + + function onFetchTitleClick(checkbox) { + var displayStyle; + checkbox.checked ? displayStyle = 'hidden' : displayStyle = 'visible'; + $('#titleLine').css({'visibility' : displayStyle }); + } + + function hideEditBox() { + $('#fadeOverlay').fadeOut("slow", function() {$("#fadeOverlay").remove();}); + } + + function emitChanged(pages) + { + ignoreNextChanged = true; + external.speedDial.changed(pages); + } + + function addSpeedDial() + { + onEditClick(addBox('', NEW_PAGE, '')); + alignPage(); + } + + function onEditClick(box) { + editingId = $(box).index(); + var boxUrl = $(box).children('a').first().attr('href'); + var boxTitle = escapeTitle($(box).children('span').first().text()); + if (boxUrl === '') + boxUrl = 'http://'; + + $('body').append('<div id="fadeOverlay" style="opacity:0.9;display:none;position:fixed;left:0;' + + 'top:0;width:100%;height:100%;z-index:9999;background:#85784A;">' + + '<div id="overlay-edit" onkeypress="return onEditKeyPress(event)">' + + '<img src="' + $(box).children('img').first().attr('src') + '"> ' + + '<table class="formTable"><tr><td>' + URL + ': </td><td>' + + '<input type="text" id="formUrl" value="' + boxUrl + '"></td></tr>' + + '<tr id="titleLine"><td>' + TITLE + ': </td><td>' + + '<input type="text" id="formTitle" value="' + boxTitle + '"></td></tr>' + + '<tr><td></td><td><input type="checkbox" id="fetchTitle" onclick="onFetchTitleClick(this)">' + + '<label for="fetchTitle"> ' + TITLE_FETCHTITLE + ' </label></td></tr>' + + '</table><p class="buttonbox">' + + '<input type="button" value=" @CLOSE@ " onClick="hideEditBox();">' + + '<input type="button" value=" ' + EDIT + ' " onClick="boxEdited()"></p>' + + '</div></div>'); + + $('#fadeOverlay').css({'filter' : 'alpha(opacity=90)'}).fadeIn(); + $('#fadeOverlay').click(function() {hideEditBox()}); + $('#overlay-edit').click(function(event) { event.stopPropagation(); }); + $('#formUrl').focus(); + } + + function onReloadClick(box) { + var url = $(box).children('a').first().attr('href'); + var img = $(box).children('img').first(); + + if (url === '') + return; + + $(img).attr('src', LOADING_IMAGE); + external.speedDial.loadThumbnail(url, false); + } + + function boxEdited() { + if (editingId == -1) + return; + + external.speedDial.urlFromUserInput($('#formUrl').attr("value"), function(newUrl) { + var box = document.getElementById('quickdial').getElementsByTagName('div')[editingId]; + var a = box.getElementsByTagName('a')[0]; + var originalUrl = a.getAttribute('href'); + setBoxUrl(editingId, newUrl); + setBoxTitle(editingId, $('#formTitle').attr("value")); + var changedUrl = a.getAttribute('href'); + var fetchTitleChecked = document.getElementById('fetchTitle').checked; + + var pages = allPages() + + if (fetchTitleChecked || (originalUrl != changedUrl && changedUrl !== '') ) { + var img = box.getElementsByTagName('img')[0]; + img.setAttribute('src', LOADING_IMAGE); + + $('#fadeOverlay').fadeOut("slow", function() { + $("#fadeOverlay").remove(); + }); + external.speedDial.loadThumbnail(a.getAttribute('href'), fetchTitleChecked); + external.speedDial.removeImageForUrl(a.getAttribute('href')); + } else { + hideEditBox(); + } + emitChanged(pages); + }); + } + + function allPages() { + var urls = $('a[class="boxUrl"]'); + var titles = $('span[class="boxTitle"]'); + var value = ""; + $('div.entry').each(function(i) { + var url = $(this).children('a').first().attr('href'); + var title = $(this).children('span[class="boxTitle"]').first().text(); + var img = $(this).children('img').first().attr('src'); + value += 'url:"' + escapeUrl(url) + '"|title:"' + escapeTitle(title) + '"|img:"' + escapeUrl(img) + '";'; + }); + + return value; + } + + function addBox(url, title, img_source) { + var div = document.createElement('div'); + div.setAttribute('class', 'entry'); + var img = document.createElement('img'); + img.setAttribute('src', img_source); + var a = document.createElement('a'); + a.setAttribute('href', url); + a.setAttribute('class', 'boxUrl'); + var span1 = document.createElement('span'); + span1.setAttribute('class', 'boxTitle'); + span1.innerText = unescapeTitle(title); + var span2 = document.createElement('span'); + span2.setAttribute('class', 'edit'); + span2.setAttribute('onClick', 'onEditClick(parentNode)'); + span2.setAttribute('title', TITLE_EDIT); + var span3 = document.createElement('span'); + span3.setAttribute('class', 'close'); + span3.setAttribute('onClick', 'onRemoveClick(parentNode)'); + span3.setAttribute('title', TITLE_REMOVE); + var span4 = document.createElement('span'); + span4.setAttribute('class', 'reload'); + span4.setAttribute('onClick', 'onReloadClick(parentNode)'); + span4.setAttribute('title', TITLE_RELOAD); + + div.appendChild(img); + div.appendChild(img); + div.appendChild(a); + div.appendChild(span1); + div.appendChild(span2); + div.appendChild(span3); + div.appendChild(span4); + + document.getElementById("quickdial").appendChild(div); + + if (img_source == LOADING_IMAGE) { + external.speedDial.loadThumbnail(url, false); + } + + return div; + } + + function setBoxImage(id, img_source) { + var box = document.getElementById('quickdial').getElementsByTagName('div')[id]; + if (box === undefined) + return; + + var img = box.getElementsByTagName('img')[0]; + img.setAttribute('src', img_source + '?' + new Date()); + } + + function setTitleToUrl(url, title) { + var changed = false; + var boxes = document.getElementById('quickdial').getElementsByTagName('div'); + for (i = 0; i < boxes.length; ++i) { + var box = boxes[i]; + + if (box === undefined) + continue; + + var boxUrl = box.getElementsByTagName('a')[0].getAttribute('href'); + if (url != boxUrl) + continue; + + var span = box.getElementsByTagName('span')[0]; + if (span.innerText != title) { + changed = true; + span.innerText = title; + } + } + + emitChanged(allPages()); + } + + function setImageToUrl(url, img_source) { + var aElement = $('a[href="' + url + '"]'); + $(aElement).each(function() { + var box = $(this).parent(); + var imgElement = $(box).children("img").first(); + if ($(imgElement).size() == 0) + return; + + $(imgElement).attr('src', img_source); + }); + } + + function setBoxUrl(id, url) { + var box = document.getElementById('quickdial').getElementsByTagName('div')[id]; + if (box === undefined) + return; + + var a = box.getElementsByTagName('a')[0]; + a.setAttribute('href', url); + } + + function setBoxTitle(id, title) { + var box = document.getElementById('quickdial').getElementsByTagName('div')[id]; + if (box === undefined) + return; + + var span = box.getElementsByTagName('span')[0]; + span.innerText = title; + } + + function removeBox(id) { + if (confirm("@TITLE-WARN@")) + var box = document.getElementById('quickdial').getElementsByTagName('div')[id]; + if (box === undefined) + return; + + var url = box.getElementsByTagName('a')[0].getAttribute('href'); + document.getElementById("quickdial").removeChild(box); + alignPage(); + + external.speedDial.removeImageForUrl(url); + emitChanged(allPages()); + } + + function alignPage() { + $('head').append('<style>#quickdial img[src*=".png"]{height:auto;width:'+DIAL_WIDTH+'px}</style>'); + $('#quickdial div.entry').css({'width' : DIAL_WIDTH + 'px', + 'height' : Math.round(DIAL_WIDTH / 1.54) + 'px'}); + + var width = $(window).width(); + var height = $(window).height(); + var boxWidth = Math.floor(DIAL_WIDTH + 30); + var boxHeight = Math.floor(Math.round(DIAL_WIDTH / 1.54) + 40); + + var maxBoxes = Math.floor(width / boxWidth); + if (maxBoxes > MAX_PAGES_ROW) maxBoxes = MAX_PAGES_ROW; + if (maxBoxes < 1) maxBoxes = 1; + + var maxwidth = maxBoxes * boxWidth; + $("#quickdial").css('width', maxwidth + 'px'); + + var boxesCount = $("#quickdial").children("div").size(); + var rows = Math.ceil(boxesCount / maxBoxes); + var margintop = (height - rows * boxHeight) / 2; + + if (margintop < 0) margintop = 0; + + $("#quickdial").css('margin-top', margintop + 'px'); + } + + + function saveSettings() { + MAX_PAGES_ROW = $('#PgInRow').val(); + DIAL_WIDTH = parseInt($('#SdSize').val()); + + external.speedDial.setPagesInRow(MAX_PAGES_ROW); + external.speedDial.setSdSize(DIAL_WIDTH); + + alignPage(); + } + + + function sdSizeToggle() { + var check = document.getElementById('SdSizeToggle'); + var SdSize = document.getElementById('SdSize'); + var SdSizeSl = document.getElementById('sliderValueSd'); + + SdSize.disabled = (check.checked ? false : true); + SdSize.value = (check.checked ? SdSize.value : 231); + SdSizeSl.innerHTML = (check.checked ? DIAL_WIDTH : 231); + } + + function configureSpeedDial() + { + // Load settings + $('#PgInRow').val(MAX_PAGES_ROW); + $('#sliderValuePg').html(MAX_PAGES_ROW); + $('#SdSize').val(DIAL_WIDTH); + $('#SdSizeToggle').prop('checked', DIAL_WIDTH != 231); + $('#sliderValueSd').html(DIAL_WIDTH); + $('#SdSizeToggle').is(':checked') ? $('#SdSize').removeAttr('disabled') : $('#SdSize').attr('disabled', 'disabled'); + + // Show dialog + $('#fadeOverlay2').css({'filter' : 'alpha(opacity=100)'}).fadeIn(); + $('#fadeOverlay2').click(function() { $(this).fadeOut('slow'); }); + $('#settingsBox').click(function(event) { event.stopPropagation(); }); + } + + function reloadAll() { + if (confirm("@TITLE-WARN-REL@")) + $('div.entry').each(function(i) { + onReloadClick($(this)); + }); + } + +</script> +</head> + +<body> + <div id="quickdial"></div> + <a onClick="configureSpeedDial();" title="@SETTINGS-TITLE@" class="sett"></a> + <a onClick="addSpeedDial();" title="@ADD-TITLE@" class="add"></a> + + <script type="text/javascript"> + function init() + { + @INITIAL-SCRIPT@ + + external.speedDial.pagesChanged.connect(function() { + if (ignoreNextChanged) { + ignoreNextChanged = false; + return; + } + window.location.reload(); + }); + + external.speedDial.thumbnailLoaded.connect(setImageToUrl); + external.speedDial.pageTitleLoaded.connect(setTitleToUrl); + + alignPage(); + $(window).resize(function() { alignPage(); }); + $("div").disableSelection(); + $("#quickdial").sortable({ + revert: true, + cursor: 'move', + containment: 'document', + opacity: 0.8, + distance: 40, + update: function(event, ui) { + emitChanged(allPages()); + } + }); + } + + // Initialize + if (window.external) { + init(); + } else { + document.addEventListener('_eric_external_created', init); + } + </script> + <div id="fadeOverlay2" style="opacity:0.9;display:none;position:fixed;left:0;top:0;width:100%;height:100%;z-index:100;background:#85784A;"> + <div id="settingsBox"> + <div class="togop"> + <label for="PgInRow">@TXT_NRROWS@</label> + </div> + <div class="rowsel"> + <span id="sliderValuePg">@ROW-PAGES@</span> + <input id="PgInRow" type="range" min="2" max="8" value="@ROW-PAGES@" step="1" onchange="$('#sliderValuePg').html(this.value);" /> + </div> + <div class="togop"> + <input type="checkbox" name="sdsizet" id="SdSizeToggle" onchange="sdSizeToggle()" /> <label for="SdSizeToggle">@TXT_SDSIZE@</label> + </div> + <div class="rowsel"> + <span id="sliderValueSd">@SD-SIZE@</span> + <input id="SdSize" type="range" min="145" max="360" value="@SD-SIZE@" step="1" onchange="$('#sliderValueSd').html(this.value);" /> + </div> + <div class="content"> + <p class="buttonbox"> + <input type="button" value=" @CLOSE@ " onClick="$('#fadeOverlay2').fadeOut('slow');" /> + <input type="button" value=" @APPLY@ " onClick="saveSettings();$('#fadeOverlay2').fadeOut('slow');"/> + </p> + </div> + </div> + </div> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/html/startPage.html Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,138 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +<title>@TITLE@</title> +<link rel="icon" href="@FAVICON@" type="image/x-icon" /> +<style> +* { + margin: 0; + padding: 0; + font-family: "DejaVu Sans"; +} + +body { + background: -webkit-gradient(linear, left top, left bottom, from(#85784A), to(#FDFDFD), color-stop(0.5, #FDFDFD)); + background-repeat: repeat-x; + margin-top: 200px; +} + +#header, #search, #footer { + width: 600px; + margin: 10px auto; +} + +#header, #search { + border-radius: 0.8em; + padding: 25px; +} + +#header { + background: -webkit-gradient(linear, left top, left bottom, from(#D57E3E), to(#D57E3E), color-stop(0.5, #FFBA89)); + height: 25px; +} + +#header h1 { + display: inline; + font-size: 1.7em; + font-weight: bold; +} + +#header img { + display: inline; + float: right; + margin-top: -5px; +} + +#search { + background: -webkit-gradient(linear, left top, right top, from(#85784A), to(#85784A), color-stop(0.5, #C8C2AE)); + height: 50px; + color: #000; + text-align: center; + padding-top: 40px !important; +} + +#search fieldset { + border: 0; +} + +#search input[type=text] { + width: 65%; +} + +#search input[type=submit] { + width: 25%; +} + +#footer { + text-align: center; + color: #999; +} + +#footer a { + color: #555; + text-decoration: none; +} + +#footer a:hover { + text-decoration: underline; +} + </style> + <script type="text/javascript"> + function update() + { + external.startPage.providerString(function(provider) { + document.getElementById('footer').innerHTML = + provider + + ' | <a href="http://eric-ide.python-projects.org/">' + + '@ERIC_LINK@</a>'; + document.getElementById('lineEdit').placeholder = provider; + }); + + // Try to change the direction of the page: + + var newDir = '@QT_LAYOUT_DIRECTION@'; + newDir = newDir.toLowerCase(); + if ((newDir != 'ltr') && (newDir != 'rtl')) + newDir = 'ltr'; + document.getElementsByTagName('body')[0].setAttribute( + 'dir', newDir); + } + + function formSubmitted() + { + var string = lineEdit.value; + + if (string.length == 0) + return; + + external.startPage.searchUrl(string, function(url) { + window.location.href = url; + }); + } + + // Initialize + if (window.external) { + update(); + } else { + document.addEventListener('_eric_external_created', update); + } + </script> +</head> +<body onload="document.forms[0].lineEdit.select();"> + <div id="header"> + <h1>@HEADER_TITLE@</h1> + <img src="@IMAGE@" width="32" height="32"/> + </div> + <div id="search"> + <form action="javascript:formSubmitted();"> + <fieldset> + <input id="lineEdit" name="lineEdit" type="text" /> + <input type="submit" value="@SUBMIT@"/> + </fieldset> + </form> + </div> + <div id="footer"></div> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/html/tabCrashPage.html Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +<title>@TITLE@</title> +<link rel="icon" href="data:image/png;base64,@FAVICON@" type="image/x-icon" /> +<style> +body { + padding: 3em 0em; + background: -webkit-gradient(linear, left top, left bottom, from(#85784A), to(#FDFDFD), color-stop(0.5, #FDFDFD)); + background-repeat: repeat-x; +} +img { + float: left; + opacity: .8; +} +#box { + background: white; + border: 1px solid #85784A; + width: 600px; + padding: 60px; + margin: auto; + border-radius: 0.8em; +} +h1 { + font-size: 130%; + font-weight: bold; + border-bottom: 1px solid #85784A; + margin-left: 64px; +} +h2 { + font-size: 100%; + font-weight: normal; + border-bottom: 1px solid #85784A; + margin-left: 64px; +} +ul { + font-size: 100%; + padding-left: 64px; + margin: 5px 0; +} +</style> +</head> +<body> + <div id="box"> + <img src="@IMAGE@" width="48" height="48"/> + <h1>@H1@</h1> + <ul> + <li>@LI-1@</li> + <li>@LI-2@</li> + <input type="submit" id="reloadButton" value="@BUTTON@" onclick="window.location.reload()"> + </ul> + </div> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/html_rc.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,689 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.6.0) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x12\x96\ +\x00\ +\x00\x4a\x63\x78\x9c\xcd\x3c\x6b\x77\xdb\x36\x96\xdf\xf3\x2b\x18\ +\xba\x29\xa9\x46\x6f\xdb\xa9\x63\x59\x1e\x39\xb6\xd2\x68\xd7\xb1\ +\xbd\x96\xd2\xb4\x9b\xcd\xf1\xa1\x49\x58\xe2\x86\x22\x39\x24\xe5\ +\xc7\xa4\xfe\xef\x7b\x2f\x00\x92\x00\xf8\x90\x9c\x64\xe7\x54\x73\ +\x26\x96\x08\xdc\x8b\xfb\x7e\x00\x60\x0f\x9e\x9f\x9c\x1f\xcf\xfe\ +\xbc\x18\x6b\x8b\x64\xe9\x1d\x3e\x3b\x48\xff\x10\xcb\x81\x3f\x4b\ +\x92\x58\x30\x92\x84\x2d\xf2\xcf\x95\x7b\x3b\xd4\xed\xc0\x4f\x88\ +\x9f\xb4\x92\x87\x90\xe8\x1a\xff\x35\xd4\x13\x72\x9f\x74\x10\x74\ +\xa0\xd9\x0b\x2b\x8a\x49\x32\x5c\x25\x37\xad\x3d\x1d\x70\x24\x6e\ +\xe2\x91\xc3\xd1\x74\x32\x1b\xb7\x66\x93\xd9\xe9\x78\x74\xd0\x61\ +\xcf\x9e\x1d\x78\xae\xff\x45\x8b\x88\x37\xd4\x5d\xc0\xa5\x6b\x8b\ +\x88\xdc\x0c\xf5\xd1\xdb\xa3\xdf\x27\xc7\xe7\x67\x23\x5d\xc3\x75\ +\x60\x70\x69\xcd\x49\xe7\xbe\xc5\x26\x75\x00\x30\x4e\x1e\x3c\xc2\ +\x47\xe9\xe2\x76\x1c\xeb\xda\x92\x38\xae\x35\xd4\x63\x3b\x22\xc4\ +\x87\xb5\xaf\x03\xe7\x41\xfb\xfa\x4c\x83\xcf\xb5\x65\x7f\x99\x47\ +\xc1\xca\x77\xf6\xb5\xd6\x1d\xb9\xfe\xe2\x26\xad\x79\x64\x39\x2e\ +\xd0\x6f\x02\x19\xc4\x8a\x9a\x9a\x47\x6e\x12\x2d\x09\x42\xfe\xed\ +\x3a\x48\x92\x60\xd9\xd4\x6e\xa2\x60\x69\x6e\xed\xed\xfe\xba\xb7\ +\x73\xd4\x68\xc2\x04\x73\xeb\xed\x09\xfe\x0f\x7e\xd8\x81\x17\x44\ +\xad\x18\x80\xcc\x6e\x7b\xb7\xa9\xa5\x23\x8d\x81\xb2\x6c\x2b\x22\ +\x21\xb1\x92\x7d\x8d\xfd\x6d\xdd\xb3\x09\x37\x20\xc2\x7d\xad\xb7\ +\x1d\xde\x77\xfa\xfd\xf0\x5e\xd3\xdf\x11\xef\x96\x24\xae\x6d\x69\ +\x67\x64\x45\xf4\xa6\x96\x3d\x68\x6a\x47\x91\x6b\x79\x4d\x2d\xb6\ +\xfc\xb8\x15\x93\xc8\xbd\x61\x38\x28\x0d\xfb\xda\xd6\x6e\x7f\xd7\ +\x7e\xf5\x6a\xf0\xec\x91\x31\xfe\x0b\x67\x3d\x65\x77\x05\x20\x00\ +\xe6\x11\x1b\x56\xf4\x03\x9f\xe4\x14\xb4\x62\xf7\x5f\x04\xc8\xe8\ +\x76\x5f\xb0\x87\x28\x91\xd6\x82\xb8\xf3\x05\x52\xd7\x7e\xc5\x9e\ +\x2e\xad\x68\xee\xfa\xfb\x5a\x37\xbc\xc7\x55\xda\x96\xe3\xf0\x35\ +\xc2\x20\x76\x13\x37\x80\x31\xeb\x3a\x0e\xbc\x55\xc2\x91\x47\x14\ +\x43\x0f\x01\x40\x44\xec\x0b\x1d\xb8\x73\x9d\x64\xb1\xaf\xf5\x77\ +\xd2\x07\xe9\x62\xf9\x13\x51\x65\xab\xc8\x33\x47\x93\xf7\xbf\x5d\ +\x5d\x9c\x7e\x98\x8e\xb8\x70\xed\x55\x14\x23\xe3\x61\xe0\x82\x1d\ +\x46\x48\xd2\xb3\x67\x5b\x60\xa8\xf6\x17\xb0\x03\x8f\x93\x96\x12\ +\x6d\xad\x92\x80\xc1\xa1\xc1\xb4\x2c\xcf\x9d\xc3\x53\x9b\x30\xd0\ +\x4c\x12\x77\x9c\x8e\xeb\xc0\x73\x10\xa3\x80\xcf\x71\x6f\xdb\x30\ +\x3d\x7a\x28\x30\x0d\x26\x6c\x25\xee\x6d\x2a\x51\x2f\x40\x45\xa3\ +\x0d\x71\x4e\x82\xc8\x01\xd9\x73\x9e\x73\x21\xa4\x9a\xe1\xe3\xd4\ +\xcc\x39\xaf\x6f\xce\xff\x68\xbd\x39\xbf\x3c\x19\x5f\x8e\x1a\x00\ +\x21\xcb\x7f\x97\xc9\x5f\x20\xcd\x5d\xce\x39\x51\x8e\x1b\x87\x9e\ +\xf5\x00\x0c\x78\x81\xfd\x65\x50\x22\x02\x09\xd0\x5a\xa3\x40\x64\ +\x82\x2b\x9c\x8a\x0e\x94\xa8\xa9\x4a\xcc\xcd\x26\x55\xe2\xde\xaf\ +\x2f\x98\x36\x32\x91\xed\x2f\x82\x5b\x12\x69\x6d\xf0\xd0\xa4\x59\ +\x7c\x6c\x7b\x41\x4c\x4a\x9e\x83\x60\x03\xcb\x51\x59\x73\x7d\xb4\ +\x4f\x5c\x21\x0e\x2d\xbf\x7d\x1d\xdc\xcf\x30\xa0\xf0\x69\x8c\xaa\ +\x9c\xa8\xa5\x75\x9f\x99\x72\x3f\xa3\xbd\x8a\x61\xca\xe1\xde\xde\ +\x8b\x72\xf6\x2b\x2c\x07\x89\x05\xad\xdf\xed\x2f\x5c\xc7\x21\x7e\ +\x46\x19\x65\x4b\x22\x4b\xeb\x15\x2c\x3e\x7f\x52\xaf\x84\xd7\xfd\ +\x17\x02\x89\xaf\x53\xf6\x4a\xdd\xe4\xf8\xf4\x7c\x3a\x06\xcb\xf1\ +\x03\x1e\x77\x0a\xe1\x28\x5f\x4b\x64\x84\x59\x22\xd0\x04\xa1\x08\ +\x28\x70\x1d\x2d\x89\x20\xda\x84\x56\x04\x93\x06\xb2\x16\x58\x08\ +\x11\x39\xe5\x5a\xfb\x2a\x1a\x3d\x0f\x4e\xf3\x88\x3c\x48\xce\x80\ +\xb1\x77\x15\xef\x6b\xdb\xcc\x96\x29\x12\x34\x8e\x1f\x25\x2d\xd9\ +\x64\xeb\xa5\x35\x3e\x99\xcc\xfe\x9d\xc2\x42\x3e\xbf\x57\x56\x92\ +\x67\xa4\xd2\x7a\x55\x90\xd6\xab\x6f\xb1\xad\x6e\x6d\x08\xbe\x1c\ +\x9f\x9e\x1f\x9d\xfc\x3b\xe5\xc5\x78\xfd\x16\x89\xed\x30\x89\x41\ +\x5a\x40\x58\xc0\xdc\x2a\xda\xd8\xf6\x5e\xc6\xaf\x14\x2a\x5e\xed\ +\xe6\x8f\x31\x7e\xb6\x98\xa8\xf2\x3c\xc2\x1f\xb3\x1c\x57\x7c\x4e\ +\x45\x09\x61\x28\x13\xa6\x94\x05\xfa\x4f\xce\x02\xfd\x5d\x1a\xbb\ +\x25\x46\x36\x89\xfb\x1b\xd1\xad\x22\x6e\x5f\xaf\xa0\xf8\xf1\x21\ +\xb4\x42\xb4\x0d\x57\x89\x94\x4c\x53\xd0\xed\x52\x01\x6d\xa7\x32\ +\x6f\xdf\x04\xd1\x72\x66\x5d\x2b\x91\x59\xdb\xde\xed\xfe\x08\xc9\ +\xf2\x2c\x28\xac\x42\x29\xfd\x94\x57\x85\xfa\x67\xc5\x3f\xb2\x9c\ +\x50\x5a\x13\xa5\x92\x00\xca\xa1\x82\x4d\x36\x2a\x6d\xb6\x5f\x89\ +\x71\xe6\xfb\xeb\x9b\xe9\x78\x36\x9b\x9c\xfd\x56\x57\xe3\x6c\x21\ +\x71\xae\x3f\x8f\xdf\x80\x72\x36\xa1\x51\xdb\xdd\x93\x82\x61\x3f\ +\x37\x6c\x10\x66\x4a\xe9\xae\x4a\x7b\x2e\xf6\x94\xf6\xfc\x89\x48\ +\xfb\xd6\xf8\x64\x7c\x3c\x56\x4a\xc4\x5d\xc5\xea\x53\x87\xec\x65\ +\x03\x21\x54\x8f\xc0\x05\x24\x5b\x08\x07\x3d\x65\x7e\x6d\x90\x08\ +\x42\xcb\x76\x13\x30\xf5\x1e\xfb\xfd\xaf\x96\xeb\x3b\xe4\x1e\x7d\ +\xaa\x5b\x10\x50\x9b\x37\x29\x5c\x52\xbc\x36\xa3\x82\x29\x31\xc1\ +\x5e\x6a\x55\x12\x8e\x50\xf3\xac\x6b\xa2\x16\x94\xfd\x02\x27\xbd\ +\xba\x3a\x41\xa5\x2b\x09\xe6\x41\x28\xbb\x15\x6b\x38\x04\x3c\x1c\ +\x73\x36\xd0\x2f\x21\x8e\x7b\xea\x37\x11\x27\x46\x67\xae\xf5\xd7\ +\x7b\x2f\x8a\x4b\x44\xc1\x5d\xbc\x01\xff\xe0\xf9\x5a\x57\x52\xbb\ +\xc0\x11\xd7\xe6\xd6\xde\xde\x5e\xe5\x02\x62\xa8\xa9\xaa\xb5\x38\ +\x9d\x7b\x6a\xcd\xd9\xeb\xcb\x41\x45\x48\x63\xaa\x18\x4b\xac\x84\ +\x13\x80\xe9\x26\xb5\x94\x62\x37\x50\x47\x56\x41\x2a\x72\x0c\xfb\ +\x35\x7d\xac\x14\xb1\x2d\x21\x5c\x67\x9e\xc8\x03\xa8\x10\x83\x25\ +\x33\xa1\x4e\xac\x26\xa7\x94\xb7\xd6\x6e\x99\x11\x72\x7b\x7f\x7c\ +\x76\xd0\xa1\xad\xf3\xe1\x33\xe8\xa1\xed\xc8\x0d\x13\xb1\x89\xfe\ +\x5f\xeb\xd6\x62\x4f\x75\x2d\x8e\x6c\xe8\xc4\xff\xe3\xbf\x3e\x8c\ +\x2f\xff\x1c\xe9\x87\x00\x47\x07\x0e\x9f\x02\xd7\xfa\x30\x79\x02\ +\xe8\x21\xa5\xfa\xd6\x8a\x34\xac\x2f\x20\x08\x5e\x4d\xde\x1f\xfd\ +\x36\xd6\x86\x9a\x31\xe2\x4f\x5a\x10\x22\x47\xc6\x20\x9b\xf8\xe1\ +\xf2\x94\x0e\xc3\x5f\xf1\x31\xdd\x66\xa0\x03\x6c\xc3\x41\x18\xc2\ +\x52\x8f\x8e\x1c\x5d\x5c\x9c\xfe\x29\x8e\x9c\x8d\x3f\x5e\x5d\xa4\ +\xeb\xc1\x8f\x16\xfe\x28\x60\xbd\xca\x10\xd0\x9f\x2d\x5a\x3a\x16\ +\x26\x5d\x8e\xdf\x9f\xff\x2e\x50\xd0\x62\x0f\xca\x26\x22\x67\xd2\ +\x44\x5a\x5c\x15\x26\xbe\x1d\xcf\x8e\xdf\x29\x7c\xb5\xf2\x87\x22\ +\xc0\xfb\xa3\x3f\x28\x23\xd3\xab\xcb\xf3\x8f\x30\x7b\x04\x7f\x28\ +\x33\xd3\x51\x3e\xe9\x64\x72\x74\x7a\xf5\x71\x72\x32\x7b\x87\x33\ +\xa6\x27\xad\xe9\xe4\xbf\xc7\x30\x9e\x4d\xc0\x2a\x00\xbc\x63\xe2\ +\xc0\x78\xab\x97\x03\x82\x39\x05\x11\x39\x03\xdd\x1d\x2f\x2c\x7f\ +\x4e\x70\xfc\xc6\xf2\x62\xc2\x61\x6f\x56\xbe\x8d\xa9\x48\x23\xb1\ +\x6d\x85\x84\x36\x66\x26\xdd\xef\x69\x70\x2b\xa6\xb6\x49\xfb\xb5\ +\x21\xfb\x0b\xf5\x1d\xb8\x84\x4d\xcc\x8e\xde\x99\x37\x35\xe3\xe7\ +\x7f\xae\x82\x64\x60\xf0\x0c\x58\x33\xdd\x60\xd3\x2d\x48\x7f\xd2\ +\xf4\x88\x24\xab\xc8\x67\xb3\xd9\xd3\x47\x85\xb8\x95\xff\x2d\xe4\ +\x31\xc2\xe8\xa2\xfa\x06\xe4\x31\xc2\xe8\xf4\xff\x31\x9e\x46\x1f\ +\xa3\xee\x03\x54\x05\x50\x19\x88\x94\xc1\x4f\x58\x08\xfe\x55\x85\ +\x26\xe2\x2f\x99\x64\x14\x26\x71\x22\x60\x56\x39\x09\x81\x7f\x49\ +\x96\x50\x13\x1e\x7b\xae\xfd\xc5\x84\x20\x24\x92\x11\xd1\x21\x88\ +\x9c\xe6\x4f\x74\xa8\x4d\x33\xb0\x99\x6e\x7b\x15\x71\x8d\xc1\x9c\ +\xfe\x93\x3c\x5c\x44\x24\x8e\x4d\x49\xd6\xee\x8d\x66\x92\xf6\x17\ +\xf2\x70\x1c\x38\x20\xc4\xa1\xd6\xdb\x16\x87\xf1\x03\x2b\x20\x3c\ +\x71\x4c\x81\x7e\x81\x07\x6e\x7f\xe9\xd3\xc7\xec\x1b\x81\xe7\xc5\ +\x05\xfa\xbf\xaa\x0b\xfc\x64\x1a\x5b\x37\x96\x43\xce\x59\x09\x6c\ +\x34\xa0\x9f\x45\xae\x9f\xb8\x5c\xaa\xd7\x68\x55\xa1\xd6\xc0\x7f\ +\x4b\x12\x7b\x41\xad\x8e\xc9\xd5\x5e\x10\xfb\x8b\x22\x5c\x74\x33\ +\x9e\x26\xa6\x18\xac\xf3\xa5\xd2\xd9\x6d\xfa\x05\x7c\xef\x1f\xd2\ +\x44\x0c\x0d\x6c\x07\xc2\xd0\xf6\x0b\x23\xb7\x6e\xec\x42\x95\x6c\ +\xe4\xe8\x90\x6d\x6a\x84\xa7\x90\x89\x90\x69\xd0\xcd\x57\x36\xcf\ +\xf5\xa0\xc2\x2a\x60\x79\xac\xd0\x2f\x2c\x4a\x50\x43\x68\x10\x22\ +\x27\x45\xb9\xd2\x5f\xab\xc4\xd4\x63\x2f\xb8\xd3\x9b\x19\x0a\x04\ +\xfb\xc9\xd4\xc5\xd9\x7a\xa3\xcd\xcc\x0c\xd4\x50\xb5\x30\x59\xba\ +\x69\x1c\x32\x43\xe8\x9b\xe2\x06\x1d\x17\xac\xab\x24\x5c\xe5\xfa\ +\xa1\x36\x72\x0f\x39\xdc\xb7\xbc\x76\x1c\x12\xe2\x9c\xb8\xf0\xcd\ +\x96\x30\x96\xaf\x0c\x25\xc5\x34\x05\x30\xd5\x55\x99\xbd\x33\x0d\ +\xc3\x44\x14\x8b\x61\x34\xb3\x14\x43\x3d\x51\xb0\x2d\x9a\xa4\x2f\ +\x60\x31\x33\x5b\x0c\xff\x2d\xf3\xa1\x52\x6f\x14\x63\xb5\xec\x8f\ +\x03\xc9\xa8\x60\xe4\x03\x0d\x0d\x7c\x92\xbd\x70\x3d\x07\xea\x6a\ +\xd3\xb0\x50\x35\x6e\x14\x27\x66\xa3\x6d\x25\x49\x64\x1a\xb8\x15\ +\x6f\x14\xe1\x67\x3c\xd4\x89\xe1\xb3\x80\x0d\xab\x28\x01\x21\x26\ +\x7a\x53\xe4\x17\x3d\x32\xa5\x05\xfc\x11\x84\xa1\xba\x3b\x23\xd3\ +\xc0\xa3\x87\xfd\x4e\xc7\xe0\xb9\x85\x9b\x14\xee\x6e\x03\x7a\x2b\ +\x0c\x89\xef\x98\xc6\x81\xe3\xde\x6a\xae\x33\xd4\x45\xdb\xd1\x68\ +\x99\x33\xd4\xd3\x5e\xa1\xdb\x7e\x3d\x48\x4b\x2f\xba\xb7\x90\xb5\ +\x4c\x37\xee\x3d\x71\x06\xb4\xfc\xef\x0e\x0c\xed\xa5\x44\x8a\xf4\ +\x31\xb0\xf0\xea\x0e\x84\xfd\x45\x5e\x7b\xd2\xef\x69\x17\xf2\x1a\ +\x3e\x03\xa1\x3f\xe2\x07\x07\x03\xfd\xb0\x1e\x79\xc6\x86\xd8\x87\ +\xeb\xa0\x76\x88\x5c\x21\x86\xcd\xa1\xce\xe3\x8b\x1a\x4d\x6f\xa1\ +\x0c\x6d\xac\x45\x8f\x5b\x05\xb4\x38\x83\x79\x45\x03\x80\xd1\x82\ +\x09\xc0\x6c\xa3\x01\x93\x0d\xfd\x50\x5b\x83\x3c\xa1\xfd\xb7\xed\ +\x59\x48\x66\xd6\x90\x43\xed\x97\x44\xf0\x7f\x07\x69\xa3\xc5\x1a\ +\x20\xdb\xd7\x0e\x3a\xf0\x84\x3f\xad\xa5\x98\x76\x03\x42\x3b\xcf\ +\xb4\x0c\xd8\xc1\x3c\x74\xb0\x48\x6f\x45\x18\x3b\xdc\x62\x28\xa9\ +\x0c\x7b\x07\x16\x5e\x47\x73\x44\xf1\x65\x01\x50\xcf\x28\x65\x75\ +\xd6\x0f\xa2\x95\xfa\x88\x4a\x2d\x73\xa3\xa7\xd2\x4b\x09\xc9\x28\ +\x92\xd6\x4c\x33\x03\x5f\x37\xcb\x32\x68\x40\x34\x97\x81\x5d\x15\ +\x92\x4f\xb2\x70\xe3\xf5\x86\xc3\x7a\x5f\x60\x45\xc2\x7b\xa8\x69\ +\x99\xa8\xc4\xea\x14\x78\x02\xa1\x51\x98\xcd\x59\xeb\x50\xfb\x39\ +\x3c\x08\x53\x13\xca\x3a\x9f\xf5\x66\x2d\x08\x81\x41\x65\xb2\xd6\ +\x46\x6c\x1b\x5c\x43\x21\x1c\x33\x21\x48\x99\x6a\xbd\x4f\xd6\x61\ +\x67\x02\xa0\xfd\x00\x32\xad\x89\xcb\x08\x15\x0b\x6a\x38\x5c\x2b\ +\x00\x70\xfe\x43\xf6\x2f\x06\xdd\x9a\x04\xca\x72\xf4\x8d\xeb\x41\ +\xd6\xc2\xfc\x6c\x58\x5e\xb8\xb0\x4c\x1e\xe7\x86\xaf\xbb\x0d\xe3\ +\x91\xa5\xd9\x89\x2f\xc6\xff\xaa\x12\x47\x4c\xc0\x92\x70\x1e\x15\ +\x60\x31\x2c\x15\xa1\x59\x10\xd2\xbe\x6a\xf4\x4b\x1b\x0f\x44\x2f\ +\x22\x20\x6a\x6e\x31\xe4\x03\x4d\xc5\xc7\xfd\x18\xa3\x4e\x60\xaf\ +\x62\xb3\xb2\x74\xbc\xa4\xbb\xbf\xa5\x89\x0f\x93\xd2\xea\x7b\x32\ +\x1a\x06\xc5\x12\x60\x29\x1a\x0a\xea\xc0\xc4\xb5\xaa\xc8\x5a\x2c\ +\x38\x4b\xba\x03\x34\x62\x2c\x6d\xca\x8d\x6d\xa3\xb6\x06\x41\x9e\ +\x67\x8b\xd5\xf2\xda\xb7\x5c\xda\x05\x34\x59\xdd\x59\x21\x26\xc1\ +\xe0\xd4\xda\x3a\x2f\x0e\xb0\x93\x5b\x43\x74\x09\x25\xb0\xf6\xdb\ +\x28\x58\x7e\x88\x49\x34\x41\x77\x30\x65\xed\x51\xfe\x74\xea\x14\ +\x7a\x43\x28\xe8\x7c\x72\xf7\x41\xee\x5d\x52\xa1\xe3\x86\xc6\x50\ +\x73\x40\xeb\x4b\xb4\x95\x39\x49\xc6\x1e\xc1\xaf\x6f\x1e\x26\x90\ +\xcf\xb3\xc3\x49\x40\x9e\x8f\xc5\x6f\x1e\x66\xd6\xfc\xcc\x5a\x12\ +\xd3\x00\x37\x31\x1a\x9f\x32\xc6\x3e\x0f\x0a\x4b\x58\xb0\x00\xd6\ +\xc9\xe5\xf0\x60\x1b\x9f\xba\x25\x50\x41\xe4\xce\x5d\x60\x9e\x55\ +\x1f\x16\x42\x1f\x01\x77\x2e\x78\x3e\x29\xda\x0f\x7e\x62\x82\xce\ +\x82\x7d\x5a\x46\x4d\x53\xe3\x9c\x97\xcd\x64\x05\x93\x30\x37\x95\ +\x25\x1d\x50\xa5\xd9\x28\x92\xc8\x4b\xd3\xcd\x29\x44\xa0\x3c\x6c\ +\x1f\xf3\xbe\xa1\x46\xfc\xf9\x64\xf4\x72\x36\x5f\x46\x59\xc0\x4f\ +\xeb\x64\xa4\xc7\xf3\xb0\x8a\x8d\xcd\x46\xf5\x7c\x34\xc9\x22\x3d\ +\x7f\xfd\xa5\x99\xa2\xf4\x9f\x0f\x45\x4e\x7f\xfe\x59\xfc\xf5\x9c\ +\x39\x9f\xa6\x9a\x56\x4a\x0d\xf3\xe9\x6a\xed\x53\xe7\x2e\xe8\x9f\ +\xd2\xb6\x9c\xe3\xce\xbf\x20\xd1\x72\xb7\x2d\x00\x3e\xad\xcf\x29\ +\x4d\x04\x35\xcd\x4f\x61\xfe\x63\xc9\x33\xfc\xac\x0d\x23\xe5\x06\ +\xd3\x2c\x1a\xc8\xe6\x0b\x30\x2a\x27\x78\x68\xf5\x36\x88\xd0\x13\ +\xca\x17\x51\x30\x3e\xb2\xde\xbc\x28\x0b\x39\x41\xcb\x30\xd2\xaf\ +\x62\xeb\x27\xb4\xe4\x55\x4d\x5b\x66\xa0\xc5\x24\x12\xd3\x44\x60\ +\x58\x9f\xd2\x12\x84\xfa\xb5\xfe\x59\x4d\x19\xb4\x5e\xe4\x93\xb1\ +\xd1\x11\xe6\xb3\xca\xa8\x00\x41\xbd\x19\x00\x74\x5d\xca\x80\xd9\ +\x65\x07\x30\x17\x62\xd9\x8b\x3c\x99\xba\x65\x51\x33\xcd\x73\xb4\ +\x68\x7b\x52\xa2\x93\x28\x2f\xc5\x51\xc5\x87\xd2\xc2\x15\x51\xa6\ +\xf9\x53\x45\x58\xd9\x4e\xa8\x28\x50\x32\x2f\xc1\x9f\x81\xbb\x7d\ +\x5a\x1c\x2b\x3b\x5f\x58\x22\xff\x45\x09\x17\x87\xa5\x6d\x3b\x3a\ +\x05\x16\x54\xe1\x31\xfb\xd2\xc1\x81\x21\x5b\x86\xba\x5f\x43\xa9\ +\xa8\xec\xf2\xd1\x10\x69\xfa\xa5\xcb\x35\x91\xe5\xab\x38\x58\x45\ +\x36\x29\xee\xd9\xdc\x8a\x81\xd5\x8e\x88\x95\x10\x1e\x80\x78\xd2\ +\xca\x09\x41\xed\xcb\xb1\x86\xca\x1f\xa2\x8d\xc1\x8d\xa2\xac\x52\ +\xa9\x42\x4e\xe5\x2d\x34\xd8\x15\x81\x4c\xa0\x5d\xc6\x6e\xd5\xe0\ +\xb6\x44\xcc\x96\x82\x97\xda\x5a\x13\x8d\xb3\x66\x52\xc6\x19\xf3\ +\x29\x95\x35\xb4\xbe\x5e\x0d\x01\x6c\x3b\x21\x87\xa1\xf3\x6b\x96\ +\xe0\xc9\x4b\x05\x70\x7d\x9f\x44\x33\x30\x64\xdc\x1b\x2d\xd9\xff\ +\x2d\x12\xd5\x7f\x22\x51\xfd\x4a\x8d\xd2\x9a\xb9\x7e\x32\xef\x1d\ +\x70\xba\xb8\xd5\xc3\x0e\x3e\xcf\x02\x87\x34\xd6\x61\xa0\x6c\x00\ +\x7c\x7e\x50\x51\xc2\xd3\xf6\x13\x79\xda\xae\xe2\x89\xde\xf8\x59\ +\x37\x5b\x62\x4a\xdc\x4f\xae\x61\x4b\xc5\x21\xb3\xc5\x4e\x52\x4a\ +\x18\xdb\x79\x22\x63\x3b\x55\x8c\xb1\xdb\x26\xeb\xa6\x2b\x9c\xe5\ +\x2d\x4a\x0d\x67\x2a\x0e\x95\x33\xac\x33\xc4\xf8\x84\x31\x82\xed\ +\x72\x1d\x63\x64\xa5\x11\x6d\xf0\x8d\xa3\x56\xcd\x18\xf5\x8f\x35\ +\xe3\xfd\x35\xe3\xdb\x6b\xc6\x77\x24\xc6\x2a\x8a\x4f\x3d\xab\xfd\ +\xf5\x86\x84\x01\x30\xaa\x7d\x58\x1e\xcb\xb0\xb1\x91\x4b\x34\x25\ +\x7d\x3e\xbd\xbd\xc2\xcf\x63\x21\x51\x00\x15\xe5\x69\x82\x15\xf7\ +\xb4\x08\x32\x5d\xa7\x2e\x47\xfc\xa0\xde\xc7\x15\x9b\x1e\xbe\x9d\ +\x4a\xbb\xd2\x95\xef\x90\x1b\xd7\x87\x0a\xae\xbe\xcf\xfb\xb6\x42\ +\x79\x7d\x5e\xc1\x84\xfb\x0f\xcc\xc3\xd0\x04\x69\x27\xe0\x83\x95\ +\x67\x42\x80\x87\x46\xdf\x59\xc0\x93\x3d\xcf\xb0\xaa\xc4\x6c\xf5\ +\x98\x51\x11\x27\xad\xc5\xbe\x57\xa0\x39\x56\x68\xc6\xc0\xb6\x00\ +\x65\x77\xa0\xb9\xda\x01\x5b\xa1\xed\x11\x7f\x9e\x2c\x06\xda\xcb\ +\x97\xa5\xb5\x19\xd3\x2a\x9d\xfa\xc9\xfd\xac\x34\x08\x1b\xa8\x07\ +\x3f\x78\x4f\xc5\xf5\x57\x44\x81\x96\xf6\xed\xd7\xb5\xb4\x1b\xf4\ +\x83\xe9\x16\xc6\xf3\x21\x47\xfb\x24\x4a\xe8\xfd\x88\x3a\x3a\x58\ +\xa4\x2d\x74\x57\xb8\x2a\xbd\xca\x97\x27\xe1\xe7\xc3\xa2\xbe\x33\ +\x0a\x2a\xce\x6a\xd2\x8f\x82\x6a\x28\x1e\xa5\xa6\x9f\xc7\x32\x47\ +\x16\x9b\x86\xbc\x19\xa8\xb6\x50\xea\xd2\x82\x85\x56\x3b\xb6\xc5\ +\x85\x91\x76\x11\xec\x55\x06\xf4\x84\x15\xdf\x99\x96\xfa\x82\x9f\ +\xcc\x14\x40\x2d\xfe\xab\xed\x2b\x2d\xb3\x59\x86\xa9\x28\xc7\x45\ +\x32\xe4\x5d\x2d\x1d\x46\x75\x61\x57\x4b\xd5\x0f\xdd\xaf\xca\x88\ +\xc2\x37\x02\x80\x16\xb0\xd9\x6e\xd1\x42\x0a\xe1\x84\xb1\x24\xc2\ +\x8b\xdb\x5e\xa5\x65\x67\x55\x8f\x96\x6f\xa9\x60\x24\x55\x8e\xbf\ +\xff\x6e\x21\xf4\x69\xfb\x4c\x6b\x8b\xe7\x72\x59\xb0\x22\x15\xa5\ +\x51\x1a\x20\xff\x4e\xf2\xf8\x96\xf8\x50\xe7\xcc\xaa\x40\xf2\x7b\ +\x07\xae\xa3\x6e\x74\x42\xd8\x02\xdb\x5e\x9a\x3a\xbf\x1d\xf3\xf1\ +\xe8\xf2\x6c\xa4\x37\x1a\x7f\x5b\x61\xad\x7e\x44\x4c\xdf\xa8\x94\ +\x62\x62\x63\xa5\x14\x06\x85\x8a\xa3\xe7\x3c\x4c\x6e\xb2\xcf\x23\ +\xf7\x7c\x4f\x8a\xac\xc2\xaa\xf2\x6d\x01\x7c\xef\x4c\x3c\xda\x65\ +\x97\xd5\xe4\x17\x5d\x3e\x41\x50\xf9\x65\xa8\xb7\x43\x7f\xae\x7f\ +\xfe\xca\x8f\x60\xe9\xd5\x54\x76\x32\x6b\xbc\xcc\x2f\x37\xbd\x34\ +\xc2\xfb\xc7\xf4\xce\x9b\x1c\x7f\x8d\xb2\x37\x7b\xb2\x03\x16\x8a\ +\x0a\xcf\x57\x84\x8b\x52\x10\xc4\xc3\x7b\xa3\x59\xba\x33\x56\xf2\ +\x31\x18\x69\x88\xe4\xbd\x95\x2c\xda\xf4\x38\xd8\x14\xf0\x75\xb4\ +\x5e\x7b\x77\xa7\xc1\xf1\x4a\x3b\x13\x68\x1e\x94\x04\x1a\xc7\xef\ +\x5c\xdf\x09\xee\x1a\x6d\xfa\x44\x3d\xd4\x67\xab\x48\xf3\xd8\xa3\ +\x92\xd3\xff\x8f\x1c\x25\xa5\xe7\xc6\x0b\x82\xc8\x94\xf8\xdb\xee\ +\x16\x61\xde\xa5\xf8\x05\xa0\x75\xfc\xec\x74\x55\x66\x96\xd6\xfd\ +\x1b\x5e\xab\x09\x78\x18\x8b\x9d\x8c\x34\xe5\x8e\x40\x06\x74\x28\ +\x5f\x6a\x6b\x48\xe8\xc4\x91\x0a\xf8\x03\xad\x27\xc1\xf4\x8a\xd4\ +\xa5\xd2\xce\x66\xfd\x92\x51\x25\x5a\x8d\xbe\x25\xfa\x15\xda\x0a\ +\x37\x95\x66\x8e\x84\xe9\x53\x95\x00\xad\x0e\x8f\x41\x66\x4c\x57\ +\x0a\xa2\x2c\x49\x83\x29\xea\x69\xf6\x95\x75\x81\x57\x54\x53\xf1\ +\xd9\x04\xba\x16\x01\x63\x27\xa3\x5b\x01\x62\xb7\x43\x93\x20\x04\ +\x48\x93\x9b\x4a\x8b\xa1\xfa\x25\xd7\x6e\x03\x10\xf4\x95\xfe\x2a\ +\x87\x3c\x80\x12\x40\x42\xd4\x95\x4e\xb9\x4a\x44\x92\x5f\x54\xa5\ +\x72\x49\x21\x33\xc1\x20\x20\xde\xb3\xc7\xbf\x79\xb2\xb3\x6e\xc9\ +\x94\xdf\xc9\x95\xe2\x82\x7a\xa1\x11\x7d\xf7\x62\x3e\xf1\x2f\x83\ +\x3b\xf0\xd7\x5b\xbc\x7f\x93\x33\x2d\x5d\x6c\x0c\xf1\x5d\xd4\x89\ +\xcf\x8e\xac\xa6\xce\x14\xa4\x9a\x42\xac\x09\x77\x90\x79\x69\x08\ +\xa3\xab\x98\xb2\xf5\xd5\x9e\xdb\x01\x20\x5b\x48\xf0\x0c\x71\xad\ +\x92\xbb\x3e\xaa\x1c\x28\xf8\x2c\x98\xcf\x3d\x52\xd8\xa8\xa6\x07\ +\x32\x75\x19\x6c\x2a\x40\xab\x1b\x6d\x6c\x6c\x3d\x74\x39\xdc\xd4\ +\xab\x83\x8c\x3d\xd7\x21\xd1\xef\xb8\x95\x3a\x95\x36\x4f\xb2\x2f\ +\x0c\x4b\xdb\x71\x63\x3c\xec\xc7\xe2\x9e\xdd\x7e\x13\x2e\xb3\xd1\ +\x0e\x0f\xe2\x25\x56\xfd\x02\x0a\x0e\x99\xee\xa3\x17\xc0\xa4\xf1\ +\x7d\xad\xbf\xdd\x2b\x00\x4f\x3d\x56\x62\xbc\x9b\xbd\x3f\x2d\x43\ +\x21\x98\x8d\x88\x40\x4d\x5c\xb4\xc6\x98\xaf\x22\x52\x7d\xf7\xab\ +\xd3\xd1\x4e\xf1\xcd\xac\xf4\x7a\xb9\x94\x72\x64\xb3\xad\x32\x2b\ +\x9c\x29\xc8\xf3\x02\xf7\xd2\xf1\xed\xea\x5a\x00\xc9\xbc\x25\xdb\ +\x2b\xce\x4a\xcd\xa3\x1d\x46\x41\x68\x1a\x5c\x0c\xe0\xab\x82\x18\ +\xa0\x3f\x93\x05\xa9\x50\x85\x5a\x66\x54\x6d\xba\x98\x0b\x91\x61\ +\x3f\x5d\xab\x01\x42\x97\xc8\x66\x05\xc6\x11\x6d\x1b\x52\x1b\x81\ +\x59\xfb\xf2\x2c\x4b\x1e\x6f\x6a\xc2\x5c\xc1\xcb\x40\x09\xd3\x45\ +\x70\xa7\x61\x50\x0a\xe6\x12\x51\xc2\x21\x5b\x7f\xfd\x7d\x8a\x5e\ +\x77\xc3\x0b\x15\xfd\xd2\x1b\x15\x59\xcf\x96\x9e\x07\x1a\x78\x1e\ +\x68\x94\x5c\x87\x10\xde\x46\xf8\x9e\xdb\x15\x65\x37\x07\xd9\x86\ +\xe6\x91\xe7\x15\xee\x06\x94\x95\xcc\x78\x05\x5d\x2a\x9b\x39\x85\ +\x4f\x38\xae\xc2\x8f\xbc\x29\xca\xc5\xa0\x1e\xe8\xe5\x5e\x26\xbc\ +\x24\xd0\x61\xff\xf1\x81\x67\x07\x78\xcf\x8f\xbd\x14\x90\xdd\x8d\ +\xcb\x33\x0d\xbf\x2d\xc3\x86\xad\xfc\xe6\x4d\x99\x87\x0e\x74\xd6\ +\x53\x0c\xf5\x51\xfa\x7e\x15\xff\x4f\x11\xe8\xe9\x85\x23\x14\x3f\ +\xe2\xb4\x8a\x18\xe5\x9b\x9e\x02\xae\xa3\x93\x13\x15\x0d\xcc\xe5\ +\x58\x18\x9a\x0d\x5e\x78\xc8\xd4\xe4\xfa\x6e\x52\x88\x26\xa3\xc9\ +\xd9\x64\x06\xfe\xd5\x9a\x1e\x5f\x4e\x2e\x66\xa3\xda\xa4\x45\x8f\ +\x41\x79\x11\x8e\xef\x3d\xf9\xc4\x4e\xaa\x37\x16\xe8\x26\xaa\x7a\ +\x3d\xb6\x4c\x95\x35\x57\xfe\xd5\xa9\x69\x9f\x23\x69\x59\xfa\xc5\ +\x6a\xd4\xb6\x17\xd8\xd4\x76\xf9\xbb\x9d\xa2\x63\x3d\xae\x49\xcd\ +\x49\xba\x6d\x8b\x61\x56\x60\x54\xda\xae\xa9\x4f\xd2\x28\x28\xda\ +\x55\x17\x51\xe4\x7b\x92\xb5\x59\x1b\x3f\x79\xc1\x1d\x11\x5a\xb0\ +\x49\x9e\x2f\x82\x28\xde\xce\x8b\x3c\x1e\xb9\xa6\xf4\xf5\x43\xe6\ +\xc7\x95\x65\x55\x1c\x44\xf4\x8e\x9c\x29\xab\x27\x82\x80\x10\x25\ +\x2c\x61\xca\x8d\x49\xfa\xe2\xa0\x81\x51\x55\x69\x5a\x70\x83\xcf\ +\x72\x7d\x4c\xde\x30\x21\x4d\xe9\xca\xa4\xec\x2d\xbb\x6e\x7b\x4f\ +\x1e\x01\xba\x13\xcb\xb7\xc9\x3e\xd4\xf9\xf2\xc8\x2a\x74\xac\x04\ +\x9e\xcb\x81\xab\xa9\xad\x4a\x43\x44\x6d\xcb\x98\x59\x43\xc9\x86\ +\x51\xfa\x84\x7e\x81\x50\x3f\x01\xcf\x01\x31\x81\x0a\xe8\x13\x34\ +\x6c\x6e\x66\xa9\xee\xa5\xc0\x47\xfd\x8c\xe3\x52\xaf\x21\x64\xf5\ +\x0d\x78\xf2\x18\x89\x3f\x05\x66\x09\x14\x0e\xa6\x71\x45\x22\xd7\ +\xbe\x4a\x31\x5e\xb1\x23\x26\x4c\x41\x88\x50\xa2\x2d\x0f\x66\xf4\ +\x57\xc9\xfd\xe4\xfe\xb7\x5e\x50\xde\xe8\x0a\x32\xfc\x28\xbf\x81\ +\xcc\xd9\xcc\x28\x12\x92\x4e\x36\xc8\x87\x79\x40\xa3\x6f\x29\x0a\ +\x63\x30\x2a\xdc\x07\xe5\xe5\x8c\x7e\x38\x9a\xfd\x31\xbb\x3a\xbb\ +\x84\xaa\x64\x3a\x4a\x6f\x7f\xe6\xf8\xf2\x48\xad\xa2\x67\xaf\xdd\ +\xc9\xf8\xe9\x1e\x12\xa5\x4e\x2c\x81\x60\x8d\xfc\x3d\x26\x10\x30\ +\x4c\x92\xa0\xd8\x4d\x4d\x04\x4b\x89\xe2\x51\x37\x42\x03\xd3\xf1\ +\x55\xd7\xa1\x0e\x62\x87\x3e\x69\xa8\xef\x65\xb7\x38\x05\xa4\xa8\ +\x12\x12\x0e\xf5\x1e\xbd\x33\x4b\xed\x72\xa8\x57\x96\x62\x98\xcf\ +\x58\xc5\x89\x29\xa1\xb3\x11\xb7\x25\xc2\x2c\xbf\xc3\xeb\x5b\x4b\ +\xf8\x1d\x3b\x18\x55\xf8\x55\x62\xb1\x96\x12\x09\x94\xbb\x05\xa4\ +\xe4\x67\xff\x3a\x0e\x07\xa2\x9a\x24\x58\xa6\xab\xe9\x09\x7d\xe1\ +\xeb\xff\x43\x57\x53\x48\x81\xd9\x1b\x65\xb5\x9a\x62\x74\x95\x29\ +\xaa\xb7\xb3\xcb\x55\xb5\xfd\xaa\x9b\x2b\x2b\xc5\xba\x89\xaa\xf2\ +\xfa\xf4\x5b\x54\xc5\xdf\x1a\x96\xb9\x2d\xbb\x9e\x2c\x05\xab\xa7\ +\xde\x46\x2e\xa9\x21\xd5\x42\x51\x22\x78\xed\x1a\x50\x30\xb0\x17\ +\x1b\x35\xe9\x3a\xb2\xdc\x5a\x0f\x36\x59\x56\x5a\x15\x6f\x30\x97\ +\x4b\x4d\xac\xc2\xd8\xd7\x83\x0e\xab\xdb\x0e\x3a\xec\xbf\x25\xf5\ +\x7f\x9e\xbc\x80\x7b\ +\x00\x00\x04\x7b\ +\x3c\ +\x21\x44\x4f\x43\x54\x59\x50\x45\x20\x68\x74\x6d\x6c\x3e\x0a\x3c\ +\x68\x74\x6d\x6c\x3e\x0a\x3c\x68\x65\x61\x64\x3e\x0a\x3c\x6d\x65\ +\x74\x61\x20\x63\x68\x61\x72\x73\x65\x74\x3d\x22\x75\x74\x66\x2d\ +\x38\x22\x3e\x20\x0a\x3c\x6d\x65\x74\x61\x20\x68\x74\x74\x70\x2d\ +\x65\x71\x75\x69\x76\x3d\x22\x63\x6f\x6e\x74\x65\x6e\x74\x2d\x74\ +\x79\x70\x65\x22\x20\x63\x6f\x6e\x74\x65\x6e\x74\x3d\x22\x74\x65\ +\x78\x74\x2f\x68\x74\x6d\x6c\x3b\x20\x63\x68\x61\x72\x73\x65\x74\ +\x3d\x75\x74\x66\x2d\x38\x22\x3e\x0a\x3c\x74\x69\x74\x6c\x65\x3e\ +\x40\x54\x49\x54\x4c\x45\x40\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\ +\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x69\x63\x6f\x6e\x22\ +\x20\x68\x72\x65\x66\x3d\x22\x64\x61\x74\x61\x3a\x69\x6d\x61\x67\ +\x65\x2f\x70\x6e\x67\x3b\x62\x61\x73\x65\x36\x34\x2c\x40\x46\x41\ +\x56\x49\x43\x4f\x4e\x40\x22\x20\x74\x79\x70\x65\x3d\x22\x69\x6d\ +\x61\x67\x65\x2f\x78\x2d\x69\x63\x6f\x6e\x22\x20\x2f\x3e\x0a\x3c\ +\x73\x74\x79\x6c\x65\x3e\x0a\x62\x6f\x64\x79\x20\x7b\x0a\x20\x20\ +\x70\x61\x64\x64\x69\x6e\x67\x3a\x20\x33\x65\x6d\x20\x30\x65\x6d\ +\x3b\x0a\x20\x20\x62\x61\x63\x6b\x67\x72\x6f\x75\x6e\x64\x3a\x20\ +\x2d\x77\x65\x62\x6b\x69\x74\x2d\x67\x72\x61\x64\x69\x65\x6e\x74\ +\x28\x6c\x69\x6e\x65\x61\x72\x2c\x20\x6c\x65\x66\x74\x20\x74\x6f\ +\x70\x2c\x20\x6c\x65\x66\x74\x20\x62\x6f\x74\x74\x6f\x6d\x2c\x20\ +\x66\x72\x6f\x6d\x28\x23\x38\x35\x37\x38\x34\x41\x29\x2c\x20\x74\ +\x6f\x28\x23\x46\x44\x46\x44\x46\x44\x29\x2c\x20\x63\x6f\x6c\x6f\ +\x72\x2d\x73\x74\x6f\x70\x28\x30\x2e\x35\x2c\x20\x23\x46\x44\x46\ +\x44\x46\x44\x29\x29\x3b\x0a\x20\x20\x62\x61\x63\x6b\x67\x72\x6f\ +\x75\x6e\x64\x2d\x72\x65\x70\x65\x61\x74\x3a\x20\x72\x65\x70\x65\ +\x61\x74\x2d\x78\x3b\x0a\x7d\x0a\x69\x6d\x67\x20\x7b\x0a\x20\x20\ +\x66\x6c\x6f\x61\x74\x3a\x20\x6c\x65\x66\x74\x3b\x0a\x20\x20\x6f\ +\x70\x61\x63\x69\x74\x79\x3a\x20\x2e\x38\x3b\x0a\x7d\x0a\x23\x62\ +\x6f\x78\x20\x7b\x0a\x20\x20\x62\x61\x63\x6b\x67\x72\x6f\x75\x6e\ +\x64\x3a\x20\x77\x68\x69\x74\x65\x3b\x0a\x20\x20\x62\x6f\x72\x64\ +\x65\x72\x3a\x20\x31\x70\x78\x20\x73\x6f\x6c\x69\x64\x20\x23\x38\ +\x35\x37\x38\x34\x41\x3b\x0a\x20\x20\x77\x69\x64\x74\x68\x3a\x20\ +\x36\x30\x30\x70\x78\x3b\x0a\x20\x20\x70\x61\x64\x64\x69\x6e\x67\ +\x3a\x20\x36\x30\x70\x78\x3b\x0a\x20\x20\x6d\x61\x72\x67\x69\x6e\ +\x3a\x20\x61\x75\x74\x6f\x3b\x0a\x20\x20\x62\x6f\x72\x64\x65\x72\ +\x2d\x72\x61\x64\x69\x75\x73\x3a\x20\x30\x2e\x38\x65\x6d\x3b\x0a\ +\x7d\x0a\x68\x31\x20\x7b\x0a\x20\x20\x66\x6f\x6e\x74\x2d\x73\x69\ +\x7a\x65\x3a\x20\x31\x33\x30\x25\x3b\x0a\x20\x20\x66\x6f\x6e\x74\ +\x2d\x77\x65\x69\x67\x68\x74\x3a\x20\x62\x6f\x6c\x64\x3b\x0a\x20\ +\x20\x62\x6f\x72\x64\x65\x72\x2d\x62\x6f\x74\x74\x6f\x6d\x3a\x20\ +\x31\x70\x78\x20\x73\x6f\x6c\x69\x64\x20\x23\x38\x35\x37\x38\x34\ +\x41\x3b\x0a\x20\x20\x6d\x61\x72\x67\x69\x6e\x2d\x6c\x65\x66\x74\ +\x3a\x20\x36\x34\x70\x78\x3b\x0a\x7d\x0a\x68\x32\x20\x7b\x0a\x20\ +\x20\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\x20\x31\x30\x30\x25\ +\x3b\x0a\x20\x20\x66\x6f\x6e\x74\x2d\x77\x65\x69\x67\x68\x74\x3a\ +\x20\x6e\x6f\x72\x6d\x61\x6c\x3b\x0a\x20\x20\x62\x6f\x72\x64\x65\ +\x72\x2d\x62\x6f\x74\x74\x6f\x6d\x3a\x20\x31\x70\x78\x20\x73\x6f\ +\x6c\x69\x64\x20\x23\x38\x35\x37\x38\x34\x41\x3b\x0a\x20\x20\x6d\ +\x61\x72\x67\x69\x6e\x2d\x6c\x65\x66\x74\x3a\x20\x36\x34\x70\x78\ +\x3b\x0a\x7d\x0a\x75\x6c\x20\x7b\x0a\x20\x20\x66\x6f\x6e\x74\x2d\ +\x73\x69\x7a\x65\x3a\x20\x31\x30\x30\x25\x3b\x0a\x20\x20\x70\x61\ +\x64\x64\x69\x6e\x67\x2d\x6c\x65\x66\x74\x3a\x20\x36\x34\x70\x78\ +\x3b\x0a\x20\x20\x6d\x61\x72\x67\x69\x6e\x3a\x20\x35\x70\x78\x20\ +\x30\x3b\x0a\x7d\x0a\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x0a\x3c\x2f\ +\x68\x65\x61\x64\x3e\x0a\x3c\x62\x6f\x64\x79\x3e\x0a\x20\x20\x3c\ +\x64\x69\x76\x20\x69\x64\x3d\x22\x62\x6f\x78\x22\x3e\x0a\x20\x20\ +\x20\x20\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x22\x40\x49\x4d\x41\ +\x47\x45\x40\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x34\x38\x22\x20\ +\x68\x65\x69\x67\x68\x74\x3d\x22\x34\x38\x22\x2f\x3e\x0a\x20\x20\ +\x20\x20\x3c\x68\x31\x3e\x40\x48\x31\x40\x3c\x2f\x68\x31\x3e\x0a\ +\x20\x20\x20\x20\x3c\x75\x6c\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\ +\x6c\x69\x3e\x40\x4c\x49\x2d\x31\x40\x3c\x2f\x6c\x69\x3e\x0a\x20\ +\x20\x20\x20\x20\x20\x3c\x6c\x69\x3e\x40\x4c\x49\x2d\x32\x40\x3c\ +\x2f\x6c\x69\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x69\x6e\x70\x75\ +\x74\x20\x74\x79\x70\x65\x3d\x22\x73\x75\x62\x6d\x69\x74\x22\x20\ +\x69\x64\x3d\x22\x72\x65\x6c\x6f\x61\x64\x42\x75\x74\x74\x6f\x6e\ +\x22\x20\x76\x61\x6c\x75\x65\x3d\x22\x40\x42\x55\x54\x54\x4f\x4e\ +\x40\x22\x20\x6f\x6e\x63\x6c\x69\x63\x6b\x3d\x22\x77\x69\x6e\x64\ +\x6f\x77\x2e\x6c\x6f\x63\x61\x74\x69\x6f\x6e\x2e\x72\x65\x6c\x6f\ +\x61\x64\x28\x29\x22\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x75\x6c\x3e\ +\x0a\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x62\x6f\x64\x79\ +\x3e\x0a\x3c\x2f\x68\x74\x6d\x6c\x3e\x0a\ +\x00\x00\x03\x7c\ +\x3c\ +\x21\x44\x4f\x43\x54\x59\x50\x45\x20\x68\x74\x6d\x6c\x3e\x0a\x3c\ +\x68\x74\x6d\x6c\x3e\x0a\x3c\x68\x65\x61\x64\x3e\x0a\x3c\x6d\x65\ +\x74\x61\x20\x63\x68\x61\x72\x73\x65\x74\x3d\x22\x75\x74\x66\x2d\ +\x38\x22\x3e\x20\x0a\x3c\x6d\x65\x74\x61\x20\x68\x74\x74\x70\x2d\ +\x65\x71\x75\x69\x76\x3d\x22\x63\x6f\x6e\x74\x65\x6e\x74\x2d\x74\ +\x79\x70\x65\x22\x20\x63\x6f\x6e\x74\x65\x6e\x74\x3d\x22\x74\x65\ +\x78\x74\x2f\x68\x74\x6d\x6c\x3b\x20\x63\x68\x61\x72\x73\x65\x74\ +\x3d\x75\x74\x66\x2d\x38\x22\x3e\x0a\x3c\x74\x69\x74\x6c\x65\x3e\ +\x40\x54\x49\x54\x4c\x45\x40\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\ +\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x69\x63\x6f\x6e\x22\ +\x20\x68\x72\x65\x66\x3d\x22\x40\x46\x41\x56\x49\x43\x4f\x4e\x40\ +\x22\x20\x74\x79\x70\x65\x3d\x22\x69\x6d\x61\x67\x65\x2f\x78\x2d\ +\x69\x63\x6f\x6e\x22\x20\x2f\x3e\x0a\x3c\x73\x74\x79\x6c\x65\x3e\ +\x0a\x62\x6f\x64\x79\x20\x7b\x0a\x20\x20\x70\x61\x64\x64\x69\x6e\ +\x67\x3a\x20\x33\x65\x6d\x20\x30\x65\x6d\x3b\x0a\x20\x20\x62\x61\ +\x63\x6b\x67\x72\x6f\x75\x6e\x64\x3a\x20\x2d\x77\x65\x62\x6b\x69\ +\x74\x2d\x67\x72\x61\x64\x69\x65\x6e\x74\x28\x6c\x69\x6e\x65\x61\ +\x72\x2c\x20\x6c\x65\x66\x74\x20\x74\x6f\x70\x2c\x20\x6c\x65\x66\ +\x74\x20\x62\x6f\x74\x74\x6f\x6d\x2c\x20\x66\x72\x6f\x6d\x28\x23\ +\x38\x35\x37\x38\x34\x41\x29\x2c\x20\x74\x6f\x28\x23\x46\x44\x46\ +\x44\x46\x44\x29\x2c\x20\x63\x6f\x6c\x6f\x72\x2d\x73\x74\x6f\x70\ +\x28\x30\x2e\x35\x2c\x20\x23\x46\x44\x46\x44\x46\x44\x29\x29\x3b\ +\x0a\x20\x20\x62\x61\x63\x6b\x67\x72\x6f\x75\x6e\x64\x2d\x72\x65\ +\x70\x65\x61\x74\x3a\x20\x72\x65\x70\x65\x61\x74\x2d\x78\x3b\x0a\ +\x7d\x0a\x23\x62\x6f\x78\x20\x7b\x0a\x20\x20\x62\x61\x63\x6b\x67\ +\x72\x6f\x75\x6e\x64\x3a\x20\x77\x68\x69\x74\x65\x3b\x0a\x20\x20\ +\x62\x6f\x72\x64\x65\x72\x3a\x20\x31\x70\x78\x20\x73\x6f\x6c\x69\ +\x64\x20\x23\x38\x35\x37\x38\x34\x41\x3b\x0a\x20\x20\x6d\x61\x78\ +\x2d\x77\x69\x64\x74\x68\x3a\x20\x36\x30\x30\x70\x78\x3b\x0a\x20\ +\x20\x68\x65\x69\x67\x68\x74\x3a\x20\x35\x30\x25\x3b\x0a\x20\x20\ +\x70\x61\x64\x64\x69\x6e\x67\x3a\x20\x34\x30\x70\x78\x3b\x0a\x20\ +\x20\x70\x61\x64\x64\x69\x6e\x67\x2d\x62\x6f\x74\x74\x6f\x6d\x3a\ +\x20\x31\x30\x70\x78\x3b\x0a\x20\x20\x6d\x61\x72\x67\x69\x6e\x3a\ +\x20\x61\x75\x74\x6f\x3b\x0a\x20\x20\x62\x6f\x72\x64\x65\x72\x2d\ +\x72\x61\x64\x69\x75\x73\x3a\x20\x30\x2e\x38\x65\x6d\x3b\x0a\x20\ +\x20\x74\x65\x78\x74\x2d\x61\x6c\x69\x67\x6e\x3a\x20\x63\x65\x6e\ +\x74\x65\x72\x3b\x0a\x20\x20\x76\x65\x72\x74\x69\x63\x61\x6c\x2d\ +\x61\x6c\x69\x67\x6e\x3a\x20\x6d\x69\x64\x64\x6c\x65\x3b\x0a\x20\ +\x20\x6d\x61\x72\x67\x69\x6e\x3a\x20\x61\x75\x74\x6f\x3b\x0a\x7d\ +\x0a\x68\x31\x20\x7b\x0a\x20\x20\x66\x6f\x6e\x74\x2d\x73\x69\x7a\ +\x65\x3a\x20\x31\x33\x30\x25\x3b\x0a\x20\x20\x66\x6f\x6e\x74\x2d\ +\x77\x65\x69\x67\x68\x74\x3a\x20\x62\x6f\x6c\x64\x3b\x0a\x20\x20\ +\x62\x6f\x72\x64\x65\x72\x2d\x62\x6f\x74\x74\x6f\x6d\x3a\x20\x31\ +\x70\x78\x20\x73\x6f\x6c\x69\x64\x20\x23\x38\x35\x37\x38\x34\x41\ +\x3b\x0a\x20\x20\x6d\x61\x72\x67\x69\x6e\x2d\x62\x6f\x74\x74\x6f\ +\x6d\x3a\x20\x30\x70\x78\x3b\x0a\x7d\x0a\x3c\x2f\x73\x74\x79\x6c\ +\x65\x3e\x0a\x3c\x2f\x68\x65\x61\x64\x3e\x0a\x3c\x62\x6f\x64\x79\ +\x3e\x0a\x20\x20\x3c\x64\x69\x76\x20\x69\x64\x3d\x22\x62\x6f\x78\ +\x22\x3e\x0a\x20\x20\x20\x20\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\ +\x22\x40\x49\x4d\x41\x47\x45\x40\x22\x20\x77\x69\x64\x74\x68\x3d\ +\x22\x36\x34\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x36\x34\x22\ +\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x68\x31\x3e\x41\x64\x42\x6c\x6f\ +\x63\x6b\x20\x50\x6c\x75\x73\x3c\x2f\x68\x31\x3e\x0a\x20\x20\x20\ +\x20\x3c\x70\x3e\x40\x4d\x45\x53\x53\x41\x47\x45\x40\x3c\x2f\x70\ +\x3e\x0a\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x62\x6f\x64\ +\x79\x3e\x0a\x3c\x2f\x68\x74\x6d\x6c\x3e\x0a\ +\x00\x00\x0c\x87\ +\x3c\ +\x21\x44\x4f\x43\x54\x59\x50\x45\x20\x68\x74\x6d\x6c\x3e\x0a\x3c\ +\x68\x74\x6d\x6c\x3e\x0a\x3c\x68\x65\x61\x64\x3e\x0a\x3c\x6d\x65\ +\x74\x61\x20\x63\x68\x61\x72\x73\x65\x74\x3d\x22\x75\x74\x66\x2d\ +\x38\x22\x3e\x20\x0a\x3c\x6d\x65\x74\x61\x20\x68\x74\x74\x70\x2d\ +\x65\x71\x75\x69\x76\x3d\x22\x63\x6f\x6e\x74\x65\x6e\x74\x2d\x74\ +\x79\x70\x65\x22\x20\x63\x6f\x6e\x74\x65\x6e\x74\x3d\x22\x74\x65\ +\x78\x74\x2f\x68\x74\x6d\x6c\x3b\x20\x63\x68\x61\x72\x73\x65\x74\ +\x3d\x75\x74\x66\x2d\x38\x22\x3e\x0a\x3c\x74\x69\x74\x6c\x65\x3e\ +\x40\x54\x49\x54\x4c\x45\x40\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\ +\x3c\x6c\x69\x6e\x6b\x20\x72\x65\x6c\x3d\x22\x69\x63\x6f\x6e\x22\ +\x20\x68\x72\x65\x66\x3d\x22\x40\x46\x41\x56\x49\x43\x4f\x4e\x40\ +\x22\x20\x74\x79\x70\x65\x3d\x22\x69\x6d\x61\x67\x65\x2f\x78\x2d\ +\x69\x63\x6f\x6e\x22\x20\x2f\x3e\x0a\x3c\x73\x74\x79\x6c\x65\x3e\ +\x0a\x2a\x20\x7b\x0a\x20\x20\x20\x20\x6d\x61\x72\x67\x69\x6e\x3a\ +\x20\x30\x3b\x0a\x20\x20\x20\x20\x70\x61\x64\x64\x69\x6e\x67\x3a\ +\x20\x30\x3b\x0a\x20\x20\x20\x20\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ +\x69\x6c\x79\x3a\x20\x22\x44\x65\x6a\x61\x56\x75\x20\x53\x61\x6e\ +\x73\x22\x3b\x0a\x7d\x0a\x0a\x62\x6f\x64\x79\x20\x7b\x0a\x20\x20\ +\x20\x20\x62\x61\x63\x6b\x67\x72\x6f\x75\x6e\x64\x3a\x20\x2d\x77\ +\x65\x62\x6b\x69\x74\x2d\x67\x72\x61\x64\x69\x65\x6e\x74\x28\x6c\ +\x69\x6e\x65\x61\x72\x2c\x20\x6c\x65\x66\x74\x20\x74\x6f\x70\x2c\ +\x20\x6c\x65\x66\x74\x20\x62\x6f\x74\x74\x6f\x6d\x2c\x20\x66\x72\ +\x6f\x6d\x28\x23\x38\x35\x37\x38\x34\x41\x29\x2c\x20\x74\x6f\x28\ +\x23\x46\x44\x46\x44\x46\x44\x29\x2c\x20\x63\x6f\x6c\x6f\x72\x2d\ +\x73\x74\x6f\x70\x28\x30\x2e\x35\x2c\x20\x23\x46\x44\x46\x44\x46\ +\x44\x29\x29\x3b\x0a\x20\x20\x20\x20\x62\x61\x63\x6b\x67\x72\x6f\ +\x75\x6e\x64\x2d\x72\x65\x70\x65\x61\x74\x3a\x20\x72\x65\x70\x65\ +\x61\x74\x2d\x78\x3b\x0a\x20\x20\x20\x20\x6d\x61\x72\x67\x69\x6e\ +\x2d\x74\x6f\x70\x3a\x20\x32\x30\x30\x70\x78\x3b\x0a\x7d\x0a\x0a\ +\x23\x68\x65\x61\x64\x65\x72\x2c\x20\x23\x73\x65\x61\x72\x63\x68\ +\x2c\x20\x23\x66\x6f\x6f\x74\x65\x72\x20\x7b\x0a\x20\x20\x20\x20\ +\x77\x69\x64\x74\x68\x3a\x20\x36\x30\x30\x70\x78\x3b\x0a\x20\x20\ +\x20\x20\x6d\x61\x72\x67\x69\x6e\x3a\x20\x31\x30\x70\x78\x20\x61\ +\x75\x74\x6f\x3b\x0a\x7d\x0a\x0a\x23\x68\x65\x61\x64\x65\x72\x2c\ +\x20\x23\x73\x65\x61\x72\x63\x68\x20\x7b\x0a\x20\x20\x20\x20\x62\ +\x6f\x72\x64\x65\x72\x2d\x72\x61\x64\x69\x75\x73\x3a\x20\x30\x2e\ +\x38\x65\x6d\x3b\x0a\x20\x20\x20\x20\x70\x61\x64\x64\x69\x6e\x67\ +\x3a\x20\x32\x35\x70\x78\x3b\x0a\x7d\x0a\x0a\x23\x68\x65\x61\x64\ +\x65\x72\x20\x7b\x0a\x20\x20\x20\x20\x62\x61\x63\x6b\x67\x72\x6f\ +\x75\x6e\x64\x3a\x20\x2d\x77\x65\x62\x6b\x69\x74\x2d\x67\x72\x61\ +\x64\x69\x65\x6e\x74\x28\x6c\x69\x6e\x65\x61\x72\x2c\x20\x6c\x65\ +\x66\x74\x20\x74\x6f\x70\x2c\x20\x6c\x65\x66\x74\x20\x62\x6f\x74\ +\x74\x6f\x6d\x2c\x20\x66\x72\x6f\x6d\x28\x23\x44\x35\x37\x45\x33\ +\x45\x29\x2c\x20\x74\x6f\x28\x23\x44\x35\x37\x45\x33\x45\x29\x2c\ +\x20\x63\x6f\x6c\x6f\x72\x2d\x73\x74\x6f\x70\x28\x30\x2e\x35\x2c\ +\x20\x23\x46\x46\x42\x41\x38\x39\x29\x29\x3b\x0a\x20\x20\x20\x20\ +\x68\x65\x69\x67\x68\x74\x3a\x20\x32\x35\x70\x78\x3b\x0a\x7d\x0a\ +\x0a\x23\x68\x65\x61\x64\x65\x72\x20\x68\x31\x20\x7b\x0a\x20\x20\ +\x20\x20\x64\x69\x73\x70\x6c\x61\x79\x3a\x20\x69\x6e\x6c\x69\x6e\ +\x65\x3b\x0a\x20\x20\x20\x20\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\ +\x3a\x20\x31\x2e\x37\x65\x6d\x3b\x0a\x20\x20\x20\x20\x66\x6f\x6e\ +\x74\x2d\x77\x65\x69\x67\x68\x74\x3a\x20\x62\x6f\x6c\x64\x3b\x0a\ +\x7d\x0a\x0a\x23\x68\x65\x61\x64\x65\x72\x20\x69\x6d\x67\x20\x7b\ +\x0a\x20\x20\x20\x20\x64\x69\x73\x70\x6c\x61\x79\x3a\x20\x69\x6e\ +\x6c\x69\x6e\x65\x3b\x0a\x20\x20\x20\x20\x66\x6c\x6f\x61\x74\x3a\ +\x20\x72\x69\x67\x68\x74\x3b\x0a\x20\x20\x20\x20\x6d\x61\x72\x67\ +\x69\x6e\x2d\x74\x6f\x70\x3a\x20\x2d\x35\x70\x78\x3b\x0a\x7d\x0a\ +\x0a\x23\x73\x65\x61\x72\x63\x68\x20\x7b\x0a\x20\x20\x20\x20\x62\ +\x61\x63\x6b\x67\x72\x6f\x75\x6e\x64\x3a\x20\x2d\x77\x65\x62\x6b\ +\x69\x74\x2d\x67\x72\x61\x64\x69\x65\x6e\x74\x28\x6c\x69\x6e\x65\ +\x61\x72\x2c\x20\x6c\x65\x66\x74\x20\x74\x6f\x70\x2c\x20\x72\x69\ +\x67\x68\x74\x20\x74\x6f\x70\x2c\x20\x66\x72\x6f\x6d\x28\x23\x38\ +\x35\x37\x38\x34\x41\x29\x2c\x20\x74\x6f\x28\x23\x38\x35\x37\x38\ +\x34\x41\x29\x2c\x20\x63\x6f\x6c\x6f\x72\x2d\x73\x74\x6f\x70\x28\ +\x30\x2e\x35\x2c\x20\x23\x43\x38\x43\x32\x41\x45\x29\x29\x3b\x0a\ +\x20\x20\x20\x20\x68\x65\x69\x67\x68\x74\x3a\x20\x35\x30\x70\x78\ +\x3b\x0a\x20\x20\x20\x20\x63\x6f\x6c\x6f\x72\x3a\x20\x23\x30\x30\ +\x30\x3b\x0a\x20\x20\x20\x20\x74\x65\x78\x74\x2d\x61\x6c\x69\x67\ +\x6e\x3a\x20\x63\x65\x6e\x74\x65\x72\x3b\x0a\x20\x20\x20\x20\x70\ +\x61\x64\x64\x69\x6e\x67\x2d\x74\x6f\x70\x3a\x20\x34\x30\x70\x78\ +\x20\x21\x69\x6d\x70\x6f\x72\x74\x61\x6e\x74\x3b\x0a\x7d\x0a\x0a\ +\x23\x73\x65\x61\x72\x63\x68\x20\x66\x69\x65\x6c\x64\x73\x65\x74\ +\x20\x7b\x0a\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x3a\x20\x30\ +\x3b\x0a\x7d\x0a\x0a\x23\x73\x65\x61\x72\x63\x68\x20\x69\x6e\x70\ +\x75\x74\x5b\x74\x79\x70\x65\x3d\x74\x65\x78\x74\x5d\x20\x7b\x0a\ +\x20\x20\x20\x20\x77\x69\x64\x74\x68\x3a\x20\x36\x35\x25\x3b\x0a\ +\x7d\x0a\x0a\x23\x73\x65\x61\x72\x63\x68\x20\x69\x6e\x70\x75\x74\ +\x5b\x74\x79\x70\x65\x3d\x73\x75\x62\x6d\x69\x74\x5d\x20\x7b\x0a\ +\x20\x20\x20\x20\x77\x69\x64\x74\x68\x3a\x20\x32\x35\x25\x3b\x0a\ +\x7d\x0a\x0a\x23\x66\x6f\x6f\x74\x65\x72\x20\x7b\x0a\x20\x20\x20\ +\x20\x74\x65\x78\x74\x2d\x61\x6c\x69\x67\x6e\x3a\x20\x63\x65\x6e\ +\x74\x65\x72\x3b\x0a\x20\x20\x20\x20\x63\x6f\x6c\x6f\x72\x3a\x20\ +\x23\x39\x39\x39\x3b\x0a\x7d\x0a\x0a\x23\x66\x6f\x6f\x74\x65\x72\ +\x20\x61\x20\x7b\x0a\x20\x20\x20\x20\x63\x6f\x6c\x6f\x72\x3a\x20\ +\x23\x35\x35\x35\x3b\x0a\x20\x20\x20\x20\x74\x65\x78\x74\x2d\x64\ +\x65\x63\x6f\x72\x61\x74\x69\x6f\x6e\x3a\x20\x6e\x6f\x6e\x65\x3b\ +\x0a\x7d\x0a\x0a\x23\x66\x6f\x6f\x74\x65\x72\x20\x61\x3a\x68\x6f\ +\x76\x65\x72\x20\x7b\x0a\x20\x20\x20\x20\x74\x65\x78\x74\x2d\x64\ +\x65\x63\x6f\x72\x61\x74\x69\x6f\x6e\x3a\x20\x75\x6e\x64\x65\x72\ +\x6c\x69\x6e\x65\x3b\x0a\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\x74\ +\x79\x6c\x65\x3e\x0a\x20\x20\x20\x20\x3c\x73\x63\x72\x69\x70\x74\ +\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x2f\x6a\x61\x76\x61\ +\x73\x63\x72\x69\x70\x74\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\ +\x20\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x75\x70\x64\x61\x74\x65\ +\x28\x29\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x7b\x0a\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x65\x78\x74\x65\x72\x6e\x61\ +\x6c\x2e\x73\x74\x61\x72\x74\x50\x61\x67\x65\x2e\x70\x72\x6f\x76\ +\x69\x64\x65\x72\x53\x74\x72\x69\x6e\x67\x28\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x70\x72\x6f\x76\x69\x64\x65\x72\x29\x20\x7b\x0a\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x67\x65\x74\x45\x6c\x65\x6d\ +\x65\x6e\x74\x42\x79\x49\x64\x28\x27\x66\x6f\x6f\x74\x65\x72\x27\ +\x29\x2e\x69\x6e\x6e\x65\x72\x48\x54\x4d\x4c\x20\x3d\x20\x0a\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x0a\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x2b\x20\x27\x20\x7c\x20\x3c\x61\x20\x68\x72\x65\x66\x3d\x22\x68\ +\x74\x74\x70\x3a\x2f\x2f\x65\x72\x69\x63\x2d\x69\x64\x65\x2e\x70\ +\x79\x74\x68\x6f\x6e\x2d\x70\x72\x6f\x6a\x65\x63\x74\x73\x2e\x6f\ +\x72\x67\x2f\x22\x3e\x27\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x2b\x20\x27\x40\x45\ +\x52\x49\x43\x5f\x4c\x49\x4e\x4b\x40\x3c\x2f\x61\x3e\x27\x3b\x0a\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x67\x65\x74\x45\x6c\x65\x6d\ +\x65\x6e\x74\x42\x79\x49\x64\x28\x27\x6c\x69\x6e\x65\x45\x64\x69\ +\x74\x27\x29\x2e\x70\x6c\x61\x63\x65\x68\x6f\x6c\x64\x65\x72\x20\ +\x3d\x20\x70\x72\x6f\x76\x69\x64\x65\x72\x3b\x0a\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x7d\x29\x3b\x0a\x0a\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x2f\x2f\x20\x54\x72\x79\x20\ +\x74\x6f\x20\x63\x68\x61\x6e\x67\x65\x20\x74\x68\x65\x20\x64\x69\ +\x72\x65\x63\x74\x69\x6f\x6e\x20\x6f\x66\x20\x74\x68\x65\x20\x70\ +\x61\x67\x65\x3a\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x76\x61\x72\x20\x6e\x65\x77\x44\x69\x72\x20\x3d\x20\x27\ +\x40\x51\x54\x5f\x4c\x41\x59\x4f\x55\x54\x5f\x44\x49\x52\x45\x43\ +\x54\x49\x4f\x4e\x40\x27\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x6e\x65\x77\x44\x69\x72\x20\x3d\x20\x6e\x65\x77\ +\x44\x69\x72\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\ +\x29\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x69\ +\x66\x20\x28\x28\x6e\x65\x77\x44\x69\x72\x20\x21\x3d\x20\x27\x6c\ +\x74\x72\x27\x29\x20\x26\x26\x20\x28\x6e\x65\x77\x44\x69\x72\x20\ +\x21\x3d\x20\x27\x72\x74\x6c\x27\x29\x29\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x6e\x65\x77\x44\x69\ +\x72\x20\x3d\x20\x27\x6c\x74\x72\x27\x3b\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\ +\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\ +\x4e\x61\x6d\x65\x28\x27\x62\x6f\x64\x79\x27\x29\x5b\x30\x5d\x2e\ +\x73\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x27\x64\ +\x69\x72\x27\x2c\x20\x6e\x65\x77\x44\x69\x72\x29\x3b\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x20\x7d\x0a\x0a\x20\x20\x20\x20\x20\x20\x20\ +\x20\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x66\x6f\x72\x6d\x53\x75\ +\x62\x6d\x69\x74\x74\x65\x64\x28\x29\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x76\x61\x72\x20\x73\x74\x72\x69\x6e\x67\x20\x3d\x20\x6c\x69\x6e\ +\x65\x45\x64\x69\x74\x2e\x76\x61\x6c\x75\x65\x3b\x0a\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x69\x66\x20\x28\x73\x74\ +\x72\x69\x6e\x67\x2e\x6c\x65\x6e\x67\x74\x68\x20\x3d\x3d\x20\x30\ +\x29\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x72\x65\x74\x75\x72\x6e\x3b\x0a\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x65\x78\x74\x65\x72\x6e\x61\x6c\x2e\ +\x73\x74\x61\x72\x74\x50\x61\x67\x65\x2e\x73\x65\x61\x72\x63\x68\ +\x55\x72\x6c\x28\x73\x74\x72\x69\x6e\x67\x2c\x20\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x75\x72\x6c\x29\x20\x7b\x0a\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x77\x69\x6e\x64\ +\x6f\x77\x2e\x6c\x6f\x63\x61\x74\x69\x6f\x6e\x2e\x68\x72\x65\x66\ +\x20\x3d\x20\x75\x72\x6c\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x7d\x29\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\ +\x7d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x20\x2f\x2f\x20\x49\x6e\x69\x74\x69\x61\x6c\x69\x7a\x65\ +\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x69\x66\x20\x28\x77\x69\x6e\ +\x64\x6f\x77\x2e\x65\x78\x74\x65\x72\x6e\x61\x6c\x29\x20\x7b\x0a\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x75\x70\x64\x61\ +\x74\x65\x28\x29\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x7d\x20\ +\x65\x6c\x73\x65\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x61\x64\x64\x45\ +\x76\x65\x6e\x74\x4c\x69\x73\x74\x65\x6e\x65\x72\x28\x27\x5f\x65\ +\x72\x69\x63\x5f\x65\x78\x74\x65\x72\x6e\x61\x6c\x5f\x63\x72\x65\ +\x61\x74\x65\x64\x27\x2c\x20\x75\x70\x64\x61\x74\x65\x29\x3b\x0a\ +\x20\x20\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\ +\x73\x63\x72\x69\x70\x74\x3e\x0a\x3c\x2f\x68\x65\x61\x64\x3e\x0a\ +\x3c\x62\x6f\x64\x79\x20\x6f\x6e\x6c\x6f\x61\x64\x3d\x22\x64\x6f\ +\x63\x75\x6d\x65\x6e\x74\x2e\x66\x6f\x72\x6d\x73\x5b\x30\x5d\x2e\ +\x6c\x69\x6e\x65\x45\x64\x69\x74\x2e\x73\x65\x6c\x65\x63\x74\x28\ +\x29\x3b\x22\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x20\x69\x64\ +\x3d\x22\x68\x65\x61\x64\x65\x72\x22\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x20\x3c\x68\x31\x3e\x40\x48\x45\x41\x44\x45\x52\x5f\x54\ +\x49\x54\x4c\x45\x40\x3c\x2f\x68\x31\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x20\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x22\x40\x49\x4d\ +\x41\x47\x45\x40\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x33\x32\x22\ +\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x33\x32\x22\x2f\x3e\x0a\x20\ +\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x64\ +\x69\x76\x20\x69\x64\x3d\x22\x73\x65\x61\x72\x63\x68\x22\x3e\x0a\ +\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x66\x6f\x72\x6d\x20\x61\x63\ +\x74\x69\x6f\x6e\x3d\x22\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\ +\x3a\x66\x6f\x72\x6d\x53\x75\x62\x6d\x69\x74\x74\x65\x64\x28\x29\ +\x3b\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x3c\x66\x69\x65\x6c\x64\x73\x65\x74\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x69\x6e\x70\x75\ +\x74\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x45\x64\x69\x74\x22\x20\ +\x6e\x61\x6d\x65\x3d\x22\x6c\x69\x6e\x65\x45\x64\x69\x74\x22\x20\ +\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x22\x20\x2f\x3e\x0a\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3c\ +\x69\x6e\x70\x75\x74\x20\x74\x79\x70\x65\x3d\x22\x73\x75\x62\x6d\ +\x69\x74\x22\x20\x76\x61\x6c\x75\x65\x3d\x22\x40\x53\x55\x42\x4d\ +\x49\x54\x40\x22\x2f\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x3c\x2f\x66\x69\x65\x6c\x64\x73\x65\x74\x3e\x0a\x20\ +\x20\x20\x20\x20\x20\x20\x20\x3c\x2f\x66\x6f\x72\x6d\x3e\x0a\x20\ +\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x64\ +\x69\x76\x20\x69\x64\x3d\x22\x66\x6f\x6f\x74\x65\x72\x22\x3e\x3c\ +\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x62\x6f\x64\x79\x3e\x0a\x3c\x2f\ +\x68\x74\x6d\x6c\x3e\x0a\ +" + +qt_resource_name = b"\ +\x00\x04\ +\x00\x06\xfb\x3c\ +\x00\x68\ +\x00\x74\x00\x6d\x00\x6c\ +\x00\x12\ +\x06\x3e\x39\x5c\ +\x00\x73\ +\x00\x70\x00\x65\x00\x65\x00\x64\x00\x64\x00\x69\x00\x61\x00\x6c\x00\x50\x00\x61\x00\x67\x00\x65\x00\x2e\x00\x68\x00\x74\x00\x6d\ +\x00\x6c\ +\x00\x11\ +\x0d\xbe\x8d\x7c\ +\x00\x74\ +\x00\x61\x00\x62\x00\x43\x00\x72\x00\x61\x00\x73\x00\x68\x00\x50\x00\x61\x00\x67\x00\x65\x00\x2e\x00\x68\x00\x74\x00\x6d\x00\x6c\ +\ +\x00\x10\ +\x0b\xe9\x1d\x9c\ +\x00\x61\ +\x00\x64\x00\x62\x00\x6c\x00\x6f\x00\x63\x00\x6b\x00\x50\x00\x61\x00\x67\x00\x65\x00\x2e\x00\x68\x00\x74\x00\x6d\x00\x6c\ +\x00\x0e\ +\x08\x97\xc9\x7c\ +\x00\x73\ +\x00\x74\x00\x61\x00\x72\x00\x74\x00\x50\x00\x61\x00\x67\x00\x65\x00\x2e\x00\x68\x00\x74\x00\x6d\x00\x6c\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x02\ +\x00\x00\x00\x0e\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\x86\x00\x00\x00\x00\x00\x01\x00\x00\x1a\x99\ +\x00\x00\x00\x60\x00\x00\x00\x00\x00\x01\x00\x00\x17\x19\ +\x00\x00\x00\x38\x00\x00\x00\x00\x00\x01\x00\x00\x12\x9a\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/icons.qrc Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,17 @@ +<!DOCTYPE RCC> +<RCC version="1.0"> +<qresource> + <file>icons/adBlockPlus16.png</file> + <file>icons/adBlockPlus64.png</file> + <file>icons/ericWeb16.png</file> + <file>icons/ericWeb32.png</file> + <file>icons/box-border-small.png</file> + <file>icons/brokenPage.png</file> + <file>icons/close.png</file> + <file>icons/edit.png</file> + <file>icons/loading.gif</file> + <file>icons/plus.png</file> + <file>icons/reload.png</file> + <file>icons/setting.png</file> +</qresource> +</RCC>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/icons_rc.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,2034 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.6.0) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x0c\x69\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ +\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x2e\x23\x00\x00\ +\x2e\x23\x01\x78\xa5\x3f\x76\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ +\xde\x03\x0c\x12\x05\x12\x78\xfa\xe0\x84\x00\x00\x0b\xf6\x49\x44\ +\x41\x54\x58\xc3\x9d\xd7\x69\x90\x5c\xd5\x75\xc0\xf1\xff\x7d\xef\ +\x75\xbf\x5e\xa6\x7b\xf6\xa5\x47\xcc\x22\x89\x19\x49\xa3\x05\x09\ +\x89\x45\x80\x89\x01\xe1\x24\x88\xb0\x83\x65\x4a\x04\x41\x22\x30\ +\xd8\xae\x80\x29\x8c\x01\x83\x08\x81\xc4\xd8\x89\x31\x01\x1b\x03\ +\xe5\x38\x18\x0a\x01\x02\x04\x48\x48\x02\x8d\x24\x04\x48\x62\x98\ +\xd1\x3e\x1b\x33\x9a\x9e\x7d\xed\xe9\x6d\xe6\x75\xbf\x5e\xde\x7b\ +\x37\x1f\x08\x95\x38\x89\xcb\x84\xfb\xf5\xde\x3a\xe7\x57\xe7\xd4\ +\xbd\x75\x8f\xe0\x6b\xac\x4d\x0f\x2d\xa5\x76\xe1\x49\x4e\x1c\xbc\ +\x91\xfa\xc6\x1a\x7a\x3a\x5b\xc5\xb3\xcf\xed\x95\xff\xf3\x9c\x94\ +\x52\x00\xee\x9e\xfd\x5b\xf4\x4f\xc3\x9f\xb8\xae\x1b\x7a\x2a\xee\ +\x7f\x44\x38\x5f\xee\x9f\xd3\xd8\x84\xf8\x3a\x80\xf6\xc9\x66\xf6\ +\x6e\x69\x51\x57\x5e\x7e\x15\xe7\xd7\x2f\xb6\xbf\x4c\xf6\xdb\xb7\ +\x1e\xac\xde\xd5\xbc\xf7\xbc\xb1\xd1\xd8\x19\x65\x41\xf3\x74\x33\ +\x6f\x56\xba\x82\x35\xa5\x8d\x73\x8b\x0a\xaa\xfc\x51\xaf\xab\x24\ +\x7b\x2a\x40\xf5\xd6\x8d\x45\xb7\x3e\xb5\x69\xe7\xeb\xf2\xd1\x5f\ +\x6f\x73\xbe\x12\xa0\xf5\xf0\x0b\x9c\xb5\x72\x23\xef\x3e\x7b\x37\ +\x57\xdc\xf1\x24\x00\xef\x6f\x7b\x3c\x60\xc8\x9a\x79\x71\xd3\x5c\ +\x39\x15\x99\xf8\x96\xdb\x23\x56\xf9\x5c\x95\xf5\xc5\xa5\x73\xd4\ +\xda\xe5\x75\x0c\x4d\x4d\x49\xd5\x6b\x39\x5d\xc7\x5f\x45\xe4\xdf\ +\x91\x65\xe2\x5c\x8c\xa1\x3c\xed\xc7\x5a\xb5\xd8\xa8\xff\x27\xdb\ +\x0f\x8c\x3e\xfe\x97\x97\x56\x09\xed\x4f\x25\x7f\xf5\x99\xbb\x39\ +\x6b\xe5\x46\x9e\x7e\x78\xbd\x72\xc5\x1d\x4f\x3a\x72\xf8\x2d\xdf\ +\x0b\xfb\x0f\xdf\xd3\xdc\x62\xdc\x92\x2f\x1a\xac\x4e\xd6\x09\xbd\ +\x62\xd1\x14\x8b\xab\xeb\xa8\x58\x70\x36\x1d\xf1\x23\x52\xcc\xa6\ +\xf9\xf0\x95\x87\xc5\xca\x9a\x90\x9a\xf1\x16\x51\x10\xf5\xd0\x4e\ +\x07\x8d\x91\x69\x2a\x64\x9a\xa4\x66\x5f\x01\x3c\x9e\x68\x9f\x40\ +\xfd\x63\x89\xff\xe5\x67\xff\x8a\x35\xfe\x09\x75\x73\x42\xec\xf8\ +\xb8\x9d\x1d\x1f\x1e\xc7\x8e\x1c\x5c\xbf\xb3\xa3\x7f\xf3\x9e\x7d\ +\x9d\xd7\x9d\x79\xc6\x77\x02\x3b\xb7\xec\xd1\x56\xcc\xd1\x88\xe7\ +\xc6\x45\x7e\xdc\xcf\xf0\x47\xbf\x67\xb2\x3b\x2c\x3c\x89\x29\xd1\ +\xd9\x6b\xf0\xad\x6f\xfc\x15\x71\xb3\x0f\x23\x61\x20\x47\x92\x4c\ +\x77\x65\x49\xc4\x35\xbc\xba\xda\x7e\x2c\x9c\x7b\xb9\x61\x39\xe2\ +\x8f\x02\x8a\x6b\x06\xd9\xfe\xfe\x28\x3b\x3e\x6e\x67\x72\xf8\xb5\ +\xd0\x3d\xb7\xdf\xf6\xa2\xd3\xd4\xf4\x80\x6b\xce\x92\xd2\xd6\x3d\ +\xcd\xb2\xad\xb5\x4d\x1d\x1d\xee\x16\xc7\x3e\x3b\x22\x2e\xb8\xfa\ +\x9b\x68\x5a\x8a\x0b\xaf\x6a\x61\x66\x50\xa7\xf8\xea\x12\x1a\x8b\ +\x1b\xb1\x74\x49\xb4\xf5\x1d\x06\x0e\xc6\xf1\x44\x6d\x3e\xea\xd3\ +\x30\x4c\x89\xcb\xe7\xdb\xda\x35\x98\xde\x7d\xf1\x92\xda\xff\xbb\ +\x05\x5b\x76\x3d\x44\x77\xcf\x29\xf1\x1a\x9d\xf2\x8e\xf5\x2b\xe6\ +\xdd\x79\xff\xb3\x3b\x1a\x2e\xf8\xf6\x02\xcb\x56\x9d\x89\xc9\x76\ +\xc5\x8c\x27\x44\x2a\x9d\xc0\x4a\x5b\xb8\x8a\x21\x54\x73\x08\x99\ +\xf7\xa3\x16\xf9\xc8\x27\xf2\xec\xde\x32\x4c\x49\xbc\x8b\x89\xee\ +\x18\x75\x39\x95\xfe\xa1\x2c\xd2\xed\x62\xd1\x0a\x0d\x99\x53\x29\ +\x2f\x0e\x66\x21\xc2\x9c\x79\xa5\xfc\x2f\xc0\xfb\xfb\x7e\xc1\x9f\ +\x5f\xf4\x43\x00\xf9\xc8\x73\x1b\xe6\xa5\x44\xd9\xae\x0b\x37\xfc\ +\xa0\x21\x26\x5d\xb2\xe7\xcd\x37\x15\x4e\x66\x09\xce\xab\x66\xea\ +\x40\x17\x59\xd3\xe1\xcc\x4b\x16\xd1\x7b\xdc\x43\x6f\x74\x12\xff\ +\xaa\x85\xb8\x16\x98\xa4\xdf\x8d\xd2\xb4\xa0\x92\xc8\x64\x94\x53\ +\x8e\x8f\xb2\x15\x3a\xb9\xe4\x14\xa6\x61\x4b\x69\xe7\x05\x41\x33\ +\x0c\xe0\xd5\x90\x7f\x00\xd8\x75\xe8\x31\x56\x2e\x0c\x01\x88\xad\ +\xbf\xba\xad\xb8\xdd\xa3\xbe\xa1\x2e\xbd\x78\x7e\xf3\x6f\x9a\xb1\ +\xdb\x77\x8b\x3b\xef\xbb\x9a\x77\xd2\x0b\x29\x58\xae\xe0\xf7\x29\ +\x14\x04\x73\x2c\x38\xdf\xc3\xd2\xe5\x59\xa2\xaf\x64\xe9\x7b\x77\ +\x12\xe1\xd5\x29\xcd\xcc\xf2\xf6\xef\xc3\x84\x96\x95\x60\x99\x51\ +\x88\x66\xf1\xe6\x05\x16\xc2\xd1\x85\x6b\x36\x6d\xa6\x9b\xd7\x9f\ +\x13\xd4\x56\xaf\xff\xae\xa5\xfc\x77\xc0\x87\x67\x64\x28\x2b\xfe\ +\x0e\x52\x1e\xf1\x0c\xcf\x69\x7a\x31\xd6\x5f\xb5\x22\xb1\xe5\x03\ +\x71\xd3\xba\x6a\x6e\x7e\x6e\x35\x29\xdf\x11\x26\xe3\x2d\x5c\x74\ +\xd9\x02\x54\xe9\x27\xd2\x6f\x90\xf5\x14\x32\x10\x49\x11\x3a\xdd\ +\x47\xdb\xd6\x51\xa6\xf7\x45\x39\x70\x60\x94\xb3\x6e\x29\xc5\x65\ +\x0c\x91\xed\xcd\x92\x8d\x48\x84\x25\x65\x36\x9d\x57\x35\x32\xcf\ +\xff\x76\x6b\x62\xf8\xd2\x1b\xd6\xda\xdf\x5c\x79\x1b\x7f\x00\x58\ +\xd9\x69\x08\x80\xef\x3d\xfd\xf4\x63\xa9\x33\x4b\x2e\xff\x9b\x4d\ +\x77\x48\xad\x41\x13\x2d\xf4\x71\x64\xcf\x00\xff\xfe\x8b\x66\x0a\ +\xbd\x69\xe2\x6d\xc7\x29\xaf\x4e\xb2\x6a\xcd\x62\xd4\xe1\x71\xd2\ +\xfd\x0e\xda\xac\x45\xa4\x67\x96\xf0\x64\x84\x92\x46\x1f\x7d\xcd\ +\xc3\xe4\xc6\x4d\x8a\x55\x0b\xb7\x96\x93\x66\xca\x11\x5e\xd5\xf9\ +\x20\xe4\x3d\xed\x51\x80\x9b\xef\xd9\x2c\x85\x10\xff\x05\x78\xf3\ +\xf9\xb5\x44\x56\x3e\x29\xde\x69\x7e\xf0\xa2\x40\x49\xc1\x0f\x2a\ +\x6b\xca\x9d\x18\x1d\xa2\xf3\xbd\x4f\x68\x7f\xe2\x55\xc6\xfb\x7a\ +\xc8\xcf\x58\xc8\x78\x86\xf0\xbe\x23\x0c\x75\x24\x09\x06\x26\x69\ +\xdb\x3e\xcd\xe8\x87\x49\x4e\x1c\x1c\x46\x0b\xa4\x69\x5a\x39\x4b\ +\x4d\x60\x9a\xd6\x0f\x0c\xcc\x8c\x64\x26\x2d\xe4\xac\xe5\x16\xaa\ +\x87\xed\x4d\x73\x8a\xd6\xfd\x6c\xdb\x40\xea\x87\x57\x96\xb1\xeb\ +\xb5\xfb\x91\x52\x7e\xf1\x14\x4b\x29\x69\xfc\xe9\x06\x7a\xef\x7f\ +\x91\x47\xde\xbb\x6f\xef\x8c\x2c\xbe\x68\xf2\x78\x0b\x7a\x75\x00\ +\x7d\x7a\x14\x19\x8b\x12\x19\x48\x90\x30\x24\x73\x1a\x5d\xd4\x94\ +\x2b\x7c\xb2\x3b\x42\x55\xa9\x1b\x05\x87\x7d\x9f\x65\xb9\xe1\xde\ +\x4b\x18\x3f\xba\x8c\xf2\x82\x2a\x7c\x7e\xc1\x74\x24\x4b\x6f\x77\ +\x1f\x9a\xd9\x42\xb1\x7f\x78\xf7\xa6\x0d\x63\x57\xae\xd8\x80\x09\ +\xf0\xc2\x83\x6b\xd8\xf8\x78\x33\x00\x62\xac\xeb\x7a\xaa\x9f\xd9\ +\x02\xbf\x82\x17\xdb\x36\xad\x0d\x5b\xfe\xed\xd1\xf1\x01\x99\xee\ +\x3e\x24\x12\x1d\xe0\xf8\x6c\x96\xaf\xae\xc0\xb6\x0c\x5a\xde\x1b\ +\x64\x51\x7d\x31\x66\x7c\x94\x63\x47\x25\x45\x67\x78\xa8\x2c\xbc\ +\x86\x50\xf1\x25\x2c\x6a\x6a\xa0\xbf\x3f\xca\x74\xcc\x60\x74\x34\ +\x81\x23\x05\x1e\xbf\x8e\xe3\x38\xa0\x68\x13\xdd\xed\x43\x6f\x77\ +\xb4\xdc\xf3\xfd\xda\x85\xbf\x53\x86\xba\x6f\xc9\xc3\x8d\xc0\x2b\ +\x5f\x54\xa0\x79\xbc\x47\xb9\xa4\xaa\x41\xfe\xe3\x89\x3d\x3b\x7a\ +\x0f\xff\xf3\xa5\x25\x89\xa8\x9a\xb4\x7d\x2c\xac\x9d\xe0\xd8\xdb\ +\x06\x99\x1c\x14\xf8\x54\x06\x07\xe2\xd8\x45\x6e\x16\x5f\xb2\x84\ +\x91\x6d\x93\x2c\x3b\xff\x61\x6a\x6a\xeb\x59\x75\xde\x3c\x0e\x1f\ +\x1d\xe5\x54\x7f\x94\x83\x9f\x0d\x53\x3d\xb7\x9c\xb2\xd2\x02\xc6\ +\xc2\x63\xb8\x55\x55\x56\xd5\x97\x4b\x3b\x6f\x29\x47\x0e\xf5\x9d\ +\x6a\xdb\xb3\xfd\x1c\xdb\xd9\x19\xfb\xb2\xf5\xaa\x74\x76\xf3\x8d\ +\xc7\x3e\x57\xd6\x2c\x34\x2e\x38\xd0\x71\xfc\x1f\x26\x3e\xfd\x50\ +\x58\x33\x51\x31\x71\x62\x9c\xc8\x60\x1c\x43\xd1\x39\xd4\x92\xa4\ +\xf0\xdc\x6a\xea\xce\xaa\x62\x3c\x61\x12\x8a\x4b\xa4\xeb\x7b\x2c\ +\x5b\xb5\x84\xf1\x69\x83\xb9\xa7\x57\x22\x5c\x6e\x4c\x07\xbc\x01\ +\x0f\xe1\xfe\x08\xab\xcf\x9b\x8f\x2a\x1c\xdc\xba\x5b\x0c\x8d\xc4\ +\x44\xd6\xb4\xe4\x92\x15\xb5\xa5\xf1\x19\xcf\x35\x33\xd1\xba\x57\ +\x42\x8d\xeb\x72\x33\x91\xdd\x52\x8d\x35\xcd\x13\x7b\xee\xfe\x3b\ +\x67\xf9\xc6\xeb\x9f\xb7\x0b\xaa\x4e\x0f\xef\x7f\x4f\x24\xa6\x66\ +\x28\x52\xb3\x8c\x25\xdd\x04\xfd\x82\xb9\xc5\x92\x44\x77\x8c\xf1\ +\xce\x29\x42\xc2\x21\x6b\xaf\x63\xcd\xda\x8b\xe9\x0a\x4f\xb3\x78\ +\x49\x2d\xe5\x15\x41\x3c\xba\xc6\x6b\xdb\x4e\x32\x93\xb5\xc9\xda\ +\x0e\x86\x99\xe7\xed\x0f\x7a\x88\xc4\xd3\x8c\x4d\x18\x08\x4d\x15\ +\x81\x42\x9f\x2c\x2c\xf6\x95\xa6\x66\xad\x86\xc1\x8e\xfb\x5f\xfd\ +\xfe\x4f\x76\xa2\x7e\xf6\xc6\x7e\xa4\x94\x0b\xba\xc6\x4e\x3e\xd9\ +\xde\xb2\x9f\x74\x64\x90\xe8\xe7\x11\x66\x92\x50\xe6\x36\xc9\xc4\ +\xb2\x18\x69\x1b\x49\x1e\xb7\x22\x49\xc7\xbd\xcc\x5b\x72\x33\x8a\ +\xee\x45\x75\xeb\x5c\x75\xf9\x62\xa6\x12\x19\x62\x86\xc5\xe4\x6c\ +\x8e\xba\xfa\x72\x4c\x4b\x22\x2d\x8b\xa8\x61\xb1\xb4\xb1\x8c\xb1\ +\x98\x41\xda\xcc\x63\x66\x2c\x71\xe4\xd0\x29\x67\x6e\x7d\x59\x93\ +\xaa\x9d\xbd\xff\x83\xb7\xfe\x76\x40\x01\xd8\x4b\xeb\xb5\x9d\x13\ +\x27\xa8\x3a\xa3\x51\xca\x60\x29\x96\x57\xc5\xe5\x86\xf1\x88\x20\ +\x9d\x92\x90\xb3\xd0\x73\x92\x12\x04\x85\x15\x6b\x31\x6d\x1f\x6e\ +\x8f\xce\x95\x97\x35\x31\x38\x6e\xa0\x6a\x2a\x52\x53\x58\xb6\xb8\ +\x9a\x92\x12\x3f\x2e\xbf\x4e\x6b\x7f\x9c\xe2\x32\x1f\x6d\x7d\x71\ +\x54\x8f\x8e\xd4\x54\x14\xdd\x45\xc3\x8a\x79\x8a\xbb\xbc\xc8\x2a\ +\x2e\x2b\xb8\x17\x40\x91\xb2\x4b\x3b\x78\x60\xe8\xda\x79\xf3\x57\ +\xdb\x89\xf0\x5e\x11\xd2\x62\xa0\x7a\x30\xd2\x12\x2b\xab\x60\xa3\ +\x60\xce\x4a\x62\xb3\x79\xf4\xc2\x4a\xfa\x23\x73\x51\x5d\x2a\xbd\ +\xc3\x09\xa2\x33\x59\x84\xaa\x90\xb6\x24\x8e\x94\xd4\xcd\x09\x72\ +\x6a\x24\x4e\x5b\x6f\x04\x4f\xc0\x4b\x4a\x0a\xdc\x01\x1d\xe1\x71\ +\x63\xab\x0a\x86\x69\x51\x16\x2a\x22\x58\x12\x74\xf4\x02\xef\x0a\ +\x9f\xff\xf6\xa0\x02\xb5\xee\x50\xb9\xed\x3b\xb1\xef\x0d\xc2\xc7\ +\xc7\xe8\x09\x27\xf0\x3a\x79\x3c\x1e\x70\x34\x87\x78\x22\x47\x36\ +\xe3\x60\x49\x08\x0f\xfb\x51\x5d\x5e\x3e\x6a\x1b\xe2\x70\x77\x84\ +\xb8\x91\xc3\xe5\x52\xc1\x91\x54\x96\xfa\xe8\x1c\x88\xe2\xd6\x15\ +\x2e\x5a\x75\x1a\x81\x22\x1f\x8e\xc7\x85\xe2\x75\xa3\x78\x34\x54\ +\x8f\x0b\x97\x57\x23\x14\x0a\x52\x56\xec\xd3\x3c\x01\x6f\x95\xcb\ +\x5b\x58\xa3\xdd\xfb\xd2\xbd\x7a\x64\x4a\x0f\x56\x35\x14\xc8\x92\ +\xae\x31\x26\x27\xa2\x08\xc7\x21\xe3\x08\x90\x0e\x45\x3e\x15\x1c\ +\x07\x97\x90\xa4\x33\x2e\xf4\x72\x1f\xb1\x94\x85\x91\xc8\x10\x9b\ +\xcd\x62\x0c\xc6\x69\xeb\x9d\xa4\xaa\xa2\x10\x29\x25\x15\xa5\x05\ +\x58\x52\xd2\x3e\x99\xc6\x23\x21\x9f\xcd\x83\x10\x38\x36\x28\xba\ +\x1b\xd5\xe5\x22\x3e\x6d\x08\x45\x75\x09\x29\x9d\x02\xe5\xd1\x9b\ +\x1e\xcb\x5f\xb6\xfe\xaf\x33\x9f\x6f\x7b\x97\x70\x67\x04\x47\x75\ +\x23\x4d\x9b\x60\x26\x87\xae\x0b\xbc\xaa\x83\xb0\x41\x45\x52\x5a\ +\xee\x23\x1a\x37\x51\x5d\x0a\x1e\x8f\x8b\xcd\xbb\x7b\xf0\xfb\x5d\ +\x2c\x6f\xac\xc2\x01\x3a\x22\x26\x6f\x75\x44\x18\x4d\xe5\x99\xb6\ +\x00\xaf\x0b\x45\x77\x23\x3c\x6e\xd0\x14\x14\x45\x92\xfb\xcf\x1b\ +\x12\xf0\x29\xd8\x12\x5b\xf1\x89\x12\xe3\xe0\x4b\xef\x1e\x4d\x4e\ +\x98\x4e\x62\x22\x67\x9b\xa3\xb3\xd2\x98\x91\x8e\x21\x1d\x47\x98\ +\x8e\x4c\x1b\xc2\xd1\x14\xe1\x48\x5c\x96\x65\xcf\xd8\x39\x33\x4d\ +\x26\xeb\x90\x73\x1c\xea\x43\x01\xee\x7a\xae\x85\x68\x2a\xc3\xe1\ +\x51\x83\x13\xd3\x26\x8e\xdb\xcd\xa7\x63\x69\xbc\x01\x0f\x8a\x47\ +\x47\x78\x34\x50\x24\x9a\x47\xc5\xb6\x6c\x8e\x1e\x1b\x64\x2c\x3c\ +\xe1\xc4\xa6\xe2\x59\x4d\x53\x93\x2a\xc0\x8f\x36\xde\xd0\x57\xa0\ +\x67\xce\x12\x96\x93\xd3\x35\x8f\xad\x8b\x6c\x4e\x08\xd5\x0c\xea\ +\x5a\x3a\x18\xd0\x92\x5e\xaf\x16\x2b\x0a\x16\x0e\x4b\xd2\x71\x83\ +\xe5\xe5\x9a\x37\x88\xab\xc0\xc3\xe1\x91\x59\x36\xac\x5d\xc8\x96\ +\xa3\x11\x4e\xab\x2e\xe0\xd2\xa5\xd5\x14\x06\x74\x72\x8a\x4a\x56\ +\x7e\x51\x35\xc7\x01\xcb\x16\x14\x79\x34\x1c\x33\xcf\xd4\x68\x0c\ +\x3b\x95\xb2\x72\xc9\xcc\xc4\x48\x4f\xf3\xa3\xda\x1d\xdf\xbd\x8e\ +\x6b\x6f\xb8\xf3\xf0\xe6\xcd\x3f\xbe\xcb\x91\xc1\x15\xf9\xac\xed\ +\xc9\x18\x49\x19\x9d\x1c\x15\xaa\xc4\x2a\xf0\xea\xf9\xbc\x9d\x9b\ +\x01\x91\x12\xb6\x11\x48\x84\xc7\x7e\xda\x9f\x2a\xa9\x2a\x2a\xf6\ +\x71\x7a\x28\x40\xef\xb8\x41\x59\xb1\x07\xa1\x69\x7c\xa3\xa1\x94\ +\x78\xd6\xe1\xbc\xac\xc3\xa9\x58\x9a\xf7\x8e\x8f\x93\xcf\xda\x54\ +\x97\xf8\x10\xb3\xb3\x84\x13\x29\xb0\x1d\x7c\x2e\xb7\x2b\x93\x4c\ +\xed\x82\x4f\x2c\xed\xd9\xdf\xbc\x41\x5f\xdf\x47\x62\xfe\xfc\x0b\ +\x0f\x00\x07\xfe\xd4\x37\xfd\xc6\x35\x0b\x9a\xa6\x66\x72\x3f\x32\ +\x53\x59\x99\x4d\x98\x22\x35\x92\x64\xe9\xd2\x10\x4d\xa1\x20\x42\ +\x51\x40\x48\x6c\x45\x50\x51\xa8\xb3\x61\x75\x0d\x89\x64\x86\x13\ +\x5d\xa3\xb4\x9e\x4a\xa3\x3a\xe0\x38\x8e\xc4\xc9\x8b\xb1\xd1\xe8\ +\xcf\x01\xb4\xd7\x5f\xdf\xc4\xfc\xf9\x17\xca\x97\x5f\x7e\x80\xc1\ +\xa1\x09\x91\x33\x2d\xf2\x66\x86\xf8\xf4\x08\xd2\xb2\x28\xf0\xea\ +\xe4\x6d\x0b\x97\x92\x93\xc6\x4c\xa3\xd6\x54\x35\xf5\xb4\xd9\x97\ +\x58\xd7\x66\x14\x86\x74\x45\x6a\x32\x97\x17\x9d\x43\x49\xdc\x85\ +\x7e\x16\xa5\x73\x94\x78\xdd\x08\x55\xa5\x48\x57\x48\x9b\x79\x66\ +\x67\x14\x8e\x9c\x8a\xe1\xb1\x2c\xf2\x8e\xb4\x15\x29\xd5\x89\xa1\ +\xd8\xc3\xe9\xe8\x2f\xfb\xe6\x2e\xfd\x27\xf1\x95\x47\xb3\x7b\xbf\ +\xfd\x67\xfc\xfc\xb5\xfd\x00\x1c\xde\xff\xf6\x4d\xcf\x6f\x9e\xfc\ +\xfb\x8f\xa3\x05\x73\xa9\x08\x3a\xd2\xef\x51\x28\xf2\x71\xf5\xb9\ +\xb5\x5c\xbf\xac\x0a\x97\x22\xc8\x58\x36\x3d\xe3\x06\xad\xdd\x13\ +\xec\xf8\x38\x8c\x9a\xb3\x9d\xfc\x54\x54\x31\x7a\xc7\x5e\x71\x3a\ +\x1e\xba\x29\x5b\x79\x1f\x89\xc9\x27\x1c\xf5\xab\x02\x0e\x76\x0c\ +\xf2\xe0\xad\xeb\x90\x43\x23\xd4\x3d\xfa\xbb\x93\x37\xcf\x4f\xfd\ +\xdb\xce\x9d\xfd\xae\xa0\xdf\x73\x7e\x7d\x43\xa5\x55\x5d\x1e\xb0\ +\x6b\x4b\xbd\xca\xa2\x32\x1f\x39\xdb\x11\x03\xb1\x0c\x53\xf1\x94\ +\x9c\x49\xe7\xad\xf0\xb8\x61\x8b\xc9\x98\x66\x86\x27\x9e\x1a\x39\ +\xf9\xe0\xed\x29\x90\x99\xd4\x01\x59\x39\xff\x99\xaf\x37\x9c\x26\ +\x22\x9d\x62\xfb\xb6\x21\x65\xfd\xad\x7f\x61\x9f\xd6\xf8\xe3\xb3\ +\x5d\xe5\x55\x4f\xba\x6a\x2a\x16\xaf\x3a\xbb\xae\xf0\xbc\xa5\x21\ +\xe2\x19\x8b\xf8\x4c\x86\x74\x32\x43\xb8\x77\x22\x3d\xde\x3d\x72\ +\x58\xc4\x13\xf7\x9f\xf8\xf4\x81\x03\x25\xa1\x47\xd4\xd8\xf8\x23\ +\xf6\x97\xb1\xfe\xdf\x00\x29\x25\x42\x5c\x03\x6c\xe5\x97\x2f\x49\ +\xee\xba\xe9\x8b\x10\x85\x95\x77\xd7\xaa\xe5\xe5\xf5\x45\x35\x15\ +\x42\x78\x74\xcc\xd9\x2c\xd9\xa9\x29\x29\x63\xc9\xa1\xd8\xc8\x13\ +\x03\x00\x6b\xee\x3c\x46\xf3\xaf\x97\xb3\xf4\xd2\x13\x9c\xdc\xbd\ +\x0c\x80\xff\x00\x1c\x7c\x06\xd9\x9b\xd6\x5b\xe7\x00\x00\x00\x00\ +\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x03\xb1\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x09\x70\x48\x59\x73\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01\x5e\x1a\ +\x91\x1c\x00\x00\x03\x56\x49\x44\x41\x54\x48\xc7\xdd\x95\x4d\x68\ +\x5c\x55\x14\xc7\x7f\xf7\xe3\xbd\x97\xe9\x34\x69\x5a\x93\x60\xdb\ +\x80\x60\xc1\x12\x9a\x20\x0d\x49\x88\x50\xca\x43\xbb\x70\x93\x85\ +\x62\x41\x2b\xea\x42\xb0\xb8\xb2\x20\x08\x6e\x64\xd0\x55\x4b\x2d\ +\x48\x37\x22\x48\x10\x8c\x20\xba\x09\x45\xa4\x12\xe2\x98\x8a\x29\ +\x1a\xa4\x92\x6a\x6d\x4a\x9b\xb4\x19\x9d\x98\xc4\xb1\xd3\x99\x79\ +\xf3\xde\x9b\xf7\xee\x75\x51\x2c\xe4\x63\x31\xc1\x6e\xf4\xec\xee\ +\xc7\x39\xbf\x7b\xee\xb9\xff\x73\xe1\xbf\x6e\xa2\x99\x4d\xfd\x9f\ +\x3e\xd4\xff\xdc\x23\x2f\x7d\x38\xbe\xf0\xc9\xa3\x8e\x94\xc1\xc1\ +\xae\xc1\xc9\xa1\xae\x43\x6f\x3c\xbb\xef\xd5\x5f\xb7\x04\xc8\xe7\ +\xf3\x76\xb3\x4d\x1f\x94\x4f\xf3\xee\x91\xf7\x99\x2d\xcd\x90\xda\ +\x98\x30\x89\x28\xdd\x5e\x65\xdf\x9d\x83\x9b\x06\xf5\x7d\xff\x5e\ +\x5c\xbd\x7e\x31\x9f\xcf\x6f\x70\xb8\xb6\x67\x8e\x6f\x0a\x13\x74\ +\x74\x08\x04\x29\x5e\xb5\x8d\xc9\xf9\x2f\x58\x9c\x2d\x6f\x16\x7c\ +\xcd\x58\x6f\x76\x82\x5c\x2e\xb7\x26\xb3\x87\x3f\xda\xf5\x43\x6c\ +\xa2\x81\xdf\x6a\xab\x08\x04\xd9\x44\x33\x2f\xaf\xce\x8c\xe5\x26\ +\x06\xd7\xf9\x6d\xb8\x01\xdd\x4c\x0d\x52\x63\x48\xad\x21\x6c\xc4\ +\x08\xa0\xc5\x1a\x8c\x69\xae\xc8\x4d\x01\x4c\x6a\x30\x58\x6a\x71\ +\x08\x08\xda\x74\x4a\x6a\xec\xfd\x03\x24\xc6\x24\xc6\x1a\xea\xc9\ +\xdd\x0c\x8c\x34\x24\xa9\x49\xb6\x04\x38\x3b\x73\xb6\xb7\x20\x0b\ +\x9c\xeb\x3c\x07\xa7\xf9\xde\xd3\x5a\x68\x29\xd1\x52\x26\x89\xb1\ +\x7d\x49\x9a\x12\x36\x42\xa4\x10\xc4\x6e\x8c\xc5\xf6\x0d\x7e\xde\ +\x3d\xed\x69\xad\x1d\xad\xd0\x12\xbb\xcc\x4f\xd4\xf6\xec\x67\xa2\ +\xf8\x59\xcf\x91\xdd\x47\xaf\xac\x01\xcc\xfd\x39\xf7\xde\xa9\xc7\ +\x4f\x51\x11\x65\x8e\xc6\x4f\x0f\x4a\x29\x11\xc2\x82\x00\x29\x04\ +\x59\x37\x63\x83\x30\x10\x00\x9e\xf2\xec\xf1\xde\xd7\xb3\x4a\xa8\ +\x61\x29\x24\x5a\x2a\xb6\x39\x59\xb6\xeb\xed\x34\x08\xf8\xea\xd6\ +\xd8\x3b\xc0\x33\x6b\x00\xa3\x97\x46\x87\x0f\x74\xf4\x70\xa3\x72\ +\x85\x62\x58\x44\x6a\x85\x52\xa0\x94\x44\x0b\xc9\x5c\x60\x44\xc3\ +\x84\x48\x01\xd3\x2b\x93\x42\x0b\x05\x48\x5a\x9d\x76\x8c\x51\xfc\ +\xb2\x74\x8d\x1b\xc5\x45\x5e\x1c\x1a\x61\x29\x28\x3c\xb9\xe1\x8a\ +\x82\x28\xb0\x97\x57\x7e\x26\x30\x65\x6a\x69\x1d\x65\x25\xca\x48\ +\x1c\xa3\x70\xd4\xdd\x6c\x94\x54\x78\xca\xa3\x45\xb5\xa0\x71\xa9\ +\xc4\x55\xbe\xfb\xe3\x22\xf3\xa5\x5b\x54\xc2\x90\xac\x69\xa7\xd6\ +\xa8\xd2\x30\xe9\xc6\x1a\x58\x63\xc7\xa7\x16\x2e\x1c\xeb\xe9\xec\ +\xc1\x53\x6d\x68\xa9\xf0\x94\x8b\x87\x8b\x46\xd1\xea\x64\xec\xc5\ +\xe5\xf3\xc2\xca\x98\xa1\xce\x27\x6c\x10\xd7\x85\x12\x9a\xbe\x07\ +\x0e\xd0\xb7\xab\x17\x8b\xc5\x95\x8a\x42\x78\x95\xc4\xa4\xe3\x9b\ +\x01\x4e\x64\xa2\xcc\xb1\xa9\xcb\x53\x14\xdd\xe2\x0c\x02\x90\x20\ +\x24\x89\xab\x54\xdf\x53\xfb\x47\xb2\x5a\x5b\x32\x6e\x96\xdb\xf5\ +\xbf\xc4\x97\x0b\xe7\x6b\x9e\x23\x67\x5d\xa5\xb4\xab\x14\xae\xd6\ +\xec\x8c\x3a\x06\xba\xdb\xbb\x89\x68\x9c\xd8\xf8\x4c\xdf\x66\xe5\ +\x64\xfe\x24\xf9\x7c\x9e\xdc\x9b\xb9\x7b\x0a\xb5\x80\x39\x63\xa7\ +\x83\x24\x18\xd6\x8e\xc4\xd3\x92\xba\x09\x48\x4c\x32\x7b\xf3\x85\ +\xca\x63\x6b\x95\xfc\xb2\xf5\x7d\x1f\xdf\xf7\x57\xfe\x99\x93\x4d\ +\x29\xd9\xa2\xab\x71\x15\x8b\x45\x29\x41\x64\x42\x52\xd3\x9c\x86\ +\x9a\x53\xb2\x35\x54\xa2\x0a\xa9\x13\x92\x71\x15\x51\x5c\x25\x69\ +\xb2\x57\x34\x05\xc0\xc0\x6a\xbd\x8c\x67\x22\xea\x8d\x06\x36\x89\ +\x48\xed\xbf\x00\xac\xef\x8a\x1f\xcb\x51\x5c\x62\x16\x4b\x15\x56\ +\x74\x8d\xbd\xdb\xba\xd9\x5b\xef\x1e\x78\x2d\xf7\xbc\xdd\x32\x60\ +\x7d\x3f\x07\x98\x5b\xfe\x1a\xe9\x2d\xb2\x50\x2d\x61\x52\x4d\x97\ +\xde\x8d\xd3\xea\xe0\xf7\xfb\xf7\xe7\xcb\x1c\x19\x7b\xf0\xd0\x1d\ +\xd4\x99\x0b\x4b\xbf\x0f\xb4\x7b\x3b\xea\x87\x77\x1e\xfe\xf6\xd2\ +\xf5\x1f\x5f\x59\x78\xab\x70\x93\xff\xbd\xfd\x0d\xf6\xbd\x75\x1d\ +\x3b\xea\x7e\x89\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\ +\x00\x00\x03\x30\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0e\x4d\x00\x00\x0e\x9c\ +\x01\xde\xf6\x9c\x57\x00\x00\x02\xd2\x49\x44\x41\x54\x38\x8d\x6d\ +\x93\x4d\x68\x5c\x55\x14\x80\xbf\x73\xdf\x9b\xe9\xc4\x21\xcc\x4c\ +\x4c\xc6\x2a\x22\xb6\xa9\x93\x44\x0c\xa3\x62\xc1\x2c\x14\x9d\xd2\ +\x12\x35\x85\x52\x5a\x11\x6c\xa5\x1b\x71\x57\xe8\xce\xc5\x74\x37\ +\x82\x2e\xa5\x4b\x05\xcb\x20\xc6\x45\xdd\x48\x4a\xf0\x27\xf1\x07\ +\x69\xa4\x86\xc2\x98\xd0\xc4\x50\x14\x1a\x4d\x9b\x57\xf3\x63\xe0\ +\xbd\xe4\xdd\xf7\xee\xbd\x2e\x06\xa7\xb1\x7a\x56\xe7\x1c\x38\x1f\ +\x87\xc3\x77\x84\x7b\x62\x69\x69\xc9\x69\xad\xef\x6d\x03\x90\xcd\ +\x66\xa9\x54\x2a\xb2\xbb\xe7\xef\x2e\x5a\xad\x96\xdb\xba\xd0\xc0\ +\x9f\xbe\x8c\x12\xc1\xcb\x08\xce\x82\xb5\x0e\xa7\x1c\x5b\xcf\x8f\ +\xd1\x6a\xb5\x5c\xb5\x5a\xed\x40\x3a\xc9\xcc\xcc\x8c\x53\xcd\x0b\ +\xdc\xf7\xed\x57\x78\x22\x74\xed\xf5\x31\xeb\x16\x44\xf0\xef\x57\ +\x6c\xdf\x4a\x30\xc6\x12\xd5\x0e\x63\x4e\x9f\x65\x64\x64\x44\x3a\ +\x80\xe9\xe9\x69\xe7\x35\x3f\xa0\x77\x76\x8a\x6c\xc6\xc3\xf7\x05\ +\x3f\xeb\xa1\x3c\x01\x0b\x56\x1c\x26\xb5\x24\x3b\x16\x9d\x1a\xd6\ +\x0e\x1e\x22\x3d\xf5\x26\xb5\x5a\x4d\x64\x62\x62\xc2\x75\x7d\xfa\ +\x11\xe5\xab\xdf\xb3\x99\x11\x36\x8d\x63\xef\x9e\x0c\x7b\xba\x3d\ +\x82\x6d\x8b\x00\xbd\x5d\x42\x12\x59\x56\xa2\x04\x8b\xa3\x94\x3a\ +\xf4\xb3\x2f\x12\xbf\x76\x06\x15\x04\x01\x3d\x3f\x7c\x83\x38\x21\ +\x1e\x3d\xc6\xd8\x42\xc0\xce\xcb\x27\x59\x13\xc7\xa1\xd9\xdf\x79\ +\xe1\xc7\xdf\x28\xbc\x7b\x91\x2b\x5b\x09\x47\x17\x02\x8e\xce\xdf\ +\x22\x7f\xfe\x3d\xf4\x77\x5f\xb3\xba\xba\x8a\x0a\xc3\x10\x25\xa0\ +\xb2\x42\xe1\xf1\xa7\xd8\xd9\xdc\xa0\xfb\x89\x2a\x6e\xbb\x7d\x9e\ +\xcf\xde\x38\xc6\xc3\x07\x9f\x23\xdf\xdf\x0f\xc0\xf8\xe9\x93\x3c\ +\xfd\xfa\x19\x36\x4a\xbd\x84\x61\xd8\x06\x58\x0b\x6b\x91\xa1\x6f\ +\xb8\x4a\xeb\xd2\x38\xbd\x83\xc3\xfc\xa9\x0c\x69\x9a\xf2\xcc\x5b\ +\xe7\xb8\x73\x63\x91\x70\xf9\x0f\xd2\x34\x25\xd7\x53\x20\x35\x06\ +\x67\xcc\x5d\x80\x01\xee\x28\x45\x71\x7f\x85\x6b\x1f\x5f\xa4\x7b\ +\x5f\x3f\xb7\xb7\x1c\x71\x1c\xb3\x7c\x6d\x96\x5c\xdf\x83\xa8\x62\ +\x81\x38\x8e\x39\xf2\xce\xfb\xfc\xd4\xfc\x90\xe2\xe6\x3a\x51\x14\ +\xe1\x87\x61\x48\x92\x18\xa2\x47\x2b\x58\xa5\x38\xde\xbc\x84\x01\ +\x72\xfb\x0f\xa0\xb5\xe6\xe7\x4f\x9a\x0c\xbe\x7a\x0a\x3f\x5f\x44\ +\x6b\xcd\xf8\x93\xfb\x18\x44\xe8\x43\xda\x1b\x44\x51\x84\x4e\x0c\ +\xde\xc0\x10\x2b\x73\x2d\xa6\xce\xbf\xcd\xed\xeb\xf3\x94\x2a\x43\ +\x68\xad\x39\xf1\xf9\x14\x37\xaf\x5e\x61\xe3\xfa\x3c\x5a\x6b\x4a\ +\x0a\xf2\x16\x92\x24\x25\x8a\x22\xa4\x5e\xaf\xbb\x07\xa6\x26\xc9\ +\x2d\x2d\xe0\x7b\x8a\x03\x39\x9f\x65\xe7\x40\x14\x4a\xb5\x45\xe9\ +\x51\x0a\x27\x8e\x20\x4a\x29\x88\xa3\xa8\x60\xee\x91\x01\x56\x6a\ +\xa3\x6d\x91\xea\xf5\xba\x2b\x7f\x39\x49\x75\xe5\x17\x24\x01\x25\ +\x42\xae\xe0\x63\x22\x07\x80\x97\x17\xe2\xbf\x0c\xd6\x39\x5c\xc6\ +\xd1\x7a\x68\x80\xd5\xc3\x2f\xd1\x68\x34\xa4\xa3\x72\xbd\x5e\x77\ +\xe5\x2f\x2e\x33\xf8\xeb\x02\x9e\x08\xa2\xa4\x6d\xa2\x03\xac\xc3\ +\xe2\x48\x8d\x63\xb1\x7f\x88\xe0\xc8\x2b\x34\x1a\x8d\xbb\x2a\xef\ +\x86\x94\x26\x27\x78\x6c\x71\x0e\x2f\xeb\xe1\x2c\xe0\x40\x7c\x30\ +\xb1\xe1\xc6\xd0\x30\xeb\xa3\x63\x9d\xe1\xff\x00\xfe\x81\x04\x41\ +\xf0\xbf\xef\x5c\x2e\x97\xff\x35\x0c\xf0\x37\x5c\xfc\x53\xd5\x3a\ +\x96\xb0\xec\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x04\x2c\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x2e\x23\x00\x00\ +\x2e\x23\x01\x78\xa5\x3f\x76\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ +\xde\x03\x0c\x12\x06\x08\xae\xb5\x4a\x3d\x00\x00\x03\xb9\x49\x44\ +\x41\x54\x38\xcb\x5d\xcb\x5b\x4c\x5b\x75\x00\x07\xe0\xdf\xff\x9c\ +\xd3\x9e\x76\x6d\x69\xc1\x52\x84\xca\x40\x2e\x16\xc7\x9d\x31\x67\ +\x9c\x66\x03\x27\x73\x2f\x8a\x71\x2c\x12\x12\x12\x1f\xa6\x89\x2c\ +\x73\xc9\x36\x37\x13\x6f\x0f\x5e\xa2\xcc\x88\x0f\xb8\x87\x3d\x78\ +\x99\x09\x09\x4b\x34\xd1\x38\x86\x3a\xb9\x3a\x04\x06\x1d\x84\x6b\ +\xa1\x2d\xd0\x0b\xa5\xed\xa1\x17\x7a\x4a\xdb\x73\xf3\xc9\x17\xbf\ +\xf7\x8f\xe0\x7f\xea\x00\xac\xf6\x78\xb0\x7f\xb1\x10\x00\xd0\x07\ +\x50\xbf\x9d\x69\x36\x15\x55\xd5\x3f\x22\xe6\xab\xcd\xac\xbc\x64\ +\xcd\x65\x33\x82\x73\x79\x67\xa2\xa7\x7b\x36\xc8\xfc\x17\xaf\xff\ +\x74\x15\xee\x73\x9f\xe3\xd9\xf0\x3e\xd5\x4e\xb4\xf2\xbb\x57\x3a\ +\xcb\xac\x95\xcd\xaf\x2f\x14\x72\xf5\xad\x79\xe5\x15\xe1\x4c\xf2\ +\xf1\xd8\xf4\x10\x76\x84\x2d\x18\x92\x31\xec\x39\x82\xc3\x25\x96\ +\x83\x2d\x04\x00\x2e\x5f\xac\xc7\xf5\x1e\x3b\x00\xe0\x30\xa0\x39\ +\xd6\xf5\xd2\x25\x5e\x6f\x7c\x7f\x6e\x88\x66\x6d\x2f\x04\x51\x7b\ +\x54\xc0\xae\x5f\x40\x69\x41\x03\xbc\x53\x03\xd8\x18\xf7\x80\x4f\ +\x88\x23\xb6\x23\xc5\x2d\x14\x00\x68\xee\xd8\x71\xfe\xe7\x5e\xb4\ +\xb5\xe1\xc0\xe9\xdb\xbd\xb7\x72\x5a\xda\x3f\x76\xb8\xfd\x78\x30\ +\xf5\x9d\x1c\xe7\xb6\x51\x7a\x92\x81\x62\x52\x63\xda\x31\x89\xb9\ +\xbf\xfd\xf0\x2b\x40\xda\xc4\xc4\x3e\xb9\xb1\x92\xa1\x15\x1f\x8f\ +\xc1\x6c\x99\x4a\xda\x1a\xe9\x12\x53\xd5\x37\x74\x9e\xa5\xa3\xb1\ +\x22\x28\x91\x1c\x95\x8a\x11\x52\xe4\xe9\x4e\x03\x16\x07\x78\x84\ +\x97\x05\x78\xe7\x37\x40\xcb\xe9\x8c\x56\x94\x69\xa3\x4a\xee\x9e\ +\x73\x8a\x76\x82\xa1\x2f\x80\xa6\x77\xf0\xf5\xc4\xa7\x6f\xe9\x8a\ +\x6d\xbd\xae\xe1\x11\x29\x15\x72\xd0\xc9\x1d\x0e\x12\x05\x04\xdc\ +\x5e\x48\xfb\x09\xe8\xf2\x53\x70\xfc\xa9\xc8\x8f\xb2\x22\x65\xc8\ +\xa2\x6f\x5c\x7b\xbe\xbc\xab\xf1\xa3\x15\x85\x00\xc0\xbc\x32\x9a\ +\xfd\xfd\xef\x77\xdc\x81\x99\x87\x9a\x02\x36\xc2\xae\x8e\xfa\x90\ +\x65\x12\x11\xe6\x62\x30\x9b\x4f\x23\xbb\xf0\x14\x84\x3d\x23\x7c\ +\xbe\x10\x88\xb2\xb5\xb5\xe6\x2e\x6b\x5d\x9e\x79\xd3\xde\xda\x71\ +\x93\xd0\x00\xf0\x58\x53\xcb\xb5\x8d\x91\x6f\x4f\x1d\x88\x6d\x52\ +\xf6\xb1\x20\xd1\x54\x18\x91\x10\xd3\x38\x56\xdd\x83\x27\x6a\xda\ +\x50\x5d\x57\x8b\xd1\x87\x01\x94\x54\x14\x2b\x12\x2c\xc6\xd2\x43\ +\xe6\x8e\xe8\x5e\xcd\xf4\xc4\x1f\x17\x5c\x74\xdf\xe2\x6d\x53\x44\ +\xc5\xf6\x4c\xf5\xf5\x9b\xa3\x7e\x9e\x98\x73\x68\x44\x96\x7d\x28\ +\x50\xbf\x08\xe4\x34\xe1\xe5\xd6\x2a\xdc\x9b\x74\x81\x97\x15\x4c\ +\xce\x7b\xc9\x92\x33\x44\x68\x8d\x5a\xa5\xa5\x94\x13\x14\x9e\xfa\ +\x81\xa2\x8c\xf9\x15\x54\xc2\x63\x63\x58\x0d\x38\x5f\x1a\x9b\x0b\ +\x31\x68\x24\x82\x38\x7d\x1c\x95\x35\x79\x90\x28\x1a\x87\x8f\x94\ +\x40\x9d\x63\xc4\x1e\xab\x86\xd6\x92\x85\xcd\x9d\x38\x61\xb2\xb3\ +\x8a\xac\xe5\xb9\x8d\x54\x38\xb0\x5e\xe4\x78\x30\x02\x29\x1e\x05\ +\x51\x53\x90\x64\x05\xb1\x3d\x3d\x26\x96\x44\xc5\xbe\x16\x86\x27\ +\x18\xc7\x36\xc7\x43\xaf\x67\x90\x7b\xd0\x0c\xc5\xa8\x03\xa3\xd3\ +\x92\x86\xa3\x25\xe0\x79\xb1\x99\xe1\x9c\xc1\x48\x68\x66\x05\x11\ +\x2f\x0f\x0d\x14\x68\x55\x04\x1a\x35\x03\x5a\x21\xc4\xee\xe4\x94\ +\x8c\xd1\x43\x12\x94\x0a\x9b\xbc\x80\x24\xcb\x82\xe8\xb5\x60\x29\ +\x82\xd5\xb5\x20\x64\x31\x93\x62\xaa\xa4\xe8\xfd\x1d\x83\xe5\x33\ +\x29\x8f\xb7\xa6\x12\x9c\x64\xd0\xe8\x32\x3a\x2d\x1f\xdd\xd8\x4d\ +\xbc\x1d\x88\xa4\x35\x26\x5e\x42\x43\xa5\x05\x87\x74\x5a\x0c\xaf\ +\x85\x11\x57\x13\x45\xf0\xa4\xc9\xd2\xec\x3a\xb4\xa9\xf4\x10\x01\ +\x80\xfb\xeb\x23\x54\x55\x7e\x21\x9d\xda\xf5\xca\xf7\x06\x26\xa9\ +\xd7\x7e\xb9\x22\x9c\x4c\x5c\xfd\xd1\xa9\xb7\x75\xe8\x9f\xb4\x2a\ +\x6f\x9c\xad\x23\x65\x16\x13\xb8\xfd\x0c\xee\x8e\xaf\x63\x6c\x70\ +\x11\x8c\x2f\x3c\x4b\x85\x62\xc7\x19\xcf\xea\x2d\x7c\xf9\xd5\x7b\ +\x68\xef\x1f\x93\x18\x0a\xd0\xab\x21\xb5\x7b\x40\x14\xe5\xf2\x85\ +\x13\x67\x7e\x4d\x46\xf7\xd3\xe7\xc6\x1d\x1c\x5e\x6d\xb0\x60\x76\ +\x4b\xc4\x6e\x28\x01\x46\x10\x97\x52\xf1\x54\xa7\x7f\xed\xc3\x04\ +\x01\x00\x87\xeb\x2e\xb8\x40\x1c\xdb\xae\x05\xc4\xc2\x22\x3c\x3e\ +\x2b\x3e\xe8\xee\x02\x00\x3c\xf7\xca\xcd\x4a\xc5\x64\x38\x5b\x5d\ +\x5b\x58\xe0\xf2\x44\x64\xce\xbf\xfb\x17\xef\x0e\x0d\x2e\xff\x73\ +\x29\xfa\xcc\xf9\x7e\xfc\x0b\x0b\xb9\xbe\x45\x31\x2c\x8e\xcb\x00\ +\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x20\x26\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\ +\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x02\x15\ +\x0e\x23\x2f\xab\xc9\xe5\x0b\x00\x00\x00\x1d\x69\x54\x58\x74\x43\ +\x6f\x6d\x6d\x65\x6e\x74\x00\x00\x00\x00\x00\x43\x72\x65\x61\x74\ +\x65\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\x64\x2e\x65\x07\ +\x00\x00\x1f\x7d\x49\x44\x41\x54\x78\xda\xcd\x9b\x69\x90\x5d\xd7\ +\x71\xdf\x7f\x7d\xee\xfa\x96\xd9\x07\xc4\x00\x33\x18\x02\x03\xae\ +\x22\x45\x53\x0b\x25\xd2\x91\x44\x53\x8a\x2d\x89\x91\x17\xad\xa4\ +\x2c\x5b\x52\x12\xc9\xf1\x56\x72\xca\xa9\xe4\x4b\x2a\x1f\x52\x89\ +\xbf\x38\x49\x39\x72\xe2\x4d\x92\x63\x4b\xb1\x55\x12\x4d\x49\xb4\ +\xc5\x45\x24\x41\x10\xa0\x44\x09\x80\x28\x42\x22\x41\x12\xfb\x3a\ +\x33\x00\x66\x06\xb3\xbf\xed\xde\x7b\x4e\xe7\xc3\xb9\x6f\x30\x00\ +\x41\xed\x76\xe5\x56\xdd\x79\x6f\xee\xf6\x4e\xf7\xe9\xfe\x77\xf7\ +\xff\xf4\x85\x1f\x73\x7b\xf1\xc5\x17\xf9\xff\x65\xfb\x49\xc6\x12\ +\xfe\x34\x06\x70\xe0\xc0\x81\xde\x9e\x9e\x9e\x9f\x37\xc6\xbc\x4e\ +\x44\x7a\x01\xfb\x8f\x24\x6b\xa0\xaa\xcb\xce\xb9\xef\xac\xac\xac\ +\x3c\x7e\xf3\xcd\x37\x2f\xff\xa4\x0f\x94\x1f\xe7\xa6\x3d\x7b\xf6\ +\x70\xfb\xed\xb7\x03\xf0\xf5\xaf\x7f\xbd\x32\x3a\x3a\x7a\x6f\xad\ +\x56\xfb\x70\x14\x45\x37\x1b\x63\x22\x40\x45\x04\x91\x2b\x3f\xfe\ +\xf2\xe3\xaa\xfa\x8a\xbf\xa5\xaa\xeb\xcf\x8b\x73\x2e\xcf\xf3\xfc\ +\x40\xa3\xd1\xf8\xdc\xd4\xd4\xd4\x17\xde\xfc\xe6\x37\xb7\x2e\x1f\ +\xd3\x3f\xea\xf6\xe0\x83\x0f\xae\x7d\x7f\xe0\x81\x07\x92\xe3\xc7\ +\x8f\x7f\xe8\xdc\xb9\x73\xdf\x5c\x5c\x5c\x6c\xaf\x36\x1a\xda\x6c\ +\xb5\xb4\xd5\x6e\x6b\xbb\xd3\xd1\x4e\xb9\x67\x59\xe6\xf7\x3c\xd7\ +\x2c\xcf\x35\xcf\x73\x2d\x8a\x42\xf3\xa2\xf0\x9f\xeb\x8e\x67\x79\ +\xbe\x76\x7d\xa7\xd3\xd1\x76\xbb\xad\xad\x56\x4b\x5b\xad\x96\x36\ +\x5b\x2d\x5d\x5d\x5d\xd5\x85\x85\x85\xf6\xb9\x73\xe7\xbe\x79\xfc\ +\xf8\xf1\x0f\x3d\xf0\xc0\x03\xc9\x95\xc6\xf6\x8f\xb2\xed\xd9\xb3\ +\x67\xed\xfb\xfd\xf7\xdf\x9f\x1e\x3e\x7c\xf8\xdd\xd3\xd3\xd3\x7b\ +\x16\x16\x16\x8a\x46\xa3\xa1\xed\x2c\xd3\x8e\xb5\x2e\x73\x4e\x73\ +\xa7\x5a\xa8\xdf\x6d\xb9\xbb\x72\xbf\xd2\xe6\xd6\xed\xdd\xeb\x0b\ +\x55\xcd\x55\x35\x73\x4e\x33\xe7\xb4\x63\xad\x6b\x67\x99\x36\x1a\ +\x0d\x5d\x58\x58\x28\xa6\xa7\xa7\xf7\x1c\x3e\x7c\xf8\xdd\xf7\xdf\ +\x7f\x7f\x7a\xa5\x31\xfe\x54\x5d\x60\xff\xfe\xfd\xbc\xe6\x35\xaf\ +\x01\x60\xe7\xce\x9d\xb5\xcd\x9b\x37\xbf\xa5\x5e\xaf\xff\xa7\x4a\ +\xb5\x7a\x47\x9a\x24\x88\x88\xae\x9c\x3c\xc1\xe2\xb1\xe3\x92\x35\ +\x56\x41\x04\xb8\xd4\x0d\x8c\xc8\xda\x2f\x0a\x82\x5e\xb4\x73\x7f\ +\x5c\x41\xfd\x1f\x7f\x4e\xb5\xbc\x46\x41\x95\xb8\x56\xa3\x6f\xfb\ +\x76\xed\xdd\xba\x0d\x55\x95\x76\xa7\x43\xab\xd9\xfc\xd6\xea\xea\ +\xea\x7f\x39\x7b\xf6\xec\x53\x77\xdd\x75\x57\xe3\xf2\xb1\xfe\x54\ +\x14\xf0\xdd\xef\x7e\x97\x5b\x6f\xbd\xb5\xeb\xf3\xf5\x91\x91\x91\ +\xb7\x56\xab\xd5\xff\x90\x56\x2a\xff\x2c\x89\x63\x44\x55\x57\xcf\ +\x9c\x91\x43\x9f\xfe\x33\x0e\xff\xcd\xe7\x58\x99\x5d\xea\xca\xb3\ +\xf6\xd9\xfd\x31\x53\xfe\xaf\x57\x18\x88\x00\xee\xb2\xeb\xd7\x3f\ +\xa3\x77\x43\x1f\xd7\xfe\xda\x87\xb9\xfe\xe3\xbf\x45\x6d\xcb\x16\ +\x45\x44\xda\xed\x36\xed\x76\xfb\xe9\x56\xab\xf5\x87\xe7\xcf\x9f\ +\xdf\xf9\xa6\x37\xbd\x69\xf5\xf2\x31\xff\xd8\x0a\x78\xee\xb9\xe7\ +\x00\xb8\xe5\x96\x5b\xba\xc2\xf7\x8c\x8c\x8c\xdc\x5d\xad\x56\x7f\ +\x2f\x4d\xd3\x3b\x92\x24\x51\x50\x56\x4f\x9d\x92\x83\x9f\xfe\x14\ +\x8b\x0f\xfd\x3d\x3d\x4b\x33\xf4\xd7\x13\x50\xc1\xb5\x1d\x61\xcd\ +\x80\x03\x2d\x20\x88\x04\x13\x09\xae\x50\x34\x57\x4c\xc5\x5b\x84\ +\x6d\x28\x26\x11\x24\x14\x5c\xee\x70\x05\x98\x00\x54\xa0\x58\x75\ +\x04\x55\x03\xa2\x2c\xae\x76\x58\xe9\xbb\x8a\xfe\xbb\x7f\x89\xeb\ +\x3f\xf6\x1b\xd4\xc7\xb7\xaa\x0a\x74\x3a\x1d\xe9\x74\x3a\xdf\x6a\ +\x36\x9b\x9f\x3c\x77\xee\xdc\xc3\x6f\x7e\xf3\x9b\x57\xae\x34\xfe\ +\x9f\x28\x0c\x3e\xf9\xe4\x93\x7d\x23\x23\x23\xbf\x52\xad\x56\xff\ +\x4d\x9a\xa6\x77\xc4\x71\x8c\x3a\xc7\xea\x99\xd3\x1c\xfa\xf4\x9f\ +\xb3\xf2\xe8\x57\xe9\x59\x9c\xa5\x1e\x45\x68\x0e\xa2\x10\x24\x01\ +\x61\x2a\xa0\x82\x6d\x39\xd4\x2a\xd6\x2a\x88\x60\x52\x83\xa9\x88\ +\x9f\xdd\x4c\xd1\x42\xa1\xf0\xf3\x6d\x22\x21\x48\x05\x54\x71\x99\ +\x57\x1e\x02\xf5\x20\x86\xc5\x39\x56\x1e\xff\x2a\x07\x51\xae\xff\ +\xd8\x6f\x52\x1f\x1f\x27\x8e\x63\x44\xe4\x0e\x11\x61\x64\x64\x24\ +\x7d\xf2\xc9\x27\x1f\xb8\xeb\xae\xbb\x96\x7e\x22\x17\x58\x6f\x42\ +\x4f\x3d\xf5\x54\xff\xe8\xe8\xe8\xfb\x2a\x95\xca\x47\x93\x24\xb9\ +\x2d\x8a\xe3\x58\x40\x9b\x53\x53\xf2\xe2\xff\xfa\x24\xab\x3b\x1e\ +\xa6\x67\x71\x81\x6a\x28\x84\x89\xc1\x84\x02\x4e\xd0\x8e\x12\xd6\ +\x0d\x58\x50\xab\x98\x48\x30\xa1\x17\xc8\x15\x8a\x49\xbd\xe1\xdb\ +\xa6\xc3\x24\x06\x13\x94\x16\x60\x15\x29\xa7\xa6\x68\x94\x96\x62\ +\x14\x97\x2b\x79\xe6\x68\x5a\x65\xa5\x67\x80\xfa\xdb\xee\xe6\x55\ +\x9f\xf8\x04\xd5\xd1\x31\x75\xaa\x92\x67\x59\x96\x65\xd9\xb7\x5b\ +\xad\xd6\x5f\x4f\x4d\x4d\xdd\xff\x96\xb7\xbc\x65\xf1\x07\xb9\x83\ +\xb9\xd2\xc1\xbd\x7b\xf7\xae\xdd\xb0\x7b\xf7\xee\xbe\x4d\x9b\x36\ +\xbd\x2f\x49\x92\x8f\xc4\x71\xfc\xda\x30\x0c\x63\x13\x04\xb4\x67\ +\xce\xf1\xfc\x27\xff\x88\x95\x47\x1e\xa2\x6f\x79\x81\x9a\x11\xa2\ +\xc0\x10\xc6\x86\xb0\x12\x78\x45\x58\x10\x27\x88\x13\x0c\x42\x18\ +\x1b\xa2\x6a\xf7\x9c\xc1\x38\x83\x71\x10\x60\x88\x12\x43\x54\xf3\ +\x0a\x0c\xf0\xe7\xc5\x09\xc6\xf9\xfb\xc2\x34\xf0\xf7\x07\x86\x2a\ +\x42\xef\xca\x02\x2b\x8f\x3e\xc4\x81\x4f\xfe\x11\xed\xf3\xe7\x08\ +\xc2\x80\x28\x8a\xe2\x38\x8e\x5f\x9b\x24\xc9\x47\x36\x6d\xda\xf4\ +\xbe\xdd\xbb\x77\xf7\x01\xdc\x7a\xeb\xad\xec\xdd\xbb\xf7\x87\xb3\ +\x80\xf5\x09\xc5\x13\x4f\x3c\xd1\x3b\x36\x36\xf6\x9e\x5a\xad\xf6\ +\xb1\x24\x49\x5e\x1b\x45\x51\x25\xae\x56\x59\x3a\x76\x4c\x5f\xf8\ +\xe3\xff\x29\xab\x0f\xfd\x03\xfd\xed\x15\xfa\x7a\x22\x0c\x7e\xd6\ +\x25\x10\x82\x48\xbc\xdf\xe7\x4a\x54\x37\xa8\x13\xef\xf3\x81\xf7\ +\x73\xb5\x0a\x0e\x4c\xd5\xc3\x9b\x6d\x80\x09\x81\x40\xd0\x42\x51\ +\xa7\x48\xe4\xcf\x15\x2b\x8a\x24\xa0\x06\x5c\xae\xa8\x55\x34\x00\ +\xe7\x1c\xcb\xab\x39\x8b\x69\x2f\xb5\x77\xfe\x22\x37\xfd\xde\xbf\ +\xd5\xde\xed\xdb\x25\x6b\x36\x29\xf2\xbc\xd5\xe9\x74\x9e\x6d\x34\ +\x1a\x9f\x99\x9c\x9c\xfc\xf2\xdb\xde\xf6\xb6\xe5\x57\x4a\x96\xcc\ +\xe5\x49\x4e\xf7\x82\xa7\x9f\x7e\xba\x3e\x3a\x3a\x7a\x77\xb5\x5a\ +\xfd\x8d\x24\x49\x6e\x8b\xa2\xa8\x12\x55\xab\xcc\x1f\x3c\xa8\x2f\ +\xfd\xe9\x9f\xc8\xfc\xfd\x5f\x64\xa0\xb9\x44\x5f\x35\x22\x4c\x02\ +\x82\xc4\x60\x8c\x60\x1c\x48\xe1\xf7\x30\x31\x84\xa9\x21\xaa\x08\ +\x41\x28\x88\x03\x32\x10\x2b\x04\x89\x10\x56\x84\x30\xed\x9e\x13\ +\x24\x53\xc4\x79\xb0\x8c\xaa\x42\x58\x09\x30\xdd\x73\x16\xc4\x82\ +\x09\x84\xa8\x62\x88\xd2\x80\xde\x34\x62\xa0\xb9\xc4\xfc\x97\xbf\ +\xc0\x4b\x7f\xf6\x27\x32\x7f\xf0\xa0\x46\x95\x0a\x61\x14\x55\x92\ +\x24\xb9\xad\x5a\xad\xfe\xc6\xe8\xe8\xe8\xdd\x4f\x3f\xfd\x74\x1d\ +\xe0\xf6\xdb\x6f\x7f\x59\xb2\xb4\xa6\x80\x1d\x3b\x76\xf0\xae\x77\ +\xbd\xab\x3b\xf3\xb5\xc1\xc1\xc1\xb7\x56\x2a\x95\x4f\x24\x49\x72\ +\x47\x18\x86\xb1\x09\x43\x96\x8e\x1e\xd5\x43\x9f\xfe\x94\x9c\xfb\ +\xec\xa7\xd9\x12\x5b\xfa\x7b\x52\x0c\x5e\x30\xb1\x42\x60\x84\x20\ +\x36\x04\xa9\x21\x48\x0c\x62\x05\xca\x3d\x10\xf1\xca\xa8\x1b\xc2\ +\x54\x10\xdb\xdd\x0d\x81\x7a\xd3\x8f\xea\x86\x30\x11\x8c\x0a\xe4\ +\xfe\xb9\xc6\x79\x25\x45\x15\xe3\x5d\x47\x04\x0a\x4a\xb7\x32\xf4\ +\xf7\x24\x6c\x49\x95\x73\x9f\xfd\x34\x47\x3e\xfd\x29\x59\x3a\x76\ +\x4c\x4d\x10\x10\x86\x61\x9c\x24\xc9\x1d\x95\x4a\xe5\x13\x83\x83\ +\x83\x6f\x7d\xe2\x89\x27\x6a\x00\xef\x7a\xd7\xbb\xd8\xb1\x63\xc7\ +\x45\x17\xd8\xbd\x7b\x37\x00\x77\xde\x79\x27\x00\xf7\xdd\x77\x5f\ +\x7a\xe3\x8d\x37\xbe\xb5\x56\xab\xfd\xc7\x4a\xa5\xf2\xb3\x69\x9a\ +\x02\x68\x7b\x76\x96\xe7\xff\xfb\xff\x90\xa5\xaf\x7c\x81\x2d\xa9\ +\xa3\xb7\x9e\xe0\x1c\xb8\x86\x62\x62\x81\x42\x08\x13\x21\xac\x1b\ +\x4c\x22\x60\xa1\x98\x71\x04\x35\x0f\x82\x12\x42\xd8\x13\x10\x54\ +\x05\xd7\x56\x8a\x79\x87\x24\x94\xb1\x4e\x09\x07\x0c\x92\x0a\xb6\ +\xa9\x14\xcb\x16\xb5\x20\x06\x6c\x1b\x82\x61\x41\x02\xc5\x76\x94\ +\x62\xc5\x61\x33\x07\x81\x0f\xa7\xa6\x06\xc6\x08\x4b\xab\x1d\xce\ +\xb4\x84\xbe\x77\xdf\xc3\xab\x7f\xff\xdf\x69\xb2\x61\x18\x10\xe9\ +\x74\x3a\xb4\x5a\xad\x6f\x36\x1a\x8d\x3f\x78\xe9\xa5\x97\x76\x7e\ +\xe0\x03\x1f\x68\x97\xd8\xc6\x9d\x77\xde\x79\x29\x06\x7c\xe6\x33\ +\x9f\x49\xde\xf0\x86\x37\xdc\x5d\xab\xd5\xfe\x7d\xa5\x52\xf1\x71\ +\x5e\x95\xd6\xb9\x73\xf2\xd2\xff\xfe\x24\xcd\x47\x1e\x62\xb0\xb9\ +\x44\x5f\x6f\xea\xfd\x59\x3d\xa2\x87\x35\x3f\xdb\x02\x98\xd8\x5b\ +\x01\x16\xdc\xaa\x12\x0d\x1a\xb0\x3e\x22\x98\x54\xbc\x72\x3a\x8a\ +\x6b\x83\xe9\xf3\x3f\x6f\x97\x1c\xa6\x66\x90\x08\x5c\x47\xd1\xcc\ +\x63\x80\x33\x4a\x7e\xc1\x11\xf4\x79\x7c\xb0\x6d\x87\xcb\x14\x35\ +\x8a\x0a\xd8\x86\x2b\xf1\x41\xb0\x99\x63\x69\xb9\xcd\x7c\xb5\x8f\ +\xea\x3b\xfe\x05\x37\xfc\xee\x27\xa8\x6c\x1c\x51\x44\xe8\x74\x3a\ +\xd2\x6e\xb7\xbf\xd5\x68\x34\xfe\xdb\xbe\x7d\xfb\x1e\xfe\xd8\xc7\ +\x3e\xd6\xe9\x82\x7d\xd0\x15\xfe\xf3\x9f\xff\x7c\xed\x96\x5b\x6e\ +\x79\x6f\xad\x56\xfb\xfd\x34\x4d\x6f\x4f\xd2\x04\x51\xa5\x71\xe6\ +\x0c\x87\xff\xe2\x4f\xa5\xfd\xe4\xa3\x0c\x76\x96\xa9\x47\x31\x64\ +\x52\x9a\xbe\x10\x24\x86\xb0\x62\x08\x22\x83\x58\xaf\x10\x29\x40\ +\x72\x21\xac\x05\x04\x35\xe3\x7d\xdc\xfa\xe3\x74\x4a\x77\xa9\x1b\ +\x82\x9a\xf8\x90\x59\x78\xa5\xd0\xf1\x85\xb4\x89\xbb\xe7\xfc\x73\ +\x34\xf7\x80\xaa\x39\x98\x50\x08\x6a\x06\x91\x12\x4c\x73\x0f\x9c\ +\xb6\x05\x49\x12\x12\x49\x87\xd5\xe9\x29\x96\x66\xe7\xe9\xb9\xe6\ +\x5a\xe2\xde\x5e\x82\x20\x10\x60\x0b\xb0\x75\x78\x78\xb8\x71\xe7\ +\x9d\x77\x1e\xfb\xd2\x97\xbe\x94\x7f\xfc\xe3\x1f\xf7\x0a\xd8\xb5\ +\x6b\x57\xff\xf8\xf8\xf8\xfb\x6b\xb5\xda\x6f\x56\x2a\x95\x37\x24\ +\x69\x2a\xa2\xaa\xab\xa7\x4f\xc9\xd1\xbf\xfc\x8c\x34\x1f\x7f\x84\ +\xbe\xd5\x45\x6a\x12\x10\x88\x4f\x52\xc2\xd4\x20\x81\xf7\xd3\xc0\ +\x78\x7f\x36\xf8\xe3\x41\x45\x08\x02\x41\xb2\xae\x80\xa5\xcf\xa6\ +\x86\xb0\xba\xee\x98\xc1\x03\x63\x2e\x84\x55\x7f\x9f\x98\x12\x2c\ +\xd5\x2b\xcd\xb5\x14\x53\xf7\x96\x23\x94\xd1\xc5\x51\x46\x19\x90\ +\x58\x30\xb1\xf7\x24\x51\x21\x44\x30\xad\x16\xab\xa7\x27\x59\x5e\ +\x5e\x91\xda\xb6\x09\x89\xfb\xfb\x35\x8c\x22\x11\x91\xcd\xc6\x98\ +\xd1\x81\x81\x81\xce\xbd\xf7\xde\x7b\xe2\x4d\x6f\x7a\x53\xdb\x00\ +\xd4\x6a\xb5\xb7\xa7\x69\xfa\x91\x24\x49\x5e\x1b\xc5\xb1\x41\x55\ +\x57\x4f\x9f\x96\x63\x7f\xf5\x97\x34\xbf\xf6\x10\x3d\x0b\xf3\xd4\ +\xc4\xc7\x64\x29\xe3\x79\x10\x1b\x82\xa8\x1c\xac\x05\x32\x45\x80\ +\x20\x16\x6f\x11\x15\x03\xb9\x9f\x6d\x29\x04\x03\x84\xa9\x10\xf6\ +\xfa\x88\x21\x19\x98\x42\x90\x5c\x30\x85\x10\xa4\xa6\x3c\xb7\x2e\ +\x22\x14\x3e\xaa\x84\x69\x69\x65\xa9\xf1\x4a\xe8\x00\x85\x07\xc8\ +\xa0\x74\xb9\x30\xf1\x2e\x69\x0a\xf1\x79\xc2\xd2\x02\xad\xc7\x1e\ +\xe6\xf8\x67\xff\x92\xc6\xe9\xd3\x82\x53\x8d\xe2\xd8\x24\x49\xf2\ +\xda\x34\x4d\x3f\x52\xab\xd5\xde\xde\x4d\x85\x0d\xf0\xfa\x28\x8a\ +\x6e\x8a\xa2\x28\x0e\xc2\x90\xe5\x63\xc7\x38\xfe\x57\xff\x87\xd5\ +\x47\x1e\xa4\x6f\x71\x9e\x5a\x1a\x12\xc6\x81\x0f\x71\x5a\x9a\x24\ +\x3e\xdd\x35\x81\x1f\x84\x84\x7e\x76\xc8\x40\xb5\x74\x8f\x14\x82\ +\xe4\xe2\x71\xc9\xbc\xcf\x4b\xee\x05\x36\x55\x01\x2d\x5d\xa6\x34\ +\x7f\x29\x84\x20\x02\x62\x5f\x2d\x9a\x96\x81\x36\xa8\x51\xc8\x7c\ +\x2e\x61\x22\xc1\xa1\x60\x9d\x77\x0d\x29\x9f\x6f\xf0\x96\x47\x40\ +\xcd\x29\x6e\xfe\x02\xcb\x0f\x3d\xc8\x51\x07\x13\x1f\xfd\x57\xf4\ +\x4c\x6c\x27\x8a\xa2\xb8\x28\x8a\x9b\x80\xd7\x03\x7f\x17\x02\x71\ +\x51\x14\x7d\x22\x12\x19\x63\x40\x44\xdb\x27\x4f\xca\xfc\xdf\x7d\ +\x91\xe1\xa2\x45\x3d\x8e\x09\x23\x1f\x8f\x8d\x08\xb4\x0a\x74\x39\ +\x43\xe3\xd2\x4c\x43\x90\x24\xc2\x44\x31\x14\xa1\x9f\x71\x01\x71\ +\xbe\xc0\x09\x2a\x5e\x68\x2d\x1c\x34\xdb\xd0\x74\x5e\x71\xc9\xc5\ +\xf2\x58\x0a\x41\x0b\x01\xe3\x43\x1b\x49\x80\xd4\x0c\x8a\xa2\x2b\ +\xde\xc7\xc5\x78\x1c\x30\x36\x83\xb8\x40\x2d\x68\xcb\xa2\x4e\x71\ +\x01\xd8\x86\x85\x48\x90\x54\x10\x15\x02\x31\xd4\xa3\x00\x5d\x9c\ +\xe3\xc2\xdf\x7d\x91\xd6\xcf\xdd\x25\x3d\xd7\x5c\xa3\xc6\x18\x11\ +\x91\xa8\x28\x8a\x3e\x20\x0e\x81\x38\xcb\x32\x51\xf5\xc4\x93\x82\ +\x84\xed\x26\xfd\x0b\xf3\xf4\x8e\x0c\x60\xd4\x9b\x96\xcf\xe7\x0b\ +\xc2\xc1\x61\xd2\x89\x09\xa2\x81\x3a\xb8\xb2\xc4\x35\x19\x6e\x7a\ +\x1a\x37\x77\x01\x43\x41\x90\x78\xd3\x27\x2f\x77\xeb\x08\x7a\x87\ +\x08\xb6\x6c\xc1\x0c\xf4\x41\xe1\x6f\x34\xf5\xb2\x34\x9e\x2f\x50\ +\xcd\x20\x6b\xe1\x66\xe7\x71\xf3\x0b\x68\x2b\x47\x82\x32\x1d\xae\ +\x7b\x73\x93\xb4\x8a\x6c\xbc\x0e\x33\xba\x09\x57\x14\xc4\xab\x0e\ +\x35\x40\x00\x45\xcb\xe2\x6c\x81\xda\x36\xf9\xe2\x22\x3a\x3d\x8b\ +\x2e\x2e\xd0\x5b\x8d\x29\xce\xcf\x13\xb6\x5b\x94\x46\x8b\xaa\x6a\ +\x96\x65\x02\x44\x21\x10\xe4\x79\x1e\x38\xe7\xa4\xcb\xbf\xc5\x81\ +\xa1\xaf\x16\x78\x40\x52\xef\x6f\xea\x14\xbb\xda\xa6\xfe\xd6\xdb\ +\xd9\xf4\x5f\xff\x80\x78\xf3\x96\xb5\xba\xbd\x38\x7f\x96\xe5\x4f\ +\xfd\x39\xd9\xce\xbf\xc7\xb4\x67\x30\x95\x08\xb1\x06\xb7\x02\x18\ +\x41\xdb\x16\xee\xb8\x85\xe4\x77\x7e\x9f\xf8\x96\x9f\x59\x47\x84\ +\xf8\x64\x5c\x5b\x0d\x74\xf9\x02\x6e\x6e\x0a\xfb\xec\x33\xe4\x4f\ +\xee\xc2\x1e\x3e\x88\x5b\x6d\xf9\x7a\x22\x35\x38\x2d\x30\x9b\x46\ +\xa8\x7e\xe8\xb7\xa9\xbd\xf3\xfd\x5c\x89\x45\xb4\xab\xab\xe4\x67\ +\xa7\x69\xbf\x74\x80\x95\xa7\x76\xb1\xfa\xf4\x4e\xb2\xb9\x19\xfa\ +\xeb\x01\x49\x68\xd6\xf8\x45\xe7\x9c\xe4\x79\x1e\x00\x61\x57\x01\ +\x62\xad\x5d\x77\x81\xa2\x0e\xcf\xd9\xa8\xf7\xb9\x30\x16\x6c\x54\ +\x25\xdc\xb6\x95\x78\xf3\x96\x4b\x0a\x09\x73\xd5\x46\x82\x6d\x5b\ +\xa1\xa7\x17\x6d\xcc\xf8\x4a\xb0\x4c\x5b\x83\x41\xa1\xdd\x50\x5a\ +\x81\x12\xa9\x12\x5f\x46\x92\xa0\x20\x95\x1a\xa6\x52\x23\xd8\x38\ +\x4e\x78\xd3\x1d\x84\x3f\x77\x37\xd9\xfd\x7f\x41\xbe\xeb\x61\xf4\ +\x78\x03\x71\xe0\x54\xc8\x33\x25\x2a\xf4\x15\xcb\xd8\xb0\x5e\x27\ +\xbc\xf6\x3a\x2a\xd7\x5e\x47\xcf\x5b\x7f\x81\xe5\x1d\x0f\x73\xfe\ +\xcf\xff\x98\x7c\xe1\x19\x9c\x73\x6b\xf2\x59\x6b\xc9\xf3\x5c\x80\ +\xc0\x5c\x89\x95\x55\xa7\x68\x07\x5c\xc7\x97\xad\x18\x50\x57\x10\ +\x5d\x37\x41\x72\xc3\x75\x2f\xe7\xaa\xc5\x10\x6d\x1b\xc7\x0c\x6f\ +\xc4\xb5\x42\xdc\xac\x43\x97\x94\xa0\xcf\x63\x84\x8b\xa1\x48\x15\ +\x17\xac\x11\x5c\xa8\xb5\x74\x1a\xab\x74\x9a\xab\x14\xad\x16\xea\ +\xdc\x9a\x60\xc1\xe6\xad\x84\xef\xb8\x07\x19\xdb\x82\xe9\x0d\xb0\ +\xf3\x8a\x9d\x71\xe4\x8b\x0e\xdb\xd1\x4b\x18\x63\xe7\x1c\xce\x59\ +\x5c\x79\xff\x7a\x65\xf4\xbd\xfd\x5d\xf4\xbe\xfd\x97\x09\x7a\x36\ +\x78\x17\x5e\xc7\x30\x77\x3f\xc3\x4b\x14\x50\x5e\x20\x02\x12\x81\ +\x89\x7d\xfc\x56\x0b\x2e\xcb\x49\xb6\x6c\x27\xdc\x76\xed\x95\xcb\ +\xca\x8d\x63\x98\xf1\xab\xe1\xe4\x0b\x04\xd9\xaa\x0f\x57\x6d\xa0\ +\xe2\x93\x1f\xb2\x92\xef\x2a\x85\x6c\x2e\x2d\x70\xe0\xef\xef\xc7\ +\x39\x4b\xda\x3b\xc0\xc6\x57\xdd\xcc\xf0\xc4\x35\xc4\x95\x2a\x12\ +\x04\x04\x9b\xae\xc6\x8d\xbf\x0a\xf7\x9d\x93\x04\xbd\x0e\x75\x86\ +\xa2\x66\x20\x94\xf5\x9c\x39\xf3\xa7\x4f\x32\x7f\xfa\x14\xce\x39\ +\xea\xc3\x1b\xe8\xdf\x3c\x4a\xb5\x7f\x00\x63\x0c\x26\xad\x30\xf0\ +\xcb\xef\x66\xe1\xe1\x87\x20\x0a\xd7\x94\xbc\x5e\x11\x21\x50\x6a\ +\xd1\xa1\x5a\x2a\xc3\xf8\x8c\x4b\x82\x92\xd4\xb4\x8a\xd3\x80\x78\ +\x6c\x2b\xe9\xd8\x38\x00\xce\x5a\xb2\x56\x8b\xb8\x52\xc1\x04\x01\ +\xe1\xc6\x51\xc2\xf1\x71\x8a\x81\x3a\x2c\xad\x22\x2a\xb0\x02\xb4\ +\x15\x6d\xe3\x51\x7e\x9d\xa1\xe5\x8b\x0b\x5c\xf8\xca\xe7\x20\xcf\ +\x28\xf2\x80\xe5\xdb\x6f\xc3\xdc\xf3\x51\x46\x5e\xfd\x7a\x7f\x41\ +\x94\x90\x0f\x6e\x01\x35\x44\x69\xc9\x2b\x24\xc6\x87\xdb\x35\x4b\ +\x75\x4c\x7f\xeb\x29\x0e\x7f\xf9\xf3\x34\xe6\xe6\x49\x37\x6f\x63\ +\xeb\x3f\x7f\x07\xd7\xbf\xf3\x9d\xf4\x8f\x6c\x46\x44\xa8\x4c\x5c\ +\x43\xb4\x6d\x2b\x54\x52\xaf\x00\xbd\x68\x39\x6b\x0a\x58\xf3\xff\ +\x2e\x07\x6b\x3d\x45\xe5\x72\xc5\x00\x06\x87\xd9\x34\x42\xb2\x75\ +\x1b\x51\x5f\x3f\x00\x59\xb3\xc9\xd9\x03\xcf\x33\x34\x31\x41\x7d\ +\xc3\x55\x44\x3d\x75\xc2\x4d\xe3\x14\xb5\x21\xf4\xdc\x39\x1c\xea\ +\x79\x81\x58\x7c\x9c\x0f\x29\x99\x62\xaf\x87\x48\x0b\xb6\x15\x33\ +\x24\x45\x46\x73\x36\x23\x7b\x21\xc6\x4e\x4f\x52\xdc\xf4\x3a\x42\ +\xe3\x73\x80\x4e\x1c\x11\x05\xa0\x2d\x87\x16\x0e\xd7\xe3\xc0\xae\ +\x77\x57\x25\x9a\x9b\xa3\xff\xe8\x11\xa2\xc9\xb3\x74\x4e\x9c\xe2\ +\x82\x38\xe6\xc6\xc7\xe8\x1f\xd9\xbc\xce\xa5\xc6\xd0\xb4\xba\x26\ +\x63\x17\x07\x5e\xa6\x80\xf5\x2c\xb5\xb3\x0a\xae\x54\x88\x58\xc2\ +\xed\xdb\x09\xb7\x8c\x23\x71\xe4\x15\xb0\xba\xc2\xe4\x53\x4f\x42\ +\x91\x53\xe9\xeb\x27\x4a\x53\xcc\xc6\x51\x18\x1e\xc3\x1d\x3a\x84\ +\xb1\x0e\x8d\xd5\xc7\x65\x2b\x10\xc0\x3a\x92\x9b\x00\x65\xb4\x2a\ +\xd4\x9c\x21\x6f\xa5\xac\xf6\x0e\x10\x26\xd5\x35\x23\x51\x55\x5a\ +\x8b\xe7\x09\x70\xb8\x0e\x3e\xee\x77\x4a\xee\xb0\x0b\xa2\x0a\x3d\ +\x61\xc4\xd5\xd5\x1a\x59\xb5\xce\xaa\x16\x64\xe7\x4f\x53\x4c\x9d\ +\x59\x7b\x86\x88\xe0\x7a\xfa\x21\x8a\xbc\x60\xe5\xf1\x4b\x14\xd0\ +\x45\xc8\xf5\x3c\x91\x04\xa5\x0b\x38\xc1\x59\x88\xc6\x27\x08\x47\ +\x46\x51\x13\xe0\x54\xc9\x97\x16\x59\xd9\xfb\x14\x0b\x3d\x3d\x8c\ +\xdc\xf2\x33\x84\x69\x8a\x8c\x8c\x22\xdb\x26\xd0\xef\x7d\x1d\x69\ +\xb4\xd0\x1c\x5c\x53\x71\x6d\xc5\x15\x0e\x67\x1d\x0a\x38\x55\x88\ +\x12\x64\xe2\x67\xb0\x6a\x91\xb1\x0a\xb5\x5b\xdf\x88\xd9\x3a\x81\ +\x31\x82\x05\x5a\x17\xe6\x68\x1f\x3c\x44\x3d\x2b\x90\x44\x20\x14\ +\x24\x16\x54\xbc\x1a\x7d\xa4\x72\x88\x58\xc2\xc8\x4b\xd2\x53\x80\ +\x86\x29\xf5\x4a\xa5\xbc\xc6\x21\x41\x40\x3b\xeb\x90\xd8\xe2\xe2\ +\x04\x5c\xee\x02\x97\xad\xbf\x79\x0c\x08\x40\x4a\x53\xd4\xb0\x4a\ +\x3c\xba\x95\x70\x70\x08\x44\xc8\xda\x6d\x9a\x67\x27\x89\x8e\xbd\ +\x48\x3e\x36\x81\x6d\xb6\x70\x7d\xfd\x04\x57\x6d\x24\xd8\xba\x15\ +\xdb\x57\x41\x8a\x16\x7a\x41\xb1\x40\xd1\xf6\x4a\x50\xa7\x9e\xf7\ +\xb7\x05\x66\xe8\x2a\xa2\x8f\xfd\x67\x54\x14\x89\xab\x04\xbd\x7d\ +\x48\xad\x8e\x5a\xcb\xca\xec\x0c\xd3\x5f\xdf\x05\x07\x8f\x20\xb9\ +\x85\xbe\xb2\x68\x4a\x14\x0d\xfd\x33\x9c\x3a\x54\x1d\x84\x01\x54\ +\x62\x4c\x5f\x42\xa0\x75\xa2\x89\x9b\xa8\x4e\x5c\x8b\x2d\x31\xc2\ +\x04\x01\xad\xf3\xd3\x04\xad\x66\xb9\x18\xa3\x2f\x07\xc1\xae\x39\ +\xac\x73\x2d\x9c\x53\x9c\x2a\x62\x95\x60\xeb\x16\xc2\xb1\x51\x24\ +\x4d\x51\xa0\x39\x7f\x81\xd9\xfd\xcf\x30\x94\x77\x60\xf2\x0c\x8d\ +\xd9\xf3\x04\x83\x83\x04\x49\x42\xb0\x61\x33\xb2\x61\x0b\x6e\x66\ +\x1e\x50\x82\x01\xc1\x34\x58\xe3\xf1\x1c\xe0\xac\x83\x30\x46\xae\ +\xbe\xc6\x27\x42\xea\xf1\x41\x8c\xa1\xbd\xba\xca\xf4\xb3\x7b\x39\ +\xf5\xe0\xff\xe5\x3a\xbd\x80\x29\x7c\x14\xc2\xe9\x1a\x9b\xec\x95\ +\xe8\xad\x36\x1c\xd9\x0c\x37\xdd\x8a\x1b\x6f\x52\x7f\xcd\xcf\x52\ +\xb9\xeb\xe7\x89\xb7\x4d\xe0\xb2\x0c\x55\xa5\x68\xb7\x29\x26\x27\ +\xa1\xd9\xf2\x18\x54\x0a\x7e\x65\x17\xd0\x8b\x20\x68\xdb\x8a\x69\ +\x3b\x50\x25\xda\xba\x0d\xd9\xb4\x19\x17\x45\x38\xa0\x7d\xe1\x02\ +\x17\xf6\x7c\x9b\xcd\x0d\xa1\x68\x2e\xb0\x78\xe2\x08\xc9\xe8\x18\ +\x95\x38\xc1\xf6\x5f\x45\x67\xf8\x7a\xcc\xdc\xb3\xa4\x83\xa1\x1f\ +\xac\x51\x48\x1c\x4e\x1c\xb6\x34\xdd\x35\xb0\x59\xf3\x39\x1f\x59\ +\x82\xb4\xc2\xf8\xeb\xdf\xc8\x55\xed\x0f\x23\x5f\xf8\x33\x68\xcf\ +\x52\xcc\x5a\xb2\x8e\x23\x37\x8e\xb8\x65\x4b\x25\xfa\xc5\x82\x9e\ +\x9f\x7b\x27\xbc\xe5\x17\x3c\x28\x18\x03\x81\x41\xad\xc3\x8a\xa0\ +\x45\xc1\x91\x5d\x4f\xd2\x38\x72\x9c\x81\xac\xc0\x18\xb3\x86\x77\ +\x97\xb8\xc0\xe5\x18\x20\xe2\xab\x3b\x89\x05\xd4\x10\x8f\x6d\x25\ +\xda\x30\x82\x44\x31\xce\x29\xc5\xfc\x2c\xee\xe0\x77\x08\x22\x47\ +\xfb\xdc\x0c\xed\x63\x47\xb0\xaf\xb9\x0d\xed\x1b\x20\xd8\xb8\x91\ +\xe8\xd5\x37\x52\x3c\xea\x17\x43\x24\x16\x6c\xc7\x2b\xd4\x59\xb7\ +\x06\x60\x9d\x95\x65\x26\x9f\xfd\x36\xea\x2c\x51\xa5\x4e\xff\xf8\ +\x38\xd5\xa1\x61\xc2\x38\x26\xe8\x1d\xa0\xf2\xc6\xb7\xd2\x39\x7d\ +\x92\xe2\xc4\x17\xa0\xa7\x8d\xa9\x0b\x52\xf7\x96\xa4\xeb\x73\x97\ +\x20\x44\xa2\xe8\xb2\xfc\x52\xb1\x79\xce\xd2\xd4\x24\xa7\xbf\xfa\ +\x65\xc2\xe9\xd3\x04\x5d\x8e\xed\x95\x30\xe0\x12\x96\x5c\x00\xe3\ +\x57\x70\x64\x68\x98\x68\xcb\x38\xa6\x56\x03\x11\x3a\x8d\x06\xf9\ +\xf2\x12\x7d\x7d\xbd\x44\x1b\x37\x12\x65\x06\x1a\x6d\x5c\x27\xc3\ +\xda\x02\x53\xeb\x21\xbe\x7a\x2b\xf9\x55\x43\xd8\xe3\x17\x10\xe3\ +\x28\x5a\xea\xa9\x2c\xab\xde\x77\x81\xe6\xcc\x0c\x2f\x7c\xf2\x0f\ +\x21\xcf\x50\x0c\xf5\x1b\xaf\x63\xfc\x17\xee\x66\xec\xb6\x37\x51\ +\x19\x18\x42\xeb\xfd\xd8\x57\xbd\x91\x9c\xfb\x89\xa2\x32\x22\x45\ +\x0a\x81\x4f\xd5\xbb\x02\xd8\xa2\x83\x2d\x0a\xbf\x8a\x64\x1d\x45\ +\x96\xd1\x59\x5d\xe1\xc2\xb1\x63\x9c\x7a\xf4\x61\xec\xd3\xbb\xa9\ +\xb5\x57\x09\x4c\x77\x61\x56\x5e\x9e\x09\x5e\xb4\x80\x75\x28\x59\ +\x28\xce\x7a\x1f\x0b\xb6\x6c\x81\x24\xc6\x15\x05\x36\xcf\x48\xc7\ +\xc6\xd9\xfc\xaf\x7f\x97\x38\x8a\x89\x73\x07\xa3\x63\x98\x4a\x15\ +\x9b\xe7\xbe\x2c\xee\x1f\xc6\x4e\xdc\x84\x9c\xd8\x45\x18\x19\x5f\ +\x4c\x19\x8f\x29\xea\xbc\x1b\x98\xbc\xcd\xe8\x85\x63\xc4\x79\x46\ +\xb6\x6c\x69\xce\x4f\xb3\xda\xd3\x4b\x63\x6c\x82\xb4\x7f\x08\x15\ +\x41\xae\x9e\xc0\xd6\x6b\x48\x63\x99\x22\xf7\x69\xb0\x2b\xdc\xda\ +\x78\x9d\x73\x9c\x3f\x74\x88\xb9\x43\x07\xc9\x5b\x2d\x6c\x9e\x93\ +\xaf\x2c\xd1\x3a\x7f\x8e\xd6\x91\xa3\xd8\x17\x0f\xb0\xa1\xb5\x42\ +\x54\xb8\x4b\x00\xee\xfb\x24\x42\xeb\xf2\x80\x02\xac\x1a\xe2\x91\ +\x51\xc2\xcd\xa3\x10\x46\xd8\xa2\xc0\x88\x50\xdd\xb2\x15\x19\xbb\ +\x1a\x05\x12\xca\x04\x27\x08\x70\x85\xf7\x4b\x57\xef\x87\x9b\x6e\ +\xc3\xee\x7c\x82\x30\x0d\xc1\x81\x0b\x40\x71\x6b\xb3\x17\x18\xd8\ +\x36\x54\xa7\x52\x74\xc8\x8d\x63\x31\xcb\xe9\xcc\xce\xe2\x96\x97\ +\x70\x28\x2a\x42\xd0\xdf\x4f\xde\xd7\x0f\x0b\xe7\xb0\x1d\x87\x6d\ +\x2b\x36\x2f\x05\x50\xc5\x15\x05\x8b\xcf\x7d\x97\xe9\x7f\xf8\x32\ +\xed\xb9\x79\x04\x07\x59\x0b\x5d\x5a\x22\x5e\x5c\x66\xd0\x2a\xf5\ +\x28\xf4\x39\x84\xbb\x58\x80\xfe\xc0\x3c\x40\x7d\x11\x08\x95\x0a\ +\xd1\x96\x71\xc2\x81\x41\xd4\x18\x70\x0e\x09\x43\x4c\x9c\xbc\xac\ +\x1c\x53\xeb\x50\x67\xb1\x45\x8e\xd4\xea\x44\x37\xdc\x4c\x3b\x48\ +\x08\xda\x4a\xd1\x76\xe5\xcc\x95\xc5\x8b\xfa\xff\xf3\x95\x8c\xb0\ +\xc8\x71\x6d\x4b\xb5\x56\x23\x19\x1e\x24\xa9\x56\xb1\x85\x2d\x81\ +\x52\x68\x4b\x40\x50\xae\xab\xab\x28\xaa\xae\x4c\xdd\xbd\x35\x85\ +\xf3\xb3\xf4\x9f\x3c\x44\x7e\x76\x06\x11\x43\x10\x08\x91\x18\xe2\ +\x20\xc0\x04\x1e\x17\xf2\x2b\xb4\xdd\x7c\xff\x3c\x40\x14\x8d\x04\ +\x19\x1e\x24\x9a\x98\x40\x92\xc4\x87\x94\x4e\x87\x4e\xa3\x41\xd1\ +\x69\xaf\xa5\xb5\xdd\x94\x2c\x8c\x63\xa2\x6a\x95\x30\x4e\x90\x28\ +\x26\xda\xb8\x89\xc6\xa6\xab\x29\xa6\x8f\x53\x34\x2c\xb6\xe3\x13\ +\x21\xd7\x15\x40\x05\x1b\x0d\x92\x4b\x86\xf4\x0b\xe6\xc6\xed\x04\ +\xaf\x7d\x1d\x66\xc3\x46\x5c\x91\xfb\x59\x6a\xb7\x69\xcd\x5c\xa0\ +\x12\x58\xbf\x86\x10\x2b\x1a\x5c\x54\xa2\xaa\xa3\x16\x1b\x92\x9e\ +\x14\xd7\xaa\x50\xb4\xf0\x8a\x2e\x93\x2d\x4f\x9f\xfb\xda\x06\xb9\ +\x72\xdf\xd1\x95\xf3\x00\xa7\x38\x0c\x3a\x38\x4c\xb0\x6d\x3b\x1a\ +\x04\x88\x31\xcc\x1d\x3f\xc6\x81\xaf\x7c\x89\x33\xdf\xde\x43\x10\ +\x45\x65\xd7\x12\x60\x2c\xc3\x37\xdc\xc4\x0d\xef\xf8\x45\x46\x6f\ +\x7d\x0d\x61\x10\xe2\x92\x94\xe2\xf5\xb7\x11\xee\x38\xe9\x31\x40\ +\xf4\x62\xd1\x55\x14\x04\x83\x43\xa4\xbf\xf5\x7b\x04\xce\x41\x5a\ +\xc7\x8c\x8f\x23\xc3\x1b\x21\xad\xe0\xac\xc5\xda\x82\xe5\xa9\xd3\ +\x98\x95\x65\x30\x0e\xeb\xc0\xe6\x65\x1e\xe0\x1c\xea\x9c\x27\x69\ +\x0a\xbf\xe4\x6e\x33\xc5\xa9\x40\xd0\x5d\x49\x2e\x6b\x1a\x51\xac\ +\x5b\x17\x71\xbb\xe4\xc9\xf7\x77\x01\xc5\x3a\x43\x30\xb8\x91\x74\ +\xfc\x6a\x08\x02\xd4\x18\x96\x8f\x1d\x41\x9f\xfe\x06\x63\x07\x5e\ +\x24\x4a\x22\x44\xfc\xb2\x95\x09\x1c\x79\x7b\x85\xec\xba\xeb\xc9\ +\xae\xbb\x1e\x93\xa6\x90\x26\xc4\x6f\xbc\x03\xbb\xf3\x7e\x9c\x71\ +\x68\x54\x9a\xb0\x75\xa8\x75\x50\xad\x11\xfe\xec\x5d\x17\xa3\x4f\ +\x18\xfa\x38\x0e\x20\x86\xce\xf2\x32\x87\xbe\xf4\x79\x36\x6b\x93\ +\xce\xbc\xc5\x15\x96\x3c\xb5\xd8\xce\xc5\x38\xae\xce\xe1\xda\x8e\ +\x7c\xd9\xe1\x56\x1d\x92\x1a\x24\x2e\x59\x6a\x05\x9b\xf9\x88\x63\ +\xf3\x2e\xea\xcb\x95\x5d\xe0\x65\xc5\x90\x31\xc8\x60\x1f\xf1\xab\ +\x6e\x24\xac\x54\x10\x63\xc8\x0b\x8b\x9e\x3a\x4d\xcf\xd9\x29\x7a\ +\x13\x25\xaa\x94\xa1\xa9\x80\x30\x14\x16\xcf\x4e\xc1\xd4\x24\xb6\ +\xd5\xf2\x1d\x40\x71\x42\xe5\xda\xeb\x59\xee\x1d\xc0\x2e\x2c\x63\ +\x33\x8b\x77\xeb\x92\x08\x15\x83\x24\xe9\x25\x21\x49\xfd\x09\x16\ +\xcf\x4e\x73\x7a\xc7\x43\x98\x3d\xbb\x30\x2b\x6d\xcf\x1d\x5a\x45\ +\x52\xf5\x33\x2c\xdd\xe7\x08\x9a\x80\x54\xbc\x7b\x38\xa7\x90\x7b\ +\xf3\x77\xae\x24\x72\xe4\xe5\xed\x38\x3f\x44\x31\x24\xb8\x34\xa5\ +\x55\xad\x32\x73\xf2\x24\x26\x8c\x68\x2c\xcc\xb3\x7a\xf0\x30\x49\ +\xab\x45\x1c\x19\x8c\x94\x99\x65\x01\xd2\x86\xb8\xdd\x24\x3f\x7a\ +\x8c\xb9\x17\x0f\xb0\xda\x69\x21\x4e\x61\x65\x85\x4e\xcf\x26\x8a\ +\xce\x24\xed\xc5\x06\x8b\xd3\x53\xe4\x27\x8e\xe1\xda\xed\x75\xf8\ +\xe1\xf3\xfa\xac\xb9\x42\x67\x71\x81\xa5\x93\x27\x59\xdc\xbf\x1f\ +\x79\xe1\x79\xc6\x16\xe6\x30\x56\x3d\x00\x2b\x58\x2d\x58\x99\x9f\ +\xc5\x1e\x3d\x8a\xeb\xb4\x21\x2f\xe8\x2c\x2c\x80\x2d\x20\x04\xd7\ +\x71\xd8\xac\xeb\x6e\xde\xfc\xdd\xba\xe8\xe6\x09\xce\x2b\x58\x00\ +\x5d\x98\x28\x19\x91\x70\x60\x40\x64\xe3\x26\x8e\x7c\xf9\x4b\xb4\ +\x1f\x7f\x1c\x4c\x80\x74\x9a\x0c\x9c\x99\x64\x28\x2f\x70\xa1\xff\ +\x91\xf2\x79\x88\x40\x12\x05\x2c\xbc\xf0\x1c\x53\x4b\x0b\x74\x7a\ +\x7b\x11\x20\xca\x2d\xa3\x33\xa7\x49\x43\xa1\x79\xfa\x04\x67\xff\ +\xf6\xaf\x29\x1e\xec\x87\x52\xfb\xdd\x2e\x10\xcd\x14\x8a\x0e\xd2\ +\x69\x22\xf3\xf3\xc4\x73\xb3\x0c\xda\x0e\x3d\x71\x0c\x45\xd7\x94\ +\x85\xf6\xec\x3c\xb3\x0f\xfd\x03\x2b\xcf\x7c\x1b\xb5\x16\x93\x2b\ +\x23\x33\x53\xf4\x2c\x37\x90\x42\x71\xca\x25\xd5\xa2\x07\x42\xa8\ +\xde\x7c\x23\xe1\xe0\x20\x65\x25\x24\xeb\xfa\xb5\xbc\x02\xf2\x3c\ +\x6f\x34\x9b\xcd\x22\xcb\x32\xc2\x38\x96\x74\xeb\x36\xbd\xea\x97\ +\x7e\x59\x2e\x7c\xee\xaf\x59\x7d\x7a\x0f\x0a\x54\x42\x88\xd2\x18\ +\x09\x23\x0a\x27\x18\xd5\x72\x61\x04\x34\x12\x82\x28\x20\xba\x30\ +\x43\x38\x79\x86\x5c\x9d\x27\x22\x82\x80\x70\xb8\x4a\x14\x85\x54\ +\x96\x2e\xd0\x3a\x3b\x8d\x5a\xbb\x56\x6e\xfb\x16\x19\xd0\xa6\x12\ +\x8a\xef\x30\xa9\x06\x21\xbd\x69\x48\xad\xc7\x87\xda\xa2\xed\x28\ +\xda\xe5\xd2\x57\xd6\x24\x98\x79\x1e\xf3\xbd\x67\xfc\xac\xb6\x15\ +\x4d\x13\x5c\x12\x43\xee\x85\x27\xf4\x5c\x86\x53\x87\x4a\x40\x65\ +\xfb\x35\x6c\xb8\xe7\x5e\xd2\x6d\x13\x8a\x73\x92\x65\x19\xcd\x66\ +\xb3\xc8\xf3\xbc\xd1\x55\x80\xce\xcc\xcc\xbc\x34\x39\x39\x79\x32\ +\x4d\xd3\x9e\x8d\x41\x10\x56\x47\x46\xd8\xf4\xde\xf7\x91\x0a\xcc\ +\xdd\x77\x1f\xed\x93\xc7\x11\x57\x78\x9a\x1c\xc5\xaa\x2f\x35\x05\ +\xf1\x09\x46\xee\x4d\xa1\x1e\x84\xf4\xf4\x44\x7e\xcd\x4f\x7d\x4d\ +\x61\xc4\x40\xa6\xf4\x07\x01\x83\x3d\x35\x24\xe8\x22\xb2\x10\x94\ +\x3d\x9e\x36\x2a\x01\xca\x74\x13\x16\xf5\xb3\x2e\x42\xd1\x2d\xd2\ +\x14\x12\x23\x6c\xae\xa4\x8c\x06\x09\xaa\x50\xc4\xd6\x27\x6d\x65\ +\x5f\xa1\xba\x6e\x06\xeb\xd0\x20\x22\xdd\x3a\xc1\xf0\xfb\x3e\x40\ +\xff\xaf\xbc\x07\xed\x1f\xa0\xd9\x6a\x32\x3b\x3b\x5b\x4c\x4e\x4e\ +\x9e\x9c\x99\x99\x79\x09\x9f\x9f\x91\x2c\x2d\x2d\x35\x92\x24\x89\ +\xa3\x28\xda\x98\x26\xc9\x50\xa5\x52\x91\xa0\x56\xd3\xca\xb6\x09\ +\x31\x81\xa1\x3d\x39\x45\xb1\xb4\x84\x2b\xac\xaf\x0f\x42\x5f\x57\ +\x28\xfe\x07\xd5\xe1\x5b\x5b\x04\x24\x12\x4c\x59\x9b\x14\x6d\x8f\ +\xc2\x9a\x79\xbe\x91\x08\x24\x36\x28\x50\xb4\x7c\x32\x53\xe4\x3e\ +\xbb\x33\x69\x49\x78\xa8\xc3\xe5\x8a\xcd\x1d\xd6\x2a\x2e\x73\xbe\ +\x49\x2a\x2a\xc1\xab\xbc\xde\x5a\x1f\x02\x25\x04\x82\x2e\xcd\xa5\ +\xd8\xc2\x41\x10\x92\x5e\xbd\x9d\x0d\xf7\xdc\x43\xff\x7b\xde\x83\ +\xeb\xeb\xd3\x4e\xa7\x23\x67\xcf\x9e\xd5\x43\x87\x0e\x1d\x79\xe6\ +\x99\x67\x1e\x7c\xec\xb1\xc7\x1e\x9b\x9d\x9d\x9d\x0f\x80\x60\x76\ +\x76\x36\x3f\x72\xe4\xc8\x5c\xa5\x52\x21\x8a\xa2\x91\x38\x8e\x07\ +\x7b\x7a\x7b\xc5\x54\x2a\x5a\x9d\xb8\x06\x11\x95\xec\xec\x34\xf9\ +\xc2\x22\x58\x77\x91\xdb\x2b\x57\x6a\x25\x10\x14\xf1\x40\xda\xf5\ +\x41\xeb\x97\xb3\x25\x64\xcd\x27\x11\x0f\x33\x2e\x2f\x41\x28\xea\ +\xc6\x64\x5d\x2b\xd5\xbb\x35\x88\x1a\x01\x51\x6c\xc1\x9a\x65\xd8\ +\xdc\xb7\xda\x69\xd9\x57\xe1\x4d\x1d\xd4\x09\xb6\xcc\x0f\x30\x01\ +\x95\x6d\x13\x0c\xbf\xff\xfd\x0c\xbe\xf7\xfd\xea\x7a\xfb\xc8\xad\ +\x95\xd3\xa7\x4e\x71\xe0\xc0\x81\x63\xdf\xfc\xe6\x37\x1f\xb8\xef\ +\xbe\xfb\x1e\x3a\x7d\xfa\xf4\x19\xa0\x15\x74\xd7\x67\x5a\xad\x56\ +\x7e\xf4\xe8\xd1\xf3\x49\x92\xb4\xe2\x38\xde\x14\xc7\xf1\x50\x5f\ +\x7f\x3f\x52\x49\xa9\xdd\x70\xa3\x18\x55\x3a\x93\xa7\xc9\xe6\x2e\ +\xac\x09\xae\x56\x3d\x6d\x66\xc0\x18\xf5\x1d\x23\xb9\x4f\x42\x5c\ +\xee\xdb\xe2\x24\x34\xe0\xb3\x68\x5c\xae\x14\x1d\x3f\x53\x12\x97\ +\x4d\x51\x52\x0a\x97\x95\x25\x73\xb9\x0e\x21\x21\x68\xd9\x03\x50\ +\x74\x1c\x45\xae\xb8\x52\x19\x84\x5d\xc1\xfd\x33\xbb\x33\xef\xd4\ +\x51\xd9\x3e\xc1\x86\x0f\x7c\x90\xc1\x0f\x7e\x08\x5b\xaf\x53\x58\ +\xcb\x89\xe3\xc7\xe5\xb9\xe7\x9e\x3b\xf2\x8d\x6f\x7c\xe3\x8b\x5f\ +\xf9\xca\x57\xbe\xb6\xb4\xb4\x34\x89\xe7\xac\x9b\xdd\x06\x09\x07\ +\xd8\x2c\xcb\xf2\x53\xa7\x4e\xcd\x85\x61\xb8\x9c\xa6\xe9\x58\x1c\ +\xc7\x43\xbd\xfd\xfd\x22\x49\xa2\x3d\x37\xbf\x1a\x51\xa4\x79\xf4\ +\x18\xf9\xfc\x05\x4c\x14\x96\x83\xd7\xb5\x19\x52\xe7\x85\x92\xd0\ +\xcf\x9e\x2b\xd4\x77\x7f\xba\x8b\x33\x29\x51\x49\x4b\x59\x20\xf4\ +\x94\x9b\xed\xf8\xf5\x3d\xc2\x12\xa8\x9d\xef\x1d\x76\x40\xd1\xb4\ +\xde\x52\x02\x59\x8b\xef\x5d\x58\x28\x0a\x8f\xf2\x2a\x8a\xed\x74\ +\x48\xc7\xaf\x66\xe4\xd7\x3f\xca\xf0\xaf\xff\xba\x16\x69\x4a\x6e\ +\xad\x74\x85\x7f\xea\xa9\xa7\xfe\xe6\x91\x47\x1e\xd9\xb9\xb2\xb2\ +\x32\x09\x2c\x02\x2d\xc0\x76\x2d\xc0\x89\x88\x05\x6c\xa7\xd3\xc9\ +\xce\x9f\x3f\xbf\x14\xc7\xf1\x6a\x9a\xa6\x9b\x2a\x69\x3a\x54\xad\ +\xd5\x24\xa8\x56\xa5\x76\xdd\x75\x1a\x55\x2a\xd2\x38\x74\x90\x6c\ +\x61\xbe\x64\x5f\x14\xe7\x04\x6b\x3d\xe5\x2c\xeb\x92\x0f\x97\x97\ +\xa0\x54\xb6\xbe\x11\x78\x41\xbd\x39\x7b\x8e\xc0\xe6\x1e\xb4\x24\ +\xf4\x1c\x84\x73\x4a\x51\x5c\xea\xeb\x5d\xc5\x38\xf5\xee\x51\xe4\ +\xba\x66\xf2\x8a\x62\xdb\x1d\xe2\xd1\x51\xc6\x7e\xf3\xb7\x19\xba\ +\xf7\x83\x6a\x6b\x35\x69\xb7\x5a\x32\x79\xe6\x0c\xcf\x3f\xff\xfc\ +\xe1\xa7\x9f\x7e\xfa\x0b\x3b\x76\xec\xd8\xbd\xb8\xb8\x78\x1a\x58\ +\x14\x91\x06\x90\xe3\xf9\x95\x8b\x15\x40\xa9\x04\xd7\x6a\xb5\x3a\ +\x33\x33\x33\x0b\x95\x4a\x25\x8b\xa2\x68\x43\x25\x4d\xfb\xd2\x38\ +\x0e\xe2\xa1\x21\xa9\x6c\xbd\x5a\xa3\x9e\x1e\x69\x1e\x3d\x4a\xbe\ +\xb0\xe0\x99\xf3\xb2\x5f\x68\xad\xde\xee\x22\xb2\x2b\xc1\xf2\x22\ +\xc3\xee\xf9\x46\xeb\xcf\xa9\x78\xcc\x70\x76\x5d\xfc\xb6\x25\x06\ +\x94\x4f\xb2\xa5\x85\x75\x85\x2f\xd7\x36\x7c\xc1\xe3\x7c\x5a\x9d\ +\x8e\x5f\xcd\x96\xdf\xfa\x1d\x06\xde\xfb\x5e\x65\x68\x48\x56\x17\ +\x17\x99\x9a\x9a\xca\x9f\x7f\xfe\xf9\x23\x7b\xf7\xee\xfd\xf2\xe3\ +\x8f\x3f\xfe\xc4\xdc\xdc\xdc\xa9\xae\xf0\xaa\x9a\x77\xd3\xb0\xe0\ +\xb2\x2c\x71\x4d\x09\x8d\x46\x23\x9b\x99\x99\xb9\x50\xa9\x54\x5c\ +\x1c\xc7\x43\x95\x4a\xa5\x2f\x0a\x82\x20\x1d\x1a\x96\xea\xf6\x6b\ +\x34\x88\x22\x69\x9d\x3e\x43\xbe\xbc\xe4\xeb\x70\xfc\xb4\xbb\x6e\ +\x58\x2a\x0f\xad\x37\x79\xe7\x74\x4d\x40\xa4\x34\x79\xf1\xdc\x83\ +\xb3\x9e\x2c\xed\x0a\x4f\xe8\xc3\xa2\x2d\x13\x1c\x57\x86\x38\xd5\ +\x12\x14\x71\x38\x0c\x95\xad\xdb\xd9\xfc\xd1\x7f\xc9\xd0\x3d\x1f\ +\x54\x06\x87\x64\xf9\xc2\x1c\x67\xcf\x9e\xcd\x0e\x1c\x38\x70\x74\ +\xdf\xbe\x7d\x0f\x3e\xf6\xd8\x63\x8f\x9e\x3f\x7f\xfe\xf4\x95\x84\ +\xbf\x92\x02\xba\x4a\x70\x7e\xb5\x79\xb5\x33\x37\x37\x37\x97\x24\ +\x89\x89\xe3\x78\x30\x4d\xd3\xbe\x24\x0c\x83\xa8\xb7\x57\x7a\x6e\ +\x7c\x95\xaa\x75\xd2\x99\x9e\x26\x5f\x5a\x42\x0b\xeb\x05\x2f\x53\ +\x4f\x5f\x72\x7a\x25\x38\x57\xe6\xfb\xee\x22\x25\xdd\x6d\x8b\x77\ +\x0e\x4f\x94\x96\xd7\xbb\x2e\x75\x8e\x77\x07\x6b\x75\xcd\x9a\xdc\ +\xc5\xe5\x6d\x34\x88\xa8\x6c\x9b\x60\xe4\xde\x0f\xb2\xf1\xc3\x1f\ +\x51\x5b\xab\xc9\xca\xc2\x02\x53\x53\x53\xd9\x0b\x2f\xbc\x70\x74\ +\xef\xde\xbd\x5f\xdb\xb1\x63\xc7\x23\xd3\xd3\xd3\xa7\x80\x05\x11\ +\x69\x5e\x2e\xfc\x2b\x29\xa0\x9b\xa5\x5b\xc0\x2e\x2f\x2f\xb7\x66\ +\x66\x66\xe6\xa2\x28\x92\x28\x8a\x86\xd2\x34\x1d\xa8\x56\xab\xc6\ +\x24\x09\xf5\x1b\x6f\x14\xa3\x8e\xf6\xe4\x14\xd9\xe2\x92\x6f\x50\ +\x30\xbe\x27\x00\x29\x73\x83\x12\xb8\x5c\xb9\xe6\xe8\x43\xa6\x9f\ +\x59\xd5\x75\x16\x51\xce\xb8\x67\x7a\xdc\xda\x71\x67\x3d\x76\x78\ +\x32\x44\xb1\xd6\xc7\xf9\xca\xc4\x04\x9b\xee\xb9\x97\x0d\xbf\xf6\ +\x6b\xd8\x34\xa5\xd5\x6c\xca\x99\x33\x67\x8a\x03\x07\x0e\x1c\xd9\ +\xb3\x67\xcf\x23\x3b\x76\xec\x78\x78\x6a\x6a\xea\x78\x09\x78\x0d\ +\xdf\xe5\xf0\xf2\xb6\x82\xef\xa7\x00\xd7\x7d\x7f\x61\x65\x65\xa5\ +\x31\x37\x37\x37\x17\x86\xa1\x0b\x82\x60\x43\x9a\xa6\xc3\x7d\x7d\ +\x7d\x62\x92\x44\xeb\xd7\x5e\x87\xc1\x48\x6b\x72\x92\xce\xc2\xfc\ +\x1a\xf3\xeb\x5c\x59\xfa\xae\x95\x18\xac\x2d\xbe\x6a\xd7\x15\xd6\ +\x7a\x11\xbc\xc7\x3b\x55\x6c\xd7\x55\xba\xee\xd4\xcd\xe9\x9d\x17\ +\x5e\x4d\x40\x75\xfb\xb5\x6c\xbe\xf7\x57\xd9\x70\xcf\x3d\xea\x6a\ +\x35\xf2\xa2\x90\x13\x27\x4e\xf0\xbd\xef\x7d\xef\xf0\xde\xbd\x7b\ +\xbf\xba\x73\xe7\xce\x47\xa6\xa7\xa7\x8f\x77\x43\x5d\x17\xf0\xae\ +\xf8\x1a\xda\x0f\x68\xa7\xef\x2a\xc1\xae\xac\xac\xb4\x2e\x5c\xb8\ +\x30\x63\x8c\xe9\xa8\xea\xc6\x30\x0c\x87\x87\x86\x86\x90\x34\xa5\ +\xb6\x7d\xbb\x04\x41\x40\x7b\x6a\x8a\xf6\xcc\x9c\x4f\x56\xba\xb4\ +\x9a\xf1\x4d\x53\x97\x08\xee\xca\xc6\xa6\xf2\xb5\x99\xae\x95\x58\ +\xab\x17\x5d\x48\x64\xcd\x0d\x9c\xfa\x64\xc9\x39\xa8\x5f\x77\x3d\ +\xa3\xbf\xfa\x21\x36\xbc\xef\x7d\x68\x6f\x2f\x85\xb5\x1c\x3c\x78\ +\x50\x9e\x7d\xf6\xd9\x43\xfb\xf6\xed\xbb\x7f\xf7\xee\xdd\x5f\x9b\ +\x9e\x9e\x3e\x09\x2c\x95\xc2\x67\xaf\x24\xfc\x0f\xa3\x80\xcb\x95\ +\xd0\x39\x7f\xfe\xfc\x9c\x31\x66\x25\x08\x82\xd1\x20\x08\x86\x07\ +\x07\x07\x25\xa8\xd5\xb4\xb6\x7d\xbb\x04\x61\x48\xeb\xc4\x71\x1a\ +\x8b\x8b\xd8\xf2\xfd\x87\xc2\x2a\x85\xf3\xbb\x75\x17\xbf\x17\xd6\ +\xef\xd6\x2a\x79\x79\x6e\xfd\xf9\xbc\x3c\x5f\xa8\x5f\x10\xb6\x40\ +\xef\xb6\xad\x6c\xf9\xc8\x47\xd9\xf0\x81\xf7\xa3\x7d\xfd\x9a\xe5\ +\xb9\xbc\xf8\xe2\x8b\xb2\x7f\xff\xfe\xc3\x7b\xf6\xec\xf9\xdb\x5d\ +\xbb\x76\x3d\xd1\x05\xbc\x32\xce\xe7\xdf\x4f\xf8\x1f\xf6\x8d\x11\ +\x05\x0a\x11\x69\xa9\x2a\x73\x73\x73\x6e\xf7\xee\xdd\x3b\xad\xb5\ +\xd6\x39\xf7\x11\xe0\xba\xeb\x6f\xb8\x41\xaa\x03\x03\x3a\xfc\x9e\ +\xf7\x30\x74\xfb\xed\xe2\x96\x97\xaf\xdc\x8f\x2f\xeb\x8f\xe8\x2b\ +\x74\xee\xeb\x25\x2f\x0d\x5d\xb2\x64\xd9\xdb\x8b\x6c\xde\xac\x32\ +\x30\x40\xab\xd9\x92\x43\x07\x0f\xf2\xec\xb3\xcf\x1e\xde\xb3\x67\ +\xcf\x67\xbf\xf1\x8d\x6f\xec\x9e\x9f\x9f\x3f\x0d\x2c\x97\x63\x2d\ +\x7e\x90\xf0\x3f\xca\x2b\x33\x4e\x55\x73\x11\xdf\x4c\x36\x3f\x3f\ +\x7f\x6a\xe7\xce\x9d\x4f\x1a\x63\x22\x11\xf9\xd5\x30\x0c\xaf\xd9\ +\xba\x6d\x9b\xe9\xdd\xb8\x91\x78\x7c\x5c\x83\x20\x90\x4b\x49\xd3\ +\xf5\x0b\x37\x3f\x68\x4c\xf2\xf2\x37\xad\x4a\xaa\xda\x5a\xab\x59\ +\xa7\x23\xcb\x8b\x8b\x9c\x3c\x71\xc2\xed\xdf\xbf\xff\xe8\xbe\x7d\ +\xfb\x3e\xbf\x73\xe7\xce\x27\x57\x57\x57\xcf\xfc\xa8\xc2\xff\xa8\ +\xaf\xce\xaa\xaa\xae\x59\xc2\xea\xea\xea\xe4\xe3\x8f\x3f\xfe\x68\ +\x10\x04\x49\x18\x86\xef\x06\xae\x19\xdb\xb2\x25\x4c\xd3\x6e\x91\ +\xaf\xfc\x54\x37\xaf\x50\x69\xb7\xdb\x4c\x9e\x39\x53\xec\xdf\xbf\ +\xff\xe8\x33\xcf\x3c\xf3\x95\xc7\x1f\x7f\xfc\xd1\x46\xa3\x31\xf5\ +\xe3\x08\xff\xe3\xbc\x3b\xac\xa5\x25\xa0\xaa\xd2\x68\x34\xd8\xb5\ +\x6b\xd7\x83\xc6\x18\x03\xbc\x73\x69\x69\x69\x6b\x4f\x4f\x4f\xa8\ +\xfa\xd3\x96\x7e\x6d\xcd\x52\x56\x56\x56\x8a\xe3\xc7\x8f\x9f\xdc\ +\xb7\x6f\xdf\x23\xbb\x76\xed\x7a\xf0\x32\xe1\xf3\x7f\x92\x77\x87\ +\xcb\x2d\x06\x52\xa0\x77\x6c\x6c\x6c\xeb\xeb\x5e\xf7\xba\x3b\xfb\ +\xfb\xfb\x5f\x15\x04\x41\xdd\x39\xa7\x3f\xe1\xb3\xaf\xa8\x7c\x63\ +\x8c\x58\x6b\x57\x17\x17\x17\x5f\xfc\xce\x77\xbe\xb3\x7b\x72\x72\ +\xf2\x24\xb0\x8c\x6f\xc7\xca\xfe\xc9\x5e\x9e\x5e\x77\x6f\x58\xf6\ +\x81\xd5\x81\x1e\xa0\x56\x2a\x26\x58\xf7\x2e\xe4\x4f\x45\xf8\x75\ +\xc9\x59\x56\x26\x36\x2b\xc0\x6a\x89\xf6\x3f\x92\xd9\xaf\xdf\xfe\ +\x1f\xba\xc9\xa0\xf1\xd3\x43\x85\x2f\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x02\x73\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\ +\x01\x42\x28\x9b\x78\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ +\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x01\xf0\x49\x44\ +\x41\x54\x38\x8d\xa5\x92\x4f\x6b\x1a\x61\x10\xc6\x7f\xae\x8a\x55\ +\x8a\xed\xcd\x20\x14\xb2\x87\x88\x87\x42\x4d\x3c\x49\x3d\xec\x25\ +\x64\x03\x7b\xc9\xc1\xdc\x3c\xf5\xb0\xb7\xc6\x9b\x5f\x20\x1f\x40\ +\xea\xb9\x9f\xc0\xe0\xa9\x37\x21\x14\x12\x59\x58\xd8\x84\x42\xa1\ +\xe0\x69\x89\x60\x4c\x2f\x49\xb5\xa8\xc4\x3f\xd3\x43\xde\xb5\x26\ +\x48\x28\x74\xe0\x85\x79\x67\x9e\x79\x98\x99\x67\x42\x22\xc2\xff\ +\x58\xe4\x99\x5c\x1a\x78\xa9\xfc\xdf\x40\x6f\x2d\x4a\x44\x56\xdf\ +\x2b\x11\xd1\x45\x24\x6d\x59\xd6\x01\x70\x03\xdc\x58\x96\x75\x20\ +\x22\x69\x11\xd9\x54\x98\x65\xcd\xa3\x62\xdb\xb6\x77\x43\xa1\x90\ +\x97\x4a\xa5\x8e\x63\xb1\xd8\x09\x70\x09\x5c\xc6\x62\xb1\x93\x54\ +\x2a\x75\x1c\x0e\x87\x1d\xdb\xb6\x77\x57\x49\x42\x2b\x3b\xd8\x8c\ +\x46\xa3\x9f\x67\xb3\x59\x52\xb5\x3e\x01\xc6\x2a\x17\x07\x5e\x00\ +\xa3\x48\x24\x72\x37\x9d\x4e\x3f\x00\x3e\x80\xb6\x32\xcd\x7d\xb9\ +\x5c\xfe\x02\x0c\x81\x6b\xe0\x56\x91\x4c\x94\x7f\x0d\xfc\x52\x98\ +\xfb\xa7\x4b\x4c\x97\x4a\xa5\x9d\x56\xab\xb5\xa5\x0a\x48\x26\x93\ +\x77\x95\x4a\xe5\x1b\x40\xad\x56\x7b\x37\x18\x0c\x5e\x03\x34\x9b\ +\xcd\xad\xe1\x70\xb8\xd3\x68\x34\x00\x7a\xc1\xfc\x19\xe0\x02\x68\ +\x01\x4d\xa0\x59\xad\x56\x8f\x44\x64\x5b\x44\xb6\xab\xd5\xea\x51\ +\x10\x57\x98\x0b\x11\xc9\x88\xc8\x5f\x19\x35\x4d\xfb\xb1\x58\x2c\ +\x42\xc1\xbf\xdd\x6e\x0f\x80\xfe\x8a\x3f\x51\xa9\x89\xa6\x69\x3f\ +\x97\x75\x81\xce\xae\xeb\x9e\x19\x86\xd1\x09\xe6\x76\x1c\x67\x5f\ +\xd7\xf5\x43\x5d\xd7\x0f\x1d\xc7\xd9\x0f\xe2\x86\x61\x74\x5c\xd7\ +\x3d\x53\xb7\xb1\x24\xe8\xe5\xf3\xf9\xd3\x6c\x36\x7b\x15\x00\xe7\ +\xf3\xf9\xc0\xf7\xfd\x3d\xdf\xf7\xf7\xe6\xf3\x79\xd0\xc1\x24\x9b\ +\xcd\x5e\xe5\xf3\xf9\x53\xd4\x61\x3d\x92\x31\x91\x48\x7c\x1c\x8f\ +\x03\xe5\xd6\x5b\x3c\x1e\x67\x34\x1a\x7d\x62\x8d\x8c\xb7\x9e\xe7\ +\xb9\xb9\x5c\xae\x5f\xaf\xd7\x3b\xa6\x69\x76\x79\xb8\x83\xb1\x69\ +\x9a\xdd\x7a\xbd\xde\xc9\xe5\x72\x7d\xcf\xf3\x5c\x1e\x64\xe5\x69\ +\x07\x00\x1b\xc0\x7b\xa0\xeb\x38\x4e\xba\x58\x2c\xbe\x05\x38\x3f\ +\x3f\xff\x5e\x28\x14\x7a\xc0\x1b\xa0\x1d\x2c\x77\x1d\xc1\xaa\x65\ +\x00\x43\xf9\x5f\x81\xce\x3a\xd0\x73\x04\xff\x64\x7f\x00\x19\x1e\ +\x09\x5d\x0e\xd9\xff\x6d\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\ +\x60\x82\ +\x00\x00\x02\x4a\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\ +\x01\x42\x28\x9b\x78\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ +\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x01\xc7\x49\x44\ +\x41\x54\x38\x8d\xa5\x93\xcd\x6a\x1a\x61\x14\x86\x1f\x33\x0e\x05\ +\x4d\x47\xda\x88\x64\x53\x14\x83\x43\x76\xc6\x95\x4c\x5d\xb9\x33\ +\x05\x17\xdd\x64\x99\x45\x2e\x20\xbd\x82\x76\x51\x7a\x03\xbd\x82\ +\x76\x93\x95\x0c\x82\x8b\xe2\x76\x32\xe8\x6a\x04\x85\x04\xa2\xd0\ +\xa2\x14\x84\x80\xc5\x9f\x5a\xb5\xf0\xa5\xa7\x1b\x2d\x93\xa1\x29\ +\x14\x0f\x9c\xcd\x39\xe7\x79\x39\x3f\xdf\x17\x12\x11\xb6\xb1\x9d\ +\xad\xe8\x07\x04\x12\x40\x1a\x88\xfa\x62\x51\xe0\x60\x9d\xfb\xa7\ +\xc0\x7e\x3e\x9f\x3f\xd6\x75\xfd\xa3\x6d\xdb\xe6\x1a\x8c\xda\xb6\ +\x6d\xea\xba\xfe\x21\x9f\xcf\x1f\x03\xfb\xf7\x08\x11\xd9\x78\xc2\ +\xb2\xac\x53\xa0\x0b\x5c\x69\x9a\x76\x59\xad\x56\x8f\xaa\xd5\xea\ +\x91\xa6\x69\x97\xc0\x15\xd0\xb5\x2c\xeb\x54\x44\x12\x1b\x2e\xe4\ +\x5b\x62\x3a\x95\x4a\xbd\x1f\x0c\x06\x4f\x00\x01\x1e\x85\xc3\xe1\ +\x39\x80\x52\x6a\x17\xf8\x09\x84\x92\xc9\xe4\xb8\xdf\xef\xbf\x02\ +\xbe\x04\x47\xb8\xf5\x3c\xef\x22\x93\xc9\x5c\x03\x0b\x60\xac\x94\ +\xd2\x94\x52\x1a\x30\x06\x16\x99\x4c\xe6\xda\xf3\xbc\x0b\xe0\x76\ +\x03\x85\x02\x67\x34\x46\xa3\xd1\x8b\x42\xa1\xf0\xbc\xd7\xeb\x3d\ +\xf3\x27\x4c\xd3\xfc\xda\x68\x34\x9a\xf1\x78\xfc\x13\x30\x7b\x68\ +\x89\x77\xad\x56\xab\xb7\x5c\x2e\xf7\xd6\x5d\xfc\xf1\xd5\x6a\xf5\ +\xb4\xdd\x6e\x77\x81\x3b\x3f\xe0\xef\x20\x5a\xaf\xd7\x0f\xca\xe5\ +\xf2\x6b\xa5\x94\x02\xe6\x01\xf1\x5d\x5d\xd7\xb5\x5a\xad\xf6\xae\ +\x54\x2a\x7d\x06\x7e\x04\x05\xd2\xb1\x58\xec\xcd\x6c\x36\xfb\x05\ +\x7c\x07\xc8\x66\xb3\xdf\x00\x3a\x9d\xce\xde\xba\xe6\xb1\x61\x18\ +\x3b\xd3\xe9\xf4\xed\xdf\x96\x38\xaf\x54\x2a\xcd\x48\x24\x32\x04\ +\x16\xb9\x5c\x6e\xe8\x38\xce\x8d\xe3\x38\x37\xb9\x5c\x6e\x08\x2c\ +\x22\x91\xc8\xb0\x52\xa9\x34\xef\x75\xe7\x7b\x07\x88\xc8\xa1\xeb\ +\xba\x67\xc5\x62\xf1\x7c\x32\x99\x9c\x88\x88\x21\x22\xc6\x64\x32\ +\x39\x29\x16\x8b\xe7\xae\xeb\x9e\x89\xc8\xa1\x9f\x09\x0a\x20\x22\ +\xa6\x88\xbc\x5c\xc3\x9b\x98\xb1\x8e\x99\xc1\xfa\xe0\x19\xff\xdb\ +\xb6\xfe\x8d\xbf\x01\x7c\x52\x08\x38\x8e\xd7\x1d\xc8\x00\x00\x00\ +\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x07\x84\ +\x47\ +\x49\x46\x38\x39\x61\x36\x00\x37\x00\xf3\x00\x00\xff\xff\xff\x00\ +\x00\x00\x78\x78\x78\x1c\x1c\x1c\x0e\x0e\x0e\xd8\xd8\xd8\x54\x54\ +\x54\xdc\xdc\xdc\xc4\xc4\xc4\x48\x48\x48\x8a\x8a\x8a\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x21\xff\x0b\x4e\ +\x45\x54\x53\x43\x41\x50\x45\x32\x2e\x30\x03\x01\x00\x00\x00\x21\ +\xfe\x1a\x43\x72\x65\x61\x74\x65\x64\x20\x77\x69\x74\x68\x20\x61\ +\x6a\x61\x78\x6c\x6f\x61\x64\x2e\x69\x6e\x66\x6f\x00\x21\xf9\x04\ +\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\x00\x36\x00\x37\x00\x00\x04\ +\xcc\x10\xc8\x49\xab\xbd\x38\xeb\xcd\xbb\xff\x60\x28\x8e\x64\x69\ +\x9e\x25\x92\x10\x44\x82\xa0\xa7\xc2\xce\x84\x02\x93\x08\x4d\xbf\ +\x77\xb8\xea\xac\x44\x2f\x04\xa4\x0d\x41\xc5\xd9\xf1\xf3\x03\x0a\ +\x97\x9d\x5c\x91\x77\x2c\x18\x58\x86\x02\x46\xa6\xb3\x2d\x0b\x03\ +\xda\x40\x7b\x51\x05\xa9\x16\xc1\x60\x20\x18\x5d\x75\x06\x93\x80\ +\xd6\x26\x16\x4d\xe1\xd9\x40\x94\xc4\x8b\x45\x6f\x34\x71\x25\x73\ +\x33\x75\x20\x07\x79\x2c\x03\x07\x27\x6a\x6c\x24\x07\x6f\x06\x8d\ +\x50\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x1c\x8f\x87\x96\xa1\x1a\ +\x85\x2c\xa2\x47\xa6\x04\xa8\x14\x8a\x04\x7b\x96\xae\xb0\x17\xb2\ +\x97\xb5\x18\xaa\xac\x3d\xb9\xa5\x6b\xba\x43\xa4\x9f\xc2\xc3\xc4\ +\xc5\xc6\xc7\xc8\x9b\xc1\x8e\xbe\x24\xbc\x72\x74\x23\xb7\x25\xd3\ +\x20\xd5\xd2\x7f\x22\xcf\x84\xd1\x23\xcb\x72\xcd\xc9\x9e\xdf\xc5\ +\xdb\xc5\xd7\xc3\xe8\xc2\xe6\xe5\xe1\xe2\xef\x3d\x11\x00\x21\xf9\ +\x04\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\x00\x36\x00\x37\x00\x00\ +\x04\xce\x10\xc8\x49\xab\xbd\x38\xeb\xcd\xbb\xff\x60\x28\x8e\x64\ +\x69\x9e\x68\xaa\x16\x06\x41\x18\x85\x5a\x16\x83\xeb\x0e\xb1\x2c\ +\xb6\xb6\x6b\xe8\xa2\x5e\x0f\x18\x12\xda\x88\x20\x5e\xef\x87\xf4\ +\x1c\x6a\xb6\xc1\xe1\x82\x48\xb8\x12\x88\xa6\xe4\xc0\x33\x4c\x2d\ +\x0a\xa3\x62\x24\x18\x0c\x04\x26\x84\xd1\x95\x05\x09\x7a\x68\x92\ +\x75\x9d\x08\x41\x6f\xa5\xf5\x11\x74\x27\x0c\xf2\x7a\x04\x21\x6f\ +\x36\x71\x23\x73\x46\x75\x83\x66\x86\x23\x6a\x6b\x6d\x5a\x1a\x61\ +\x42\x63\x92\x1c\x55\x57\x91\x97\x9c\x9d\x9e\x9f\xa0\xa1\x6e\x8c\ +\x97\x65\x67\x1a\x84\x2e\x8d\x40\xa9\x04\xab\x14\x7d\x7f\x4d\xb1\ +\x19\xb4\xb3\x3d\xb2\x17\xad\xaf\x32\xbb\xa8\xa4\x92\xa6\xbc\xa2\ +\xc4\xc5\xc6\xc7\xc8\xc9\xca\x27\xc2\x29\xcd\x64\x70\xcc\xd1\x22\ +\xb6\x25\xd5\x7c\xb8\x27\xd7\x1f\xbe\x26\xdd\x22\xcf\xcc\xc0\xcb\ +\x9e\xe1\xc7\xdf\xc6\xdb\xc4\xea\xa2\xe8\xc6\xe6\xe4\xf1\x27\x11\ +\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\x00\x36\x00\ +\x37\x00\x00\x04\xce\x10\xc8\x49\xab\xbd\x38\xeb\xcd\xbb\xff\x60\ +\x28\x8e\x64\x69\x9e\x68\xaa\xae\xac\x30\x0c\x02\x5b\x0a\x44\x4d\ +\xc4\xb2\x38\xd8\xc4\x90\xeb\x3c\xdf\x0f\x44\xb3\xe1\x86\x1f\x17\ +\x0c\x99\x2b\x18\x6a\x86\x02\x49\x79\x24\x15\x76\xb6\x81\x34\x54\ +\xac\x55\x45\x4f\x1e\xc1\x00\xcc\x9a\xc4\xb6\x72\x4d\x48\x42\xd7\ +\x44\xdd\x9b\x29\xcc\x23\xc3\x5f\x5f\xd1\x01\xbb\x3e\x30\x3b\x07\ +\x61\x06\x7e\x7f\x85\x86\x87\x88\x89\x8a\x12\x08\x09\x35\x09\x08\ +\x8b\x15\x0a\x68\x0a\x19\x54\x43\x08\x6e\x04\x91\x16\x71\x79\x29\ +\x8e\x6e\x09\x17\x7c\x3d\x3f\x9b\x6f\x16\xa6\x6c\x2c\xa9\x04\x17\ +\x9f\x3f\xa2\x68\xa4\xb1\x78\x99\x9b\x9d\x92\x94\x62\x96\x92\x13\ +\x8d\x8f\xbb\xc0\xc5\xc6\xc7\xc8\xc9\x13\x98\x28\xcc\x70\x3c\xa0\ +\x44\xd0\x23\xac\x27\xd5\x6a\xa7\x26\xd7\x5c\xd3\x26\xb2\x23\xce\ +\xde\xb8\xca\x88\xe1\xc5\xdf\xc6\xdb\xc5\xea\xc0\xe8\xc6\xe6\xe4\ +\xf1\x26\x11\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\ +\x00\x36\x00\x37\x00\x00\x04\xcd\x10\xc8\x49\xab\xbd\x38\xeb\xcd\ +\xbb\xff\x60\x28\x8e\x64\x69\x9e\x68\xaa\xae\xac\x30\x0c\x02\x5b\ +\x0a\x44\x4d\xc4\xb2\x38\xd8\xc4\x90\xeb\x3c\xdf\x0f\x44\xb3\xe1\ +\x86\x1f\x17\x0c\xc9\x34\x29\x8f\xce\x17\x94\xc8\x9b\x8a\x8a\x35\ +\x6b\x67\x67\x13\x96\xb8\x35\x2f\x08\xdc\x3b\x91\xc5\xc9\xea\x09\ +\x7b\x23\x3d\x53\xef\xa6\x7c\x4e\xaf\xdb\xef\xf8\xbc\xaa\x60\xa8\ +\x19\x0a\x1a\x71\x3f\x05\x67\x80\x17\x6c\x5a\x29\x7d\x3c\x04\x06\ +\x18\x67\x43\x8c\x36\x8f\x41\x91\x92\x04\x18\x88\x43\x8b\x3c\x8e\ +\x99\x52\x48\x07\x67\x07\x7a\x13\x07\x8b\x06\xa4\xa5\xab\xac\xad\ +\xae\x27\x08\x09\x35\x09\x08\xac\x0a\x92\x0a\x23\x82\x23\x08\x97\ +\x04\xb5\x54\x46\x25\xb2\x97\x09\x21\x90\x24\xbe\x35\xc7\x95\xc9\ +\xca\x21\x9a\x24\xc4\x92\xc6\xd0\xa0\x25\xbd\x97\xc0\x79\xb7\x8c\ +\xb9\x48\xbb\x15\xb1\xb3\xdb\x3f\xd1\xab\xc8\xe8\xcd\xab\xe7\xec\ +\xd7\xaf\xf0\x2b\x11\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\ +\x00\x00\x00\x36\x00\x37\x00\x00\x04\xcb\x10\xc8\x49\xab\xbd\x38\ +\xeb\xcd\xbb\xff\x60\x28\x8e\x64\x69\x9e\x68\xaa\xae\xac\x30\x0c\ +\x02\x5b\x0a\x44\x4d\xc4\xb2\x38\xd8\xc4\x90\xeb\x3c\xdf\x0f\x44\ +\xb3\xe1\x86\x1f\x17\x0c\xc9\x34\x29\x8f\xce\x17\x94\xc8\x9b\x8a\ +\x8a\x35\x6b\x67\x67\x13\x96\xb8\x35\x2f\x08\xdc\x3b\x91\xc5\xc9\ +\xea\x09\x7b\x23\x3d\x53\xef\xa6\x7c\x4e\xaf\xdb\xef\xf8\x3c\x3e\ +\x8e\xe4\x5f\xd8\x5a\x2a\x80\x19\x67\x48\x85\x18\x87\x3f\x89\x16\ +\x83\x43\x8d\x18\x7e\x3f\x91\x7a\x94\x95\x96\x97\x98\x99\x4c\x05\ +\x06\x35\x06\x05\x6e\x52\x27\x05\x67\xa0\x21\x8f\x23\x9d\x3c\x04\ +\x06\x40\x5d\x26\xab\x36\xae\x61\xb0\xb1\x04\x57\x6a\x25\xaa\x3c\ +\xad\x57\xa2\x19\x08\x09\x35\x09\x08\x17\x07\x67\x07\x43\x0a\xb1\ +\x0a\xc6\xaa\x06\xc9\x3f\x08\xb6\x04\xc5\x79\xc2\xb6\x09\x7a\xd4\ +\x35\xdb\xdc\x7a\xd8\xb1\xda\x79\xd3\xb6\xd6\x79\xcb\xab\xcd\x95\ +\xc1\xc3\xe7\x22\x11\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\ +\x00\x00\x00\x36\x00\x37\x00\x00\x04\xcc\x10\xc8\x49\xab\xbd\x38\ +\xeb\xcd\xbb\xff\x60\x28\x8e\x64\x69\x9e\x68\xaa\xae\xac\x30\x0c\ +\x02\x5b\x0a\x44\x4d\xc4\xb2\x38\xd8\xc4\x90\xeb\x3c\xdf\x0f\x44\ +\xb3\xe1\x86\x1f\x17\x0c\xc9\x34\x29\x8f\xce\x17\x94\xc8\x9b\x8a\ +\x8a\x35\x6b\x67\x67\x13\x96\xb8\x35\x2f\x08\xdc\x3b\x91\xc5\xc9\ +\xea\x09\x7b\x23\x3d\x53\xef\xa6\x7c\x4e\xaf\xdb\xef\xf8\x3c\x3e\ +\x8e\xe4\x5f\xd8\x5a\x2a\x80\x19\x67\x48\x85\x18\x87\x3f\x89\x16\ +\x83\x43\x8d\x18\x7e\x3f\x91\x7a\x94\x95\x96\x97\x98\x00\x08\x09\ +\x35\x09\x08\x96\x0a\x3c\x35\x0a\x94\x08\xa2\x36\x9f\x33\x52\x22\ +\x9c\xa7\x04\x09\x33\x6a\x20\xae\x36\x5f\x41\x21\xb4\x35\xb6\x5d\ +\x21\xad\xa7\xb0\x6e\xb2\x1f\xa6\xae\xa9\x6e\xab\x22\xa1\xa2\xa4\ +\x17\x05\x06\x35\x06\x05\x48\x9b\x9d\xc6\x15\x05\x67\xd2\x94\xcf\ +\xa2\x06\x95\xb4\xdf\xae\x95\xdc\x3c\xde\x94\x07\x67\x07\x96\x07\ +\xdc\x06\xea\x99\x28\x11\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\ +\x00\x00\x00\x00\x36\x00\x37\x00\x00\x04\xcc\x10\xc8\x49\xab\xbd\ +\x38\xeb\xcd\xbb\xff\x60\x28\x8e\x64\x69\x9e\x68\xaa\xae\xac\x30\ +\x0c\x02\x5b\x0a\x44\x4d\xc4\xb2\x38\xd8\xc4\x90\xeb\x3c\xdf\x0f\ +\x44\xb3\xe1\x86\x1f\x17\x0c\xc9\x34\x29\x8f\xce\x17\x94\xc8\x9b\ +\x8a\x8a\x35\x6b\x67\x67\x13\x96\xb8\x35\x2f\x08\xdc\x3b\x91\xc5\ +\xc9\xea\x09\x7b\x23\x3d\x53\xef\xa6\x7c\x4e\xaf\xdb\xef\x22\x44\ +\xa2\x96\x40\xe0\x2b\x0a\x3c\x35\x0a\x7f\x12\x08\x82\x36\x7e\x3f\ +\x71\x16\x7b\x88\x04\x09\x8b\x6a\x16\x8f\x36\x3f\x67\x17\x95\x35\ +\x97\x41\x17\x8e\x88\x91\x39\x6c\x5a\x87\x8f\x8a\xa2\x52\x19\x81\ +\x82\x84\x85\x86\x8e\x7d\xae\xb2\xb3\xb4\xb5\x4c\x05\x06\x35\x06\ +\x05\xb3\x05\x67\xbc\x51\x4b\x22\xb9\x82\x06\x4e\x93\x20\x95\x26\ +\x98\x21\xca\x5f\x9d\x21\xc4\x3c\xc6\x33\xc8\x1f\x07\x67\x07\x6b\ +\xa9\x23\x07\xc4\x06\xda\xb6\x1d\x8c\xb2\xa3\xb4\xcc\xb2\xe8\xae\ +\xe6\xb4\xe4\xe2\xef\x27\x11\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\ +\x2c\x00\x00\x00\x00\x36\x00\x37\x00\x00\x04\xcc\x10\xc8\x49\xab\ +\xbd\x38\xeb\xcd\xbb\xff\x60\x28\x8e\x64\x69\x9e\x68\xaa\xae\xac\ +\x30\x0c\x02\x5b\x0a\x44\x4d\xc4\xb2\x38\xd8\xc4\x90\xeb\x3c\xdf\ +\x0f\x44\xb3\xe1\x7e\x88\x44\x2d\x81\xd0\xb8\x60\x43\x80\x82\x57\ +\x53\x44\x39\x08\xaa\xad\x39\x7b\x1d\x3f\x4a\x2d\x21\x31\xe3\x7d\ +\x3b\x62\x5b\x69\x67\x13\x7e\xd2\xb5\x75\x30\x14\xd6\x92\x49\xc5\ +\xda\x19\x9b\xe6\xe2\xbd\x23\x53\x54\x56\x57\x58\x61\x4c\x85\x89\ +\x8a\x8b\x8c\x8d\x57\x05\x06\x35\x06\x05\x8e\x16\x05\x6c\x35\x03\ +\x94\x43\x4f\x7b\x13\x91\x54\x06\x9c\x66\x18\x69\x43\x98\x3d\xa5\ +\x62\xa7\x73\x17\xa0\x3c\xa2\x3f\x79\x37\x18\x07\xa8\x03\x07\x51\ +\x9d\x1a\x07\xa0\x06\xb9\x95\xc1\xc2\xc3\xc4\xc5\xc6\xc7\x1d\xbb\ +\x28\xca\x22\xb3\x9e\x44\xa4\x40\x6d\x27\xb7\x23\xd5\x26\xd7\x21\ +\xce\x27\xdb\x23\xcc\x26\xdf\xc8\x89\xe1\xc2\xdd\xc4\xd9\xc3\xe8\ +\xe5\xd1\xc5\xe4\xe2\xef\x25\x11\x00\x3b\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\ +\x00\x00\x03\x16\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\ +\x01\x42\x28\x9b\x78\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ +\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x02\x93\x49\x44\ +\x41\x54\x38\x8d\x95\x91\xbd\x4f\x1b\x41\x10\xc5\x67\xe1\x96\xbb\ +\xf3\x25\xd8\x46\xe1\x82\x20\x91\x2c\x45\x38\x42\x51\x94\x02\x19\ +\x21\x04\x1d\xa2\xa3\x80\xa3\x30\x05\x25\x12\x15\x32\x14\x44\x48\ +\xfc\x05\x54\x96\x28\x29\x4c\x45\x0f\xae\xa0\xc0\x20\x0a\x7a\xa0\ +\x20\x46\x48\x24\x0e\x8e\xb1\x8d\xe3\x23\xb7\xc7\xde\xde\xee\x5e\ +\x8a\xc4\x16\x1f\x49\xc1\x2b\x67\xde\xfc\xf4\x66\x06\x65\xb3\xd9\ +\x81\x20\x08\x52\x08\xa1\x38\x3c\x43\x41\x10\xe4\x11\x42\x69\xc5\ +\xf7\xfd\x85\x91\x91\x91\x01\xc3\x30\xd4\xe7\x00\x08\x21\xd1\xc3\ +\xc3\xc3\x05\x45\x4a\x19\x07\x80\x6a\xbd\x5e\x77\x9f\x03\xc0\x18\ +\x87\xa4\x94\x71\x85\x52\x0a\x94\xd2\x27\xc3\xb9\x5c\x2e\xba\xb7\ +\xb7\xf7\x2a\x16\x8b\xb9\xc9\x64\xb2\x64\x18\x86\xb8\xdf\x17\x42\ +\xb8\x94\xd2\x76\x85\x73\x8e\x82\x20\x68\x36\x6a\xb5\x9a\x92\x4a\ +\xa5\x3e\x5e\x5c\x5c\x84\x55\x55\x85\x83\x83\x03\xd8\xd8\xd8\x78\ +\x37\x3f\x3f\xff\x65\x6a\x6a\xea\xfa\x3e\x84\x73\x8e\x14\xc6\x18\ +\xe2\x9c\x37\x8b\x2b\x2b\x2b\x1f\x6e\x6f\x6f\xdb\x97\x96\x96\x2a\ +\x63\x63\x63\x2e\x00\xc0\xc9\xc9\x49\x5b\x22\x91\x70\xe6\xe6\xe6\ +\x3e\x25\x93\xc9\x6f\xc3\xc3\xc3\x3f\x01\x00\x18\x63\xa8\x85\x73\ +\x8e\xa4\x94\x20\xa5\x84\xb3\xb3\x33\xa3\x5c\x2e\x47\xa7\xa7\xa7\ +\x6b\x96\x65\x5d\xa9\xaa\x5a\x50\x55\xf5\x6a\x68\x68\xe8\x47\x26\ +\x93\xe9\xa8\x56\xab\xd1\xf5\xf5\xf5\xf7\x9e\xe7\x21\x29\x25\x70\ +\xce\x51\x0b\x63\x0c\x49\x29\x61\x7b\x7b\xbb\x6b\x6b\x6b\xeb\xad\ +\x69\x9a\x80\x31\xf6\x8e\x8e\x8e\x24\x63\x8c\x33\xc6\xd8\xf1\xf1\ +\x31\xdf\xdd\xdd\x8d\x99\xa6\x09\x9a\xa6\x69\xe9\x74\x3a\x2e\xa5\ +\x6c\x26\x00\x29\x25\x10\x42\xc2\xf9\x7c\xbe\x4b\xd7\x75\xd8\xd9\ +\xd9\x31\x4d\xd3\x0c\x1a\xc9\x84\x10\x6d\xcb\xcb\xcb\x15\x5d\xd7\ +\x61\x76\x76\xf6\x7a\x7c\x7c\xdc\xfd\x9b\x00\x94\xc6\x0a\x33\x33\ +\x33\x25\x00\x78\x71\x7e\x7e\xfe\x72\x71\x71\xb1\x18\x8d\x46\x5d\ +\x29\x25\x00\x00\xf4\xf6\xf6\x3a\x99\x4c\x46\xd5\x75\x1d\xfa\xfa\ +\xfa\xca\xba\xae\x13\xc6\xd8\x9f\x23\x36\x00\xae\xeb\xda\x96\x65\ +\x95\x4e\x4f\x4f\xef\x22\x91\x48\x79\x6d\x6d\x2d\xde\xdf\xdf\x5f\ +\xc1\x18\xcb\x5c\x2e\xf7\xc6\x71\x9c\xc8\xc4\xc4\x44\x05\x63\x5c\ +\xa7\x94\x8a\xe6\x17\xee\x1f\x11\x63\x5c\x1e\x1c\x1c\xbc\xcb\x66\ +\xb3\x86\x6d\xdb\x1d\x9b\x9b\x9b\xaf\x01\x00\x4c\xd3\x04\xcb\xb2\ +\x2a\x89\x44\xa2\xe4\x38\x8e\x78\xf0\x46\xdf\xf7\x51\x23\xaa\xe7\ +\x79\xbe\xe7\x79\xb5\xd1\xd1\x51\x6d\x72\x72\xf2\xeb\xcd\xcd\x0d\ +\xb6\x6d\x5b\xe9\xe9\xe9\xf1\x11\x42\xbf\x6c\xdb\xae\x35\xbc\x00\ +\x00\xbe\xef\x3f\x04\x34\x44\x08\xa1\x84\x90\xef\x9a\xa6\x69\x9d\ +\x9d\x9d\xd8\xb6\xed\x3b\x21\x04\x87\x47\xf2\x7d\x1f\x29\x00\x70\ +\x59\x28\x14\xc2\xdd\xdd\xdd\xec\xb1\x81\x10\x42\x01\x80\x3e\xae\ +\x03\x00\x14\x8b\xc5\x36\x00\xb8\x54\x42\xa1\xd0\xea\xfe\xfe\xfe\ +\x67\x21\x44\xec\x5f\xc6\xff\xa9\xb5\xb5\xf5\x32\x1c\x0e\xaf\xfe\ +\x06\x47\x78\x84\x8a\xac\x27\x36\x0e\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x0b\x2d\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\xb0\x00\x00\x00\x7f\x08\x06\x00\x00\x00\x4f\x09\x48\x3f\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\ +\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x02\x18\ +\x13\x26\x10\x86\x4c\xd4\x8d\x00\x00\x0a\xad\x49\x44\x41\x54\x78\ +\xda\xed\x9d\x7d\x8c\x1d\x55\x19\xc6\x9f\xf7\x9d\x33\xf7\x63\xb7\ +\x1f\x6e\xb7\xdd\xb5\x95\xc6\xd6\xc0\xd6\xd6\x6a\xa2\x25\x01\x12\ +\x04\x34\x90\x28\x84\x10\x44\x4c\xd0\x04\x63\x24\x8a\x46\x08\x06\ +\x48\x34\x86\xc6\xe0\x3f\xc6\x36\x01\xb5\x2a\xa8\x21\x36\x18\x35\ +\x40\x88\x04\x0d\x92\x6a\x88\x46\x0b\x28\x51\x12\x15\x0a\xc5\x02\ +\x02\x02\x05\x04\xba\xed\xde\xbd\xf3\x71\x5e\xff\x98\x99\x7b\xe7\ +\xde\xdd\x6d\x31\xa1\xdd\x1e\xf2\xfc\x9a\xec\xbd\xf7\xcc\x99\x33\ +\x67\xde\xf3\xce\xdb\xe7\x9c\x39\x73\x46\xb0\xf9\x62\x4c\x24\xfb\ +\x44\x44\x60\x66\x80\x88\x08\x00\x03\x04\x66\x02\xa0\x9f\x56\xfd\ +\x06\xaa\x4f\xcc\xf7\xbb\xc8\x8d\xaa\x3c\xc0\xac\x97\x6e\xc5\x06\ +\xf4\x8e\x37\x1f\xe5\xfe\xa8\xf6\xaf\xbe\xd7\xb7\xcf\x53\xe6\x82\ +\xcc\x57\xc6\x82\x79\x01\x41\x59\x66\x45\x59\x0f\xe9\xfd\xb4\x81\ +\x73\xb5\x21\x23\xf4\xce\x1b\xd6\x2b\x4b\x16\x38\x9c\x1d\xa1\xfc\ +\x61\x9b\x0e\xec\x53\x1d\x7f\xc8\xce\xc3\x75\x9b\xb7\xdc\x5a\x79\ +\x83\xb6\x34\xc0\x8e\xd0\x1e\xc3\xed\x32\x4f\x59\x73\xea\x5c\xb5\ +\xf9\xe0\xf1\xac\x57\xa7\xa2\x5c\xab\x9f\xe2\xc0\xa7\x88\x95\x7e\ +\x69\xb5\xa6\x32\x37\x99\x3e\x29\x28\x8c\x20\x65\x21\x52\x5a\x45\ +\x00\x20\x95\x86\x66\x22\x6a\x88\xd4\x8b\xa8\x87\x8a\x8a\x89\xb7\ +\x5a\xed\x04\x52\x54\x4a\x6a\xc7\x95\xfe\x4f\xe9\xa7\x19\x0c\xfd\ +\x4b\x44\x06\x2f\x01\xab\xf6\x2e\x1a\xbe\xdf\xea\x65\xde\x5a\x9e\ +\x7e\x99\xd5\xc9\xd7\x8e\x5b\x6b\xea\x7e\x8a\x00\x62\xb5\xbc\xf5\ +\x63\x1e\xce\xc5\x16\xb8\x5c\x87\xeb\x62\xf3\xe7\x9f\x73\x2e\x75\ +\x3b\x49\xbf\x61\x07\x6c\x66\x47\xb0\x47\xb9\x7d\x4e\xba\x2c\x74\ +\x89\xc8\x3c\xdb\x0f\x93\xae\x35\x1b\x0d\xe5\x33\x18\x44\x8b\x8b\ +\xa6\x7e\xec\xb9\x6d\x36\x84\xce\x3d\xaf\xd2\x1d\xca\xcd\x66\x5e\ +\xc4\x14\xde\x14\xe6\x15\xde\x47\xe6\x7d\x6c\x89\x97\xca\xd3\x45\ +\xac\x76\xb1\x9b\x99\x89\x2b\x9d\x5e\xfa\xad\x5c\x34\x73\x2a\x0d\ +\x4d\xc5\xb9\x04\x91\xfb\xed\xce\x6f\xbf\xf3\xd1\xdd\x3f\xba\xb6\ +\x3d\x32\x3a\xd2\x6c\xb6\xdb\x52\xbf\xb4\x08\x79\x93\x30\x33\x9b\ +\xed\x76\x3a\x67\x9d\xf7\xe5\x1f\x3e\xfd\x9f\xfd\xcf\x9f\x7d\xe9\ +\x95\x4f\xc7\x12\x65\xb1\xcf\xb2\x06\x52\x6f\x43\xf1\x1c\x22\x26\ +\x13\x53\x5b\xb4\x8c\xb8\x10\x11\x4d\x10\x6b\x57\xe2\x28\x83\xc6\ +\x3b\xbe\xf0\xbe\xef\x2d\x5b\x36\xb6\x42\x55\xe9\xb0\xe4\x98\xe3\ +\xbd\xb7\x0f\x9e\x7b\xe5\x8e\xf5\x67\x7e\xfc\x4f\x0e\x79\xda\xb4\ +\x34\x8f\x91\x7a\x00\xbe\x94\xb4\xa5\x03\x03\x02\x11\x4d\xcc\x69\ +\xa6\xb1\xdb\x76\xdd\xd5\x93\xa3\x87\x1e\xdc\xde\x6a\xb6\xda\x34\ +\x23\x59\x6c\x66\xbb\xb3\x9d\x99\xd1\x53\xae\xb9\xfa\x1b\xdb\x5f\ +\x8c\x2d\xcb\x1a\x56\x38\x31\x44\x4c\xa5\x40\x53\xc4\x9a\x69\xec\ +\xb6\x6f\xbd\x66\xf5\xb2\xce\x43\x37\xd2\x79\xc9\xf1\x42\xab\xd9\ +\x6a\x2f\xed\x3c\x74\xe3\xf6\xad\xd7\xae\xce\x24\x76\xa9\x34\xb4\ +\x54\xd5\xe2\x00\x88\x99\x49\xaa\xce\x75\xd0\x6c\x8d\x4c\xdf\xbf\ +\x2d\x6e\xb5\xe3\x81\x50\x6e\xd2\x98\x9e\x7e\x6d\xb2\xd9\x1e\xcb\ +\x0c\x12\x19\x8c\x92\x82\xbc\xe9\x08\xc4\x04\x96\x9f\xb0\x6e\xf3\ +\xe3\xfb\x9f\xfd\xfb\xb8\x8a\x25\xd5\xb6\x38\x8e\xe3\xd1\xe9\xfb\ +\xb7\x75\xa4\x79\x19\x80\xd9\x18\x69\x2a\x80\x38\x03\x24\x97\x58\ +\x33\x44\xf1\xf7\x2f\xdf\xf4\x9d\x56\xab\xdd\xaa\xf7\x47\x73\x2f\ +\x23\x99\xd7\xb1\xa5\xcb\x27\x56\x9f\x73\xee\xa5\x2b\x68\x66\x72\ +\xb4\xd9\xfb\xc4\x63\xcb\x93\x5c\x0f\x39\xf5\xaf\x46\x6a\x33\x02\ +\x78\x00\x68\xb5\xda\xad\x7f\xed\xba\xf5\xb4\x77\x9f\x7d\xc9\x1f\ +\x33\x8b\x72\x27\x99\x39\x98\x49\xa2\xce\x25\x1a\x37\x96\x2f\x1f\ +\x1b\xab\x17\x94\x7b\x19\x49\x32\xff\x8e\xf3\x2f\xbc\x6c\x3d\xcd\ +\x4a\x8e\x15\x27\x9d\xb8\x61\xdd\x49\x27\x6e\xc0\x63\xfb\x9e\xb9\ +\xef\xa9\x7f\xde\x3b\xe2\xd4\x0e\x56\xdb\x1e\xd8\x75\xd3\x17\x13\ +\x8d\xff\xdc\x40\x96\xc5\x96\x7b\xcd\x24\x8e\x52\x38\x77\xfb\x0d\ +\x5b\xd7\xd5\x47\x1b\xbc\x49\x23\xf3\x3a\x76\xfe\x85\x9f\xa3\xf3\ +\x92\x45\x61\xc3\xbb\xd6\x7e\x68\x6c\xf5\xe6\xd4\x9b\x34\xaa\x34\ +\x55\x95\xdb\x6f\xd8\xba\x2e\x81\x73\xa9\x45\x91\xe6\xa2\x9a\x8b\ +\xc6\xfb\x1f\xb9\xed\xaa\x01\xe1\xbc\x64\x22\x1b\x1d\x5d\x3a\x49\ +\x33\x92\x45\x1d\x81\x38\xf0\xdc\xe6\x89\x13\xde\xfb\x4a\x3d\x6d\ +\x6a\xfd\xda\xc9\x1c\x1a\xe7\x1a\xa9\x7a\x88\x66\x88\xa2\xa9\x4d\ +\xa7\xed\xab\x67\x3a\xf0\xda\x4b\x93\x67\x9d\x7d\xf1\x4a\x9a\x90\ +\x2c\x26\x67\x7c\xf8\xa2\x55\xcf\x3e\xf5\x8f\xa9\x7a\xda\xfe\x27\ +\x77\x9f\x92\x69\x14\x79\xa8\xaa\x41\x15\xb0\xe8\xe5\x17\xf6\x6e\ +\xa9\x67\x32\x20\xa2\xf9\xc8\xf1\x80\x41\x06\x7c\xf1\xe5\x17\xf6\ +\x6e\x11\x20\x32\x88\x2a\x20\x0a\x68\x24\x73\x76\x02\x87\xca\xc8\ +\x71\xe2\xc0\x83\xc3\xb6\xe5\x34\xa9\xc8\x00\xd5\xb2\xcb\xa6\x34\ +\x13\x09\xcc\xad\x15\x10\xa8\x07\x94\x93\x73\x48\x88\xba\xc2\x60\ +\x45\x04\x7e\x23\xd3\x64\x09\x39\x9e\x28\x42\xae\x40\x21\xe5\x3f\ +\x42\x42\x0a\xc0\x56\x4c\xfd\x55\x30\xfa\x92\x80\xd1\xaa\x57\x47\ +\x48\xb0\x0e\x4c\x48\xd0\x0e\xcc\x4e\x1c\x09\xaf\x17\x57\x8f\xc0\ +\xd4\x10\x24\xb8\x5e\x5c\xf1\x47\x8b\x47\xbf\x09\x09\x56\x42\x08\ +\x07\x22\x48\x78\x0a\xa2\x37\x0e\x4c\x05\x41\x42\x54\x10\xbd\xf5\ +\x24\x84\x9d\x38\x12\xb2\x84\x30\xce\x3b\x23\x81\x6b\x60\x42\x02\ +\x14\x11\x00\x00\x57\xfb\x4e\x48\x68\xbd\xb8\xea\x4e\x1c\x3d\x98\ +\x04\x18\x80\xcd\x2a\x07\xa6\x8c\x20\x21\x06\x60\xe1\x5c\x08\x12\ +\x68\x00\xee\x0d\xa3\x31\x00\x93\x80\x61\x04\x26\x41\x0b\x61\x65\ +\x1f\x8e\x04\xae\x81\xe9\xbd\x24\xc4\xd8\xdb\x93\x10\x14\xc0\x24\ +\xc0\x08\xdc\x77\x60\xa3\x86\x20\x21\x6b\x60\x01\x97\x85\x20\xa1\ +\xc6\x61\x3e\x52\x44\xc2\x8c\xbd\xe5\x2b\xbb\x78\x2b\x99\x84\x19\ +\x7b\xcb\xf7\xcd\x15\x13\xda\x29\x21\x48\x88\x11\x18\x06\x35\x18\ +\x25\x04\x09\x57\x03\x73\x55\x29\x12\xa8\xef\x02\x00\x6f\x64\x90\ +\xb0\xd1\xe2\x95\xb3\x34\x04\x09\x52\x04\x83\x8b\xfb\x91\xa0\x35\ +\x84\x82\x9d\x38\x12\xa8\xff\x5a\xff\x4e\x1c\xed\x41\x42\x93\x10\ +\x80\x54\xb3\xd1\x18\x81\x09\x3b\x71\x84\x1c\x4b\x05\x51\x5f\x9d\ +\x92\x01\x98\x04\xa7\x20\x7a\xcf\xc4\x71\x36\x25\x09\xb4\x13\xd7\ +\x7f\xa4\x88\x1e\x4c\x02\xec\xc4\xf5\x1f\xab\xa7\x08\x26\x61\x7a\ +\x30\x97\x57\x25\x41\x6b\x08\x4e\x68\x27\x61\x47\x61\x2e\x6c\x42\ +\xc2\x8c\xbf\x52\x88\x08\xbe\x23\x83\x04\x2d\x22\xf8\x58\x3d\x09\ +\x53\x3c\xf4\xd7\x46\xa3\x00\x26\x61\x4a\x88\x72\x18\x8d\x11\x98\ +\x84\xd8\x7d\x63\x04\x26\x6f\x01\x0f\xe6\x30\x1a\x09\xb6\x07\xd7\ +\x9b\x0f\x4c\x48\x88\x11\x58\x7a\x2f\x3a\xa4\x0f\x93\x00\x23\x30\ +\x80\x62\x5d\x08\x42\x82\xd6\xc0\x84\x84\x0a\x3b\x71\x24\x6c\x09\ +\xc1\x95\x79\x08\x25\x04\x21\xc7\x3a\x00\x0b\x1d\x98\xbc\x15\x34\ +\x30\x47\x21\x48\xb8\x2a\xc2\x4a\x0d\x4c\x19\x4c\x82\xec\xc7\x09\ +\x1f\x29\x22\x81\x46\x5f\xeb\xdd\x4a\xe6\xca\x3c\x24\xcc\x4e\x1c\ +\x27\xb4\x93\xe0\x45\x04\x47\x21\x48\xd0\xf0\xa1\x4e\x12\xac\x06\ +\xee\x3b\x30\x35\x30\x09\x50\x03\x17\x8f\xd5\x0b\x23\x30\x09\x35\ +\x02\x0b\x1c\xa3\x2f\x09\x5c\x03\xd3\x83\x49\xb0\x71\x98\xc3\x68\ +\x24\x54\x11\x5c\xfc\xe1\x30\x1a\x09\x35\xf8\xa2\xb6\x3e\x30\x21\ +\x21\x47\x60\xaa\x08\x12\x5c\x04\xae\xad\x0f\xcc\x7e\x1c\x09\x2e\ +\x00\x4b\x6d\x7d\x60\x42\xc2\x0c\xc0\x5c\x1f\x98\x04\x2e\x84\x29\ +\x21\x48\xc0\x9d\x38\x4a\x08\x12\xac\x86\xe8\x4d\x68\x27\x24\xcc\ +\x08\xcc\xb5\xd1\x48\xe8\x0a\x82\xaf\x9a\x25\x81\x2a\x88\xfa\x33\ +\x71\xf4\x60\x12\x66\x14\xe6\x2b\x06\x48\xd0\x1a\x82\x9d\x38\x12\ +\xac\x86\x28\xbc\x98\x0e\x4c\x42\xf6\x60\x3a\x30\x09\x55\x42\xf4\ +\xe6\x42\x18\x87\xd1\x48\x80\x01\xb8\x17\x81\x85\x2b\xf3\x90\x00\ +\x03\x30\x67\xa3\x91\xf0\x03\x30\x1d\x98\x04\x2d\x81\xe9\xc0\x24\ +\xe8\x38\x4c\x07\x26\x01\x4b\x08\xe9\x8d\x03\xb3\x17\x47\x42\xd3\ +\x10\x85\x17\x6b\xff\x17\x21\x61\xf9\x6f\x79\x27\x8e\xd1\x97\x04\ +\x2a\x21\x00\xa8\x51\x42\x90\x10\x23\xf0\xe0\x8b\x0e\x29\x21\x48\ +\x60\x11\xb8\xfc\xe4\x13\x19\x24\x60\x0d\x61\xd4\xc0\x24\x94\x0e\ +\xdb\xdc\x54\x2b\x23\xb0\x61\xfe\xf7\x7d\xd3\xb3\xc9\xf1\xa6\x18\ +\x00\x00\xde\x7b\x03\x60\x6a\xe6\x55\x01\x33\x83\x9d\x71\xde\x15\ +\x77\xd4\x33\x29\x90\xd3\x6e\xe4\x38\x89\xc0\x03\xbe\x38\xdb\xed\ +\xa6\x22\x30\x11\x81\x0a\xcc\x2b\x2c\xbf\x73\xe7\xd6\x73\x06\x1c\ +\x58\x6d\xf6\x77\xbf\xf9\xe9\x4b\x34\x1f\x59\x4c\xee\xfc\xf9\xb6\ +\xe9\x83\xaf\x3f\xbf\xa4\x9e\xb6\xf1\xd4\x4f\xef\x14\xb3\x5c\x60\ +\x5e\x15\xde\x04\x96\x47\x6b\x3e\x72\x73\x3d\x93\x53\x1c\x9a\xed\ +\x26\xfb\x69\x42\xb2\x98\x34\x47\xc6\xff\xbd\x62\xfc\xed\x4f\xd7\ +\xd3\xfe\xf6\xe8\x13\xaf\xc2\x7c\xae\x30\x53\x35\xf3\x6a\x3e\xbf\ +\xfc\xfa\xef\x3e\x57\x6a\x8b\x22\x02\x8b\x25\x1b\x37\x9f\xfe\xc0\ +\x3d\x77\xef\xdc\x4b\x33\x92\xc5\xe0\x9e\xbb\x77\xee\xcd\xbb\x07\ +\xba\x2a\x96\xd4\xf5\xef\xe5\xd7\xef\x78\x2e\x82\xe5\xe2\x73\xef\ +\x14\xde\x3b\xe4\x59\x26\x3e\x7d\xe5\xd5\xff\x4e\xaf\x1a\x5f\xb9\ +\xac\xca\xbc\x6f\xcf\xef\xd7\x6e\xd8\x74\xe6\x1f\x7e\x7d\xd7\x2d\ +\x49\xd6\x3d\xb0\xf6\x82\x4f\x5c\xb5\x8c\x66\x25\x47\x9b\xbb\x6e\ +\xbb\xf1\x80\x6b\x2e\x7b\x26\xef\x1e\xe8\x8e\x8c\x8e\xbe\x5c\xdf\ +\xb6\xfe\x03\x97\xdc\x22\xb2\x3b\x75\x96\x67\x11\xbc\x97\x89\xa9\ +\x2d\xed\xae\xb6\x1a\x33\x68\x8e\xa4\x1a\xbf\xed\x27\x57\xbe\xff\ +\x9b\x23\xed\x76\x63\xa0\xd7\x67\xd2\xc8\x3c\x46\xbd\x97\x96\x07\ +\x22\xf0\xce\x07\x39\x4a\xa3\x0d\x2a\xc8\x55\x6c\xd6\x29\x0e\xd5\ +\x23\x2f\x00\x9c\x7e\xee\x15\x77\xac\x39\xe3\x92\xdd\xb1\xa5\xaf\ +\x8d\xa0\x3b\xd3\xb4\x6e\xe2\x44\xc4\x9c\x4f\x33\xa7\x9a\x64\x16\ +\x75\xee\xdb\xb7\xe2\x5b\xe7\x4c\xbd\xfe\x95\x46\xdc\x70\x75\x39\ +\xd1\x88\x90\x20\xe2\xc8\x1a\x59\x1c\x92\x24\xc9\xbe\xba\xed\xc7\ +\x7b\x04\x79\x27\x46\x9a\xc4\x96\x65\x02\x58\xb4\x64\xe5\x1a\x15\ +\x31\x81\xa8\x18\x14\x7f\x7d\xfc\xa9\x74\x74\x7c\xe3\xc3\x6b\x97\ +\x4e\x9f\x1a\xc7\x71\x44\xd3\x91\xc5\xe6\xf4\x8f\x7e\xe9\x8e\x9b\ +\x7f\xf5\xc8\x2f\x6f\xbd\x7b\xd7\x8b\xb1\xe5\x87\x9a\xc8\x92\x18\ +\x69\x06\x33\x2f\x13\x53\x5b\x62\x00\x9a\x8b\x73\x99\x38\xd7\x41\ +\xa3\x9d\xaa\x5b\x62\x88\x96\xec\xf8\xec\x49\x5f\x5b\x39\x3e\xbe\ +\x44\x79\xaf\x99\x2c\x02\xde\x0c\xab\x37\x5e\xb4\xf3\xcc\xcf\x5c\ +\xbb\x47\x2c\x3f\x18\x23\x3f\xd8\xb6\x6e\x27\xb6\x2c\x53\xcb\x32\ +\x00\x5e\x26\x36\x9c\xec\x60\xa6\x00\xd4\x8b\x8b\x52\x89\xe2\x0e\ +\x1a\xcd\x5c\xdc\x48\x2e\x51\x1b\x86\xf6\x75\x9f\xff\xe4\xaa\x55\ +\xe9\x5f\x2e\xbb\xe0\x53\xd7\xdd\xfb\xe0\xae\x9b\x3e\x46\xd3\x92\ +\xa3\xc5\xc1\x99\x99\x64\xcd\x7b\x2e\xfe\xc5\x03\x0f\x3f\xf2\xfa\ +\xd7\x7f\xf0\xb3\x17\x21\xd6\x89\x7c\xd6\x89\xe0\x67\xda\x98\xed\ +\x3a\x9f\xa7\x91\xf8\x1c\x66\x1e\x80\x97\xc9\x0d\x27\x47\x06\xa8\ +\x00\x62\x66\xa5\x13\x3b\x97\x41\x1a\x99\xc4\x8d\x0c\x51\xd3\x8b\ +\x34\x0c\xda\x80\x21\x86\x48\x04\xb3\xa8\x5c\xa0\x15\x80\xf5\x9f\ +\xcb\xaf\x22\x75\xf1\xbc\x47\x99\x86\xe2\x46\x60\xf5\x09\xab\xe5\ +\x1b\xfa\xdd\x4b\xab\xa5\x57\x65\x49\x6d\x5b\xbd\xcc\xfa\x7e\xc3\ +\xff\x51\xfc\xbf\xf9\x31\x5c\xd7\xc3\xec\x3b\x8c\x2c\x50\xce\x61\ +\xbb\x2c\xf3\x94\x61\x43\xe5\xd9\x50\x61\x87\xab\x33\xde\xe0\x36\ +\x1b\xb6\x79\xcd\xc6\x0b\xb5\xc3\x7c\x75\x96\x5a\x59\xc3\x65\x1e\ +\xa9\x9c\x5e\xdd\xc4\x6a\x75\xcc\x61\x96\x43\x90\x8a\xe5\x89\x02\ +\x49\x64\x79\xb7\x81\x2c\x89\xe0\x13\xe7\xd3\x2c\x42\x9e\x8b\x88\ +\x07\x60\x66\xe6\x9d\x01\x26\x80\xb7\x62\x5e\x84\x57\xe4\x68\x5a\ +\x6e\x0e\xea\x33\xf8\x34\x83\x26\x99\x45\xce\x24\x72\x06\x44\x30\ +\x89\x00\xa8\x59\xb5\xa0\x44\x61\x75\x11\x81\xf9\xbe\x1f\xf7\x26\ +\x57\xd8\xe0\xb9\x0e\x7e\x2f\xf6\x17\xab\xce\x65\xc8\xa6\x65\x9e\ +\xaa\xec\x5e\xb9\x95\x2d\x7c\xbf\x71\xcc\xaa\x72\x6d\xc0\xeb\x44\ +\x00\xf3\x55\x19\xe5\x67\xb5\xbf\xd5\x8e\x35\xec\x37\x56\x6b\x9f\ +\x5a\xb9\xc5\x6e\x83\xe7\x8d\xb2\x4e\x3d\xdf\xab\x39\x5f\x6f\xbf\ +\x7a\xda\xbc\xde\x55\xb4\x44\x71\x8e\x06\x29\xcd\x2b\x43\x4e\x5b\ +\xd9\x01\x03\x36\x2c\x6b\x36\x5f\x1d\x50\x3f\xcf\xfe\xd5\x31\xc7\ +\xf7\x06\xfc\x7b\xb0\xce\x03\xb6\xaf\xd5\xb9\xb7\x4f\xd9\x0e\x73\ +\xeb\x76\xb8\x7a\x94\xf6\xab\xea\x6d\x30\x11\x78\x33\x9f\xab\x21\ +\x57\x58\xe6\x90\x67\x11\xf2\xd4\xf9\x2c\x77\xe2\x73\x08\xbc\x41\ +\xbc\x15\x05\x78\x11\xb1\xff\x01\xed\xeb\x89\x21\x3a\xe3\x55\x01\ +\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x20\xb1\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\xe7\x00\x00\x00\x82\x08\x06\x00\x00\x00\xbd\xcd\xdf\xc9\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09\x70\x48\x59\x73\x00\x00\xdd\x75\x00\x00\xdd\x75\x01\ +\xac\x87\xc3\x83\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x02\x18\ +\x12\x22\x32\x36\x82\x3a\x5a\x00\x00\x20\x00\x49\x44\x41\x54\x78\ +\xda\xed\x5d\x69\x73\xdb\xd8\xb1\x3d\x20\x01\x02\x04\xc1\x55\x8b\ +\x25\x79\xec\xf1\x54\x3e\xa4\xf2\xff\x7f\x4a\xaa\x52\xa9\x54\xc5\ +\x9b\x6c\xd9\x96\x44\x71\x01\xb1\x72\x7b\x1f\x26\xe7\xa6\x79\x05\ +\x90\xd4\x1b\xcf\x8c\x63\x77\x57\xb9\xb4\x70\x15\x8d\x73\xbb\xfb\ +\xf4\xe9\x6e\x67\xbb\xdd\x6e\xa1\xa6\xa6\xf6\xcd\x59\x43\x3f\x02\ +\x35\x35\x05\xa7\x9a\x9a\x9a\x82\x53\x4d\x4d\xc1\xa9\xa6\xa6\xa6\ +\xe0\x54\x53\x53\x70\xaa\xa9\xa9\x29\x38\xd5\xd4\xd4\x14\x9c\x6a\ +\x6a\x0a\x4e\x35\x35\x35\x05\xa7\x9a\x9a\x82\x53\x4d\x4d\x4d\xc1\ +\xa9\xa6\xa6\xa6\xe0\x54\x53\x53\x70\xaa\xa9\xa9\x29\x38\xd5\xd4\ +\x14\x9c\x6a\x6a\x6a\x0a\x4e\x35\xb5\x1f\xd1\x5c\xfd\x08\xbe\x5d\ +\xdb\x6e\xb7\xd8\x6e\xb7\x58\xaf\xd7\x58\xaf\xd7\x28\xcb\x12\x49\ +\x92\x60\xbb\xdd\x62\x38\x1c\xa2\xdd\x6e\x1f\xfd\x5c\x59\x96\xe1\ +\xe1\xe1\x01\xeb\xf5\x1a\xbe\xef\xc3\xf3\x3c\xb4\x5a\x2d\xb4\x5a\ +\x2d\x34\x9b\x4d\x38\x8e\x03\xc7\x71\x00\xc0\x7c\x55\x53\x70\xaa\ +\x09\x30\x6e\x36\x1b\xac\xd7\x6b\x14\x45\x81\x2c\xcb\xcc\xbf\xa2\ +\x28\xcc\x6d\x41\x10\xa0\xd3\xe9\x3c\x09\x9c\xab\xd5\x0a\xf3\xf9\ +\x1c\x59\x96\xa1\xd1\x68\xc0\x71\x1c\x34\x1a\x0d\xb4\x5a\x2d\xf8\ +\xbe\x8f\x20\x08\xd0\x6e\xb7\xd1\x6a\xb5\xe0\x79\x9e\x01\xac\x9a\ +\x82\xf3\x87\x07\xe5\x7a\xbd\x46\x96\x65\x58\x2c\x16\x48\xd3\x14\ +\x79\x9e\x63\xb5\x5a\xc1\x71\x1c\xb8\xae\x8b\x76\xbb\x8d\x46\xe3\ +\xd7\x2c\xa4\xd9\x6c\x9a\xef\x8f\xce\x5f\x1a\x0d\xb4\xdb\x6d\xb8\ +\xae\x8b\xcd\x66\x63\x0e\x82\x34\x4d\x71\x7f\x7f\x8f\xe5\x72\x89\ +\x66\xb3\x89\x30\x0c\xd1\xeb\xf5\x10\x45\x11\xa2\x28\x42\x18\x86\ +\x68\x36\x9b\xfa\x9f\xf4\x27\x98\xa3\x03\xbe\xfe\x5c\x2b\x8a\x02\ +\x0f\x0f\x0f\xb8\xbf\xbf\x47\x9e\xe7\x70\x1c\x07\xbe\xef\x23\x0c\ +\x43\xb4\x5a\x2d\x03\xc2\xcd\x66\x83\xd5\x6a\x85\xd5\x6a\x85\xcd\ +\x66\x83\xf3\xf3\x73\x0c\x87\xc3\xa3\x5f\x67\x3a\x9d\xe2\xf3\xe7\ +\xcf\xd8\x6e\xb7\x68\x34\x1a\x3b\x00\xdf\x6e\xb7\x58\x2e\x97\x28\ +\xcb\x12\x45\x51\x18\x2f\xcd\x43\x61\x34\x1a\xe1\xf4\xf4\x14\x61\ +\x18\xaa\x37\x55\xcf\xf9\xfd\x5b\x9e\xe7\xb8\xb9\xb9\xc1\x97\x2f\ +\x5f\xd0\x68\x34\x10\x86\x21\x4e\x4f\x4f\xd1\xe9\x74\xcc\xed\x59\ +\x96\xa1\x2c\x4b\xac\xd7\x6b\xe3\xbd\xca\xb2\xc4\x6a\xb5\x42\xbf\ +\xdf\x7f\xf2\x21\x10\xc7\x31\x5c\xd7\x85\xe3\x38\x28\x8a\x02\x65\ +\x59\x62\xbb\xdd\xc2\x71\x1c\x13\xc6\x36\x1a\x0d\x74\xbb\x5d\x00\ +\xc0\x72\xb9\xc4\x6a\xb5\xc2\x87\x0f\x1f\xf0\xf1\xe3\x47\x8c\x46\ +\x23\x5c\x5d\x5d\x21\x8a\xa2\x27\x7b\x6e\x35\x05\xe7\x37\x1f\xbe\ +\x16\x45\x81\xbb\xbb\x3b\x7c\xfe\xfc\x19\x8e\xe3\x60\x34\x1a\xed\ +\xe4\x8f\x79\x9e\x63\x36\x9b\x61\xb9\x5c\x62\xb3\xd9\x60\xb3\xd9\ +\x98\xc7\x93\x14\xa2\xf7\x7c\x8a\xf1\xb9\x56\xab\x15\xca\xb2\xc4\ +\x72\xb9\x7c\x14\x56\x3b\x8e\x83\xed\x76\x8b\x3c\xcf\xe1\x79\x9e\ +\x01\x6d\x10\x04\x28\x8a\x02\x5f\xbe\x7c\xc1\x74\x3a\xc5\xd9\xd9\ +\x19\x9e\x3d\x7b\x86\x30\x0c\x15\xa4\x0a\xce\xef\x23\x7c\x9d\x4c\ +\x26\x18\x8f\xc7\xc8\xf3\x1c\x41\x10\xc0\x75\x5d\xc3\x98\x12\x34\ +\x79\x9e\xef\x00\x8f\x80\x59\x2e\x97\x58\xaf\xd7\x60\x16\xf2\xd4\ +\x6c\x64\xbb\xdd\xa2\x2c\x4b\x38\x8e\x83\xf5\x7a\x8d\x46\xa3\x61\ +\xd8\x60\x19\xde\x92\xb5\x2d\xcb\x12\x00\x0c\x39\xd4\xe9\x74\xb0\ +\x5c\x2e\xb1\x58\x2c\xf0\xf6\xed\x5b\xcc\xe7\x73\x3c\x7b\xf6\x0c\ +\xc3\xe1\x10\xbe\xef\x2b\x48\x15\x9c\xff\x7b\xb6\xd9\x6c\x30\x9b\ +\xcd\x70\x77\x77\x87\x24\x49\xe0\x38\x8e\x21\x66\x58\x22\x99\xcf\ +\xe7\x28\x8a\xc2\x00\x6e\xbd\x5e\x1b\xd2\x46\x32\xb8\x04\x4f\x51\ +\x14\xc8\xf3\xfc\x49\xef\x63\xb9\x5c\x62\xb9\x5c\x1a\x8f\x28\x89\ +\xa2\x2a\xb0\x37\x1a\x0d\xe3\x69\xd3\x34\x35\xac\xee\x70\x38\x44\ +\x9e\xe7\x78\x78\x78\xc0\x74\x3a\xc5\xf9\xf9\x39\x2e\x2f\x2f\xd1\ +\xef\xf7\x95\x38\x52\x70\xfe\xef\x84\xb0\xeb\xf5\x1a\xe3\xf1\x18\ +\x9f\x3f\x7f\x46\x59\x96\x08\xc3\x10\x9e\xe7\x19\x00\x7a\x9e\x87\ +\xcd\x66\x63\x88\x9f\x2c\xcb\x4c\x38\x4b\x50\x12\xa0\x12\x64\x49\ +\x92\x3c\x19\x9c\x69\x9a\x22\x4d\x53\xf4\xfb\x7d\xe3\x8d\xe5\x57\ +\x59\xe7\xb4\xbd\xf6\x7a\xbd\x46\x9e\xe7\xa6\x8c\xe3\xfb\x3e\x5c\ +\xd7\x45\x51\x14\xf8\xf4\xe9\x13\xee\xef\xef\xf1\xea\xd5\x2b\x3c\ +\x7f\xfe\x5c\x4b\x30\x0a\xce\x6f\xdf\x96\xcb\x25\x6e\x6f\x6f\x71\ +\x7d\x7d\x8d\x56\xab\x85\xd1\x68\x64\x2e\xf4\x66\xb3\x89\x66\xb3\ +\x69\xbc\x91\xeb\xba\x3b\x80\x23\x28\x09\x72\x29\x46\xe0\x7d\x9a\ +\xcd\x26\x36\x9b\xcd\x51\xe1\xe4\x76\xbb\x35\xc2\x03\x86\xd4\xbe\ +\xef\x63\xbb\xdd\x62\xb5\x5a\x3d\x7a\x2d\x09\x56\x19\xfa\x36\x1a\ +\x8d\x1d\x0f\x1e\x04\x01\x5a\xad\x16\x92\x24\xc1\x3f\xfe\xf1\x0f\ +\x4c\xa7\x53\xfc\xf5\xaf\x7f\x85\xef\xfb\x0a\x50\x05\xe7\xb7\xe9\ +\x31\xc9\xc4\xde\xde\xde\x22\x8a\x22\x74\x3a\x1d\x73\x51\xb7\xdb\ +\xed\x9d\x9a\xa5\xcc\x29\xcb\xb2\xc4\x66\xb3\xd9\x01\x85\xe3\x38\ +\x06\xac\x0c\x77\xa3\x28\x32\xe0\x3a\xc6\x48\xf6\xf8\xbe\x8f\x66\ +\xb3\x69\xc2\x6a\xe6\x96\x0c\xa9\x99\x87\x12\x58\xcc\x7d\x9b\xcd\ +\xa6\x01\x37\x19\x5c\xcf\xf3\xcc\x7d\xa3\x28\x82\xeb\xba\xb8\xb9\ +\xb9\x41\x1c\xc7\xf8\xdb\xdf\xfe\x86\xd1\x68\xa4\x00\xfd\x8d\xa6\ +\x59\xfc\x57\xb6\x34\x4d\xf1\xf6\xed\x5b\xdc\xdf\xdf\x63\x30\x18\ +\x20\x0c\x43\x03\xb8\x76\xbb\x6d\x88\x20\xe9\xf1\xd6\xeb\xb5\x21\ +\x7c\x28\xaf\x0b\x82\xc0\x28\x79\x9a\xcd\x26\xd6\xeb\x35\x92\x24\ +\x41\xb3\xd9\x84\xeb\xba\xc8\xf3\x7c\x87\x71\xdd\x67\xab\xd5\x0a\ +\x45\x51\x20\x08\x02\xf4\xfb\x7d\x03\xac\xed\x76\x6b\x08\x1f\x82\ +\x8e\xaf\x49\xb0\x4a\x2f\xca\xfa\x28\x81\x4d\xf0\x6d\xb7\x5b\x04\ +\x41\x80\xd1\x68\x84\x3c\xcf\xf1\xf7\xbf\xff\x1d\x37\x37\x37\xd0\ +\x12\xba\x82\xf3\x9b\xb1\xd9\x6c\x86\x7f\xff\xfb\xdf\x48\xd3\x14\ +\xa3\xd1\xc8\xb0\xb0\xeb\xf5\xda\x80\x90\x3f\x4b\xaf\xe8\x38\x0e\ +\xba\xdd\x2e\x3a\x9d\x8e\xd1\xbc\xd2\xbb\xd1\x83\x6d\x36\x1b\x03\ +\x2a\x2a\x88\xf8\xbc\xc7\x90\x52\x7c\x1d\x3b\x9f\x24\x40\xb7\xdb\ +\x2d\xe2\x38\x36\x61\xb3\xf4\xa2\x9b\xcd\xc6\x94\x70\xaa\x08\x24\ +\x3e\x97\xeb\xba\xe8\x76\xbb\xd8\x6c\x36\xf8\xe7\x3f\xff\x89\x37\ +\x6f\xde\x3c\xb9\xe4\xa3\xa6\xe0\xfc\xea\x36\x99\x4c\xf0\xee\xdd\ +\x3b\x6c\xb7\x5b\x0c\x06\x03\x34\x1a\x0d\x93\xcf\xb1\x8c\x41\x56\ +\x56\x7a\x27\xfe\x6b\xb5\x5a\x88\xa2\x68\x87\x24\xf2\x7d\x7f\x87\ +\xa9\x6d\xb5\x5a\x46\x7e\x47\x4f\x7b\x6c\xa8\xcd\xd7\x5f\x2c\x16\ +\x06\x7c\xf4\x7e\xae\xeb\xa2\xd3\xe9\x18\x8f\xcc\x30\x98\xef\x91\ +\x00\x97\x64\x95\xfc\xdb\xf8\x7c\x00\xe0\xba\x2e\xa2\x28\x82\xe3\ +\x38\x78\xfd\xfa\x35\xde\xbc\x79\x73\xf4\xfb\x54\xd3\x9c\xf3\xab\ +\xdb\x74\x3a\xc5\xbb\x77\xef\xb0\x5e\xaf\xd1\xeb\xf5\x4c\x7e\x28\ +\x3d\x0c\xc9\x1b\x7a\x24\xde\x26\x43\x43\xc7\x71\x8c\x17\x93\x1e\ +\x16\x80\xf1\xa8\x0c\x89\xa9\xf0\x39\x96\x9c\x4a\xd3\x14\x49\x92\ +\x18\x35\x92\x04\x1e\x43\x58\x2a\x83\xe4\x7b\x94\x9e\x52\x8a\x1f\ +\xf8\xf7\xf1\x3d\xf3\x7d\x33\x47\x8d\xa2\x08\x71\x1c\xe3\xfd\xfb\ +\xf7\x68\xb5\x5a\xb8\xba\xba\xd2\x52\x8b\x82\xf3\x8f\xb5\xf9\x7c\ +\x8e\xb7\x6f\xdf\x62\xb5\x5a\x61\x30\x18\xec\x5c\xb8\xf4\x7a\xfc\ +\xde\xd6\xb3\xda\xde\xd3\x75\x5d\x84\x61\x88\x38\x8e\xf1\xf0\xf0\ +\x60\x84\xef\x00\x76\x44\x01\xfd\x7e\x7f\x87\xd1\x3d\x26\xe7\xa4\ +\xf0\x20\xcf\x73\x53\x52\x91\x22\x78\xd7\x75\x4d\x79\xa4\xea\x60\ +\xa1\xa7\xa4\xe6\xd6\x75\x5d\xf3\x3b\x7a\x5f\x59\x9a\xa1\x70\x61\ +\xb1\x58\xe0\xf5\xeb\xd7\x68\x34\x1a\xb8\xbc\xbc\x54\xb1\x82\x82\ +\xf3\x8f\xb1\x2c\xcb\x70\x73\x73\x83\xb2\x2c\xd1\xeb\xf5\xd0\x6c\ +\x36\xb1\x5a\xad\x0c\x40\x79\xe1\xcb\x3e\x49\xd6\x01\x25\x28\x65\ +\xf8\xe9\xba\x2e\x86\xc3\x21\xc2\x30\x44\x9a\xa6\x86\x4d\x65\x98\ +\x9c\x24\x09\xba\xdd\x2e\x7a\xbd\x1e\x82\x20\x38\xea\x7d\xb2\xec\ +\x61\x83\xba\xd5\x6a\xed\x90\x3d\x0c\x5d\xed\x5c\x95\x2d\x6c\x14\ +\x3f\xac\x56\x2b\x43\x0c\x51\xd9\xd4\x6c\x36\x2b\x05\x0e\x61\x18\ +\x22\x49\x12\xbc\x7e\xfd\x1a\x9e\xe7\xe1\xec\xec\x4c\x59\x5c\x05\ +\xe7\xef\x6b\xab\xd5\x0a\xb7\xb7\xb7\x98\x4c\x26\xe8\x76\xbb\xf0\ +\x7d\xdf\x84\x7d\x36\x30\xf9\xbd\xef\xfb\x06\x10\xb6\x0c\x4f\xaa\ +\x81\xb6\xdb\xad\x09\x0d\x09\x8c\xb2\x2c\x77\x44\x0a\x4f\xc9\x39\ +\xe9\xe9\xf8\x18\xcf\xf3\x0c\xfb\xcb\xf7\xca\x3a\x2b\xe5\x84\x41\ +\x10\x60\xb3\xd9\x20\x8e\x63\x03\x54\x09\x3a\x5b\xf3\x9b\x65\x99\ +\xa9\xdf\xf2\x36\x82\x3e\x08\x02\xc3\x62\xb7\xdb\x6d\x13\x3e\xab\ +\x29\x38\xbf\xba\x6d\x36\x1b\x3c\x3c\x3c\xe0\xe6\xe6\xc6\x5c\xc8\ +\x04\x9c\x24\x4a\x08\xcc\xf5\x7a\x6d\x42\x56\x19\x4a\xd6\x85\xa5\ +\x92\x21\x65\xfb\x56\x92\x24\x28\xcb\x12\xdd\x6e\x17\x41\x10\xa0\ +\x2c\xcb\xa3\x73\xce\x34\x4d\xf1\xf0\xf0\x60\x42\x56\xe6\x87\x0c\ +\x31\xd3\x34\xc5\x62\xb1\x30\x87\x03\x35\xb4\xfc\x7b\x64\xa8\xee\ +\x79\x1e\x5c\xd7\x7d\x54\x6a\xa1\x40\x5e\x8a\xf5\x29\xf4\xa7\xe7\ +\x9e\x4c\x26\xb8\xbe\xbe\xc6\x5f\xfe\xf2\x17\xf8\xbe\xaf\x17\x92\ +\x82\xf3\xeb\x5b\x9a\xa6\x78\xff\xfe\x3d\x1c\xc7\x31\xcd\xc8\xac\ +\x13\x4a\xd0\x49\x2f\x4a\x50\xc9\xdb\xa5\x07\x95\x79\xa8\xe3\x38\ +\x58\xad\x56\x46\x93\x2b\x0f\x85\xb2\x2c\x4d\x17\xcb\xb1\xe1\x21\ +\x4b\x38\x9e\xe7\xa1\xdb\xed\x1a\xd0\x50\x1c\x21\xdb\xd2\x78\x5f\ +\x19\x66\x4b\xf1\x01\xa3\x03\xfe\x6d\x9c\xa6\xe0\x79\x1e\x92\x24\ +\x41\x14\x45\xa6\x8b\x25\x4d\x53\x2c\x97\x4b\x14\x45\x81\x5e\xaf\ +\x07\xcf\xf3\xf0\xf1\xe3\x47\x74\xbb\x5d\x3c\x7f\xfe\x5c\xf3\xcf\ +\x03\xa6\x9f\xce\x13\x6d\xb9\x5c\xe2\xe6\xe6\xc6\x5c\xdc\x64\x36\ +\x29\x20\x90\x0a\x1f\xfe\x4c\xaf\xc9\x90\xcf\x0e\x69\xe5\xc8\x90\ +\x20\x08\x90\xe7\x39\xfe\xf5\xaf\x7f\xe1\xcd\x9b\x37\x26\xcf\x5b\ +\x2e\x97\x26\xd7\x2c\x8a\x02\x37\x37\x37\xa6\x2c\x72\x4c\x08\x4e\ +\xf5\x11\x5f\x87\x5e\xd2\x66\x67\x65\x7e\x2c\xa3\x02\x19\xc6\xb2\ +\x09\x5b\x32\xcb\x3c\x38\x66\xb3\x19\x92\x24\x81\xeb\xba\xe8\xf5\ +\x7a\xf0\x7d\xdf\xc8\x0e\x59\xf7\x7d\xf3\xe6\x8d\xf1\xcc\x6a\x0a\ +\xce\xaf\x62\xdb\xed\x16\xf3\xf9\x1c\xb7\xb7\xb7\x46\x57\x4a\x00\ +\x4a\xcf\x47\xa0\xd2\x4b\xb2\x86\x28\x73\x44\x82\x98\x0a\x1d\xea\ +\x56\x39\x19\xa1\x2c\x4b\xa3\xbb\x75\x5d\x17\x41\x10\xa0\xdb\xed\ +\x9a\xfe\x4f\x32\xa8\x4f\xfa\xcf\xfe\x4f\x29\x87\xa4\x54\x51\x14\ +\x26\x0f\x95\xa2\x03\x82\xaf\xdf\xef\x1b\x41\xbe\x24\xb6\x78\xff\ +\x20\x08\x8c\x92\x49\x7e\x06\x04\x6a\x59\x96\x08\x82\xc0\x4c\x6c\ +\xa0\x90\x62\x36\x9b\xe1\xcd\x9b\x37\x47\x2b\x9c\x14\x9c\x6a\x07\ +\xad\x2c\x4b\x5c\x5f\x5f\x9b\x0b\x53\x12\x38\x04\x9b\x14\x0d\x10\ +\x8c\x24\x81\x24\xf1\xc3\x9a\x25\xa5\x7b\x0c\x1b\x99\xbf\x11\xb0\ +\x0c\x73\x49\x26\x91\x01\x3d\x3b\x3b\x43\x18\x86\x47\xbd\xef\x30\ +\x0c\x4d\x37\x8a\x04\x27\x23\x01\xa9\xbb\xa5\xd7\x24\x69\x94\x65\ +\x99\x51\xfe\x30\x4f\x94\xe1\x38\xb5\xc2\x54\x0f\xc9\x88\x81\xe1\ +\x32\xfb\x56\xe5\x81\xf4\xe9\xd3\x27\x8c\xc7\x63\xf5\x9e\x0a\xce\ +\xaf\xe3\x35\xd9\xc3\x48\x90\xf0\x82\x24\x31\x23\x81\x29\x45\xe3\ +\xb2\xb6\xc9\x6e\x14\xd9\x40\x2d\x43\xc6\xe5\x72\x89\xd3\xd3\x53\ +\x5c\x5d\x5d\x99\x03\x40\x76\x8e\xf0\x62\xef\xf5\x7a\xe6\x79\x0e\ +\x19\x5f\x93\xe1\xad\xec\x32\xe1\x24\x3f\xe6\x9b\xb4\x3c\xcf\x91\ +\xe7\x39\xc2\x30\x34\x79\x2a\x95\x43\x92\x8d\x8d\xe3\x78\x27\x64\ +\x96\xde\x33\x49\x12\xdc\xdd\xdd\x21\x8e\xe3\x47\xe4\xd0\x76\xbb\ +\xc5\xfb\xf7\xef\x51\x14\x85\x5e\x5c\x0a\xce\xdf\x66\x45\x51\xe0\ +\xc3\x87\x0f\xf0\x3c\x6f\x87\x95\x94\xa1\xac\xac\x5d\x52\x82\x77\ +\x76\x76\x66\x66\xee\x10\xa8\x12\xc4\x36\x19\x44\x90\x0f\x87\x43\ +\x5c\x5d\x5d\x21\x0c\x43\x23\x84\x97\x9e\x4d\xd6\x4b\x0f\x31\xcb\ +\xf4\x9e\xbe\xef\x9b\xfc\x95\xa0\xa7\x28\x9e\xb5\x4b\xe9\xa9\xe9\ +\x2d\xa9\x30\x92\xa3\x4c\x38\xb2\x93\x7f\x0b\xa3\x03\xcf\xf3\xcc\ +\x81\x25\x6b\xa1\x2c\xd5\x70\x34\x27\xd9\xdb\xbb\xbb\x3b\xf5\x9e\ +\x0a\xce\xdf\xe6\x35\xef\xef\xef\x91\xa6\xa9\x01\x89\x04\x98\x0c\ +\xe5\x64\x3e\x49\x31\xbb\x94\xad\xc9\x7c\xd4\x26\x85\x64\xe8\xcb\ +\x51\x96\x14\xc1\x77\xbb\x5d\x23\x84\x97\xcf\x75\x48\x58\x4e\x10\ +\xb2\x55\xcc\x71\x1c\x23\xe5\xe3\x57\x29\xa2\x67\xbe\xc9\x99\x46\ +\x59\x96\x21\x4d\xd3\x1d\x26\x99\xe1\xb6\x7c\x6d\x86\xb1\xcc\x85\ +\x93\x24\xc1\x6c\x36\x33\xef\x81\x07\x42\x9e\xe7\x88\xe3\xd8\x8c\ +\x36\xb9\xbe\xbe\x3e\xba\x24\xf4\xa3\x99\x96\x52\x8e\xb0\x3c\xcf\ +\xf1\xf9\xf3\x67\xa3\x6f\xb5\x07\x6c\xd1\x8b\x4a\x52\x64\x34\x1a\ +\x61\x34\x1a\x19\x21\x3b\xc1\x54\xe5\x25\xe8\x09\x49\xda\xc8\xd0\ +\xb8\x6e\x1a\x7b\x95\xf7\xad\xf3\x9c\x64\x8c\x7b\xbd\x1e\x80\x5f\ +\xbb\x67\x38\x86\xd3\xee\x54\xf1\x7d\xdf\x10\x4e\x9c\x9e\x50\x35\ +\x35\x41\xb2\xbc\x14\x34\x78\x9e\x87\x30\x0c\x0d\x08\xe5\xdf\x21\ +\x3b\x70\x28\xa8\x68\xb7\xdb\x88\xe3\x18\x37\x37\x37\xf8\xe5\x97\ +\x5f\xf4\x42\x53\x70\x3e\xdd\xc6\xe3\x31\xca\xb2\x34\xa1\x9b\x0c\ +\x69\x19\xea\x11\x94\x65\x59\x62\x30\x18\x98\x96\x31\x59\x13\xb4\ +\xc3\x58\xe6\x83\xf2\xf1\xbc\x2f\x19\xd1\xe1\x70\x68\x84\x03\xd2\ +\xc3\xca\x36\x2e\xde\x56\x65\x92\x70\xa2\xba\x88\xb9\x21\x1f\x27\ +\x5b\xca\x08\x2c\xca\xf5\xaa\x6a\xa9\xf6\xc4\x04\x7b\x6d\x04\x19\ +\x68\x5b\x90\x21\x5b\xcb\xf8\xfb\x66\xb3\x89\xf1\x78\x6c\xa6\xf9\ +\xa9\x29\x38\x8f\xb6\xe5\x72\x89\xe9\x74\x6a\x88\x18\x3b\x94\x24\ +\x88\x80\x5f\xeb\x89\x6c\xbf\xe2\xa4\x02\x09\x4a\x29\xe5\x93\xe1\ +\x21\x2f\xd4\xd5\x6a\x65\xc2\x3f\xe6\xa6\x1c\xf9\xc1\xe7\x61\x3e\ +\x37\x9f\xcf\xe1\xfb\x3e\x46\xa3\xd1\xde\xb5\x0c\x14\x33\x8c\xc7\ +\x63\xa3\xee\x61\x1e\xc9\x32\x8d\x24\x7a\x28\x1e\xa8\x9a\x29\x24\ +\x73\x63\x7b\x5a\x03\x6b\xa2\x2c\x2f\xc9\x83\x8b\x1e\x96\x07\x03\ +\xef\xc7\xcf\x22\x4d\x53\xcc\x66\x33\x05\xa7\x82\xf3\x69\x96\x24\ +\x89\x21\x31\xe8\x65\x24\x73\xca\x8b\x8f\x17\x64\xa7\xd3\x31\xd3\ +\x0f\xe4\x7d\x6d\x3d\x2a\x81\x49\xd0\xda\x22\x00\xf9\x7b\x1b\xdc\ +\x00\x0c\x29\x75\xa8\x56\x58\x96\x25\xe2\x38\x36\x93\x10\x28\x9c\ +\x60\x68\x2b\x47\x64\x4a\xb0\xc9\xf7\x60\x13\x56\x55\xef\x93\x4a\ +\x22\x19\xf2\xb2\x24\x44\xdd\x71\x9e\xe7\x8f\xfe\x1e\x86\xb9\x8b\ +\xc5\xc2\x4c\x07\x54\x53\x70\x1e\x45\x04\x71\x77\x89\x14\xac\xdb\ +\x3d\x9a\xf4\x9e\xdd\x6e\x77\xc7\x23\x54\x99\x3d\x3c\xcb\x26\x77\ +\xea\xc0\x20\x5f\x97\x61\xa3\x1c\xa7\x79\x88\xad\x6d\x34\x1a\x46\ +\x9f\x6b\xdf\x6e\x77\x92\x54\x81\x54\xbe\x77\x5b\x7e\xc8\x70\x9e\ +\x25\xa6\x46\xa3\x61\xc0\x4a\x52\x8b\xf5\x54\x3b\xff\x64\x9b\x1a\ +\x0f\x10\x05\xa7\x82\xf3\xe8\x90\x76\xb1\x58\xec\x80\x50\x5e\xd4\ +\x0c\xdd\x48\x84\x64\x59\x86\x24\x49\x8c\xb2\xa6\xae\x6b\x84\x17\ +\xb5\x14\x30\xd8\xed\x65\x36\xf9\x23\xe7\xfa\xb0\xb4\x41\xcf\x74\ +\xc8\xa4\xe0\xa0\x2c\xcb\x47\x5e\x5b\x02\xb3\xea\x71\xf6\xfb\x96\ +\xdf\xcb\xbf\xc5\xce\x7f\xa9\xb9\x6d\x34\x1a\x26\xa4\xb6\x0f\x20\ +\x3e\x36\x8e\x63\x24\x49\x82\x4e\xa7\xa3\x2d\x65\x0a\xce\xe3\x58\ +\xda\xd9\x6c\x56\x3b\x99\x8e\xbf\x6f\xb5\x5a\x28\x8a\x02\x8b\xc5\ +\xa2\x76\x2c\xa4\xad\xa9\xb5\xc3\x5e\x09\x06\xa9\x0e\xe2\x01\x40\ +\xc1\x03\x27\xc7\x13\x9c\x27\x27\x27\x7b\x17\x1a\xb1\x96\x29\x01\ +\x59\xf5\x9a\x87\xc0\x68\x13\x41\x55\x51\x86\x0c\x93\x57\xab\xd5\ +\xce\x24\x78\x99\xc3\x32\xd2\x90\x51\x04\xeb\xa6\x75\xcf\xff\x23\ +\x9a\xd6\x39\xf7\x84\xb4\x24\x47\xd8\x60\x6c\x83\x8a\x53\xe7\x36\ +\x9b\x8d\x59\xa3\xb0\x5c\x2e\x1f\xad\x54\xa8\x63\x3b\xe9\xad\x6c\ +\xe0\xd0\xbb\x30\x9f\xe5\x18\x10\xb2\x9c\x52\x08\x70\x28\xe7\xa4\ +\xc8\xc0\x5e\xf1\x50\xf5\x4f\x1e\x16\xd2\xb3\xca\xef\xab\x56\x37\ +\xc8\x03\x2b\x8e\x63\x64\x59\x86\x4e\xa7\xf3\xa8\x77\xd5\xce\xb7\ +\x59\x42\xf2\x3c\xcf\xec\x0f\x55\xbd\xad\x82\xf3\xa0\xb1\xce\xc7\ +\x10\x72\x36\x9b\x99\xbc\x90\xe4\x4f\x14\x45\x3b\x3d\x97\xc7\xd4\ +\x1e\xed\x3e\x4e\xc9\xda\x56\x79\x2f\xbe\x17\x86\xd8\xcc\xed\x8e\ +\x95\xef\xc9\xfa\xab\x3c\x54\xec\x49\x0c\x12\x78\x75\x1e\xb6\xee\ +\xbe\x76\x6e\x2c\x95\x42\x55\x73\x92\x6c\x80\x93\xe1\x1d\x8f\xc7\ +\x0a\x4e\x05\xe7\x61\xa3\xe8\x9b\x5a\xd4\x24\x49\x90\x24\x89\x99\ +\xc7\xc3\x56\x28\x76\x76\xc8\x30\xd4\xf6\x40\xc7\x90\x43\x12\x9c\ +\x9c\xc7\x23\x81\x49\xa5\x0e\x43\xd4\x56\xab\x65\x46\x69\x1e\x0b\ +\xce\xaa\x39\xb4\x76\xfe\x67\x7b\x4c\x09\xa8\xaa\xb1\x98\xf6\xdf\ +\x49\x4f\xbf\xef\xb0\xb1\xc5\x0c\x14\xc6\xb3\x3d\x4e\xe5\x7c\x0a\ +\xce\xa3\x3c\x27\xdb\xb2\x9a\xcd\x26\xe6\xf3\x39\xa6\xd3\xa9\xb9\ +\xa8\xb3\x2c\x33\xa1\x23\x2f\x44\x2e\x2e\x4a\xd3\x74\x2f\x41\x23\ +\x01\xc2\xb2\x03\xc3\x57\xa9\x0a\xda\x6c\x36\xe6\x90\x90\x35\x43\ +\xb2\xb6\x87\x1a\x96\x7d\xdf\x37\xf3\x8d\x6c\x60\xd9\xef\xa9\x6e\ +\xa9\x91\x0d\xc4\xba\xbf\xc7\x0e\x5b\x6d\xe2\xa8\xaa\x74\xc4\xe7\ +\x25\xa3\x4b\xc9\x9f\x9a\x12\x42\x7b\x73\x35\xb9\x5c\x56\xf6\x69\ +\xb2\x57\x91\x6a\x97\xd5\x6a\xb5\x33\x11\x81\x42\xf2\x63\x58\x54\ +\x79\xf1\x4a\xe5\x8c\xdc\x40\x2d\xa7\x0e\x48\x80\x1e\x22\x4e\x78\ +\x1f\x4e\x4e\x60\x08\x5e\xa5\x65\xb5\x01\x54\x57\x4a\xa9\xf2\x86\ +\xb6\xf7\x94\xf9\xad\x24\xc1\xc8\x2e\xcb\x75\x86\x72\x76\xee\x7a\ +\xbd\x36\x4d\xd8\x4a\x0a\xa9\xe7\xdc\xcb\xd4\x12\x60\x32\xbf\xea\ +\x74\x3a\xc8\xb2\x0c\xd3\xe9\xd4\xf4\x60\xb2\x6f\x11\xf8\x6f\xf9\ +\xe0\x18\x0f\x53\xf7\x7b\x09\x5a\xe6\xb3\x92\xc1\xb5\xef\x53\x47\ +\x3e\x49\x72\x89\xe5\x1e\x12\x35\x75\x5e\x53\x7a\xbf\x3a\x16\xd7\ +\x06\xb4\x0c\x97\x1f\x5d\x60\x22\x1c\xe6\xca\x06\xf9\x38\xbe\x3f\ +\x1e\x38\x59\x96\xe9\x10\x6a\xf5\x9c\xfb\x8d\xde\x4f\x86\x97\x9c\ +\x86\xc7\xee\x0b\x82\x52\xce\x75\xe5\x3e\x4b\xca\xf0\x6c\x6f\xc4\ +\x10\xd9\x06\x83\x9d\xa7\xf2\x79\x78\xb1\xd6\x01\x84\x39\x65\x55\ +\x78\x2b\x6b\xa7\x92\xa8\xa9\x12\xef\x1f\x6a\x3f\xb3\x3d\xaa\xfd\ +\xd5\x5c\x50\x96\x52\x48\x7a\x4d\x8e\xf3\xb4\x85\x0f\xf2\x73\xe1\ +\x61\xa7\x62\x04\xf5\x9c\x7b\x3d\x8e\xbc\xb8\x39\x2f\x87\xc0\x64\ +\xb8\x49\x80\x3e\x3c\x3c\x98\x1c\xcf\x6e\xb3\xb2\x2f\xf2\x38\x8e\ +\x71\x77\x77\x67\x40\xcc\x92\x08\xc3\x3b\xea\x79\x29\x80\xa8\x1a\ +\xa5\x29\x3d\x4f\x9d\xe7\xdc\x77\xdb\x21\x6f\x7e\x6c\x1e\x6a\x1b\ +\x35\xb6\xb2\x36\xdc\x68\x34\x10\x45\x91\x61\xb7\xed\xd7\x90\xde\ +\x9d\xe0\x54\x53\xcf\xb9\x97\xad\x95\xa2\x76\x8e\xed\x90\xe4\x8f\ +\xbc\xa8\x09\x52\xde\x7f\xb1\x58\x20\x49\x12\xf4\x7a\xbd\x9d\xf0\ +\x6d\x3e\x9f\xe3\xd3\xa7\x4f\x46\x53\x2a\x45\xed\x1c\xfc\x4c\xe1\ +\x80\xcd\x82\x56\x85\x98\x87\xc0\x69\x33\xb0\x72\xf6\x90\x54\x17\ +\xd9\x7a\x57\xbb\x9b\xa4\xca\x6b\x56\xb1\xd3\x8c\x0a\x6c\x51\x3c\ +\x95\x3f\x9c\x7c\x20\xef\x43\xcf\x4f\xcf\xa9\x61\xad\x7a\xce\x83\ +\x9e\x93\xe1\x24\x65\x67\x72\xfd\x00\xef\x67\x13\x46\x7c\x1c\x97\ +\x06\xd9\x17\x32\x37\x79\x35\x9b\xcd\x9d\x39\x43\x64\x7f\xf9\x18\ +\x59\x4e\x39\xc4\x2a\x1f\xca\x39\x25\xc8\xed\x1d\x2c\x75\xa1\x6b\ +\x9d\x17\xb5\xc1\xba\x6f\xa2\x3d\xc3\x5c\x4e\x61\xa0\x7c\xf0\x98\ +\x83\x51\x4d\xc1\x59\x7b\xc1\xd3\xd8\x29\x41\xcf\x59\x15\xaa\xd2\ +\xb3\x31\x4c\xed\x74\x3a\xa6\x55\x4b\x12\x1e\x65\x59\x1a\xd9\x9d\ +\x9d\x97\xc9\x0b\x9b\xad\x5c\xf2\xf7\x24\x56\xec\x7f\xc7\x12\x42\ +\xf4\xbc\xb2\xf5\xcc\x2e\x7f\xc8\x7d\xa0\x92\xc8\xa9\x02\xac\xed\ +\x55\x6d\xf2\xc7\x06\x3b\xbb\x52\x18\xbe\x57\x1d\x0a\x76\x08\xaf\ +\x61\xad\x5a\x6d\xbe\x45\x0f\xc6\xb0\xd5\x5e\xc3\x2e\x2f\x22\x0a\ +\xdd\xd7\xeb\xb5\x19\x07\x42\xa6\x55\x7a\x2d\xb9\xb6\xc0\x96\xca\ +\x51\x06\xc8\x70\x53\x86\x88\xd2\x53\xc9\x10\x77\x5f\x07\x8c\x0d\ +\x44\x99\xcf\xd5\xb1\xb1\x55\xed\x63\xc7\x10\x45\xb2\x66\x6b\x77\ +\xdb\x70\xed\x03\x87\xa2\xd9\x1e\x5a\x86\xc7\x36\xc0\x15\x9c\x6a\ +\xb5\x17\x1e\xbd\x19\x2f\x6a\x39\x64\xab\x4a\x33\x4a\x00\xf8\xbe\ +\xbf\x33\x4a\x52\x5e\xf8\xf2\xf1\x36\xc1\x43\x35\x92\x6c\x4b\xa3\ +\x4a\x88\xd2\x36\x5b\x73\x7b\xc8\xd3\xc8\xc9\x09\x75\x13\x19\x6c\ +\xb0\xd4\x79\x4c\xfb\xf0\xb2\xef\x6b\x87\xd1\x7c\xed\x34\x4d\x77\ +\x08\x22\x02\x51\xfe\x9d\x55\xde\x58\xc1\xa9\x56\x09\x4a\x7a\x37\ +\x0a\xd9\xa5\xaa\xc7\xf6\x1a\xf2\xf4\xe7\xcc\x1e\x0e\xb9\xe2\xe0\ +\x65\x0e\x89\x96\x1d\x2d\x14\x1e\xc8\xd5\x7a\xcc\x6f\x79\xa1\x53\ +\x80\x2f\xf3\x53\xbe\x8f\x3c\xcf\xd1\xef\xf7\xcd\x7c\x20\x69\x45\ +\x51\x98\xd9\x3e\x75\x35\xd2\x43\x2c\x6e\xd5\x4e\x17\xbb\x94\x22\ +\x0f\x80\xe5\x72\xb9\x33\xa7\x57\x7e\x2e\x1c\x93\x52\xf5\xfa\xb2\ +\x24\xa4\x7b\x3c\x35\xe7\xac\xff\x50\x44\xff\xa1\xbd\x42\x41\x6e\ +\x7d\xae\x0a\xbf\xe4\x60\xae\xf5\x7a\x8d\x77\xef\xde\xe1\xf6\xf6\ +\xd6\x6c\x19\xeb\xf5\x7a\xa6\x86\x19\x86\x21\x4e\x4f\x4f\xcd\x26\ +\x68\x76\x90\xb0\x71\x9b\x3b\x4a\xe4\x38\x4b\xf9\x1a\x14\xe5\xd7\ +\x89\xc5\x39\xce\x84\xe4\x14\x43\x73\x1b\x80\xf6\x41\x73\xac\x4c\ +\xaf\x2a\x57\x97\xb5\x61\xa9\x37\xb6\x9f\x43\xe6\xb8\x72\x9a\x44\ +\xdd\xe7\xaa\x9e\x53\xcd\x18\x67\xae\xca\x69\x03\x76\x2f\xa7\x94\ +\x9d\x49\x4f\x52\x14\x85\x99\x00\x90\xa6\x29\xde\xbd\x7b\x07\x00\ +\x38\x39\x39\xc1\xb3\x67\xcf\xd0\xe9\x74\x00\xc0\x8c\xbb\xb4\x77\ +\x9e\x70\xbe\xab\xef\xfb\xa6\x18\xcf\x0b\xd9\xbe\x80\x0f\xb1\xb5\ +\x55\xeb\x22\x6c\x8f\x58\xb5\x2b\xa5\xaa\xf1\xbb\xaa\x4b\x65\x1f\ +\x58\x29\xd5\xab\x7a\x2d\x46\x27\x24\x88\xe8\x79\x25\x19\xa6\xe0\ +\x54\xab\xbd\xb0\x82\x20\x30\xa5\x0d\x9b\x40\x91\x13\xe7\x18\x86\ +\x71\x17\x88\x5c\x28\xdb\x6c\x36\x91\xa6\x29\xae\xaf\xaf\xb1\x5a\ +\xad\xd0\xe9\x74\x30\x18\x0c\x76\xf6\x95\xc8\x89\xe9\x72\xda\xc1\ +\xfd\xfd\xbd\x21\x85\xd8\x26\x06\xfc\x3a\xda\x92\x13\x0d\xf6\xe5\ +\x85\x36\xa9\x53\x05\xbc\xaa\x3c\xb8\xee\x7e\x55\xb3\x8e\xe4\x6b\ +\x48\xed\xaf\x24\xa4\x24\x28\x6d\xed\x30\x41\xca\x43\x84\xc2\x7f\ +\x35\x05\x67\x6d\xf8\xc6\x29\x75\xfb\x42\x2c\x79\x61\xcb\xfd\x26\ +\xae\xeb\x62\x36\x9b\xe1\xea\xea\x0a\xc0\x7f\x05\x09\xd7\xd7\xd7\ +\x68\xb5\x5a\x18\x0c\x06\xb8\xb8\xb8\x80\xef\xfb\x28\xcb\x72\x27\ +\x64\x95\x21\x1e\xd9\x5d\xe6\xab\x72\xd4\x24\x4b\x31\xfb\x06\x4b\ +\xdb\x0d\xd1\x55\xb9\x63\x1d\xe0\xf6\x09\xe0\xab\x9e\xc7\xfe\x4c\ +\xe4\x40\x33\xf9\x19\x92\xbd\xdd\x6c\x36\xe8\xf7\xfb\xe6\xf9\x79\ +\xd8\xa8\xe7\x54\x70\x1e\x34\x9e\xe0\x14\x03\xc8\x39\xab\xf6\x54\ +\x3d\x39\x15\x8f\x3f\x33\xa4\x0d\x82\xc0\x14\xdf\x39\xfa\x92\xcc\ +\xef\xe5\xe5\x25\x3a\x9d\x8e\xa9\x8b\xce\xe7\x73\xb3\xe0\x96\x80\ +\xa5\x17\x61\xaf\x63\xd5\x04\x83\x7d\xa5\x94\xaa\x7c\x51\x1e\x2a\ +\x75\xa1\xaa\x0d\xd0\x7d\x26\x0f\x01\xb6\xb8\x71\x75\xa0\x0d\x70\ +\xd6\x7a\xdb\xed\xf6\x0e\xf3\x9d\xe7\xf9\x51\x07\xa2\x82\x53\xcd\ +\x90\x41\x04\x29\x49\x17\x7a\x84\x7d\xf9\x16\xf3\xc0\xf9\x7c\x8e\ +\x28\x8a\x0c\xf8\xe8\x61\xb7\xdb\xad\x59\xf0\x73\x7a\x7a\x8a\xd1\ +\x68\x84\x30\x0c\x91\xe7\x39\xc6\xe3\xb1\x01\x68\xd5\x54\xbc\x2a\ +\x52\xa6\x4a\xfc\x7e\xcc\x04\xc0\x7d\xb7\x57\x81\xb2\xae\xf6\x69\ +\xd7\x7f\xd9\x36\xc7\xcd\x66\x00\x4c\x14\x90\x24\x89\x99\x2c\x4f\ +\x63\x03\x39\xc1\xa9\x61\xad\x82\xf3\x60\xce\xc9\x0b\x85\x2b\x15\ +\xaa\x64\x6f\x12\x14\xb2\x4c\xc2\xf0\x74\x32\x99\x20\x8a\x22\xd3\ +\x4f\x29\x81\x96\xa6\x29\x3e\x7d\xfa\x84\xf5\x7a\x8d\x8b\x8b\x0b\ +\x2c\x16\x0b\x43\x0e\x55\xcd\xe7\xb1\x43\x48\xfe\x6c\xeb\x80\x79\ +\xff\xaa\x6e\x96\xba\x69\x07\xfb\x88\x9f\x3a\x32\xc9\x06\x31\x47\ +\x63\xb2\xa6\x39\x9f\xcf\xcd\xc0\xeb\x24\x49\x4c\xfe\xce\x92\x12\ +\x07\x82\xc9\x7c\x7d\xdf\x80\x6c\x05\xa7\x9a\x01\x50\xbb\xdd\xc6\ +\x72\xb9\x34\x40\xa5\x68\xfb\x18\x89\x99\x9c\xce\xce\x31\x26\xb6\ +\x16\x97\x75\x4a\x0e\xae\xf6\x3c\x0f\xfd\x7e\xdf\x34\x73\x4b\x70\ +\x32\x4c\x94\xb5\x42\xdb\x73\xda\x21\xad\xac\x29\xda\x25\x93\x7d\ +\x93\xf4\x6c\x95\x4f\x15\x70\xed\xc7\xb1\x96\xca\xcf\x88\x6a\x27\ +\xe6\xc9\x5c\xed\xc0\xee\x93\xcd\x66\x83\xc5\x62\x61\x36\x64\xaf\ +\x56\x2b\x74\xbb\x5d\x05\xa7\x82\xf3\x38\x52\x88\xdb\xa4\x3f\x7c\ +\xf8\x60\x18\x53\x7a\xa4\xa7\x18\x37\x3c\x7b\x9e\xf7\x68\x97\xa5\ +\xe7\x79\x98\x4c\x26\x68\x36\x9b\x18\x0e\x87\x08\x82\xc0\xac\x75\ +\xa0\x07\x67\x93\x34\x3d\x24\x73\x50\x02\x95\x42\x06\xdb\xa3\xd7\ +\x0d\xd4\xaa\x0a\x5d\xed\x7c\xf3\x18\xe9\x9e\x14\xb7\x53\x1d\x25\ +\xe5\x8a\x5c\xef\x60\x13\x67\x6c\x8b\xe3\xf6\x33\x3e\x2e\x0c\x43\ +\x05\xa7\x82\xf3\x38\xf3\x7d\x1f\x61\x18\x1a\x72\x87\x1e\xac\xae\ +\xe7\xd0\xd6\xbd\xca\x61\xce\xad\x56\x0b\xbe\xef\x3f\xda\x46\xc6\ +\x52\xc2\x64\x32\x41\x51\x14\x88\xa2\x08\x61\x18\x1a\xe0\x85\x61\ +\x88\xe1\x70\x08\xcf\xf3\xcc\xf0\x2b\xd9\xd1\xb2\xd9\x6c\xd0\x6e\ +\xb7\x1f\x91\x28\x5c\xf9\xc7\x03\xc1\xee\x16\xb1\x81\x78\x88\xf8\ +\xa9\x23\x89\x98\x2b\xf2\xe0\xa8\x63\x85\xa5\x57\x66\x64\xc0\x03\ +\xa8\x28\x0a\xb3\x3d\x5b\x9b\xac\x15\x9c\x47\x33\xb6\x51\x14\xc1\ +\xf7\x7d\xa4\x69\x6a\xd4\x3b\x94\xa7\xc9\x12\x88\xcd\x82\xda\x39\ +\x21\x57\x0d\x74\x3a\x9d\x1d\x21\xb8\xbc\x78\x19\x06\x52\x3d\x33\ +\x1a\x8d\x0c\xa9\xc2\x7a\x6a\x55\x79\xa4\xce\xc3\xb5\xdb\x6d\x9c\ +\x9d\x9d\xed\x0c\x1c\x3b\x04\x9e\xaa\x9f\xab\x48\x21\xb9\xa5\x9b\ +\xec\xf2\x21\x52\x89\xe1\xad\xdc\x3f\x4a\xa5\x53\x18\x86\x66\x52\ +\xbe\xda\x7f\x0e\x32\xfd\x08\xf6\x93\x42\xec\xe0\x67\x49\x45\xe6\ +\x83\x14\xb7\xdb\x82\x6f\x7a\x08\x12\x24\xcd\x66\x73\xa7\x5d\x8c\ +\xcb\x84\x64\x4e\x28\x73\x45\xb2\x9d\x9c\x84\x20\x77\x77\xca\x95\ +\x81\xfb\x0e\x05\x86\xb5\xbe\xef\x63\x30\x18\x20\x0c\xc3\x47\xa3\ +\x2e\xab\xc0\x58\x37\xe3\xc8\x06\x1c\xc9\x1b\x19\xba\x1e\x03\x68\ +\xe9\x41\xe5\x5e\x99\x7e\xbf\x8f\x28\x8a\xf4\xa2\x53\x70\x1e\x9f\ +\x77\xb6\xdb\xed\x47\x03\x9c\xe5\xbc\x5a\x00\x66\xe5\x9f\x2c\x5f\ +\x50\x47\xcb\x90\x93\x3a\xd9\xf9\x7c\x6e\x4a\x06\x87\xba\x2f\xec\ +\xe5\x46\xf6\x7b\x93\xb5\x45\x9b\x10\x92\x60\xa0\xf0\x81\xab\x1b\ +\x38\x29\xb0\x4e\x5b\xbb\x4f\x43\x4b\x60\x92\x20\x23\x01\xb4\x0f\ +\x90\x55\x53\xe4\xc3\x30\x34\x8c\xad\xe3\x38\x18\x8d\x46\x08\x82\ +\x40\x2f\x3a\x0d\x6b\x9f\xf0\x01\xb9\x2e\xfa\xfd\x3e\x3a\x9d\x8e\ +\x99\x0b\x24\x3d\x13\x73\xad\x56\xab\xb5\x13\xe2\x51\x90\x4e\x19\ +\xa0\xec\xdf\xe4\xfd\x39\x95\xa0\xaa\xb4\xc1\x3d\x96\xd2\xc3\xda\ +\xb9\xe2\x3e\xcf\x69\xe7\x92\x7c\x7f\x24\xb4\xd2\x34\x45\x1c\xc7\ +\x3b\xd3\x1d\xea\xba\x50\xe4\x73\x72\x80\xd7\xbe\x89\x0a\x55\xef\ +\xc9\x16\x72\xb4\xdb\x6d\x33\x8e\x65\x30\x18\xa0\xdf\xef\x6b\x7d\ +\x53\x3d\xe7\xd3\xc1\xd9\xed\x76\xd1\xef\xf7\x77\x4e\x76\xe9\x59\ +\xd8\xe5\xcf\x46\x69\x8a\xde\xd9\x43\xc9\xae\x12\xd7\x75\x4d\x28\ +\x48\x6f\x6a\xb3\xa8\x04\xe6\x60\x30\xc0\xf9\xf9\xb9\x79\x9e\x3a\ +\xb6\x75\x5f\x8e\x58\xd5\xcc\x2c\xc1\x51\xe7\xa9\xea\xf6\x73\xba\ +\xae\x6b\x86\x77\x91\x75\xae\x6b\xd4\xb6\x3d\xbc\xfc\x5d\xab\xd5\ +\xda\xf1\x9a\x67\x67\x67\xa6\x19\x40\x4d\x3d\xe7\x93\x8c\xf3\x56\ +\xa9\xde\xb1\x1b\x85\xe5\x44\x02\x59\x0b\xa5\xc7\x64\xa3\x34\xd7\ +\x05\xb2\x47\x74\x38\x1c\xc2\x71\x1c\x24\x49\xb2\xa3\x27\x6d\xb7\ +\xdb\x66\x6d\xfd\xbe\xb1\x98\x76\x97\xcc\x3e\x60\x56\x49\xf7\xe4\ +\xd0\xea\x43\x53\xde\xc9\x36\xf3\xb0\xa9\x62\x7b\x0f\x79\x50\x7e\ +\x6e\x61\x18\x9a\xb6\xb9\x6e\xb7\x8b\x93\x93\x13\x65\x69\xd5\x73\ +\xfe\xb6\xd0\x76\x30\x18\x98\xdc\x52\xd6\x0e\xeb\x26\x02\xa4\x69\ +\x8a\xc5\x62\xf1\x08\xa8\x04\xf3\x62\xb1\x40\xb7\xdb\x35\x35\x4c\ +\xbb\xbe\x58\x95\xab\x55\xe5\x87\xc7\x84\x95\xf6\x74\x3d\x86\xb3\ +\xac\xa1\x52\xd3\x5a\x35\x05\x81\x39\x26\x81\x59\xf5\x9c\x75\xa0\ +\xb6\x6f\x0f\x82\x00\xbe\xef\x1b\x25\xd4\xd9\xd9\x19\xba\xdd\xae\ +\xb2\xb4\xea\x39\xff\xff\xc6\xc6\xe8\x87\x87\x87\xbd\x13\xe4\xec\ +\x3a\x22\x19\xde\xa2\x28\x4c\x01\x9e\x4c\x30\x19\x5c\xf6\x75\xda\ +\x13\xe6\xab\x40\x6f\x7f\x3d\x54\xa3\xac\xfa\x3d\x73\x50\x92\x42\ +\x14\x02\x50\x18\xc0\xbc\x54\x8e\x45\x61\x68\x2e\x95\x4e\xf6\xf3\ +\xef\x7b\x2f\x32\x9c\xe6\xc6\xb4\x7e\xbf\x8f\xb3\xb3\xb3\x47\x13\ +\xe8\xd5\x14\x9c\x4f\xf6\x9e\xc3\xe1\x10\xc3\xe1\x10\x59\x96\x3d\ +\xea\xde\x97\xfd\x89\x55\xbb\x42\xe4\x18\x0f\xe9\x1d\x29\x04\x27\ +\x58\x39\x20\x4c\x8e\x2c\xa9\x2a\xd3\xc8\xe7\xae\x62\x74\xf9\x9c\ +\xd2\x93\xdb\x9b\xa8\x65\x1f\x2a\x65\x75\x72\xfb\x34\xff\x3e\xf6\ +\x8f\xd2\x0e\x85\xc1\xf6\xe1\xc1\x43\x8a\xb9\xe6\x7c\x3e\x37\xb9\ +\xe6\x70\x38\x54\xaf\xa9\x61\xed\x6f\xb7\x4e\xa7\x83\x8b\x8b\x0b\ +\x93\x33\xd9\x53\xf4\xea\x58\xd3\x2a\x72\x45\xee\xca\x64\xe8\xcb\ +\xa9\x7d\xd3\xe9\x14\xb7\xb7\xb7\xc8\xb2\x6c\xe7\x7e\x76\xd8\x2c\ +\x67\x16\xd5\x6d\xce\xb6\xcb\x2c\x55\xbd\x9b\xf6\x6d\x52\xf4\xbf\ +\xef\xef\xd9\xc7\x16\xdb\xb7\xcb\x3d\x9c\x69\x9a\x62\x30\x18\xe0\ +\xea\xea\x4a\xbd\xa6\x82\xf3\x2b\x7d\x58\xff\x51\xed\x9c\x9d\x9d\ +\x19\x85\x50\x95\x30\xc0\xbe\xf0\xab\x58\x4c\xfb\xe2\x9d\xcf\xe7\ +\x66\x76\x90\xeb\xba\x98\x4c\x26\x98\x4c\x26\xe6\x75\x8f\x25\x5f\ +\xf6\xe5\xc1\x55\x87\x88\x5d\xa2\xe1\x2e\x15\xee\x84\x61\x1f\x69\ +\x1d\x19\x55\xb7\x8c\xd7\x36\x2e\x78\x8a\xe3\x18\xbe\xef\xe3\xf9\ +\xf3\xe7\x18\x0c\x06\x3a\x69\x4f\xc1\xf9\x75\x99\xdb\xab\xab\x2b\ +\x9c\x9c\x9c\x18\x62\x85\xde\x46\x0a\xd0\xeb\x72\xc5\xaa\xdb\xe5\ +\x5e\x4f\x00\x66\x9f\xe6\x64\x32\xc1\x74\x3a\xad\x95\xec\x49\x82\ +\x67\x9f\x47\xab\x6a\xd0\xae\x3a\x78\x48\x0a\xb1\x31\x3c\xcb\x32\ +\x13\xd2\x56\x1d\x38\xb2\xc4\x53\xe7\x85\xf9\xdc\x2c\x9d\x94\x65\ +\x89\xf3\xf3\x73\x5c\x5d\x5d\xe9\xc4\x03\xcd\x39\xbf\xae\x39\x8e\ +\x83\xc1\x60\x80\x17\x2f\x5e\x98\xe9\x76\x52\x0f\x4b\x90\xda\x8b\ +\x90\x0e\x8d\xfe\xa0\xcd\xe7\x73\x74\x3a\x1d\x44\x51\x84\x34\x4d\ +\x31\x99\x4c\x4c\xad\xd5\x26\x9a\x78\x71\x4b\x31\x3c\x7f\x96\x1a\ +\x60\x8a\x25\x6c\x01\x85\xdd\x29\x22\x77\x92\x66\x59\x86\x38\x8e\ +\x4d\xae\x59\xe7\x79\xab\x84\xf4\x04\xa4\x3c\x88\xd8\x40\x3e\x1c\ +\x0e\xf1\xf2\xe5\x4b\xad\x6b\x2a\x38\x7f\x3f\x72\xe8\xfc\xfc\x1c\ +\x59\x96\x99\x7a\x9d\xcc\xeb\xe8\x29\xaa\xda\xcb\xe4\x8a\x84\xaa\ +\x1c\x90\x63\x3e\x38\xbe\x24\xcf\x73\xb3\xc1\x8c\xb2\x3b\x5b\x30\ +\x5f\x35\x43\xc8\x9e\xaf\x6b\xf7\x7f\xd2\x5b\xf3\x3e\xec\x3b\x65\ +\xb8\x4e\x85\x53\x95\x57\xb6\x3d\x64\x5d\xfb\x19\x8d\x65\x9b\x4e\ +\xa7\x83\x17\x2f\x5e\x60\x34\x1a\x69\x38\xab\x61\xed\xef\x1b\xde\ +\x5e\x5e\x5e\xe2\xf2\xf2\xd2\x10\x39\x72\x22\x1f\x4b\x12\x72\xf0\ +\xd7\xa1\x95\x7a\x04\x8d\xdc\xf0\x1c\x45\x11\x36\x9b\x0d\xee\xef\ +\xef\x31\x9b\xcd\x76\x46\x63\xd6\xf5\x66\x56\x2d\x18\x92\x60\xa0\ +\xa4\x90\x60\xa7\x4e\x58\x7a\xd9\xaa\xc9\xf0\x75\xde\xd3\x0e\x8f\ +\xe5\x4a\x05\x1e\x46\xad\x56\x0b\x97\x97\x97\x78\xf6\xec\x99\x0a\ +\x0e\xd4\x73\xfe\xfe\x16\x45\x11\x2e\x2f\x2f\xb1\x5c\x2e\x71\x77\ +\x77\x67\xd8\x55\x5e\xa4\x1c\xf9\x48\x40\xd8\xeb\xf3\xaa\x84\x0c\ +\x72\x0f\x4b\x1c\xc7\x08\x82\x00\xed\x76\x7b\x67\x31\xef\x7a\xbd\ +\x36\x6c\x6a\x9d\x00\xc0\x5e\x8b\x60\x77\xa3\x48\x26\x57\x82\x89\ +\x9e\x9b\xe1\xed\xbe\xd5\x0c\x55\x23\x4c\x6c\xb6\x98\xaa\xa8\x8b\ +\x8b\x0b\xbc\x78\xf1\x42\xc3\x59\x05\xe7\x1f\xc7\xde\xf6\xfb\x7d\ +\x3c\x7f\xfe\x1c\x9b\xcd\x06\xe3\xf1\xd8\x00\x54\x5e\xf4\x76\x28\ +\x6b\x7b\x22\x39\xfd\x5c\x82\x80\xe4\x4c\xa3\xd1\xc0\x70\x38\x34\ +\xe3\x31\xd9\xa8\x6c\xef\xd7\xac\x1b\x91\x29\x27\xab\xcb\x55\x0f\ +\x12\x50\x3c\x48\x38\xc6\x53\x4e\x34\xd8\x37\xd8\x4b\x7e\x6f\xb3\ +\xb6\x8c\x1c\x2e\x2e\x2e\xf0\xea\xd5\x2b\x93\x37\xab\x29\x38\xff\ +\x10\xe3\x78\x11\x5e\x90\xd3\xe9\xd4\xe4\xa2\xcc\x4f\xab\x98\xd6\ +\xaa\x19\x40\x72\x4d\x81\x2c\xcc\x73\x4d\x03\xf5\xad\xfb\xf2\x3d\ +\xf9\x7b\x12\x46\xb2\x83\xc6\x16\x34\xb0\xfe\xc8\x81\x63\x0c\x41\ +\xa9\x05\xae\x0a\x93\x09\xf8\x7d\xa0\x64\x33\xf9\xc5\xc5\x05\x7e\ +\xfe\xf9\x67\x6d\xa4\x56\x70\xfe\x39\xec\xad\xeb\xba\x38\x39\x39\ +\x31\x60\x9d\x4c\x26\xc8\xb2\xcc\x5c\xdc\x12\x6c\x75\xc3\xb6\xaa\ +\x04\x0a\xd2\xdb\x25\x49\x82\xa2\x28\x10\x04\xc1\x8e\x16\xd6\x9e\ +\x97\x6b\xb3\xaf\x92\x99\xb5\x5f\x87\x92\x3d\x39\xd1\xa1\xd7\xeb\ +\x21\xcf\x73\xcc\x66\xb3\x47\xcb\x82\xf7\xd5\x72\xe5\x7b\x27\x7b\ +\x7d\x71\x71\x81\x5f\x7e\xf9\x05\xa3\xd1\x48\xdb\xc1\x14\x9c\x7f\ +\x1e\x40\x9b\xcd\xa6\x61\x21\x1d\xc7\xd9\x19\x0d\x42\xaf\x65\x87\ +\xaf\xb6\x07\xab\x1b\x21\xc2\x16\xad\x3c\xcf\x91\xa6\x29\x56\xab\ +\x95\x11\x90\x57\xf5\x61\xca\x19\x47\x75\xf5\x4f\x2e\x48\xaa\xea\ +\x15\xad\xda\xa2\xb6\x4f\xd8\x20\xbd\x32\x95\x53\x57\x57\x57\xf8\ +\xf9\xe7\x9f\x31\x1c\x0e\x15\x98\x0a\xce\x6f\xc7\x83\xba\xae\x8b\ +\x0f\x1f\x3e\x00\x80\x59\xa7\x20\xc3\x57\x7a\xb7\x2a\x50\xca\xfc\ +\xd0\x66\x48\xc9\xb2\x72\x5a\xba\x24\x8f\xe4\xce\x16\x3b\xbc\xb5\ +\xf5\xbd\x6c\x72\xae\xcb\x1b\xe5\x50\xeb\x2a\xd1\x83\x2d\x40\xa0\ +\x77\x5e\xad\x56\x68\xb5\x5a\xf8\xe9\xa7\x9f\xf0\xe2\xc5\x0b\x23\ +\xa6\x50\x53\x70\x7e\x33\x1e\x74\x30\x18\x98\x89\x01\x77\x77\x77\ +\xa6\x09\x5b\xb2\xab\xf4\x34\x54\x15\x55\x85\xb8\x64\x7c\x59\x8a\ +\xe1\x98\x4c\xe6\x9d\x12\x20\xbc\x9f\x0d\x38\x49\xec\xb0\xc4\xc3\ +\x7c\xb2\xca\x5b\xf3\x76\x3b\x37\xb5\x45\xf3\xb6\x7e\x97\x4b\x9a\ +\x5e\xbe\x7c\x89\xab\xab\x2b\x33\x27\x49\x4d\xc1\xf9\xcd\xb1\xb8\ +\xdd\x6e\x17\xaf\x5e\xbd\x82\xef\xfb\xf8\xf4\xe9\x93\x99\x82\xce\ +\xdb\xb9\xde\x8f\xa4\x8d\xf4\x66\x54\xf7\xb0\xad\x4b\x82\xcf\xde\ +\xd6\x25\x41\xc5\xe9\xf4\x24\xa2\x38\x89\x81\x0d\xe2\x04\x68\x1d\ +\x39\x25\x01\x5c\xb7\xbc\xc8\x16\xd4\x13\xc4\x27\x27\x27\x78\xf1\ +\xe2\x05\xce\xce\xce\x4c\xdd\x57\x4d\xc1\xf9\xcd\x7a\xd1\x20\x08\ +\xf0\xf2\xe5\x4b\x74\xbb\x5d\x5c\x5f\x5f\xe3\xe1\xe1\xc1\x4c\x3e\ +\xa7\xb0\x1c\xf8\xaf\x28\x9c\x6b\x0a\xa8\xd9\x9d\xcf\xe7\x28\xcb\ +\xd2\x4c\x82\x67\x8e\x59\x35\xfb\x96\xc0\x02\x60\xfa\x25\x65\x19\ +\xa7\xaa\xbb\xc5\x06\x1e\x1f\xcb\x43\xc2\x16\x1c\xd8\x1d\x30\xec\ +\x0b\x7d\xf6\xec\x19\x7e\xfa\xe9\x27\xf4\x7a\x3d\xf3\xfe\xd5\x14\ +\x9c\xff\x33\x79\x68\x18\x86\xb8\xbb\xbb\xc3\xed\xed\xad\x21\x8b\ +\x64\xc9\x43\xea\x60\xe9\xcd\xe8\x2d\x5b\xad\xd6\x4e\x9e\xc9\x71\ +\x94\x9e\xe7\xc1\xf3\x3c\xac\xd7\x6b\x43\xee\x70\x2a\x1e\x73\x5d\ +\x12\x3c\x72\xbd\xbd\x5c\xf6\x2b\xf3\x48\xae\x86\xb0\xf3\x49\x09\ +\x48\xd9\xfb\x19\x04\x01\x9e\x3f\x7f\x6e\xd6\x19\xda\x64\x97\xda\ +\x6f\xbc\x7e\xb6\x87\x96\x7e\xa8\x7d\x15\x23\x61\x12\xc7\x31\xc6\ +\xe3\x31\xc6\xe3\xb1\x91\xe3\xc9\xc9\xef\x12\x50\xb2\x34\x42\xad\ +\x2d\x67\xe1\xf2\xf7\x9c\x01\xb4\x5c\x2e\x11\x45\x11\xfa\xfd\x3e\ +\xe2\x38\x46\x1c\xc7\x3b\x84\x93\x7c\x4e\x09\x50\x02\x8e\xe5\x13\ +\xb6\x87\xd9\x4d\xde\xbc\x1f\x87\x7c\x0d\x87\x43\x5c\x5c\x5c\xa8\ +\xb7\x54\x70\x7e\x5f\x20\x2d\xcb\x12\x71\x1c\x63\x3a\x9d\x62\x3c\ +\x1e\x63\x32\x99\x98\xfd\x28\x52\x90\x6e\x0b\x12\x38\x31\xbe\xd3\ +\xe9\x98\xe5\xb9\x92\xf8\x89\xa2\x08\xdd\x6e\x17\x8b\xc5\xc2\x80\ +\x53\x0a\x1c\xa4\xa7\x26\x39\xc5\x59\xba\x9c\x42\x2f\x15\x4b\x04\ +\x3d\x47\x7c\x72\x2a\xfb\xe9\xe9\x29\x06\x83\x81\xf1\xf6\x6a\x0a\ +\xce\xef\xca\x48\xd6\x24\x49\x82\xd9\x6c\x86\xf1\x78\x8c\xe9\x74\ +\x6a\x88\x1d\x99\x4f\x4a\xaf\xc7\x29\x05\xdc\x1b\xca\x11\x9b\x5c\ +\x1d\x11\x45\x11\x66\xb3\x19\xbe\x7c\xf9\x62\xbc\xa0\xad\x48\xa2\ +\x17\x67\xce\x2b\x57\xbd\xf3\x7d\xf1\x36\xee\x8b\x19\x0c\x06\x18\ +\x8d\x46\x66\xc0\xb6\x32\xb1\x0a\xce\xef\xd6\x7b\x12\x08\x72\x08\ +\x75\x92\x24\x98\xcf\xe7\x88\xe3\xd8\xd4\x1a\x65\x4d\x51\xd6\x40\ +\xe5\x73\x31\x07\x3c\x3f\x3f\xc7\xc9\xc9\x09\x1e\x1e\x1e\xf0\xf1\ +\xe3\xc7\x47\x3b\x3b\xeb\x86\x47\x4b\x21\x01\xc1\x1a\x04\x81\x09\ +\x93\x39\x54\x5b\x41\xa9\x84\xd0\x0f\x05\x4c\x86\x9c\xec\x32\x69\ +\xb7\xdb\x18\x0c\x06\xc8\xb2\xcc\xfc\x2b\x8a\x62\x67\x2a\x5e\xd5\ +\x58\x90\x24\x49\xcc\xf3\x4f\x26\x13\x24\x49\xf2\x48\x91\x44\x0f\ +\x5c\x35\xf8\xb9\xd5\x6a\x99\x36\xb2\x76\xbb\x6d\xbe\x72\x75\xa1\ +\xdd\x55\xa3\xa6\xe0\xfc\x6e\x01\x6a\x97\x27\x24\x29\xc4\x92\x0a\ +\xc7\x48\x32\xcc\x64\xfe\xc7\x55\x08\x12\xac\xac\x4d\xf2\x76\x32\ +\xb4\xf4\xb8\xfc\x2a\xc3\x63\xce\x0b\xe2\x42\x26\xd6\x56\x29\xa0\ +\x90\x80\x94\xde\x5b\x4d\xc1\xf9\xfd\xe7\x14\xc2\x93\xf1\xab\xad\ +\x6d\x25\x88\x82\x20\xd8\x01\x24\xbf\x97\x8a\x1d\xdf\xf7\xcd\xf0\ +\xea\x4e\xa7\x53\xd9\x3f\x4a\x86\x97\xa4\x10\x47\x70\x32\x7f\xad\ +\xdb\x64\x66\x1f\x2e\xea\x3d\x15\x9c\xdf\x25\x20\x25\x30\xe5\xef\ +\x25\x58\xab\xca\x1d\xf2\xab\xed\x81\x01\x18\x90\xf9\xbe\xbf\x33\ +\xc9\xa0\x6a\xc0\x97\xf4\xa2\xf2\xeb\xa1\x7f\x0a\x4a\x05\xe7\x77\ +\x6f\xb6\xa7\xb4\x5b\xbb\x6c\x45\x4f\x55\xd3\xb3\x3d\x7f\xc8\x1e\ +\x0d\x52\xa5\xd9\xad\x92\xee\x55\xd5\x42\x25\xf9\xa4\xa0\x54\x70\ +\xfe\x50\xde\x53\x76\xa1\x48\xb0\xd6\xad\x5c\xa8\x6b\xdb\xb2\x43\ +\xcd\x2a\x26\xb6\x8a\xdd\xad\xdb\xbf\x62\x7b\x75\xbb\xab\x45\xed\ +\x0f\xbc\x4e\xb4\x94\xf2\x6d\x91\x44\x75\x5b\xab\xeb\x06\x44\x57\ +\x79\xc2\x7d\x80\xde\xf7\xb8\x3a\xb0\xab\x29\x38\xd5\x9e\x00\xe4\ +\x63\x40\x54\x05\xcc\x7d\x80\x54\x53\x70\xaa\xa9\xa9\x1d\xc3\x4b\ +\xe8\x47\xa0\xa6\xa6\xe0\x54\x53\x53\x53\x70\xaa\xa9\x29\x38\xd5\ +\xd4\xd4\x14\x9c\x6a\x6a\x0a\x4e\x35\x35\x35\x05\xa7\x9a\x9a\x9a\ +\x82\x53\x4d\x4d\xc1\xa9\xa6\xa6\xa6\xe0\x54\x53\x53\x70\xaa\xf2\ +\xa6\xcf\x7c\x00\x00\x00\x25\x49\x44\x41\x54\xa9\xa9\x29\x38\xd5\ +\xd4\xd4\x14\x9c\x6a\x6a\x0a\x4e\x35\x35\x35\x05\xa7\x9a\x9a\x82\ +\x53\x4d\x4d\x4d\xc1\xa9\xa6\xf6\x23\xda\xff\x01\xb7\xbe\xbf\x34\ +\x7c\xc5\x7a\x2d\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\ +\x00\x00\x04\x7f\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09\x70\x48\x59\x73\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01\ +\x5e\x1a\x91\x1c\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x01\x0a\ +\x17\x02\x30\xf6\x73\x58\xfb\x00\x00\x03\xff\x49\x44\x41\x54\x48\ +\xc7\xbd\x95\x6b\x4c\x9b\x75\x14\xc6\x9f\xb7\xd7\xc1\xe8\xe8\xca\ +\x66\x3b\x4a\xc9\x80\x7a\x03\x5a\xca\x8c\x81\x99\x60\x5e\x37\x2e\ +\x92\x2c\xd9\x07\x2c\x4a\xe4\x36\x32\x34\xc6\x84\x0c\xe2\x40\x12\ +\x63\x5f\xb2\xa8\x89\x41\xe2\x52\xd6\x80\x51\xc1\x38\xa5\xc8\x75\ +\x1b\x42\x3b\x88\x62\x59\x02\x6c\x0c\x07\x04\x4a\xe2\x56\x20\x8b\ +\x5c\x02\x65\xa9\xb6\x89\xbc\x57\xbf\xa0\x21\x0a\x8c\x6e\xd1\xf3\ +\xf9\x7f\xce\xef\xfc\x9f\x73\x9e\x1c\xe0\x3f\x0e\xe2\x51\xf3\x2a\ +\x2a\x2a\x4c\x99\x99\x99\x65\xa1\xa1\xa1\xa9\x1c\xcf\xc9\xfc\x7e\ +\xdf\xdd\x55\xef\x5a\x6b\x6d\x4f\xdd\xe5\x99\xb6\x19\xfa\xb1\x9a\ +\xb2\x58\xde\x2b\x59\x5c\x5a\xa4\x39\x8e\x15\x7c\x3e\x9f\x30\x37\ +\x3f\x27\xdc\x1e\x1f\x13\x3a\x3a\xdb\xf8\x8a\x77\xce\x59\x01\x48\ +\xfe\x7a\x2c\xd9\x9a\x39\x38\x38\x28\x3c\xac\x7a\x4d\x4d\x8d\x3e\ +\xf5\xf8\x0b\xf5\x52\xa9\x54\xca\x32\x1c\xc4\x12\x31\x5a\xed\x76\ +\xbe\xa3\xa3\xc3\x73\xe7\xce\x44\x0f\xc3\x30\x76\x00\xc2\xb6\x80\ +\x4d\xc8\xae\x00\x85\x62\xff\xe9\xf0\x70\x45\x08\xc7\x71\x60\x58\ +\x1a\x04\x41\xa0\xb7\xcf\xf1\xf5\xad\x5b\x63\xd5\x14\x45\x2d\x92\ +\x24\x79\x8e\x24\x49\x62\x47\x00\x00\x50\x14\xb5\xd3\x6c\xc4\x45\ +\x45\xf9\x76\x8f\x67\x1e\x31\x31\x7a\x30\x0c\x03\x91\x58\x8c\x23\ +\x1a\x8d\x1b\xc0\xf2\x76\x09\xa2\x20\xf5\x0f\x79\x36\x3e\xe1\x79\ +\xb7\x7b\x06\x2c\xcb\x80\x65\x59\xb0\x0c\x8b\xc2\xc2\x82\xf4\xf8\ +\xf8\x78\xe9\xd6\xc6\xaa\xaa\xaa\xc2\x83\x06\xa4\xa5\xa5\x3d\xa5\ +\xd7\xc7\x45\x4b\xa5\x32\xb8\x67\xdc\x20\x08\x02\x2c\xcb\x20\x56\ +\x1f\x97\x5e\x5c\x5c\xd0\x5b\x56\x56\x16\xbd\xbc\x7c\xff\xb9\xeb\ +\xd7\x9d\x5f\x28\x0e\x84\x8d\x05\x0b\x20\x48\xf2\xc5\x53\x5a\xad\ +\x96\xd0\x6a\xb5\xe8\xec\xea\xc6\x47\x1f\xd7\x82\x65\x59\xec\x93\ +\xcb\x11\xa5\x8b\x3e\x19\xba\x3f\x64\x56\x1f\xf7\xcc\x58\x64\x64\ +\x64\xc9\x90\x6b\xe8\x41\xb0\x00\x79\xa2\xc1\x98\x73\xf7\xde\x1c\ +\x3a\xbb\x7b\x60\x3a\x76\x0c\x3d\x0e\x07\x26\xa7\xa6\x00\x01\xd0\ +\xe9\x74\x08\x0b\x0b\x0b\x91\xed\x93\x11\x23\xa3\xa3\x6c\x7f\xff\ +\x40\x4b\x30\x00\x82\xa2\xde\x37\x27\x9b\x4c\x46\x43\x42\x02\x7e\ +\x5d\x5a\x82\x58\x24\x42\x6c\x4c\x0c\x5a\xdb\xdb\xc1\xf3\x3c\x94\ +\x4a\x25\x08\x82\xc0\xac\x7b\x56\xa8\xae\xac\x76\xf2\x3c\xdf\xb2\ +\xe3\x16\xfd\xb3\xb8\xcd\x66\xcd\x4b\x49\x39\xde\xb8\xb2\xb2\x02\ +\x83\xc1\x80\xec\xac\x0c\xf4\x3a\x1c\x38\x1a\xa5\x85\x29\xc9\x08\ +\x99\x5c\x86\xd5\xd5\x15\xac\x7b\xd7\x99\xd7\x2d\xf9\xdf\x33\x0c\ +\x53\x0d\x60\x05\xdb\x78\x40\xa0\x28\xea\x6f\x93\x98\xcd\x66\x71\ +\x73\xf3\x97\x96\xcb\xdf\x7e\xc3\x65\x65\x65\x09\x29\xa9\x29\x82\ +\xcb\xe5\x12\x3c\x9e\x7b\xc2\x8f\x83\x3f\x70\x79\x79\xaf\xfd\xf6\ +\x12\x49\xce\x91\x24\xe9\x12\x8b\xc5\x56\x00\x85\x9b\x66\xdd\xdd\ +\x07\x00\x50\x52\x52\xa2\x38\x91\x71\xa2\x69\x7a\x6a\x3a\xc7\xe9\ +\x74\x22\x42\x15\x01\x9d\x2e\x1a\x6a\x8d\x9a\xbf\x74\xc9\x76\xd3\ +\x6a\xb5\x5e\xa3\x69\xfa\x36\x80\x39\x00\x6b\x00\xfc\x14\x45\x6d\ +\x6c\xa6\x0b\xbb\x02\xca\xcb\xcb\xf5\xa6\x64\x43\x47\x5b\xeb\x77\ +\xc6\xf1\xf1\x9f\xa1\x52\xa9\xc0\xb2\x2c\xce\x14\x17\x09\x36\x9b\ +\xad\xeb\xe2\xa7\x17\x2b\x01\x2c\x00\xe0\x1e\xa6\xef\xbf\x00\x7e\ +\xbf\x2f\xfb\xc9\xa7\xe3\x46\x6c\xb6\x86\x88\xf9\xf9\x05\x48\xa4\ +\x12\xd0\x34\x8d\x97\xb3\xb3\x30\x39\x39\xe5\xda\x2c\xee\xd9\xeb\ +\xea\x6d\x05\x48\xaf\x5e\xed\xae\x3e\xac\x3e\x7c\xad\xa1\xe1\x33\ +\xc9\xda\xaa\x17\x22\x91\x08\x1c\xcb\x21\x29\x39\x09\xaa\x83\xaa\ +\x5f\x4a\x4b\xdf\x38\xbf\x29\x09\x82\x02\xe8\xf5\x7a\x79\xd1\x99\ +\xfc\x7a\xb5\x5a\x73\x76\x7a\x7a\x1a\x07\x14\x0a\xd0\x1b\x34\x02\ +\x81\x00\xa2\xa2\xa2\x90\x71\x32\xdd\x5b\x79\xbe\xaa\x12\xc0\xf8\ +\x56\x7d\xf7\x12\x22\x00\x78\xf3\xad\xd2\x2a\x8e\xe3\x72\x9b\x9a\ +\xbf\x7a\xd7\xbb\xfe\xe0\x15\xf3\xab\xe6\xfb\x4a\xa5\x12\x5a\xad\ +\x16\xc5\xc5\x85\x1b\x9f\xd4\xd6\x7d\xe0\xf5\x7a\xfb\xf6\xa2\xf9\ +\xb6\x3f\x38\x14\x71\xc8\x7c\xa5\xeb\xca\xcd\xe1\x1b\xc3\x8d\xc3\ +\x37\x86\x7d\x26\x43\x82\xc6\x68\x34\xd4\x27\x1a\x12\x79\x7b\x8b\ +\xfd\xf3\xdc\xdc\xdc\x3a\x00\x75\x8f\x72\x9d\x24\x00\x20\x97\xcb\ +\x7e\x57\x1f\xd1\xa8\x01\xa8\x01\xf8\xff\x60\x99\x27\x92\x93\x93\ +\x04\x47\x9f\xa3\xdb\x62\xa9\x79\xfb\xb1\x6f\x72\x63\xa3\xad\x20\ +\xfc\x60\x78\xd3\xe8\xc8\xe8\x08\x41\x10\x0b\x31\xb1\x47\x73\x7e\ +\x1a\x1c\xea\x6f\x6f\xeb\x2c\x0b\x76\xa8\x3b\x1d\x7d\xd1\x85\x0f\ +\x2f\x14\x31\x0c\x7d\x36\x10\x08\x10\x13\x13\x93\x3d\x03\xce\x81\ +\x66\x00\x4b\xc1\x0e\xf5\x7f\x8f\x3f\x01\x2e\x51\xa3\xa9\xc7\xde\ +\x69\x7b\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +" + +qt_resource_name = b"\ +\x00\x05\ +\x00\x6f\xa6\x53\ +\x00\x69\ +\x00\x63\x00\x6f\x00\x6e\x00\x73\ +\x00\x0d\ +\x0c\x7a\x4d\x87\ +\x00\x65\ +\x00\x72\x00\x69\x00\x63\x00\x57\x00\x65\x00\x62\x00\x33\x00\x32\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x08\ +\x03\xc6\x59\xa7\ +\x00\x70\ +\x00\x6c\x00\x75\x00\x73\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x11\ +\x0c\x97\x94\x07\ +\x00\x61\ +\x00\x64\x00\x42\x00\x6c\x00\x6f\x00\x63\x00\x6b\x00\x50\x00\x6c\x00\x75\x00\x73\x00\x31\x00\x36\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\ +\x00\x0d\ +\x0c\x56\x4d\x87\ +\x00\x65\ +\x00\x72\x00\x69\x00\x63\x00\x57\x00\x65\x00\x62\x00\x31\x00\x36\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x11\ +\x0c\x49\x94\x07\ +\x00\x61\ +\x00\x64\x00\x42\x00\x6c\x00\x6f\x00\x63\x00\x6b\x00\x50\x00\x6c\x00\x75\x00\x73\x00\x36\x00\x34\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\ +\x00\x08\ +\x0b\x07\x5a\x27\ +\x00\x65\ +\x00\x64\x00\x69\x00\x74\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x09\ +\x06\x98\x83\x27\ +\x00\x63\ +\x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x0b\ +\x00\xb0\xe2\x96\ +\x00\x6c\ +\x00\x6f\x00\x61\x00\x64\x00\x69\x00\x6e\x00\x67\x00\x2e\x00\x67\x00\x69\x00\x66\ +\x00\x0a\ +\x05\x78\x4f\x27\ +\x00\x72\ +\x00\x65\x00\x6c\x00\x6f\x00\x61\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x14\ +\x0b\x6a\x55\xa7\ +\x00\x62\ +\x00\x6f\x00\x78\x00\x2d\x00\x62\x00\x6f\x00\x72\x00\x64\x00\x65\x00\x72\x00\x2d\x00\x73\x00\x6d\x00\x61\x00\x6c\x00\x6c\x00\x2e\ +\x00\x70\x00\x6e\x00\x67\ +\x00\x0e\ +\x05\x17\xcb\xe7\ +\x00\x62\ +\x00\x72\x00\x6f\x00\x6b\x00\x65\x00\x6e\x00\x50\x00\x61\x00\x67\x00\x65\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x0b\ +\x00\xbd\xc0\x27\ +\x00\x73\ +\x00\x65\x00\x74\x00\x74\x00\x69\x00\x6e\x00\x67\x00\x2e\x00\x70\x00\x6e\x00\x67\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x02\ +\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x01\x00\x00\x3c\x75\ +\x00\x00\x01\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x72\xfd\ +\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x6d\ +\x00\x00\x01\x48\x00\x00\x00\x00\x00\x01\x00\x00\x52\x48\ +\x00\x00\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x43\xfd\ +\x00\x00\x00\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x27\ +\x00\x00\x00\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x37\xb0\ +\x00\x00\x01\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x47\x17\ +\x00\x00\x00\x8e\x00\x00\x00\x00\x00\x01\x00\x00\x17\x86\ +\x00\x00\x00\x6e\x00\x00\x00\x00\x00\x01\x00\x00\x13\x56\ +\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\x46\x00\x00\x00\x00\x00\x01\x00\x00\x10\x22\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/javascript.qrc Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,8 @@ +<!DOCTYPE RCC> +<RCC version="1.0"> +<qresource> + <file>javascript/jquery-ui.js</file> + <file>javascript/jquery.js</file> + <file>javascript/qwebchannel.js</file> +</qresource> +</RCC>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/javascript/jquery-ui.js Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,111 @@ +/*! + * jQuery UI 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI + */ +(function(c,j){function k(a,b){var d=a.nodeName.toLowerCase();if("area"===d){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&l(a)}return(/input|select|textarea|button|object/.test(d)?!a.disabled:"a"==d?a.href||b:b)&&l(a)}function l(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.16", +keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({propAttr:c.fn.prop||c.fn.attr,_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d= +this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this, +"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"),10);if(!isNaN(b)&&b!==0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind((c.support.selectstart?"selectstart": +"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,m,n){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(m)g-=parseFloat(c.curCSS(f,"border"+this+"Width",true))||0;if(n)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight, +outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c(this).css(h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c(this).css(h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){return k(a,!isNaN(c.attr(a,"tabindex")))},tabbable:function(a){var b=c.attr(a, +"tabindex"),d=isNaN(b);return(d||b>=0)&&k(a,!d)}});c(function(){var a=document.body,b=a.appendChild(b=document.createElement("div"));c.extend(b.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.offsetHeight===100;c.support.selectstart="onselectstart"in b;a.removeChild(b).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&& +a.element[0].parentNode)for(var e=0;e<b.length;e++)a.options[b[e][0]]&&b[e][1].apply(a.element,d)}},contains:function(a,b){return document.compareDocumentPosition?a.compareDocumentPosition(b)&16:a!==b&&a.contains(b)},hasScroll:function(a,b){if(c(a).css("overflow")==="hidden")return false;b=b&&b==="left"?"scrollLeft":"scrollTop";var d=false;if(a[b]>0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a<b+d},isOver:function(a,b,d,e,h,i){return c.ui.isOverAxis(a,d,h)&& +c.ui.isOverAxis(b,e,i)}})}})(jQuery); +;/*! + * jQuery UI Widget 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Widget + */ +(function(b,j){if(b.cleanData){var k=b.cleanData;b.cleanData=function(a){for(var c=0,d;(d=a[c])!=null;c++)try{b(d).triggerHandler("remove")}catch(e){}k(a)}}else{var l=b.fn.remove;b.fn.remove=function(a,c){return this.each(function(){if(!c)if(!a||b.filter(a,[this]).length)b("*",this).add([this]).each(function(){try{b(this).triggerHandler("remove")}catch(d){}});return l.call(b(this),a,c)})}}b.widget=function(a,c,d){var e=a.split(".")[0],f;a=a.split(".")[1];f=e+"-"+a;if(!d){d=c;c=b.Widget}b.expr[":"][f]= +function(h){return!!b.data(h,a)};b[e]=b[e]||{};b[e][a]=function(h,g){arguments.length&&this._createWidget(h,g)};c=new c;c.options=b.extend(true,{},c.options);b[e][a].prototype=b.extend(true,c,{namespace:e,widgetName:a,widgetEventPrefix:b[e][a].prototype.widgetEventPrefix||a,widgetBaseClass:f},d);b.widget.bridge(a,b[e][a])};b.widget.bridge=function(a,c){b.fn[a]=function(d){var e=typeof d==="string",f=Array.prototype.slice.call(arguments,1),h=this;d=!e&&f.length?b.extend.apply(null,[true,d].concat(f)): +d;if(e&&d.charAt(0)==="_")return h;e?this.each(function(){var g=b.data(this,a),i=g&&b.isFunction(g[d])?g[d].apply(g,f):g;if(i!==g&&i!==j){h=i;return false}}):this.each(function(){var g=b.data(this,a);g?g.option(d||{})._init():b.data(this,a,new c(d,this))});return h}};b.Widget=function(a,c){arguments.length&&this._createWidget(a,c)};b.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",options:{disabled:false},_createWidget:function(a,c){b.data(c,this.widgetName,this);this.element=b(c);this.options= +b.extend(true,{},this.options,this._getCreateOptions(),a);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()});this._create();this._trigger("create");this._init()},_getCreateOptions:function(){return b.metadata&&b.metadata.get(this.element[0])[this.widgetName]},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName);this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+ +"-disabled ui-state-disabled")},widget:function(){return this.element},option:function(a,c){var d=a;if(arguments.length===0)return b.extend({},this.options);if(typeof a==="string"){if(c===j)return this.options[a];d={};d[a]=c}this._setOptions(d);return this},_setOptions:function(a){var c=this;b.each(a,function(d,e){c._setOption(d,e)});return this},_setOption:function(a,c){this.options[a]=c;if(a==="disabled")this.widget()[c?"addClass":"removeClass"](this.widgetBaseClass+"-disabled ui-state-disabled").attr("aria-disabled", +c);return this},enable:function(){return this._setOption("disabled",false)},disable:function(){return this._setOption("disabled",true)},_trigger:function(a,c,d){var e=this.options[a];c=b.Event(c);c.type=(a===this.widgetEventPrefix?a:this.widgetEventPrefix+a).toLowerCase();d=d||{};if(c.originalEvent){a=b.event.props.length;for(var f;a;){f=b.event.props[--a];c[f]=c.originalEvent[f]}}this.element.trigger(c,d);return!(b.isFunction(e)&&e.call(this.element[0],c,d)===false||c.isDefaultPrevented())}}})(jQuery); +;/*! + * jQuery UI Mouse 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Mouse + * + * Depends: + * jquery.ui.widget.js + */ +(function(b){var d=false;b(document).mouseup(function(){d=false});b.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var a=this;this.element.bind("mousedown."+this.widgetName,function(c){return a._mouseDown(c)}).bind("click."+this.widgetName,function(c){if(true===b.data(c.target,a.widgetName+".preventClickEvent")){b.removeData(c.target,a.widgetName+".preventClickEvent");c.stopImmediatePropagation();return false}});this.started=false},_mouseDestroy:function(){this.element.unbind("."+ +this.widgetName)},_mouseDown:function(a){if(!d){this._mouseStarted&&this._mouseUp(a);this._mouseDownEvent=a;var c=this,f=a.which==1,g=typeof this.options.cancel=="string"&&a.target.nodeName?b(a.target).closest(this.options.cancel).length:false;if(!f||g||!this._mouseCapture(a))return true;this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet)this._mouseDelayTimer=setTimeout(function(){c.mouseDelayMet=true},this.options.delay);if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a)){this._mouseStarted= +this._mouseStart(a)!==false;if(!this._mouseStarted){a.preventDefault();return true}}true===b.data(a.target,this.widgetName+".preventClickEvent")&&b.removeData(a.target,this.widgetName+".preventClickEvent");this._mouseMoveDelegate=function(e){return c._mouseMove(e)};this._mouseUpDelegate=function(e){return c._mouseUp(e)};b(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);a.preventDefault();return d=true}},_mouseMove:function(a){if(b.browser.msie&& +!(document.documentMode>=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){b(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted= +false;a.target==this._mouseDownEvent.target&&b.data(a.target,this.widgetName+".preventClickEvent",true);this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery); +;/* + * jQuery UI Sortable 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Sortables + * + * Depends: + * jquery.ui.core.js + * jquery.ui.mouse.js + * jquery.ui.widget.js + */ +(function(d){d.widget("ui.sortable",d.ui.mouse,{widgetEventPrefix:"sort",options:{appendTo:"parent",axis:false,connectWith:false,containment:false,cursor:"auto",cursorAt:false,dropOnEmpty:true,forcePlaceholderSize:false,forceHelperSize:false,grid:false,handle:false,helper:"original",items:"> *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1E3},_create:function(){var a=this.options;this.containerCache={};this.element.addClass("ui-sortable"); +this.refresh();this.floating=this.items.length?a.axis==="x"||/left|right/.test(this.items[0].item.css("float"))||/inline|table-cell/.test(this.items[0].item.css("display")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var a=this.items.length-1;a>=0;a--)this.items[a].item.removeData("sortable-item");return this},_setOption:function(a,b){if(a=== +"disabled"){this.options[a]=b;this.widget()[b?"addClass":"removeClass"]("ui-sortable-disabled")}else d.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(a,b){if(this.reverting)return false;if(this.options.disabled||this.options.type=="static")return false;this._refreshItems(a);var c=null,e=this;d(a.target).parents().each(function(){if(d.data(this,"sortable-item")==e){c=d(this);return false}});if(d.data(a.target,"sortable-item")==e)c=d(a.target);if(!c)return false;if(this.options.handle&& +!b){var f=false;d(this.options.handle,c).find("*").andSelf().each(function(){if(this==a.target)f=true});if(!f)return false}this.currentItem=c;this._removeCurrentsFromItems();return true},_mouseStart:function(a,b,c){b=this.options;var e=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(a);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top, +left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]}; +this.helper[0]!=this.currentItem[0]&&this.currentItem.hide();this._createPlaceholder();b.containment&&this._setContainment();if(b.cursor){if(d("body").css("cursor"))this._storedCursor=d("body").css("cursor");d("body").css("cursor",b.cursor)}if(b.opacity){if(this.helper.css("opacity"))this._storedOpacity=this.helper.css("opacity");this.helper.css("opacity",b.opacity)}if(b.zIndex){if(this.helper.css("zIndex"))this._storedZIndex=this.helper.css("zIndex");this.helper.css("zIndex",b.zIndex)}if(this.scrollParent[0]!= +document&&this.scrollParent[0].tagName!="HTML")this.overflowOffset=this.scrollParent.offset();this._trigger("start",a,this._uiHash());this._preserveHelperProportions||this._cacheHelperProportions();if(!c)for(c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("activate",a,e._uiHash(this));if(d.ui.ddmanager)d.ui.ddmanager.current=this;d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(a); +return true},_mouseDrag:function(a){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!this.lastPositionAbs)this.lastPositionAbs=this.positionAbs;if(this.options.scroll){var b=this.options,c=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if(this.overflowOffset.top+this.scrollParent[0].offsetHeight-a.pageY<b.scrollSensitivity)this.scrollParent[0].scrollTop=c=this.scrollParent[0].scrollTop+b.scrollSpeed;else if(a.pageY-this.overflowOffset.top< +b.scrollSensitivity)this.scrollParent[0].scrollTop=c=this.scrollParent[0].scrollTop-b.scrollSpeed;if(this.overflowOffset.left+this.scrollParent[0].offsetWidth-a.pageX<b.scrollSensitivity)this.scrollParent[0].scrollLeft=c=this.scrollParent[0].scrollLeft+b.scrollSpeed;else if(a.pageX-this.overflowOffset.left<b.scrollSensitivity)this.scrollParent[0].scrollLeft=c=this.scrollParent[0].scrollLeft-b.scrollSpeed}else{if(a.pageY-d(document).scrollTop()<b.scrollSensitivity)c=d(document).scrollTop(d(document).scrollTop()- +b.scrollSpeed);else if(d(window).height()-(a.pageY-d(document).scrollTop())<b.scrollSensitivity)c=d(document).scrollTop(d(document).scrollTop()+b.scrollSpeed);if(a.pageX-d(document).scrollLeft()<b.scrollSensitivity)c=d(document).scrollLeft(d(document).scrollLeft()-b.scrollSpeed);else if(d(window).width()-(a.pageX-d(document).scrollLeft())<b.scrollSensitivity)c=d(document).scrollLeft(d(document).scrollLeft()+b.scrollSpeed)}c!==false&&d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this, +a)}this.positionAbs=this._convertPositionTo("absolute");if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";for(b=this.items.length-1;b>=0;b--){c=this.items[b];var e=c.item[0],f=this._intersectsWithPointer(c);if(f)if(e!=this.currentItem[0]&&this.placeholder[f==1?"next":"prev"]()[0]!=e&&!d.ui.contains(this.placeholder[0],e)&&(this.options.type=="semi-dynamic"?!d.ui.contains(this.element[0], +e):true)){this.direction=f==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(c))this._rearrange(a,c);else break;this._trigger("change",a,this._uiHash());break}}this._contactContainers(a);d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);this._trigger("sort",a,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(a,b){if(a){d.ui.ddmanager&&!this.options.dropBehaviour&&d.ui.ddmanager.drop(this,a);if(this.options.revert){var c=this;b=c.placeholder.offset(); +c.reverting=true;d(this.helper).animate({left:b.left-this.offset.parent.left-c.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:b.top-this.offset.parent.top-c.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){c._clear(a)})}else this._clear(a,b);return false}},cancel:function(){var a=this;if(this.dragging){this._mouseUp({target:null});this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"): +this.currentItem.show();for(var b=this.containers.length-1;b>=0;b--){this.containers[b]._trigger("deactivate",null,a._uiHash(this));if(this.containers[b].containerCache.over){this.containers[b]._trigger("out",null,a._uiHash(this));this.containers[b].containerCache.over=0}}}if(this.placeholder){this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove();d.extend(this,{helper:null, +dragging:false,reverting:false,_noFinalSort:null});this.domPosition.prev?d(this.domPosition.prev).after(this.currentItem):d(this.domPosition.parent).prepend(this.currentItem)}return this},serialize:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};d(b).each(function(){var e=(d(a.item||this).attr(a.attribute||"id")||"").match(a.expression||/(.+)[-=_](.+)/);if(e)c.push((a.key||e[1]+"[]")+"="+(a.key&&a.expression?e[1]:e[2]))});!c.length&&a.key&&c.push(a.key+"=");return c.join("&")}, +toArray:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};b.each(function(){c.push(d(a.item||this).attr(a.attribute||"id")||"")});return c},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,e=this.positionAbs.top,f=e+this.helperProportions.height,g=a.left,h=g+a.width,i=a.top,k=i+a.height,j=this.offset.click.top,l=this.offset.click.left;j=e+j>i&&e+j<k&&b+l>g&&b+l<h;return this.options.tolerance=="pointer"||this.options.forcePointerForContainers|| +this.options.tolerance!="pointer"&&this.helperProportions[this.floating?"width":"height"]>a[this.floating?"width":"height"]?j:g<b+this.helperProportions.width/2&&c-this.helperProportions.width/2<h&&i<e+this.helperProportions.height/2&&f-this.helperProportions.height/2<k},_intersectsWithPointer:function(a){var b=d.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,a.top,a.height);a=d.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,a.left,a.width);b=b&&a;a=this._getDragVerticalDirection(); +var c=this._getDragHorizontalDirection();if(!b)return false;return this.floating?c&&c=="right"||a=="down"?2:1:a&&(a=="down"?2:1)},_intersectsWithSides:function(a){var b=d.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,a.top+a.height/2,a.height);a=d.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,a.left+a.width/2,a.width);var c=this._getDragVerticalDirection(),e=this._getDragHorizontalDirection();return this.floating&&e?e=="right"&&a||e=="left"&&!a:c&&(c=="down"&&b||c=="up"&&!b)}, +_getDragVerticalDirection:function(){var a=this.positionAbs.top-this.lastPositionAbs.top;return a!=0&&(a>0?"down":"up")},_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){this._refreshItems(a);this.refreshPositions();return this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(a){var b=[],c=[],e=this._connectWith(); +if(e&&a)for(a=e.length-1;a>=0;a--)for(var f=d(e[a]),g=f.length-1;g>=0;g--){var h=d.data(f[g],"sortable");if(h&&h!=this&&!h.options.disabled)c.push([d.isFunction(h.options.items)?h.options.items.call(h.element):d(h.options.items,h.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),h])}c.push([d.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):d(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), +this]);for(a=c.length-1;a>=0;a--)c[a][0].each(function(){b.push(this)});return d(b)},_removeCurrentsFromItems:function(){for(var a=this.currentItem.find(":data(sortable-item)"),b=0;b<this.items.length;b++)for(var c=0;c<a.length;c++)a[c]==this.items[b].item[0]&&this.items.splice(b,1)},_refreshItems:function(a){this.items=[];this.containers=[this];var b=this.items,c=[[d.isFunction(this.options.items)?this.options.items.call(this.element[0],a,{item:this.currentItem}):d(this.options.items,this.element), +this]],e=this._connectWith();if(e)for(var f=e.length-1;f>=0;f--)for(var g=d(e[f]),h=g.length-1;h>=0;h--){var i=d.data(g[h],"sortable");if(i&&i!=this&&!i.options.disabled){c.push([d.isFunction(i.options.items)?i.options.items.call(i.element[0],a,{item:this.currentItem}):d(i.options.items,i.element),i]);this.containers.push(i)}}for(f=c.length-1;f>=0;f--){a=c[f][1];e=c[f][0];h=0;for(g=e.length;h<g;h++){i=d(e[h]);i.data("sortable-item",a);b.push({item:i,instance:a,width:0,height:0,left:0,top:0})}}},refreshPositions:function(a){if(this.offsetParent&& +this.helper)this.offset.parent=this._getParentOffset();for(var b=this.items.length-1;b>=0;b--){var c=this.items[b];if(!(c.instance!=this.currentContainer&&this.currentContainer&&c.item[0]!=this.currentItem[0])){var e=this.options.toleranceElement?d(this.options.toleranceElement,c.item):c.item;if(!a){c.width=e.outerWidth();c.height=e.outerHeight()}e=e.offset();c.left=e.left;c.top=e.top}}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(b= +this.containers.length-1;b>=0;b--){e=this.containers[b].element.offset();this.containers[b].containerCache.left=e.left;this.containers[b].containerCache.top=e.top;this.containers[b].containerCache.width=this.containers[b].element.outerWidth();this.containers[b].containerCache.height=this.containers[b].element.outerHeight()}return this},_createPlaceholder:function(a){var b=a||this,c=b.options;if(!c.placeholder||c.placeholder.constructor==String){var e=c.placeholder;c.placeholder={element:function(){var f= +d(document.createElement(b.currentItem[0].nodeName)).addClass(e||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!e)f.style.visibility="hidden";return f},update:function(f,g){if(!(e&&!c.forcePlaceholderSize)){g.height()||g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10));g.width()||g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")|| +0,10))}}}}b.placeholder=d(c.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);c.placeholder.update(b,b.placeholder)},_contactContainers:function(a){for(var b=null,c=null,e=this.containers.length-1;e>=0;e--)if(!d.ui.contains(this.currentItem[0],this.containers[e].element[0]))if(this._intersectsWith(this.containers[e].containerCache)){if(!(b&&d.ui.contains(this.containers[e].element[0],b.element[0]))){b=this.containers[e];c=e}}else if(this.containers[e].containerCache.over){this.containers[e]._trigger("out", +a,this._uiHash(this));this.containers[e].containerCache.over=0}if(b)if(this.containers.length===1){this.containers[c]._trigger("over",a,this._uiHash(this));this.containers[c].containerCache.over=1}else if(this.currentContainer!=this.containers[c]){b=1E4;e=null;for(var f=this.positionAbs[this.containers[c].floating?"left":"top"],g=this.items.length-1;g>=0;g--)if(d.ui.contains(this.containers[c].element[0],this.items[g].item[0])){var h=this.items[g][this.containers[c].floating?"left":"top"];if(Math.abs(h- +f)<b){b=Math.abs(h-f);e=this.items[g]}}if(e||this.options.dropOnEmpty){this.currentContainer=this.containers[c];e?this._rearrange(a,e,null,true):this._rearrange(a,null,this.containers[c].element,true);this._trigger("change",a,this._uiHash());this.containers[c]._trigger("change",a,this._uiHash(this));this.options.placeholder.update(this.currentContainer,this.placeholder);this.containers[c]._trigger("over",a,this._uiHash(this));this.containers[c].containerCache.over=1}}},_createHelper:function(a){var b= +this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a,this.currentItem])):b.helper=="clone"?this.currentItem.clone():this.currentItem;a.parents("body").length||d(b.appendTo!="parent"?b.appendTo:this.currentItem[0].parentNode)[0].appendChild(a[0]);if(a[0]==this.currentItem[0])this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")};if(a[0].style.width== +""||b.forceHelperSize)a.width(this.currentItem.width());if(a[0].style.height==""||b.forceHelperSize)a.height(this.currentItem.height());return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]||0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top= +this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a= +{top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.currentItem.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"), +10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var a=this.options;if(a.containment=="parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,d(a.containment=="document"? +document:window).width()-this.helperProportions.width-this.margins.left,(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)){var b=d(a.containment)[0];a=d(a.containment).offset();var c=d(b).css("overflow")!="hidden";this.containment=[a.left+(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0)-this.margins.left,a.top+(parseInt(d(b).css("borderTopWidth"), +10)||0)+(parseInt(d(b).css("paddingTop"),10)||0)-this.margins.top,a.left+(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,a.top+(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"),10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}},_convertPositionTo:function(a,b){if(!b)b= +this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(c[0].tagName);return{top:b.top+this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():e?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&& +this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(c[0].tagName);if(this.cssPosition=="relative"&&!(this.scrollParent[0]!=document&&this.scrollParent[0]!=this.offsetParent[0]))this.offset.relative=this._getRelativeOffset(); +var f=a.pageX,g=a.pageY;if(this.originalPosition){if(this.containment){if(a.pageX-this.offset.click.left<this.containment[0])f=this.containment[0]+this.offset.click.left;if(a.pageY-this.offset.click.top<this.containment[1])g=this.containment[1]+this.offset.click.top;if(a.pageX-this.offset.click.left>this.containment[2])f=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g- +this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.top<this.containment[1]||g-this.offset.click.top>this.containment[3])?g:!(g-this.offset.click.top<this.containment[1])?g-b.grid[1]:g+b.grid[1]:g;f=this.originalPageX+Math.round((f-this.originalPageX)/b.grid[0])*b.grid[0];f=this.containment?!(f-this.offset.click.left<this.containment[0]||f-this.offset.click.left>this.containment[2])?f:!(f-this.offset.click.left<this.containment[0])?f-b.grid[0]:f+b.grid[0]:f}}return{top:g- +this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(d.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():e?0:c.scrollTop()),left:f-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(d.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:c.scrollLeft())}},_rearrange:function(a,b,c,e){c?c[0].appendChild(this.placeholder[0]):b.item[0].parentNode.insertBefore(this.placeholder[0], +this.direction=="down"?b.item[0]:b.item[0].nextSibling);this.counter=this.counter?++this.counter:1;var f=this,g=this.counter;window.setTimeout(function(){g==f.counter&&f.refreshPositions(!e)},0)},_clear:function(a,b){this.reverting=false;var c=[];!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem);this._noFinalSort=null;if(this.helper[0]==this.currentItem[0]){for(var e in this._storedCSS)if(this._storedCSS[e]=="auto"||this._storedCSS[e]=="static")this._storedCSS[e]= +"";this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();this.fromOutside&&!b&&c.push(function(f){this._trigger("receive",f,this._uiHash(this.fromOutside))});if((this.fromOutside||this.domPosition.prev!=this.currentItem.prev().not(".ui-sortable-helper")[0]||this.domPosition.parent!=this.currentItem.parent()[0])&&!b)c.push(function(f){this._trigger("update",f,this._uiHash())});if(!d.ui.contains(this.element[0],this.currentItem[0])){b||c.push(function(f){this._trigger("remove", +f,this._uiHash())});for(e=this.containers.length-1;e>=0;e--)if(d.ui.contains(this.containers[e].element[0],this.currentItem[0])&&!b){c.push(function(f){return function(g){f._trigger("receive",g,this._uiHash(this))}}.call(this,this.containers[e]));c.push(function(f){return function(g){f._trigger("update",g,this._uiHash(this))}}.call(this,this.containers[e]))}}for(e=this.containers.length-1;e>=0;e--){b||c.push(function(f){return function(g){f._trigger("deactivate",g,this._uiHash(this))}}.call(this, +this.containers[e]));if(this.containers[e].containerCache.over){c.push(function(f){return function(g){f._trigger("out",g,this._uiHash(this))}}.call(this,this.containers[e]));this.containers[e].containerCache.over=0}}this._storedCursor&&d("body").css("cursor",this._storedCursor);this._storedOpacity&&this.helper.css("opacity",this._storedOpacity);if(this._storedZIndex)this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex);this.dragging=false;if(this.cancelHelperRemoval){if(!b){this._trigger("beforeStop", +a,this._uiHash());for(e=0;e<c.length;e++)c[e].call(this,a);this._trigger("stop",a,this._uiHash())}return false}b||this._trigger("beforeStop",a,this._uiHash());this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.helper[0]!=this.currentItem[0]&&this.helper.remove();this.helper=null;if(!b){for(e=0;e<c.length;e++)c[e].call(this,a);this._trigger("stop",a,this._uiHash())}this.fromOutside=false;return true},_trigger:function(){d.Widget.prototype._trigger.apply(this,arguments)===false&&this.cancel()}, +_uiHash:function(a){var b=a||this;return{helper:b.helper,placeholder:b.placeholder||d([]),position:b.position,originalPosition:b.originalPosition,offset:b.positionAbs,item:b.currentItem,sender:a?a.element:null}}});d.extend(d.ui.sortable,{version:"1.8.16"})})(jQuery); +; \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/javascript/jquery.js Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,4 @@ +/*! jQuery v1.7.1 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"<!doctype html>":"")+"<html><body>"),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function cb(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function ca(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bE.test(a)?d(a,e):ca(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)ca(a+"["+e+"]",b[e],c,d);else d(a,b)}function b_(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function b$(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bT,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=b$(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=b$(a,c,d,e,"*",g));return l}function bZ(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bP),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bC(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?bx:by,g=0,h=e.length;if(d>0){if(c!=="border")for(;g<h;g++)c||(d-=parseFloat(f.css(a,"padding"+e[g]))||0),c==="margin"?d+=parseFloat(f.css(a,c+e[g]))||0:d-=parseFloat(f.css(a,"border"+e[g]+"Width"))||0;return d+"px"}d=bz(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0;if(c)for(;g<h;g++)d+=parseFloat(f.css(a,"padding"+e[g]))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+e[g]+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+e[g]))||0);return d+"px"}function bp(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bf,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bo(a){var b=c.createElement("div");bh.appendChild(b),b.innerHTML=a.outerHTML;return b.firstChild}function bn(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bm(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bm)}function bm(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bl(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bk(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bj(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c+(i[c][d].namespace?".":"")+i[c][d].namespace,i[c][d],i[c][d].data)}h.data&&(h.data=f.extend({},h.data))}}function bi(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function U(a){var b=V.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function T(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(O.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(H)return H.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?m(g):h==="function"&&(!a.unique||!o.has(g))&&c.push(g)},n=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,l=j||0,j=0,k=c.length;for(;c&&l<k;l++)if(c[l].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}i=!1,c&&(a.once?e===!0?o.disable():c=[]:d&&d.length&&(e=d.shift(),o.fireWith(e[0],e[1])))},o={add:function(){if(c){var a=c.length;m(arguments),i?k=c.length:e&&e!==!0&&(j=a,n(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){i&&f<=k&&(k--,f<=l&&l--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&o.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(i?a.once||d.push([b,c]):(!a.once||!e)&&n(b,c));return this},fire:function(){o.fireWith(this,arguments);return this},fired:function(){return!!e}};return o};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){i.done.apply(i,arguments).fail.apply(i,arguments);return this},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var b,d,e,g,h,i,j,k,l,m,n,o,p,q=c.createElement("div"),r=c.documentElement;q.setAttribute("className","t"),q.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="<div "+n+"><div></div></div>"+"<table "+n+" cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="<div style='width:4px;'></div>",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[h]:a.removeAttribute?a.removeAttribute(h):a[h]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h=null;if(typeof a=="undefined"){if(this.length){h=f.data(this[0]);if(this[0].nodeType===1&&!f._data(this[0],"parsedAttrs")){e=this[0].attributes;for(var i=0,j=e.length;i<j;i++)g=e[i].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),l(this[0],g,h[g]));f._data(this[0],"parsedAttrs",!0)}}return h}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split("."),d[1]=d[1]?"."+d[1]:"";if(c===b){h=this.triggerHandler("getData"+d[1]+"!",[d[0]]),h===b&&this.length&&(h=f.data(this[0],a),h=l(this[0],a,h));return h===b&&d[1]?this.data(d[0]):h}return this.each(function(){var b=f(this),e=[d[0],c];b.triggerHandler("setData"+d[1]+"!",e),f.data(this,a,c),b.triggerHandler("changeData"+d[1]+"!",e)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise()}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h<g;h++)e=d[h],e&&(c=f.propFix[e]||e,f.attr(a,e,""),a.removeAttribute(v?e:c),u.test(e)&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!!a&&i!==3&&i!==8&&i!==2){h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]}},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; +f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k<c.length;k++){l=A.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,quick:G(g),namespace:n.join(".")},p),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d,e){var g=f.hasData(a)&&f._data(a),h,i,j,k,l,m,n,o,p,q,r,s;if(!!g&&!!(o=g.events)){b=f.trim(I(b||"")).split(" ");for(h=0;h<b.length;h++){i=A.exec(b[h])||[],j=k=i[1],l=i[2];if(!j){for(j in o)f.event.remove(a,j+b[h],c,d,!0);continue}p=f.event.special[j]||{},j=(d?p.delegateType:p.bindType)||j,r=o[j]||[],m=r.length,l=l?new RegExp("(^|\\.)"+l.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;for(n=0;n<r.length;n++)s=r[n],(e||k===s.origType)&&(!c||c.guid===s.guid)&&(!l||l.test(s.namespace))&&(!d||d===s.selector||d==="**"&&s.selector)&&(r.splice(n--,1),s.selector&&r.delegateCount--,p.remove&&p.remove.call(a,s));r.length===0&&m!==r.length&&((!p.teardown||p.teardown.call(a,l)===!1)&&f.removeEvent(a,j,g.handle),delete o[j])}f.isEmptyObject(o)&&(q=g.handle,q&&(q.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;if(E.test(h+f.event.triggered))return;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length&&!c.isPropagationStopped();l++)m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d)===!1&&c.preventDefault();c.type=h,!g&&!c.isDefaultPrevented()&&(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=[],j,k,l,m,n,o,p,q,r,s,t;g[0]=c,c.delegateTarget=this;if(e&&!c.target.disabled&&(!c.button||c.type!=="click")){m=f(this),m.context=this.ownerDocument||this;for(l=c.target;l!=this;l=l.parentNode||this){o={},q=[],m[0]=l;for(j=0;j<e;j++)r=d[j],s=r.selector,o[s]===b&&(o[s]=r.quick?H(l,r.quick):m.is(s)),o[s]&&q.push(r);q.length&&i.push({elem:l,matches:q})}}d.length>e&&i.push({elem:this,matches:d.slice(e)});for(j=0;j<i.length&&!c.isPropagationStopped();j++){p=i[j],c.currentTarget=p.elem;for(k=0;k<p.matches.length&&!c.isImmediatePropagationStopped();k++){r=p.matches[k];if(h||!c.namespace&&!r.namespace||c.namespace_re&&c.namespace_re.test(r.namespace))c.data=r.data,c.handleObj=r,n=((f.event.special[r.origType]||{}).handle||r.handler).apply(p.elem,g),n!==b&&(c.result=n,n===!1&&(c.preventDefault(),c.stopPropagation()))}}return c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?K:J):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=K;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=K;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=K,this.stopPropagation()},isDefaultPrevented:J,isPropagationStopped:J,isImmediatePropagationStopped:J},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c=this,d=a.relatedTarget,e=a.handleObj,g=e.selector,h;if(!d||d!==c&&!f.contains(c,d))a.type=e.origType,h=e.handler.apply(this,arguments),a.type=b;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){this.parentNode&&!a.isTrigger&&f.event.simulate("submit",this.parentNode,a,!0)}),d._submit_attached=!0)})},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(z.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;z.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return z.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=J;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on.call(this,a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.type+"."+e.namespace:e.type,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=J);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1||d===9){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var L=/Until$/,M=/^(?:parents|prevUntil|prevAll)/,N=/,/,O=/^.[^:#\[\.,]*$/,P=Array.prototype.slice,Q=f.expr.match.POS,R={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(T(this,a,!1),"not",a)},filter:function(a){return this.pushStack(T(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?Q.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=Q.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/<tbody/i,_=/<|&#?\w+;/,ba=/<(?:script|style)/i,bb=/<(?:script|object|embed|option|style)/i,bc=new RegExp("<(?:"+V+")","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*<!(?:\[CDATA\[|\-\-)/,bg={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() +{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bd.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bi(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,bp)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!bb.test(j)&&(f.support.checkClone||!bd.test(j))&&(f.support.html5Clone||!bc.test(j))&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1></$2>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]==="<table>"&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i<r;i++)bn(k[i]);else bn(k);k.nodeType?h.push(k):h=f.merge(h,k)}if(d){g=function(a){return!a.type||be.test(a.type)};for(j=0;h[j];j++)if(e&&f.nodeName(h[j],"script")&&(!h[j].type||h[j].type.toLowerCase()==="text/javascript"))e.push(h[j].parentNode?h[j].parentNode.removeChild(h[j]):h[j]);else{if(h[j].nodeType===1){var s=f.grep(h[j].getElementsByTagName("script"),g);h.splice.apply(h,[j+1,0].concat(s))}d.appendChild(h[j])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bq=/alpha\([^)]*\)/i,br=/opacity=([^)]*)/,bs=/([A-Z]|^ms)/g,bt=/^-?\d+(?:px)?$/i,bu=/^-?\d/,bv=/^([\-+])=([\-+.\de]+)/,bw={position:"absolute",visibility:"hidden",display:"block"},bx=["Left","Right"],by=["Top","Bottom"],bz,bA,bB;f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bz(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bv.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bz)return bz(a,c)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){if(a.offsetWidth!==0)return bC(a,b,d);f.swap(a,bw,function(){e=bC(a,b,d)});return e}},set:function(a,b){if(!bt.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",cv(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cu("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cu("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cv(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cn.test(h)?(o=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),o?(f._data(this,"toggle"+i,o==="show"?"hide":"show"),j[o]()):j[h]()):(k=co.exec(h),l=j.cur(),k?(m=parseFloat(k[2]),n=k[3]||(f.cssNumber[i]?"":"px"),n!=="px"&&(f.style(this,i,(m||1)+n),l=(m||1)/j.cur()*l,f.style(this,i,l+n)),k[1]&&(m=(k[1]==="-="?-1:1)*m+l),j.custom(l,m,n)):j.custom(l,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b]&&g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:cu("show",1),slideUp:cu("hide",1),slideToggle:cu("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=cr||cs(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){e.options.hide&&f._data(e.elem,"fxshow"+e.prop)===b&&f._data(e.elem,"fxshow"+e.prop,e.start)},h()&&f.timers.push(h)&&!cp&&(cp=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=cr||cs(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cp),cp=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(["width","height"],function(a,b){f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now)+a.unit)}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cw=/^t(?:able|d|h)$/i,cx=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cy(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.support.fixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cw.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.support.fixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/javascript/qwebchannel.js Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,413 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +"use strict"; + +var QWebChannelMessageTypes = { + signal: 1, + propertyUpdate: 2, + init: 3, + idle: 4, + debug: 5, + invokeMethod: 6, + connectToSignal: 7, + disconnectFromSignal: 8, + setProperty: 9, + response: 10, +}; + +var QWebChannel = function(transport, initCallback) +{ + if (typeof transport !== "object" || typeof transport.send !== "function") { + console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + + " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); + return; + } + + var channel = this; + this.transport = transport; + + this.send = function(data) + { + if (typeof(data) !== "string") { + data = JSON.stringify(data); + } + channel.transport.send(data); + } + + this.transport.onmessage = function(message) + { + var data = message.data; + if (typeof data === "string") { + data = JSON.parse(data); + } + switch (data.type) { + case QWebChannelMessageTypes.signal: + channel.handleSignal(data); + break; + case QWebChannelMessageTypes.response: + channel.handleResponse(data); + break; + case QWebChannelMessageTypes.propertyUpdate: + channel.handlePropertyUpdate(data); + break; + default: + console.error("invalid message received:", message.data); + break; + } + } + + this.execCallbacks = {}; + this.execId = 0; + this.exec = function(data, callback) + { + if (!callback) { + // if no callback is given, send directly + channel.send(data); + return; + } + if (channel.execId === Number.MAX_VALUE) { + // wrap + channel.execId = Number.MIN_VALUE; + } + if (data.hasOwnProperty("id")) { + console.error("Cannot exec message with property id: " + JSON.stringify(data)); + return; + } + data.id = channel.execId++; + channel.execCallbacks[data.id] = callback; + channel.send(data); + }; + + this.objects = {}; + + this.handleSignal = function(message) + { + var object = channel.objects[message.object]; + if (object) { + object.signalEmitted(message.signal, message.args); + } else { + console.warn("Unhandled signal: " + message.object + "::" + message.signal); + } + } + + this.handleResponse = function(message) + { + if (!message.hasOwnProperty("id")) { + console.error("Invalid response message received: ", JSON.stringify(message)); + return; + } + channel.execCallbacks[message.id](message.data); + delete channel.execCallbacks[message.id]; + } + + this.handlePropertyUpdate = function(message) + { + for (var i in message.data) { + var data = message.data[i]; + var object = channel.objects[data.object]; + if (object) { + object.propertyUpdate(data.signals, data.properties); + } else { + console.warn("Unhandled property update: " + data.object + "::" + data.signal); + } + } + channel.exec({type: QWebChannelMessageTypes.idle}); + } + + this.debug = function(message) + { + channel.send({type: QWebChannelMessageTypes.debug, data: message}); + }; + + channel.exec({type: QWebChannelMessageTypes.init}, function(data) { + for (var objectName in data) { + var object = new QObject(objectName, data[objectName], channel); + } + // now unwrap properties, which might reference other registered objects + for (var objectName in channel.objects) { + channel.objects[objectName].unwrapProperties(); + } + if (initCallback) { + initCallback(channel); + } + channel.exec({type: QWebChannelMessageTypes.idle}); + }); +}; + +function QObject(name, data, webChannel) +{ + this.__id__ = name; + webChannel.objects[name] = this; + + // List of callbacks that get invoked upon signal emission + this.__objectSignals__ = {}; + + // Cache of all properties, updated when a notify signal is emitted + this.__propertyCache__ = {}; + + var object = this; + + // ---------------------------------------------------------------------- + + this.unwrapQObject = function(response) + { + if (response instanceof Array) { + // support list of objects + var ret = new Array(response.length); + for (var i = 0; i < response.length; ++i) { + ret[i] = object.unwrapQObject(response[i]); + } + return ret; + } + if (!response + || !response["__QObject*__"] + || response.id === undefined) { + return response; + } + + var objectId = response.id; + if (webChannel.objects[objectId]) + return webChannel.objects[objectId]; + + if (!response.data) { + console.error("Cannot unwrap unknown QObject " + objectId + " without data."); + return; + } + + var qObject = new QObject( objectId, response.data, webChannel ); + qObject.destroyed.connect(function() { + if (webChannel.objects[objectId] === qObject) { + delete webChannel.objects[objectId]; + // reset the now deleted QObject to an empty {} object + // just assigning {} though would not have the desired effect, but the + // below also ensures all external references will see the empty map + // NOTE: this detour is necessary to workaround QTBUG-40021 + var propertyNames = []; + for (var propertyName in qObject) { + propertyNames.push(propertyName); + } + for (var idx in propertyNames) { + delete qObject[propertyNames[idx]]; + } + } + }); + // here we are already initialized, and thus must directly unwrap the properties + qObject.unwrapProperties(); + return qObject; + } + + this.unwrapProperties = function() + { + for (var propertyIdx in object.__propertyCache__) { + object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); + } + } + + function addSignal(signalData, isPropertyNotifySignal) + { + var signalName = signalData[0]; + var signalIndex = signalData[1]; + object[signalName] = { + connect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to connect to signal " + signalName); + return; + } + + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + object.__objectSignals__[signalIndex].push(callback); + + if (!isPropertyNotifySignal && signalName !== "destroyed") { + // only required for "pure" signals, handled separately for properties in propertyUpdate + // also note that we always get notified about the destroyed signal + webChannel.exec({ + type: QWebChannelMessageTypes.connectToSignal, + object: object.__id__, + signal: signalIndex + }); + } + }, + disconnect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to disconnect from signal " + signalName); + return; + } + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + var idx = object.__objectSignals__[signalIndex].indexOf(callback); + if (idx === -1) { + console.error("Cannot find connection of signal " + signalName + " to " + callback.name); + return; + } + object.__objectSignals__[signalIndex].splice(idx, 1); + if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { + // only required for "pure" signals, handled separately for properties in propertyUpdate + webChannel.exec({ + type: QWebChannelMessageTypes.disconnectFromSignal, + object: object.__id__, + signal: signalIndex + }); + } + } + }; + } + + /** + * Invokes all callbacks for the given signalname. Also works for property notify callbacks. + */ + function invokeSignalCallbacks(signalName, signalArgs) + { + var connections = object.__objectSignals__[signalName]; + if (connections) { + connections.forEach(function(callback) { + callback.apply(callback, signalArgs); + }); + } + } + + this.propertyUpdate = function(signals, propertyMap) + { + // update property cache + for (var propertyIndex in propertyMap) { + var propertyValue = propertyMap[propertyIndex]; + object.__propertyCache__[propertyIndex] = propertyValue; + } + + for (var signalName in signals) { + // Invoke all callbacks, as signalEmitted() does not. This ensures the + // property cache is updated before the callbacks are invoked. + invokeSignalCallbacks(signalName, signals[signalName]); + } + } + + this.signalEmitted = function(signalName, signalArgs) + { + invokeSignalCallbacks(signalName, signalArgs); + } + + function addMethod(methodData) + { + var methodName = methodData[0]; + var methodIdx = methodData[1]; + object[methodName] = function() { + var args = []; + var callback; + for (var i = 0; i < arguments.length; ++i) { + if (typeof arguments[i] === "function") + callback = arguments[i]; + else + args.push(arguments[i]); + } + + webChannel.exec({ + "type": QWebChannelMessageTypes.invokeMethod, + "object": object.__id__, + "method": methodIdx, + "args": args + }, function(response) { + if (response !== undefined) { + var result = object.unwrapQObject(response); + if (callback) { + (callback)(result); + } + } + }); + }; + } + + function bindGetterSetter(propertyInfo) + { + var propertyIndex = propertyInfo[0]; + var propertyName = propertyInfo[1]; + var notifySignalData = propertyInfo[2]; + // initialize property cache with current value + // NOTE: if this is an object, it is not directly unwrapped as it might + // reference other QObject that we do not know yet + object.__propertyCache__[propertyIndex] = propertyInfo[3]; + + if (notifySignalData) { + if (notifySignalData[0] === 1) { + // signal name is optimized away, reconstruct the actual name + notifySignalData[0] = propertyName + "Changed"; + } + addSignal(notifySignalData, true); + } + + Object.defineProperty(object, propertyName, { + configurable: true, + get: function () { + var propertyValue = object.__propertyCache__[propertyIndex]; + if (propertyValue === undefined) { + // This shouldn't happen + console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); + } + + return propertyValue; + }, + set: function(value) { + if (value === undefined) { + console.warn("Property setter for " + propertyName + " called with undefined value!"); + return; + } + object.__propertyCache__[propertyIndex] = value; + webChannel.exec({ + "type": QWebChannelMessageTypes.setProperty, + "object": object.__id__, + "property": propertyIndex, + "value": value + }); + } + }); + + } + + // ---------------------------------------------------------------------- + + data.methods.forEach(addMethod); + + data.properties.forEach(bindGetterSetter); + + data.signals.forEach(function(signal) { addSignal(signal, false); }); + + for (var name in data.enums) { + object[name] = data.enums[name]; + } +} + +//required for use with nodejs +if (typeof module === 'object') { + module.exports = { + QWebChannel: QWebChannel + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/javascript_rc.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,6740 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.5.1) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x0e\xfa\ +\x00\ +\x00\x39\xf5\x78\x9c\xcd\x1b\xed\x6e\xdb\x46\xf2\xbf\x9f\x62\x23\ +\x14\xad\x9c\xc8\xb4\x9d\xb4\xf7\x21\x5f\x0e\xe7\x38\x8e\xe1\xab\ +\xe3\x38\x17\xa7\x2d\xe0\x1a\xc6\x4a\x5c\x49\xac\x29\x92\x5d\x92\ +\x56\x74\x89\xde\xe6\xde\xe4\x5e\xec\x66\xf6\x7b\xc9\xa5\xa4\x14\ +\xb9\xa2\x44\x10\x85\xe4\xec\xcc\xec\x7c\xcf\x2c\xb3\xff\xf8\x0b\ +\x5e\x3b\xe2\x0f\x39\xc9\x8b\x25\x4f\xa6\xb3\x8a\xf4\x4f\x76\xc9\ +\xd3\x83\xc3\xef\xc8\xf5\x8c\x91\xb7\x15\xbc\x99\x17\x34\x5b\x92\ +\x8b\x2a\x8e\x82\x90\xdf\x92\xef\x53\xca\xff\xfb\x9f\xf4\x21\xa6\ +\x29\xcb\x4a\xf2\x92\x56\xf4\x3e\xcf\xca\x3a\xad\xc8\xf1\x8b\x01\ +\xa1\xe4\xfb\x97\xc7\x2f\xc8\x19\xcf\xeb\x82\x8c\x25\xba\x01\x49\ +\xb2\x49\xfe\x8f\xfb\x98\x8e\x22\x78\x04\x40\x75\x35\xcb\x39\x79\ +\x9d\xa4\x09\xcd\xc8\x8f\x79\x3a\x99\x90\xbf\xcd\xc5\x5d\xb4\xc0\ +\x3b\x03\xfb\x77\xc9\x45\x56\xd1\x71\x35\x24\xb3\xaa\x2a\x86\xfb\ +\xfb\x8b\xc5\x22\xfa\xb5\x8a\x92\x7c\x3f\x4d\xc6\xc0\x44\x92\x4d\ +\xf7\xd5\xd6\xae\x67\x49\x49\x26\x49\xca\x08\xfc\x16\x94\x57\x24\ +\x9f\x90\x4a\x6c\xee\x47\x36\x3a\x99\xd1\x2c\x63\x29\x99\xe7\x71\ +\x0d\x20\xe6\x15\xb9\xce\xf3\xf4\x3e\xa9\x22\x85\xe5\xab\xb7\xd7\ +\x77\x2f\x4e\xcf\xce\x2f\xef\x2e\xce\x4f\x4e\x2f\xdf\x9d\x0e\x2f\ +\xce\xae\x2e\x9e\x1e\x7e\x25\xb9\x99\xcf\x19\x1f\x27\x34\x25\x17\ +\x82\x3c\x23\xef\x4b\x3a\x65\xf8\x4e\x3d\x60\x25\x99\xe5\x69\x0c\ +\x7c\x91\x07\x9a\x26\x31\x0a\x42\xaf\x01\x6a\x92\x6b\x00\x9a\xd3\ +\x25\xa9\x61\x7d\x65\xb9\xce\x10\x0d\x1d\x8f\x73\x1e\xd3\x6c\xcc\ +\xc8\x22\xa9\x66\x82\x4b\x07\x85\x5a\x4f\xe8\x94\x33\x36\x67\x59\ +\x45\x0a\x9e\x3f\x24\x31\x8b\x0d\x38\x62\x79\x97\x4f\xaa\x05\xe5\ +\xb0\x4f\x0e\x22\x4f\x2b\xc6\x33\x5a\x25\x0f\x2c\x15\x0a\x09\x12\ +\x01\x98\x79\x09\xa4\x40\xde\x49\x06\xe8\x14\x3b\x64\xc1\x93\xaa\ +\x62\x99\x43\x71\xc4\xaa\x05\x83\x27\xcb\xbc\x26\x34\x8b\x1b\x06\ +\x14\x91\x57\xa0\x5f\xa3\x1d\x89\x57\xa0\xca\x50\x18\x59\x9c\x54\ +\x09\x18\x0d\x01\x51\xb5\x95\x2a\x80\xf7\x2c\x94\x44\x36\xa9\x39\ +\x70\xc8\x11\x09\x5a\x13\x9f\x53\x7c\xa9\xc4\xc7\x24\xcf\xe3\x8a\ +\xe0\x1b\x42\xab\x36\x56\x05\xb0\x57\x97\x5a\xcb\x67\x97\xef\xc9\ +\x05\x2b\x4b\xc6\xc9\x19\xcb\x18\x07\xc9\x5e\xd5\x23\x60\xba\xad\ +\xd7\x63\x5f\x7a\x56\x5f\xa8\xc1\x11\x43\x2e\x62\x52\x67\x31\xa0\ +\xb2\x62\x54\xe6\x65\xc9\x08\xa2\x61\x4a\x0f\x8c\x97\xb8\x9d\xa7\ +\xd1\x21\xa8\xcb\xdc\x3e\x23\x14\xcc\x18\x41\xcb\x19\x50\x18\x2d\ +\x05\xc6\x57\xa0\x04\x4f\xc1\xaf\x72\xa0\x2d\xe5\x81\x02\xa6\x45\ +\xc1\x28\x47\xb9\x83\x9a\x71\x81\x60\x55\x99\x72\x84\xa6\xfc\xf0\ +\xf4\x10\x21\x85\xc9\xba\x8f\x9f\xc1\x8a\x71\x5a\xc7\x42\xf5\x62\ +\x69\x41\xc7\xf7\x74\x8a\xb8\xc4\x76\xd4\xbe\x23\x72\x95\x32\x0a\ +\x7c\x73\xf6\x90\xb0\x85\xb6\xb8\x49\x9e\xa6\xf9\x42\x12\xb6\x2a\ +\xaa\x72\x02\x7b\xac\x39\x6b\x88\xa3\x43\x16\x88\x88\xb3\x5f\xeb\ +\x84\x0b\x4b\x2b\xc1\x3a\xd3\x14\x85\x3c\x67\x2a\x04\x94\x4a\xb1\ +\xd3\xac\x8e\x72\x3e\xdd\xd7\xfe\xb4\x9f\x4e\x8b\x34\x9a\x55\xf3\ +\x54\x6f\xce\xb1\x82\x16\x30\x38\xe8\x9e\xb7\x72\x0f\x84\x2f\x56\ +\x6b\x03\x39\x2e\xc1\xf4\xcb\x82\x09\xa7\x63\x1f\xc6\xac\xc0\xfd\ +\x0c\x9a\xc1\x72\x0a\x56\x51\x0a\x47\x18\x33\x8e\x8e\x43\x68\x2c\ +\x4d\x97\xa6\x62\x33\x18\x40\xc1\x8a\x61\x19\x4a\x4c\xdc\x11\x54\ +\x5b\xcc\xca\x31\x4f\x46\x52\xd8\xcd\x08\x0c\xea\x20\xa7\x9a\x26\ +\xa2\xd1\x36\x71\x18\x1d\x0e\x5a\x6a\x92\x1a\x86\x35\x77\xa7\x3f\ +\x9d\x9c\x5e\x5d\x9f\xbf\xb9\x8c\xaa\x0f\x95\x7c\x2d\x42\x21\xaa\ +\x91\xb9\x01\xee\xf4\xf2\xa5\x0e\x6f\x5f\xc9\xc7\x5f\xee\xda\xdf\ +\xd9\xe9\xa1\x67\x96\x15\x4f\xc6\x55\xef\x68\x67\xe7\x81\x72\xf2\ +\xd6\x86\xe0\xd7\x60\x01\xc0\xcf\xf5\xb2\x00\xd1\x3d\x27\x1f\x77\ +\x08\x5c\x65\x32\x05\x91\x0d\xc9\xe1\x40\xdc\x42\x50\x2b\x40\xa0\ +\xcb\xf7\x05\xd8\x36\x1b\x92\xa7\xf2\x71\x92\x25\x60\x07\xcf\xd4\ +\x4d\x9c\xc2\x9b\x6f\xe5\x4d\xcc\x46\xf5\x74\x48\xbe\xd3\x70\x0f\ +\xf9\x3d\x7b\xcd\x20\xd5\xc4\x43\xf2\x27\xf9\x10\xa2\x40\xc6\xc6\ +\xd5\x75\xfe\x4e\xd1\xfa\xb3\x5a\x9a\x94\xea\xd5\x2b\x9e\xcf\xf5\ +\xcb\xbf\xc8\x97\x25\xab\xae\x14\x2f\x43\xf2\x57\xf9\x8c\xb3\xb2\ +\x80\xd8\x04\xc4\x0f\x0f\x06\x3b\xab\xf6\x0e\x61\x57\x93\x3a\x1b\ +\xa3\xf6\xfa\x15\xa7\x19\x80\xf3\x6a\x20\xb8\x3f\xa1\x69\x3a\x02\ +\x85\xec\xee\xc8\x7d\x27\x13\xd2\xaf\x40\x12\xe8\x61\x1a\x92\x3c\ +\x7a\xfe\x9c\xf4\xf2\xd1\x2f\xc0\x52\x8f\x7c\xfa\x44\x9a\x00\x51\ +\xc9\xc0\xd5\x05\x94\xa6\xd3\xdb\x55\x82\x54\x3b\x2d\x73\xf0\x54\ +\xc6\x79\xce\xfb\x3d\x61\x5e\x0e\x77\xec\x03\xd8\x35\xda\xa1\x43\ +\x52\x52\x93\xd9\x00\x0c\x1f\xf1\x6b\xd4\x22\xae\xe4\xd9\x5c\xaa\ +\x8d\x8c\xd5\x0e\x8c\x8e\xa2\x1e\x79\x62\x48\xfb\x57\x8f\x9c\x81\ +\x83\x64\x90\x90\x87\x96\xd6\x10\x1e\x3f\x51\x7b\xb2\xe2\xd9\x85\ +\x67\xbd\x41\x63\x8f\x61\x50\xf1\x6a\x77\xf7\xc8\x10\xe5\xac\xaa\ +\x79\x26\xef\x57\x3b\xe2\x07\x35\x32\x36\xda\x40\x3f\x90\xaf\xf1\ +\x5f\x91\xdd\xf6\x73\x4b\xf0\x68\xc7\x02\x88\xfd\x3b\x4a\x04\x23\ +\xa4\xbb\xe2\xb5\x15\xb2\xd5\x9c\x7c\x2d\xf5\x81\x56\x9f\x4d\x3d\ +\x6d\x08\x1b\x03\x08\x40\xf8\xcf\x77\xe0\x9b\x12\x24\x99\x2c\xe5\ +\x3a\xbb\x8d\x95\x55\xa0\xe4\x3c\xf2\xb7\xec\xc2\xaf\x76\x02\xdb\ +\x89\xac\x96\x1c\xe6\xd5\xa3\x26\xff\x28\x21\xc5\x96\x82\x88\xf0\ +\xf6\x28\xb0\x41\x05\xb7\xe5\xfe\xa0\xf4\x2a\x59\xf7\xde\x4a\xb0\ +\xb1\xf1\x8c\x08\x80\x08\xf1\x37\x71\x8d\x31\xb9\x74\xc4\x8b\x48\ +\xc5\x89\x96\xbd\x69\x89\xc1\x0f\x84\x05\xe9\xc4\x4d\x1e\xf4\x35\ +\xe2\x8c\xde\x1f\x6d\x4f\xd3\xf8\xfb\x06\xaa\xff\x52\x70\x5f\x8a\ +\x6e\x23\x08\x6e\xa0\x7e\xe5\x41\x7f\x06\x0f\x31\x9b\x50\x28\xe1\ +\x03\xf8\xfd\x30\x02\x41\x55\x14\xb4\xda\xc4\x38\x1b\x33\xf0\xee\ +\x78\x08\x6e\xeb\x5a\xd0\x66\xaa\xab\x96\x09\xb3\x0f\x6c\xac\x43\ +\xa3\x48\x0a\xab\x23\xff\xe5\x39\xfa\xe3\x41\xe3\x61\xd3\x45\x07\ +\x26\x38\x85\x9c\xf5\x91\x79\xd9\x30\xb8\xfd\x7d\x7c\x9f\xe5\x36\ +\xb4\x41\xd6\xc4\xcc\x0e\xf9\x5e\x44\x82\x18\xea\x91\x71\x95\x2e\ +\x7d\xd5\x29\xe9\x37\x3d\x53\x5f\x6e\x50\xb2\x9b\xd6\xcc\xe8\xd5\ +\x7a\x6f\xe0\x5b\x97\xf5\x7c\xc4\x78\xf4\xfa\xf8\xa7\xbb\x1f\x8e\ +\x2f\xde\x9f\x06\xd8\x5c\x70\x5a\x04\x99\x30\x22\xd2\x48\xa0\x7f\ +\x11\x48\xba\xe8\x0b\xef\x9b\xd1\xf2\xcd\x22\xd3\x86\x03\x1a\x8e\ +\x7b\xbb\x2d\x6f\xf4\x8d\xe0\x04\xe8\xe5\x15\x11\xd2\xd7\x86\x20\ +\x92\x86\x36\x56\x48\xcb\x32\x6a\x87\x62\xdd\xd6\x22\x12\xec\x25\ +\xb8\x21\x7f\x87\x4f\x9e\x58\x68\xf7\x8d\xb1\x9d\x1b\xb5\xf2\x16\ +\x97\xaa\x87\xed\x25\xad\x68\xea\x06\x7f\x99\x0a\xb5\x15\xda\xe7\ +\x6e\x68\xd9\x32\xbe\xaa\xac\x6a\x77\xa1\x70\xdf\x68\x7f\x91\xf7\ +\xb7\x7e\xd0\x95\x0f\x9b\x8a\x90\x4f\x55\x00\x3c\x9d\x63\x53\x16\ +\x6b\xda\xea\xa9\xf5\x43\xca\xa7\xa5\x1b\x7d\x09\x4b\x21\xd0\x84\ +\x35\x0b\x6d\x44\xd6\xef\xbd\xcf\xe4\xfe\x62\x53\x8a\xa1\x12\x7d\ +\x3e\x31\x45\x0f\x87\xee\x73\x09\xdb\x8a\xf3\xab\x96\xd8\x74\x6c\ +\xdc\x42\x70\xc2\x57\x35\x81\xdf\x60\xa2\xe7\x2a\x4e\xe9\xb0\xdd\ +\x0e\x58\x58\x68\x34\xcc\x53\xf3\xb2\xb5\x85\x86\xad\x4f\xb3\x0d\ +\x06\xd8\xef\x88\x89\x31\x4b\x59\xc5\x36\xaf\x6f\xe7\xf9\x50\x9c\ +\xdf\x42\x9e\xd0\x8d\x91\x3e\x5a\x63\x82\x2d\x81\xc7\x55\x43\x90\ +\x1d\x25\xc1\x4d\x72\x7b\xd4\x82\xeb\x34\x6d\xe1\x80\x2d\xbb\x5e\ +\x6f\xdb\x8e\x7d\x17\xed\x34\xa6\xac\xac\x1c\xc8\xb0\xa0\x20\x12\ +\x56\x36\x94\x15\xb4\x72\xd7\x40\x9a\x96\x6e\x62\x56\xad\xda\x0c\ +\x34\x6d\x87\x7f\x6b\xef\x0e\x1b\x4d\xa2\x1b\x6c\xa3\xff\x11\x6b\ +\x9c\x61\x67\x92\xc7\x2e\x66\x15\x28\xea\x44\x43\xb3\x85\x76\xbd\ +\x90\xb6\x81\x96\xc0\x29\xa5\x38\xd4\x1a\x5e\x35\x42\xe0\x67\xf1\ +\x0e\x0d\xcd\x6a\xd0\x28\x93\x43\x96\x27\xc5\x79\x49\xe7\x38\xe5\ +\x22\x5d\xa6\x67\x4c\x2a\x63\x0b\xf2\xf6\x8d\xb8\xeb\xdb\xa5\x92\ +\xf1\x1b\xfb\xe0\x76\xa0\xb9\x0d\x16\x9a\x90\x2f\xb3\x7c\x41\xea\ +\x0c\xb3\x26\xb1\x56\x33\x20\x8b\x59\x02\x15\xe8\x5c\x0c\x37\x39\ +\x9b\x30\xce\x70\x12\x96\xe3\x8c\x09\xee\xa7\x49\x59\xc1\xa3\x58\ +\xf1\x53\x6e\xda\x4e\xc3\x01\x5a\xd1\xa9\xe1\x1f\x0e\xff\x91\xe4\ +\xed\xca\xb0\xd6\x0f\x6e\x04\xfd\xc6\xeb\x1d\x1b\x14\xdc\x77\xfd\ +\x75\x12\xf9\xcd\x76\x09\xbf\x68\x1f\xa6\x23\xd4\xca\xc9\x8c\x5a\ +\x40\xa8\x06\x8f\xee\x6d\x85\x1d\xdf\xdd\x25\xf1\xdd\x1d\x2a\x15\ +\x60\x25\x3a\x0b\x69\x84\x82\x2f\x6f\x4d\xa3\xb6\xa3\xd4\x77\x01\ +\x9a\xc0\xd9\xd3\xd8\x54\x86\xd5\x8c\x56\x64\xca\x2a\xd5\xe1\xc7\ +\xe0\xb9\xc0\x8e\x74\x4c\xc2\xe6\x49\x89\x63\x12\x97\xb8\x24\x20\ +\x93\x76\x29\xf8\x30\x49\x1d\x08\x9c\xd0\xf1\x4c\xcc\x82\x81\x80\ +\x67\x21\x32\x20\xc4\x60\x29\x38\xf9\x04\x3b\xaa\x20\x49\x68\x3a\ +\x50\x1d\x32\x99\x7f\x5d\x4a\x3a\x98\x08\x9c\x3e\x25\xcf\xb8\xfd\ +\x2d\xee\x7d\x91\xcb\x89\x1b\xd2\xa4\x94\x82\xdc\xf8\xa1\xd3\x61\ +\x28\xdd\x9a\x54\x99\x64\x65\x85\x63\x61\x10\xc9\x31\xe7\x74\x19\ +\xa8\x41\xcb\xba\x10\xad\x73\xaa\x94\xd3\xf4\x12\xbd\x61\x48\x9d\ +\xca\x95\x05\x26\x43\x23\x4a\x59\x36\xad\x66\x8d\x30\xea\x24\x29\ +\xac\xf3\xe1\xe7\x6f\xa4\xb1\xe2\x88\x3c\x79\x92\x84\x32\x07\x50\ +\x82\x14\x05\xeb\x54\x0a\xf1\x44\x60\xe8\x02\x48\x67\xe8\x56\x58\ +\x20\xd5\xe3\x4f\x97\x0f\x3e\xd2\xa8\xbc\x75\x9f\x3e\x11\xf3\xe2\ +\xa6\x77\x77\xa7\xe8\x3e\xbe\xbb\xeb\xdd\x36\x01\xcd\x8e\x12\x59\ +\xf2\xe3\xd8\x78\x82\xd3\xf6\xe6\xb6\x0c\x33\x12\xde\xe5\x28\x50\ +\x60\x8a\xca\xdf\xc1\xed\x97\x93\x01\x77\xd3\xcb\x6e\x77\x43\x54\ +\xd7\x2d\x38\xda\x09\x8b\x24\x58\x4f\x84\x7b\x07\x15\x90\xeb\xec\ +\x1e\xc2\xb3\x89\x25\x22\xf7\x9a\xed\x40\xe2\x15\x6d\x45\x5e\x57\ +\x32\xf7\xf6\x36\x16\x66\x9e\x5c\x7e\x7d\x13\xca\x25\x06\xff\x80\ +\x78\x7c\xbb\xc1\x8b\x38\x84\x14\x16\xc8\x9b\x50\x28\xe6\x4b\x16\ +\x47\x6a\x50\xd8\x37\x5e\xd5\x8a\xc5\x1b\x04\x2e\xd4\xae\xf0\x86\ +\x6c\x59\x55\x87\xeb\x55\xd0\x5c\x04\x5e\x09\xfb\x01\x7f\xc3\x71\ +\x30\xe6\x3c\x89\x25\x36\xb2\xad\x72\x42\x33\x08\x5a\x05\xd4\x3a\ +\x1f\x57\x4a\x0c\x21\x34\xbf\xd4\xe0\xd4\xb4\xc4\x40\x87\xa3\x7c\ +\x80\x45\x1d\x4c\x67\x64\x91\xd7\x69\x8c\x71\x90\xcc\xe8\x83\x9c\ +\xe6\x83\x54\x12\x4c\x93\x6c\x32\x01\x6c\x03\x32\xaa\x05\x03\x21\ +\xb4\x23\x96\x02\x57\x10\x81\xf5\x71\x40\x29\x22\x2e\xfb\x20\x8e\ +\x56\x52\x9b\x84\xd5\xc4\x1f\xcf\x86\x90\x84\xe4\x78\xde\x68\x7a\ +\x15\xd2\xcb\x37\xd7\xa7\x43\x39\xe2\x8e\x59\x95\xd7\x1c\x03\x33\ +\x68\x07\xb3\x18\x5f\xe2\x9e\x17\x39\xbf\xa7\x1c\x0f\x48\xc8\xdb\ +\xeb\x17\xef\xcf\xf6\xbe\x3d\x38\x78\x7a\xd8\xc2\x85\x06\xa3\xa3\ +\x37\xe6\x65\x6c\xfd\x6e\x02\x62\x36\x31\xca\x05\xc6\x0a\x60\x8d\ +\x3e\xf1\xf2\x70\x47\x45\x5d\xce\xfa\xee\xa3\xc0\xbc\x64\xd5\x4d\ +\x3b\x89\x3f\x20\x49\x0f\x67\x17\x61\x65\x4c\x8a\xbd\x1b\x6f\xcd\ +\x0d\x20\xba\x0d\x6c\x72\xd5\x11\x22\x57\x0e\x9b\x20\x7c\x28\x95\ +\xd0\x4a\xc5\x59\x06\x4d\x39\xa3\xf1\x52\xd4\x21\x09\xb4\x5e\xff\ +\x66\xe0\x60\x38\x34\xae\x66\x75\x49\xe6\x68\x52\x7a\x82\xa2\x9d\ +\x5f\x1c\x2f\x99\x7c\xdb\x72\xb8\x75\x75\x91\x0a\x52\x0a\xb4\x5d\ +\x38\x37\xd7\xba\x39\xb0\xb3\x35\xd2\xa2\x39\x97\xd2\x55\x89\xa4\ +\x95\xd4\x3b\x7a\xf2\x16\xdc\x8d\x83\xaf\x33\x31\x6d\xb5\xb8\xab\ +\xb5\xb6\xc3\xf9\x38\x56\xd3\x4e\x59\x9d\xbc\x14\xf1\x2c\x29\x75\ +\x9b\x78\x29\x4a\x17\x09\x12\x1a\x51\xc8\x55\xc2\x92\x9f\x13\x8b\ +\xe2\xe6\xc0\xb1\x0d\x0b\x77\x0e\x09\xeb\x83\x0f\x78\xe8\x00\xca\ +\x2d\xdd\x58\x9c\xb7\xe6\x80\x47\x5f\x2a\x82\x0e\xad\x52\xba\xc6\ +\x72\x78\x39\x43\x76\x0b\xd6\x7d\xf0\xd1\x20\xe4\x26\x9f\x17\x34\ +\xb6\xf3\x3d\x31\xdc\xc3\x08\xa1\xb8\xc1\x7f\xaa\xe2\x0e\xd3\x90\ +\xe5\x3f\xe0\x9b\xd6\x06\x43\xce\xd3\xd5\xd6\xb6\x2a\xd1\x1b\x47\ +\xa0\x8e\x89\x6c\x80\x83\x1a\x22\x14\x99\xb6\x5a\x2c\x23\x8f\x11\ +\xe3\x51\x9b\x57\x91\xd1\xc3\xa6\x43\xbe\xfe\xda\x35\x15\xa1\x02\ +\x93\x16\x3b\x75\x00\x81\x22\xcf\xc0\xeb\xd5\x99\x6e\x2c\x1c\xae\ +\x57\x40\x02\xe8\x11\xd3\xd4\x9b\xb9\x13\x2b\x28\x87\xaa\x1b\xe0\ +\x11\xcc\x86\x07\x37\xde\xc9\xb1\x40\x17\x31\x91\x61\x20\x49\x31\ +\xd9\x28\x60\x7c\x4a\x17\x74\x59\x8a\x96\x41\x14\xf1\x09\x10\xa2\ +\xa3\x5c\xa6\x2a\x62\x76\xa0\xb8\x09\xe2\x75\x72\xb1\x6c\x9c\x82\ +\x50\x78\xad\xef\xa8\x1a\xe7\x8f\x83\x4e\x34\x52\x89\x43\xab\x55\ +\xec\xa0\xba\xc1\xf5\xbc\xce\xd1\x75\x10\x76\xb5\x31\xd1\xac\x7c\ +\x22\xf6\x60\xf4\x0f\xe2\xad\x96\x21\x32\xe1\xf9\xfc\x0b\xb9\x6c\ +\xf3\xc9\xef\xe2\xb1\x3a\x95\x6f\x89\x26\x4a\xf0\xe7\xcd\xc4\x75\ +\xdf\x90\xf4\x05\x4a\x10\xf8\xde\xe1\x96\x82\x56\x35\x39\x34\x21\ +\xb1\x0e\x86\x98\x55\xa0\xaf\x0b\x0a\x57\x14\xe6\xa0\x09\x7c\xaa\ +\x59\x89\xb2\xdf\x4d\xea\x51\x59\xe0\x67\x1b\xb8\xcd\x01\x39\xec\ +\x90\xc1\x9a\x08\xb6\x1d\x11\xd9\x6e\x0a\x39\x1e\xfc\x21\x22\xdb\ +\x97\x8a\x40\xa1\x2f\x1d\xfe\x88\x61\xc8\x96\x3c\x5e\x79\xb7\xff\ +\xf8\xb1\x7c\xf3\x98\x9c\x8b\xf1\x8f\xec\x22\xec\x74\x08\x85\x8b\ +\x61\x5d\x06\x0c\xc9\x08\x1a\x67\x44\x8e\x31\x31\x60\x3b\x50\xba\ +\x1a\x58\xea\xc1\x8e\x41\x11\x29\x02\xfb\x7e\x91\x25\xa7\x4d\x52\ +\x60\x66\x58\xdf\xb7\x6e\x31\x50\xc4\x8e\xf1\xd8\x25\x50\x64\x59\ +\xcf\x2a\x37\x7b\xbc\xa8\x9b\xfc\x16\xde\x59\x1f\xe8\xb1\xf5\xab\ +\x08\xb6\x76\x0a\x35\x64\x7f\xab\x60\x6d\xdc\x97\x16\x45\xba\x34\ +\xa0\xde\x56\x1a\xf3\x92\xb5\xe7\x3c\x45\xe7\xb9\x84\xf1\x07\x0d\ +\xf2\x9a\x16\x4d\x29\x81\x3f\xc9\x99\x9b\xd5\xcd\x18\xcb\xe1\x35\ +\xa5\xba\x28\x45\x1d\x07\x42\xac\x81\xa9\xb2\x7e\xfd\x03\x4d\x6b\ +\x64\xcb\x01\xbf\xf1\x70\x35\xa2\xf4\xe6\xf2\x5c\x27\x02\x8f\x42\ +\x70\x24\x61\x98\x77\x22\x69\xa2\x2d\xb4\xa5\x52\x90\x85\xb4\x6f\ +\xdf\xbc\x07\xf8\x41\xa0\x7f\xfc\xb7\x4b\xe2\x1c\xbc\x00\xac\x38\ +\x92\x9f\xbf\xea\xee\xba\xd9\x87\x03\x4a\x5f\xae\xd8\x2b\xeb\x21\ +\xe7\x88\x01\x7f\xea\x53\x4a\xe3\x4b\xd8\xd8\xa9\x21\x6b\xd4\x98\ +\x36\x6f\xe7\x0b\xa5\x6b\xce\x6b\x2d\xc7\xdb\x53\xdb\x70\x36\x39\ +\xd8\x67\x39\xa7\x17\x50\xdc\x26\x4a\x7e\x2c\xd6\x9f\x8b\x9f\x97\ +\x81\xef\x7d\x50\x7d\xf2\xad\x6a\x96\x2c\x68\xab\x59\x92\xaf\xce\ +\x45\x7e\x77\xc0\x02\xad\x92\xc5\x78\xeb\x35\xaa\x01\x43\xc6\x33\ +\xdd\xc0\x64\x42\xc4\x97\xd6\x49\xb7\x67\x75\xce\x4c\x15\x90\xd4\ +\xe2\xd3\xca\x4d\x43\x55\xe7\xeb\x1f\xb3\x46\x4c\x59\xfd\x72\x2e\ +\x5c\x63\xe8\xca\xed\xb9\xb7\xb6\x1d\xf7\xf1\xbc\x2e\x88\x01\xf7\ +\x2a\xfb\x15\x17\x41\x6b\x7e\xeb\xdd\x6e\xce\x92\x3d\xdc\x50\x6f\ +\xdd\x79\x96\xfd\x6c\xb0\x9d\xe8\xf4\x17\x79\x1b\x33\x62\x4f\x2a\ +\xb5\x37\xb4\x76\x10\x00\xc2\x2d\x02\x08\xfe\x34\x8a\xf0\xc0\xcc\ +\xbe\x43\x41\x66\x6e\xff\x68\xdd\x30\x59\x5f\x72\x2a\x2f\x3e\xd7\ +\xdf\x30\x2b\xef\xa8\xe6\x44\x2a\x5a\x93\x52\xf4\x65\x81\xfa\x92\ +\x5e\x07\xbe\x76\x15\xd8\x28\x04\xdc\xb0\x11\xf6\xdc\x11\x14\xae\ +\x67\x0c\x02\x07\x7f\x27\xfe\xee\xdb\xe8\x3c\xc9\x43\x2e\xec\xa7\ +\x8f\xe7\xc4\x85\x6f\xf9\xb1\x37\xe8\x6b\xc0\x1e\x36\x60\x33\xa7\ +\xd6\x7c\x29\x0f\xd1\x3d\xf8\xa7\x0e\x3c\x7e\x71\x64\x86\x65\xcd\ +\xc8\x2c\x3e\xa7\x19\xd7\x9c\xe3\xb7\xf6\x0f\x98\x53\xdc\x75\x72\ +\xea\x99\xa8\x0f\xb2\xe1\x0f\xd5\xe3\xaa\x01\x49\x2a\x31\x02\xcd\ +\x5b\xf3\xb6\x02\xbb\xde\x12\xdf\x8b\x43\x4f\x17\x5f\xf3\xfc\xd3\ +\x8c\x8c\x55\x03\x1d\x8b\x86\x9a\xe0\xac\x9e\x2c\x59\xd5\x88\x5f\ +\x9f\x93\x1e\x85\x14\x9e\x35\x4f\x10\x9a\x62\x0b\x0d\xd4\x9b\x30\ +\xa0\x27\x11\x87\x82\x4d\x0e\x1e\x51\xc9\x82\x3f\x13\x69\xb6\x24\ +\x79\x51\x25\x73\x9c\x4a\x12\xba\xa0\x4b\x9c\xfd\x63\x13\x54\xf1\ +\x7a\x2c\xa7\x00\x74\x5c\xd5\x0a\xbc\x85\x2d\x48\xda\xb7\x0b\x68\ +\x89\x30\x98\x4c\x59\xdc\x5b\x77\xbe\x64\x07\x75\x4d\x9c\xf8\xd5\ +\x6b\xcd\x76\x83\x95\x83\x39\x81\x40\xcf\x36\x9f\xc1\x68\x85\xbb\ +\x7c\x0c\xda\x85\xe1\x24\x99\xd6\x9c\x8e\xf0\x33\x69\xa4\xe0\x07\ +\xa1\x29\x73\xda\x7a\xd2\xca\x38\x78\x85\xca\xa7\x2d\xf5\x1e\x6e\ +\xcd\x1a\xc8\xb6\x09\x5a\xa0\x4e\x51\xd9\x94\x33\x3c\x81\xc8\xbe\ +\xc1\x03\x08\xb0\xe7\x6c\x6d\x73\xab\x3f\xf4\x50\xb8\xa5\x17\xb9\ +\xb5\xa2\x72\x35\xaf\x17\xf8\xb9\x87\x5d\x6d\x53\xb5\x3f\xf7\xec\ +\x40\xd8\x39\xa3\x52\xc1\x3f\xd8\xcb\xb4\x1e\xa9\x91\x75\x47\x9d\ +\x28\x16\xf9\xba\x29\x5d\xdd\xf4\x05\xf7\x5d\x29\xe0\x61\x7b\x51\ +\xfa\xd2\xd1\xc6\x84\xc4\x20\x70\xca\x06\x36\x20\x00\x91\xcd\xf5\ +\x7f\x2f\xaa\x7d\x91\x3e\x6a\x1e\xcb\xf9\x3b\xfe\xac\xbe\x7f\x63\ +\x1c\x79\x68\x8b\x0d\xaf\xed\x9a\xe3\x4d\xa9\xdf\xf9\xce\x3f\xdc\ +\xe2\x6e\x9d\xfd\x05\xb0\x66\x1e\xc0\xbd\x7d\x74\x80\x8b\xad\x01\ +\xac\x1f\xed\x8d\xd0\xba\x3f\x39\xd2\x53\xdc\xd5\xff\xe5\x83\x02\ +\x71\xf2\x2a\x6b\x18\xdb\x5c\x9a\x5a\x59\xd3\x6e\x7c\x94\x65\x00\ +\x9b\xa9\xd9\x83\x57\x3d\x42\xbb\x65\x55\x1f\x59\x91\x8f\xad\x93\ +\x0d\xa8\x8b\x60\x05\xc4\x49\xbb\x6d\x53\xe1\x66\xce\xa7\x45\x11\ +\xcb\xea\xb9\xd7\x54\xa9\x5a\x5b\x7f\x64\x62\x81\xe4\x23\x5d\x59\ +\x80\x10\xf7\xf7\xbd\x81\x0e\xfe\x3f\x16\x61\xf7\x59\x1e\xb3\x5f\ +\xca\x1d\xa7\x28\x56\xff\x8f\x10\x1d\xef\x1b\x89\xff\x1b\x4d\x52\ +\xbe\x02\x6b\xc4\xef\x24\x4a\xef\x04\xc4\x31\x3f\xcf\x16\x25\x07\ +\x47\xc0\xc2\xff\x00\x9d\xa1\x00\xc6\ +\x00\x00\x24\x34\ +\x00\ +\x00\x86\x68\x78\x9c\xd5\x7d\xfb\x73\xdb\x38\xd2\xe0\xcf\xa7\xbf\ +\x42\xe6\x5d\xb9\xc4\x88\x92\xa5\x64\x76\x76\x86\x0a\xa3\xf2\xd8\ +\x9e\x8d\xeb\xcb\xeb\x62\x67\x67\xe7\x54\xba\x14\x1f\x90\xc4\x44\ +\x16\xf5\x89\x54\x12\xaf\xa4\xff\xfd\xba\xd1\x00\x08\x90\xa0\x1f\ +\xd9\xec\x55\x6d\xd5\xd4\x44\xc4\xb3\xd1\x68\xf4\x0b\xdd\xf0\xc9\ +\x93\xa3\x56\xfb\x49\xfb\xd3\xff\xde\xb2\xcd\x6d\xfb\xc3\x65\x7b\ +\xd8\xff\xa5\x3f\xfc\x19\xca\xb0\xf8\x2c\x5b\xdf\x6e\xd2\xf9\xa2\ +\x68\x3f\x1d\x0c\x87\x5e\xfb\xf4\xc3\xf5\xcb\xb7\xef\xaf\xfa\xc5\ +\xb7\xa2\xdd\x59\x14\xc5\xda\x3f\x39\xf9\xf4\xdf\xd8\x75\x9b\xf6\ +\xe3\xec\xe6\x24\x8c\xb2\x6d\xe1\x62\xd7\xf3\x6d\xb8\x6c\x2f\xd3\ +\x98\xad\x72\x96\xb4\xb7\xab\x84\x6d\xda\xc5\x82\xb5\x5f\x5f\x5e\ +\xb7\xb3\x4d\xfb\x6f\xef\x5e\xb5\xff\xce\x36\x79\x9a\xad\xda\x4f\ +\x65\xbb\xbc\x8f\x3d\x8d\x71\xfb\xd9\x66\x7e\x22\xaa\x05\x50\xa2\ +\x3e\xc9\xe2\xbc\x2f\x1a\xe1\xd4\x1f\x2e\xa1\xf2\xa4\xd5\x99\x6d\ +\x57\x71\x01\xc3\x76\x62\xef\x93\xbb\x93\x5f\xed\xcf\x9d\xd0\x8b\ +\xdc\xdd\x97\x70\xd3\x4e\x82\xb0\xbf\xca\x12\xf6\x26\xbc\x61\xfd\ +\x22\x7b\x95\x7d\x65\x9b\xb3\x30\x67\x1d\x77\x94\xce\x3a\x4e\xb8\ +\x61\xa1\x13\x04\x41\xe2\xee\x22\x68\xb9\x86\xef\x55\xf1\x06\xda\ +\x8f\x92\x20\xea\xaf\xa0\x13\x36\x3b\x0a\xfb\x8b\x0d\x9b\xed\xf7\ +\x47\xc9\x7e\x1f\x35\x8c\x77\x14\x04\xce\x4d\xb8\x76\xdc\x0d\x2b\ +\xb6\x9b\x55\x7b\x16\x2e\x73\x36\x0a\x83\xb8\xe3\xa4\x37\xf3\xc9\ +\x36\x67\x50\x1b\xfc\x4f\xa7\x9b\x74\x9d\xa9\xe3\x4e\x06\xd3\x11\ +\xb5\x3c\x3a\x0a\x8f\x8f\x97\x9d\xd0\x3d\xd0\x77\xe7\x24\x5d\xad\ +\xb7\xc5\x3e\x67\x4b\x16\x17\xfb\x82\x7d\x2b\x10\xcc\x7d\xb4\x2d\ +\x8a\x6c\xb5\xcf\xa2\x4f\x50\x7c\xd2\x2f\x58\x5e\x74\x12\x77\x0c\ +\xc0\x25\x69\x1e\x46\x4b\x96\xf8\x0e\x2e\x26\x19\x4b\x70\x23\x3f\ +\x72\xc5\xd0\x0a\x37\xf8\xb5\x13\x13\xc7\xf0\x5b\xac\x39\xef\xb8\ +\xfd\x70\x95\x5c\xb1\xe5\x0c\x7e\xcd\xd2\x65\xc1\x36\x25\x7a\x65\ +\x8f\x76\xdc\x8f\xb7\x9b\xb3\xab\xab\x4e\xb1\x48\x73\xcf\xf9\x92\ +\xe6\x69\x94\x2e\xd3\xe2\xd6\x71\x01\x8b\xce\x22\x4d\x12\xb6\x72\ +\xf6\xfb\xb8\xcf\xbe\xad\x37\x62\x9c\xbc\x4f\xe5\xbc\x93\x7b\x70\ +\xfb\x4b\xb6\x9a\x17\x8b\x43\xdc\xdf\xa6\x01\xfe\x6f\xbf\xdf\x1d\ +\x38\x9e\xf1\xa3\xff\x85\x48\xc5\xdd\xe1\x20\x05\x5b\x25\x1d\x2c\ +\xf6\x76\xa2\xdc\x77\x88\x6c\x1d\xaf\xf5\x99\xdd\x9e\xc1\x5e\xf8\ +\xbb\xd3\x57\xd7\xfe\xf0\x17\xef\xb7\xd3\xb3\xff\xba\x7a\x77\x7a\ +\x76\xe1\xff\xe2\x9d\x9d\xbe\xbb\xfa\xf8\xea\xed\xd9\x7f\xf9\x4f\ +\x07\xde\xd9\xdb\xd7\xaf\x4f\xa1\xc5\x2f\xf4\xeb\xcd\xb9\xff\xeb\ +\x50\xfe\xfc\xf8\xea\xe2\xf7\x6b\xfd\xfb\xfd\xe5\xdf\x5e\x42\xc1\ +\x33\x28\x78\x73\xfd\xfe\xed\x2b\x7f\xf8\x57\xef\xfc\xe2\xd5\xc5\ +\xf5\x85\xff\xd3\xcf\xde\xf9\xdb\x3f\xde\xf8\x3f\x0d\xbc\x0b\x18\ +\xe4\xd9\x5f\xe0\x9f\xeb\x8b\xf7\xfe\xf0\x99\x77\x71\x05\x33\x5e\ +\xf8\x4f\xff\xea\xbd\x7c\xfb\xfa\xc2\x7f\xf6\xb3\x77\xf9\xe6\xea\ +\xe2\xfd\xb5\xff\xd3\x5f\x3c\x3e\xc3\xb3\xbf\x7a\xaf\x2f\xde\x7c\ +\xc0\x81\xdf\x7c\x78\xfd\xee\xf4\xfc\xe3\xe9\xf9\xb9\x3f\x1c\xfc\ +\x55\x7e\x9e\x5f\x9c\x5d\xbe\x3e\x85\xe9\x86\x03\x55\x74\xf9\xf7\ +\xcb\xf3\x0b\x28\x19\xca\x12\x31\xdf\xe0\x17\x59\xf0\xfa\xc3\xab\ +\xeb\xcb\x77\xaf\xfe\x84\xb2\x9f\x65\xd9\xd5\x87\xdf\xae\xdf\x9f\ +\x9e\x01\x4e\x06\xbf\x7a\xef\x4e\xff\x76\xf1\x91\x43\xfd\xec\x27\ +\xfa\xf8\xf0\xce\x7f\xf6\xcc\x7b\x77\xf1\xfe\xf2\x2d\x00\xf0\xeb\ +\xc0\xa3\x15\x3f\xfb\xd5\xbb\x7a\x79\x09\x90\x0e\x7f\xf6\x08\x8b\ +\xcf\x9e\x7a\xd7\xa7\xbf\xf9\xbf\x7a\xd8\xe3\x17\xef\x8f\xcb\x37\ +\x30\xce\x15\xe0\xea\x70\x70\x47\x71\x7f\xb6\x92\x1b\xb4\x5b\x6f\ +\xb2\xf5\x69\x51\x6c\x7c\x5e\x8a\x5f\x48\x03\xf0\x33\x84\x42\xef\ +\xe3\x2c\x8b\xb7\x39\xd5\xf1\x9f\x1e\x15\x28\xfa\xe2\xc7\x55\x90\ +\x58\x71\xbb\x66\xd9\xac\x1d\x22\x3d\xad\xb6\x37\x11\xdb\x38\x63\ +\x24\x9c\x3e\x0b\xe3\x85\x4e\x92\x74\xbc\x5b\x58\x37\xca\x59\x71\ +\x9d\xde\x30\x60\x48\x7a\x8b\x18\xce\x07\x4d\x08\xc7\x3d\x3a\x3e\ +\x8e\xfa\x71\xb8\x5c\x42\xe1\xc1\x83\x23\xe1\xfa\x7c\x58\x02\xae\ +\x1f\xae\xd7\xcb\x5b\x22\xeb\x70\x33\xdf\xde\xe0\x99\x80\x76\x79\ +\xbc\xc9\x96\xcb\x77\xfc\x8c\xf8\x95\xc9\x43\x3c\xdd\xfd\x68\x93\ +\x7d\xcd\xd9\xa6\x7f\x93\xa7\xec\xf8\xf8\xa4\x93\x17\x61\x91\xc6\ +\xfb\x0d\x5b\xc2\xbf\x5f\x98\x2b\x4e\x2a\x9f\x2b\xce\xf3\x8e\xb3\ +\xce\xf2\x14\x47\x71\x5c\x77\xbf\x07\x2e\x9a\x67\xcb\x6d\xc1\xee\ +\x6a\x46\xeb\x2f\x0f\x6a\xd3\xf1\x3c\xe9\xc8\x59\xf7\x72\xd8\xfd\ +\x2c\xfd\xc6\x12\x09\x44\xe5\xf8\xaa\x29\xbc\xa1\xeb\x22\xec\xe1\ +\xb6\xc8\xf6\xb4\xe4\x86\x2e\x19\x9c\xc2\xd9\x32\xfb\x8a\x5d\xba\ +\x0d\x75\xbd\xdb\x3b\x6b\xbf\xf1\xe9\x80\x0d\xb0\xff\xee\x0c\xc4\ +\x26\x3c\x64\x6d\xf7\x02\xd7\xfa\xf1\xd0\x09\x16\x7d\xc2\xb1\x78\ +\xf7\x4e\x02\x1f\x26\xbe\x36\x06\xb2\x03\x92\x42\x0a\x72\xfd\xf0\ +\xe0\xfd\xf3\x12\x04\xe2\x37\x8d\xd8\xdd\x1d\xf0\xba\x10\xe4\xc5\ +\x27\x29\x2b\xca\x21\xa9\xb1\x03\xf4\x89\x0c\x91\x97\xd3\xa8\xee\ +\x0e\x45\x09\x16\x80\xe4\x70\x47\xb3\x6c\xd3\x41\x1a\x8c\x46\x72\ +\xda\xe3\xe3\x10\x6a\x60\x54\x39\xf9\x88\x64\x5a\x05\x52\x1c\x36\ +\xc2\xb3\x25\x29\x04\xb8\x35\xff\x96\xa4\x23\xbf\xf9\x92\x1d\x1c\ +\x03\x36\x27\x67\x97\xab\xa2\x13\x1a\x30\xba\xde\x70\xc0\x87\x3b\ +\x4a\xf3\x37\xe1\x9b\x0e\xca\x9a\x08\xe6\x1f\xc8\x55\x45\x87\x50\ +\xc9\xd4\x8e\x7b\x10\xe2\xad\x3d\x38\x78\x42\x60\x5d\x71\x09\x87\ +\x3c\xbd\x2e\x69\xf8\xd2\xa3\x14\x98\x0b\xec\x72\xbe\x5d\xaf\xb3\ +\x4d\xd1\x27\x91\x08\x27\x6c\x53\x8c\x1d\xed\xc3\xf1\x5b\xce\x4d\ +\x06\x22\x36\xc9\xbe\xc2\x1a\xbb\x0e\x08\x8c\x5e\x75\x12\xc7\xd3\ +\xb7\x00\x00\xdb\xb0\x2f\x00\xd9\x39\x9b\x85\xdb\x25\x02\x08\xc7\ +\x9d\xad\x1e\x04\xd7\x76\xc5\x21\xb3\x4f\xe3\x12\x77\xe4\xec\x6a\ +\xe2\xfc\x91\x26\xc5\xc2\xf1\x9c\x97\x0c\x75\x2b\x67\xea\x99\x4c\ +\x4f\xc9\xe5\xa4\x33\xf3\xe6\xde\x8d\x47\x72\x0f\xfb\x32\x4f\x9b\ +\x7d\xde\xa3\x6d\xf8\x7d\x99\x85\x1a\xdd\xcf\xe0\x10\x87\x49\x92\ +\xae\xe6\x4e\x97\x13\x72\xb1\xd9\x32\xa4\xc6\x01\x6e\xcc\x8d\xdb\ +\xdc\x2d\xca\x36\xa0\xa6\x51\xaf\xae\x84\xd2\xe8\xbd\xba\xa3\xf7\ +\x0d\xf0\xc8\x74\x55\x9d\xf3\x20\xcf\x4b\x7b\x7e\x40\xe2\x64\x01\ +\xa7\x24\x1a\x7c\x3c\x71\x5e\xb1\x59\x01\xa8\x78\x4f\x98\xf0\x27\ +\xce\x75\xb6\x86\xef\xdf\x32\x50\x6c\x6e\x00\x35\x0b\xd0\xb9\x0c\ +\x9d\xca\x4b\x83\x5d\xba\x5a\xb1\x0d\x1f\x82\x04\x48\xf9\xed\xf1\ +\x9f\x84\x58\xad\x8e\x0a\xbc\x16\x08\x03\xa3\x63\xf9\xed\xf1\x9f\ +\x7a\x47\xad\xe0\xc0\x25\xdb\xc4\xe1\x63\x39\xdd\x68\x1a\xa8\x6d\ +\x98\xf1\x93\x3b\x0b\xb4\x93\x9b\x6a\x0d\x49\xba\x70\x25\x67\xa4\ +\x53\x4b\x55\x70\xd1\x31\x76\xf9\x61\x5a\x78\x09\x71\xa0\x19\x50\ +\xed\x1a\x0e\x15\x50\xa1\x00\x80\xc3\x54\x01\xc0\x9b\x73\x10\x84\ +\x8c\x9c\x1d\x95\x32\x52\x03\x48\x75\x2c\x01\x82\xe1\xbf\x03\x24\ +\xbe\xb3\x30\xa5\x82\x8c\x08\x5b\x2a\x65\xa8\xe2\x4d\x1c\x1f\x36\ +\x6e\x97\x84\x45\x68\x88\x73\x2f\x51\x5a\x26\x68\x75\x58\x0d\xa5\ +\xc9\xe4\xd9\x14\x0e\x19\x17\xb8\x78\x66\x0c\x9e\x28\xc0\x43\xdd\ +\x5d\x30\x94\x98\x6b\x0f\xf0\xed\x14\x21\x9e\x37\xe4\x39\xc0\x9a\ +\x3d\xf8\x8a\x6a\xdd\x39\x33\x0c\x54\x97\x96\xd6\xc7\x4b\x02\xc9\ +\xa1\x04\x12\x3a\xa8\xc7\xbf\x00\x4e\x75\x7c\xcc\xe7\x4b\x68\x69\ +\x55\xed\x22\x54\xbc\xb4\x1f\x65\xc9\xad\x87\xec\x14\xf4\x04\x58\ +\xfd\xd9\x22\x5d\x26\xc0\x47\x55\x7d\x0c\x1a\x7a\xc1\x2e\x96\x0c\ +\xbf\x3a\x4e\x92\x7e\x01\x58\x4b\x64\x45\xfd\xbc\xb8\x5d\x32\x6f\ +\x77\x93\xae\x04\xdd\x39\xc3\xc1\x00\xd0\xea\x2d\xc4\x27\x8a\x37\ +\xc7\x13\xa7\xd9\x1f\x78\x74\x40\x89\x7e\x07\x1c\xf1\x92\x0b\xaa\ +\x31\xe0\xb8\x64\xb3\x19\xe8\x3e\xe2\x33\x08\x60\xcc\x91\x95\x5d\ +\x06\x4e\xb6\xd2\x19\x66\xba\xe2\xa2\x63\xc3\x6e\x40\xf2\x89\xd5\ +\xb8\x04\x24\x9a\x13\xeb\x65\x78\x0b\xa4\x95\xad\x98\x63\xee\x39\ +\x2a\xe2\xeb\xe5\x16\x0e\xbf\xbf\x03\x50\x6b\x7b\x1e\x72\x85\x7e\ +\x12\x4e\x51\x09\x2c\x32\x24\x54\x25\xab\x58\x1b\x66\x4d\x38\xeb\ +\xe5\x23\xe4\x13\x36\x0d\xf4\x8f\xfd\x7e\x32\x1d\xe9\x05\xfd\xf5\ +\x36\x07\x26\x0a\x23\xc3\x07\x90\xce\xc1\x43\x8a\xae\x4d\x0a\x27\ +\xa2\x13\x69\x23\x45\x53\xd8\xd8\x56\xd8\x67\xb4\x1d\x20\x16\x35\ +\xcb\xce\x55\xe0\x04\x83\x11\x7b\x1e\x09\xf1\x39\x62\xdd\xae\x1b\ +\xf6\xb3\x35\x0e\x0c\x43\xc0\x84\xd0\x6f\x0a\x22\x0d\x7f\x0d\xa7\ +\x42\x41\x54\x83\x7a\x09\x07\x27\x5b\x15\x21\x4c\x69\x57\x65\x4b\ +\xea\xc8\x6e\x10\x80\x73\xf1\xfd\x4e\x48\x63\x30\xd4\x1a\x6a\x50\ +\x98\x0e\x7f\xf6\x51\x49\x00\xbd\x15\x9b\xd1\x3c\x50\x7e\xf0\x16\ +\x61\x7e\xc5\xd5\xa0\xca\xac\x80\x06\x6e\xd1\x71\x21\xad\x54\x21\ +\xdd\x30\x33\x6d\xd3\x08\xc7\xe6\x1c\x7a\x89\x8c\x19\x24\x2a\x1f\ +\x95\x73\x69\x5f\x7c\x20\x87\x1e\x91\xa6\x4d\x9d\x50\x75\x01\x04\ +\xbf\x50\x52\x1e\x79\xc4\x08\x8b\x82\x21\x18\xcd\x54\x47\xdf\x03\ +\xc9\x72\x92\x83\x97\xe6\x6f\x01\xa0\xd3\x6f\x69\xde\xc0\x26\xda\ +\xe1\x0b\x5c\xe9\xf3\xa8\xab\x5a\x57\x5a\x7a\xcc\x03\x96\xaf\x99\ +\xa2\x60\x2b\x96\xc3\x22\x7b\xf1\x16\xb8\xef\xd5\x8a\x08\x3a\xa6\ +\x78\xc0\xe1\xbf\x0e\x39\x3f\xdc\x51\x6b\x74\x52\xf5\x86\xc0\x51\ +\x9b\xb3\xe2\x3f\xd9\x29\x72\x42\x4b\xa8\xf8\x46\x22\xf4\x8d\xa0\ +\xd6\xd7\x8f\x97\x2c\x5c\x9d\x03\x2f\x26\xf6\xf6\x39\xd0\x8a\x46\ +\xda\xef\x40\x67\xac\xf2\xb8\xc4\xc1\xc0\x4b\x46\x1d\xdc\xe2\x78\ +\xea\x1e\x05\xab\xed\x72\x39\x8a\xe1\xd0\x14\x9b\xdb\x5d\x84\x96\ +\x56\x01\x98\x9a\x83\x18\x0d\x57\xc9\x12\x54\x78\x87\x98\x0b\x08\ +\x8e\x38\x2c\x50\x9f\x71\x77\x87\xcf\xe8\x8b\x38\x30\x20\x24\x0e\ +\xc1\x12\x20\x00\xe9\x4b\x0d\x47\xda\x6f\x0d\x02\x2f\x36\x75\xaf\ +\xaa\xe8\xe2\x7e\x03\x97\x7b\x69\xd0\x39\x23\xec\x87\xd0\x9b\x60\ +\xeb\xa9\x74\x36\xb8\x51\xc7\x79\x02\x0a\x0e\x97\x72\xc0\xb8\x3a\ +\xb2\xba\x3a\x1c\xad\x86\xda\xdd\xb3\x20\xa0\xdd\x43\xa9\xf2\x2c\ +\x49\xda\x8a\xbe\x1e\xc2\x8d\x24\x17\xf5\xbf\xf2\x4d\x31\x56\x84\ +\x64\x4f\x2c\x28\xec\x03\xaf\x4d\x41\x62\xf4\xb9\x4b\xc8\x9b\x8d\ +\x42\xb3\x6c\x38\x1d\xcd\x02\xd6\x75\x7a\x4e\x37\xe4\xca\x36\x74\ +\x4d\x82\x78\x14\x03\xea\x68\xbb\x61\x0a\x25\x89\x27\xb3\x69\xd0\ +\x52\x33\x2d\x4a\x19\x1c\x91\x0c\x5e\xa0\xe1\x3b\x42\x96\x16\x44\ +\x9c\xe1\xee\xe8\x0b\x18\x76\x09\xe0\x02\xb5\x0c\x65\x04\x2b\xf3\ +\x82\xac\x65\x12\x74\x34\x33\x6f\x09\xea\x4a\xb0\x62\x5f\xdb\x00\ +\x92\xe4\x9e\x41\x24\x05\x06\x57\x20\x76\xc0\x27\x65\x95\x2b\xa7\ +\x2b\xe5\x43\xa5\x75\xec\xed\xd0\xef\x96\xaf\xc3\x98\xf9\xcc\x23\ +\xec\xa1\xbb\xcd\x0f\xc5\xc7\x05\xaa\xed\xef\x36\x0c\x2c\x14\xbf\ +\x36\x5a\xbf\xd6\x66\xbf\x97\x1d\x7f\x03\xa5\xf2\x6c\x19\xe6\xc0\ +\x83\xc0\x06\x01\x50\x44\x63\xb0\xe1\xf1\x5f\x64\x33\x34\x1c\xe2\ +\xc8\xac\xab\x10\x24\x92\xaa\x81\x33\xb5\xa1\x42\x35\x4b\x90\xad\ +\xe6\x40\x40\xa0\x99\x7b\xb3\xe0\x74\xb3\x09\x6f\x35\x20\x73\x3c\ +\xd1\x44\x30\x0a\xd1\x60\x77\x82\x06\xcc\xdd\x19\x49\x70\xc4\x8e\ +\x8f\x67\xd2\xa0\x94\x08\x12\x32\x08\x4f\x1e\x90\x37\x22\x2b\x99\ +\xa2\x68\x00\x72\x04\xdd\xd4\xf5\x5b\x09\x52\x08\xf4\x4c\xfa\xf1\ +\x22\xdc\x9c\x16\x60\xc1\x22\x1c\x1f\x15\xe3\x07\x31\xd7\xec\x4c\ +\x99\x07\x82\x4a\xc8\x0b\x82\xda\xf7\x1c\x9d\x26\x69\xfe\xbb\x6c\ +\x38\x9f\x24\x53\x77\x8c\xff\x17\xb0\xcc\x41\xb3\xf4\xe7\x38\x6d\ +\x0a\xb2\x0a\x9a\xa7\xdc\xae\xdd\x2d\x82\x74\xa4\x0b\x9b\x83\x74\ +\xb7\x3c\x64\xe2\xd1\x7c\x3c\x17\x04\x83\x9a\xda\x0e\xac\xf1\x8f\ +\xe9\x0a\x4e\x84\xeb\x1b\x0d\x3d\x4e\x77\x9d\x84\x8e\xb5\x5b\x9e\ +\xc5\xc5\x01\x37\xf0\x8f\xfa\xc1\x7b\x20\x5d\xf3\xc3\xab\x46\xd0\ +\x48\x75\xa7\x91\xa3\x43\xbf\x1d\x0b\x55\x3a\x8e\x27\xe8\xdd\xdf\ +\x29\xe7\x2c\xe1\xc1\x33\x66\xf2\xab\x64\xc5\x57\x17\xf3\x05\xf5\ +\xcb\xb9\x68\x81\x23\x42\x20\xe9\x1f\x41\xd4\x89\x45\x89\x3c\x75\ +\xad\xda\xb1\xd3\xab\xe9\xe3\x23\x8c\x78\xc6\x01\x78\x4b\xc5\x60\ +\x64\x01\xc2\x49\xc2\x73\xda\xd3\x27\x21\x03\x5c\xf0\xbd\x3e\x99\ +\x7b\x3a\x58\xda\x3e\x26\xfd\x84\x01\xbd\x67\xb7\x68\x48\x8f\x74\ +\x9c\x76\xe4\xa7\x60\xa7\x1d\x87\xca\x1d\x59\x4e\x5b\x0b\x98\xa9\ +\xc2\x66\x31\xbf\xa3\xfe\x0d\x2b\x42\xc4\x12\x52\xa6\xfc\xdd\xc7\ +\x4d\xd3\x01\x47\x1f\xc9\xa4\x02\xed\x54\xe1\x5e\x1f\x17\x0a\x71\ +\x7e\xb3\x48\x2c\x45\x2f\x34\xd0\xa2\xec\xff\x1a\x4a\x5c\x21\xc0\ +\xce\x25\x95\xea\x75\x23\xad\xa0\xe3\x3e\x60\x14\x74\xa9\xe2\x25\ +\x45\x1a\x4a\x37\x43\xe2\xc8\x3a\xce\xc8\xf4\x29\x14\x77\xeb\xb6\ +\x1c\xd5\xbc\xbd\x4d\x7b\xe8\x91\x64\xda\x00\x07\x41\xb1\x4d\xde\ +\x0d\xb1\xca\x83\xa0\xe1\x0a\x8d\x8a\x5b\x15\xae\x08\x56\xce\x52\ +\xa0\x3b\x7f\x94\x6f\xd8\x24\x43\xf2\x6b\x69\x3e\x5e\xc1\x24\x49\ +\x7d\x0d\xaa\x4e\x31\xa9\x8f\x87\x53\x60\x8a\x20\xac\x12\xe4\xba\ +\xf1\x81\x08\x07\x2c\x1f\x49\xc5\x89\x61\xdf\xc2\xa6\x96\x75\x35\ +\x33\x31\x26\x42\x8f\x88\x17\x85\x25\x15\x83\x8a\x89\xde\x97\xb2\ +\x2f\x2f\x39\x34\x0e\x5d\xc1\x4c\x05\x60\x10\xd2\x88\x23\x5c\x63\ +\x89\x7b\x83\x02\x26\xf1\xd8\x01\x5d\x84\x6f\x1a\x48\x6f\x6d\x5f\ +\x9d\xa9\x7d\x67\xef\xde\x58\x32\x7e\x2b\xf4\xe2\xb5\xe2\xca\x02\ +\xc8\xc9\xd5\xb4\xfb\xda\xea\x4b\xb8\x3d\xce\xbc\x5c\xe5\xb9\x7b\ +\x5c\x67\xee\x20\x02\xc4\x09\x06\xe0\xdb\x55\xa1\xea\x7e\xa3\x86\ +\xc3\x99\x2a\x32\xba\xb8\xcf\xf9\x2f\x47\xa7\x86\x19\x8d\xe9\x8e\ +\x43\xdf\x5e\xd1\x05\x93\xc8\xbc\xf9\x4b\x82\x44\x5e\x39\x81\x62\ +\x02\x40\xa5\xab\x70\xc9\x7b\xa0\x01\x0b\x74\x81\x3f\xf9\x2d\x86\ +\xa4\x6c\x65\xc3\x82\x7e\x36\x02\x9d\xd8\x6c\x34\xe9\xf5\x10\x5e\ +\x54\xbe\x2a\xe3\x41\xd1\xe1\x60\xb0\x0e\xc9\x04\x71\xe1\xf2\x02\ +\xb0\x63\xc8\x58\x06\x46\x0c\x2b\x5d\x37\x1a\x4b\xe3\xd8\x02\x04\ +\xf0\xcd\xc0\x0b\x96\x34\x17\xde\xcb\x77\xe4\xcb\x64\x49\x07\xc4\ +\xe0\x3d\x76\xce\x6b\x74\x94\xfe\x47\x9b\x39\x7c\x05\xa2\xd5\x39\ +\x43\x7f\x4c\xee\xc3\xef\xff\x21\x5a\x01\x8c\x42\x77\xfb\x94\x57\ +\x6d\x21\xc9\xbf\x84\x05\x5c\x7a\xea\xfb\xdc\x7f\xbc\x5d\xeb\xba\ +\x89\x68\x76\x28\x35\xc5\x8e\x03\x83\xf3\x96\x8e\xb7\x53\x22\x3e\ +\x0e\x57\x31\x5b\xfa\x8e\xcf\x2f\x6e\x05\xdb\x74\xf0\xac\x14\x58\ +\xe3\x0f\x41\xa0\x2c\xc3\x5b\x7f\x00\x67\x80\x77\xbe\xac\x48\x1c\ +\x72\x36\x35\x89\x5f\xe5\xd9\xbe\x4b\x02\x97\x56\x52\xd8\xa7\x49\ +\xce\xa1\x47\x07\x8d\x10\x31\x4c\x0c\xe8\xfd\x7c\xcf\x10\xc8\x9b\ +\xe1\xb0\x02\x8d\x49\x55\xa4\x5f\x00\x97\x67\x85\x17\x6a\x5d\xba\ +\x8e\x74\x9e\x9f\xe1\x98\x9c\xd2\x1d\x17\xd5\x17\x4d\xf8\x3d\xa2\ +\x27\xba\xac\x8a\x6c\x7d\x79\x73\xc3\x92\x14\x58\xda\x3b\x38\x54\ +\xe1\x3c\x24\xe4\x54\xd5\x48\xc2\x10\xf7\x62\x31\xb9\x41\x02\xad\ +\xe7\x0f\x17\xdb\xad\xaa\xc4\x55\x63\x00\xd6\xaa\x77\x35\x68\x73\ +\x11\x7b\xe3\x4d\xae\x68\x6e\xa9\x39\xf2\xb2\x0f\x6b\x68\x3b\xd2\ +\x0a\x70\x1c\xbe\x3c\x10\x95\xa5\xd0\x01\x53\x00\xd0\xb1\x48\x63\ +\x90\x95\x43\x6f\x2e\xad\x05\x9d\xf5\xf5\x89\x9a\x4a\xc9\x88\xfe\ +\x1f\x42\xa5\x8a\x3c\x18\x47\x1d\x59\xe6\x82\xd9\x9e\xe5\xea\x5e\ +\xca\x1c\x44\x5a\xbf\xbe\xf2\xdd\x1c\xcd\xf6\xfb\xf9\x7e\x7f\xa4\ +\x41\x7a\x16\xae\x01\xc1\x60\xfd\xb8\x86\x4b\x87\xb7\x10\x68\x05\ +\xe2\x7d\x0d\x9a\xf4\x91\x31\x05\xa7\x69\x3e\x66\xbd\xa9\xab\x63\ +\x02\xcb\xf0\x76\x76\x13\x34\x5c\xd3\x56\xa6\xc1\xe9\x4d\xa5\x81\ +\xe6\x2a\xaf\xc4\xc4\xc0\xe2\x7c\xbd\x46\x8d\xdd\x35\xb6\x43\x8e\ +\x85\x8b\xb2\x6c\x1d\xdd\x1a\xeb\x65\xd0\xf0\x28\x28\x5d\x5c\x47\ +\xf5\x3e\xb6\x2b\xa3\x91\x86\x30\xe0\xf3\xc6\xc9\x91\x1b\x54\xd5\ +\xe5\xed\x47\x00\xb5\x59\xed\xf0\x3c\xae\xb3\x4e\x77\xaf\x71\x08\ +\x20\x78\x38\x3e\x9a\xd1\xca\x34\xb7\x59\xd9\x0e\x8a\x0f\x23\x83\ +\x88\x1f\xd2\x13\x48\x1d\xfb\xe9\xbc\x53\xe3\x53\x0d\x96\x42\x03\ +\x84\x46\xd7\xed\xfa\xee\x8e\x25\x78\xee\xa8\x79\x2f\x12\xa2\x1f\ +\x79\x9e\x71\xb6\xea\x79\x8e\x2a\x97\xf6\xad\x23\xb5\x96\xbe\xfc\ +\xf1\x1a\x8e\xda\x8b\xe0\x57\xd8\x98\xa3\xb0\x4f\xa1\x37\x86\x62\ +\xaa\x1f\x7b\x93\x2c\x15\xbd\xe8\xf4\xb8\x09\xe7\xd8\x52\xb1\xe8\ +\xda\xe5\xe3\x77\x91\xb6\x65\xda\xa0\x46\xd9\x36\xa6\xe4\x95\xe4\ +\x2e\xa2\x0b\x0c\x38\xfd\xda\x12\x85\xa6\x52\x9f\x4f\xe2\xf9\xc3\ +\xda\xc0\xb2\x4e\x1d\x92\xed\x7e\x17\x7d\x18\x9d\x1f\x43\x21\xf7\ +\x6f\x8a\x62\x06\x22\x54\x4b\x9c\xb9\x20\xb0\x61\x4c\x54\xe2\x39\ +\x7d\xfc\xf1\x26\x0d\x78\x64\xcc\x9d\xad\xcb\x90\xaf\xb6\x29\xc6\ +\xca\xcd\xb7\xdd\x90\xbd\x0e\x8b\x45\xff\x26\xfc\xd6\xe1\x3f\xc2\ +\x28\xb7\xee\x70\x1f\x24\x28\xfb\x47\x2f\xa4\x7f\x5d\xef\xfe\xc6\ +\x7f\x8a\xc6\x7f\xba\xee\x0b\x43\x19\xef\x4b\x45\xa6\x94\xb3\x44\ +\x82\x4d\x56\x80\xd1\x48\x76\xe2\xd8\xae\x58\xde\x8a\xe6\x6c\xe5\ +\x88\x22\x5b\xb9\x90\x57\xb6\xc9\xf9\xc1\x77\x2b\xea\xaf\xa9\xfd\ +\x5e\x65\x9b\x02\xcd\x92\xff\x68\x05\x58\x2e\x22\xbf\x53\x09\x8e\ +\xb3\x0d\x23\x15\x58\x2b\xe4\x28\xac\x95\x36\xe9\xcb\xe8\x6c\xd6\ +\x55\xdf\x5c\x4c\x0c\xca\xad\x1a\xcc\xdb\x59\x1c\x60\xd8\x50\x73\ +\x82\xd1\xad\xe9\x75\xe6\x3b\x74\x11\xe7\x78\x21\xbf\x0a\x42\xb2\ +\xc7\x4b\xb4\x15\x8b\x8b\x3f\x52\xa9\xa8\xc8\x6b\xb5\x1b\x1e\x74\ +\x45\x25\xdb\x0d\x0c\x29\x6f\x49\xe9\xeb\x54\x56\x26\xa0\x2f\xbe\ +\x5d\x5d\xdc\xac\x8b\x5b\x9f\xbb\xbe\xc0\x5a\x8b\xd9\xbb\x65\x18\ +\xb3\x45\xb6\x84\x0d\xb9\x4a\xff\xc9\x44\x5b\x5e\xf5\x92\x2d\xd7\ +\x46\xe9\x7c\x93\x0a\x07\x9d\xb7\xe0\xde\x7e\xf9\xc1\x1b\xfa\x8e\ +\x34\xea\x1c\x2f\x2d\xd8\x4d\xee\x3b\x2f\xda\x4f\x70\x79\x61\x9c\ +\xc2\x9c\xd4\x76\x5d\xce\x27\x4a\x90\x0f\x6c\x24\x90\x74\x9f\x46\ +\xf0\xd1\xef\x2b\xd8\xeb\xb4\x48\xbf\xe0\x10\x4f\x07\xb2\x70\xcd\ +\x58\x42\x9f\xd9\x9a\xf9\x4e\x42\x12\x02\x58\x48\xb6\x64\x1b\x6e\ +\x4e\x38\xe9\x0a\xa3\x2a\x01\x65\x8e\x0c\x2e\x1a\x5e\x3c\xb3\x7a\ +\xb7\x4a\xc3\x42\x1e\x66\x62\x42\x02\xc1\x68\x11\xc7\x0b\x86\x8e\ +\x15\x43\x51\x96\x4e\x09\xdc\xf3\x9e\xda\x73\x38\x4e\xbc\x15\x6c\ +\xf1\x86\xe5\x0b\xe9\xd7\x9b\x61\x74\x08\x28\xaa\x34\x0d\xc7\x8f\ +\x74\x5c\x87\x7d\xdc\x66\xf4\x80\x7c\x73\xf6\xfb\x13\xbc\x66\xdc\ +\xf3\x63\xa6\x87\x52\xf1\x1e\x78\x47\x8b\x3f\xe8\xf2\x92\x0f\x49\ +\xf1\x71\xe9\x6a\x09\x70\xee\x39\x04\x3d\x50\x69\x97\xf7\x74\x15\ +\x97\xd7\xd0\x59\x28\xbd\xb4\x78\x7e\x4d\x1e\x18\x8b\xa4\xb2\x8e\ +\xc1\x96\x2f\x85\x2b\xf2\x3e\x07\xa0\xee\x82\xd3\x71\xd4\xd6\x7e\ +\xd7\x5d\x76\x5c\xb5\x73\x4a\x7c\x96\x06\x89\x86\x63\x43\xd4\x0b\ +\x8f\xaa\x72\x3f\x84\x75\x24\xf7\x86\xa3\xf0\x45\x30\x18\x85\xbd\ +\x9e\xab\x21\x25\x14\x48\xb1\xcd\xdc\xc3\x1a\xe7\x41\x6e\x2d\xba\ +\x56\x46\xaf\x4b\x4b\xf3\x62\xd5\x5c\x5d\x91\xe9\xda\x9c\x44\x77\ +\x38\xb6\x1c\x3b\x8e\xf8\xad\x60\x3b\xa9\xf9\xdc\x35\x9f\x52\x53\ +\x90\x66\x83\x5c\x90\xd0\x0b\xa2\xc5\xb3\x08\x64\x6a\xde\x81\xcb\ +\x6a\x4d\xd4\x71\x78\xf6\x7b\xa3\x98\x3b\x9f\xd0\x22\xc3\xd8\xce\ +\xca\x35\x3a\x6d\x98\x38\x14\x97\x88\xfc\x8e\xf0\xa8\xc7\xfc\x92\ +\xd4\x63\xe2\x52\x47\xb3\xd9\xca\x78\x47\xcb\x75\x66\xa2\xdd\x71\ +\x54\xf7\x2c\x08\xd0\x45\x19\x24\x66\x98\x91\xb2\x8d\xcb\xde\x4a\ +\x3f\xb1\x8d\x80\x03\x28\x60\x28\xf2\xfa\x6e\xb4\x10\x53\x44\x4d\ +\x59\x78\x50\x66\xc2\x56\x4a\x6c\xed\xbc\x18\xe3\x38\x91\xb0\x9f\ +\x38\x7a\x58\xb9\x65\xad\xd8\x3b\x08\x14\x2c\x33\x52\xde\x09\xa6\ +\x99\x01\x13\xb9\xd1\x80\xf3\x23\xe6\x10\xcd\x41\xac\x50\xcf\xc9\ +\x8b\x6a\xf2\xdf\x37\xd9\x0d\xed\x82\x69\x9e\xd9\xd5\x10\x8c\x2c\ +\xc0\x8b\x17\x93\x47\x96\x0e\xc9\x91\x3e\xeb\x99\xe4\x9a\x5a\x95\ +\xd8\x77\x19\xb8\x91\x4b\x7e\x42\x92\x23\xd0\x2f\x42\x48\xea\x94\ +\x7e\x82\x18\x79\x2f\x15\xa2\xc7\x03\xb6\x49\x1f\x80\xaa\x5f\xf3\ +\x40\x3b\x55\xa8\x87\x23\x07\xda\x3c\x46\x85\x6c\xac\x33\x3d\x0d\ +\x6f\x15\xc6\x27\x5a\xed\x50\xdb\xd2\x0a\xfa\xf0\xdd\x23\x6d\x8e\ +\x40\xc0\x02\xaf\x85\x2c\xdc\x68\x86\x05\x66\x3b\x2c\x39\xe8\x48\ +\xa8\x84\xa0\x7a\x65\xe8\xa9\x00\x01\xea\x25\xfe\x82\xe6\x7e\xee\ +\x28\x51\xf7\x5b\x25\x00\xde\x8e\xfb\xb5\xfc\x1d\x87\x4c\xa8\xbb\ +\xbd\x2a\x84\x1e\xae\x4e\xa8\xb7\xbd\xca\x2a\x0f\x1e\x9d\x46\x61\ +\xf4\x00\x19\x12\x1a\xdf\x0a\x34\x79\x32\x32\xb6\x6c\xf0\x5e\x94\ +\xc8\x26\xd2\x2b\x25\xd5\x04\x73\x35\xd0\x03\x68\x06\xfd\x5a\x32\ +\xba\x27\xac\xb6\x47\xa0\x03\x01\x7c\xbd\xea\x4f\x51\xf5\x27\x06\ +\x69\x08\xd5\x47\xda\x84\x61\xf2\x69\x9b\x0b\x58\x91\xf4\x05\x91\ +\x95\x0d\xc5\x54\x49\x76\xa3\xa0\xda\xa1\x85\xe2\xd7\xe8\x02\x4b\ +\x3b\x3c\x0a\x41\x47\x88\xd1\x42\xd0\x17\xb4\x39\x08\x65\x80\xb6\ +\x8a\x47\x1e\x57\x9b\x43\xa1\x80\x52\x1f\x63\x91\x26\xe5\x95\x20\ +\x1d\x0c\x4d\x53\xc3\x40\xfd\xbe\xa6\xfe\xc9\x65\xc2\xea\xce\xca\ +\x52\xca\xde\x91\x6b\x24\xa6\xd9\x71\x30\x46\xcf\x11\xe1\x4f\x54\ +\x03\x5a\x80\xe8\x5e\x80\x2e\x9c\x9c\xf1\xc2\xa0\xa1\xe9\xc8\x5e\ +\xee\xa9\x69\x0e\x7c\x4e\xa1\xf4\x95\x92\x45\x27\x56\x51\x59\x99\ +\xf7\x2d\x95\xd6\x69\x5b\x35\xaf\x1f\x17\x59\xe5\x95\x33\xd2\xfc\ +\xa4\xf1\xd9\xa7\x97\x91\xd9\xc6\xec\xff\x87\x17\xd6\x27\x97\x8d\ +\xeb\x73\xcb\x20\x74\x35\x99\xf2\x4e\xe8\x6c\x86\xef\x79\x4b\x9a\ +\xf9\x62\xa3\x2a\x0d\x80\xaf\xcf\xd1\x2a\x3e\x0a\x9c\x97\xd7\xaf\ +\x5f\x89\xfb\x2f\x19\x9b\xf6\x56\x63\x50\x7a\xc7\xaa\x6a\xa6\x2e\ +\x8e\x29\x76\xd1\x0b\x85\xcd\xbf\x4d\x5f\x86\xa8\x87\xca\x76\x40\ +\xc0\x39\xdb\x7c\xa9\xf3\x54\x21\xcd\x9b\x39\x2e\x89\x40\xd4\xb2\ +\xe2\xc0\x54\x90\x35\x35\x2b\x46\x35\x2b\x96\x6a\x56\xd9\x62\x12\ +\x4f\x35\x18\x43\x10\x2c\x5f\xf0\x7a\x1b\xc0\x64\x0a\x46\x2e\xae\ +\x85\x7c\x06\xc3\x29\x49\x6e\xc2\x15\x9c\xe8\x8d\x6b\x7e\xca\x93\ +\x22\x34\x06\xa3\xee\xf8\x18\xa3\x78\x00\xec\xdf\xd8\x22\xfc\x92\ +\x66\xdb\x0d\x06\x7a\x18\xbd\x01\x01\x78\x44\x09\xaf\xb9\x0a\xa7\ +\x20\x06\x00\x66\xf6\x9c\xab\xe9\xca\xbb\x2b\xf6\xdc\xaa\xed\xf7\ +\xa8\xb2\xa2\x90\x0a\x37\x56\xcb\x22\x58\x4d\x33\x3e\x14\x3a\xe2\ +\xfa\x81\x9c\x50\xb6\x3b\x8d\x72\x29\x33\xb3\x15\x6a\x6c\xb2\xe5\ +\x75\xd6\xd1\xe5\x86\xf2\xd1\x02\xe0\xaa\x0d\x74\x76\x6d\x85\x41\ +\x75\x8a\x9a\x7e\x23\xd2\x4a\x44\x14\xb1\x5e\xe5\xc5\x81\xa9\x12\ +\xd5\x8e\xc0\x23\x4f\x80\x3a\xb8\xe6\x29\x40\x49\xd4\xb5\x0e\xa0\ +\x87\xf8\x4a\x17\xcd\xf3\xa8\x5f\xb3\x21\x5d\x6b\x6f\x15\xc6\x19\ +\xc4\xf5\x83\x66\x34\xe8\xaa\x31\xd1\x04\x1d\x71\x75\x1c\x75\x7f\ +\x43\x6a\xd6\x60\x7e\xde\xfa\x37\x80\xd2\x33\x41\x69\xc0\x18\x0a\ +\xf6\xbb\x50\xc6\x63\xa7\xa5\x07\xec\xb1\x18\xc3\x28\xd8\xbb\xe1\ +\xc4\x16\x77\xe2\xec\x1f\x56\x9c\x21\xd4\xff\x1e\x60\x4c\xac\x51\ +\x94\xa5\xb6\x81\x89\xe6\x92\x55\x98\xee\xb8\x56\x58\xd0\x40\xb0\ +\xb5\x6e\x18\xa3\xd7\x32\xa6\x76\x15\x22\x92\xce\x57\xb0\x03\xb2\ +\xaf\x6e\x9f\xc2\xdc\xa1\xe9\x7d\xf0\xfc\x10\x80\xba\x15\x78\xb4\ +\x3d\xa9\xf7\x40\xdc\x3d\x02\x0f\xbc\x79\xd3\x28\xbd\x7b\x11\xf1\ +\x15\xa9\xb2\xc4\x43\x33\x40\x3f\x08\xa2\x0a\x2a\x0e\xb1\x74\xfc\ +\x57\xa5\xc7\x77\xca\x97\x56\xe8\x1e\xfe\x35\x16\x2e\x99\x30\xfa\ +\x8a\x2a\x76\x37\x16\x01\xe7\xbc\x15\x8a\x83\x52\x36\x45\xa6\x02\ +\x9e\x25\x93\xb9\x13\x53\xc0\x7c\x95\xc7\x8c\xff\xad\x61\x7c\xe0\ +\x6f\x95\xe1\x91\x4b\xf3\xd1\x51\x57\x88\xac\xde\x18\x4c\x2c\x19\ +\x45\xa0\x26\xec\x62\xad\x7e\x12\x4d\x85\x59\x19\xf3\x02\x1e\xec\ +\x2b\xf0\xa4\xfc\x79\x39\xfa\x40\xdf\x65\xfc\x1b\xa3\x5e\x30\xdb\ +\x08\x43\x9b\xd9\x5d\xca\xb5\xe6\x77\x9c\xcc\x82\x60\x38\x76\x56\ +\x60\x26\x39\xbe\x83\xfa\xbc\x33\xe5\xda\xfa\x51\x00\xdb\x7d\x94\ +\x90\x57\x58\xa4\x12\xd4\x3a\x03\x44\x18\x7b\x62\x4a\x46\xe9\xf9\ +\x60\x37\x69\x2f\xb9\x5d\x85\x37\x69\xec\x8c\x6d\x23\x69\x61\x2a\ +\x2d\xe6\xfa\x94\x14\x46\x4a\x40\x92\x6e\x28\x37\x2e\x20\xf8\x78\ +\x96\x9e\xef\x6c\xd7\x4e\x4d\x10\x2b\x2f\x27\xcc\xb9\x26\x44\x38\ +\x52\x7b\x33\xd1\x74\x05\x76\x44\x0e\x48\x72\xa5\x13\x20\xdc\x40\ +\x47\x1e\xc5\x1b\x8b\x73\x17\x41\xe1\xe7\x5a\xf0\xe1\x02\x5b\xd9\ +\x94\x48\xde\x5c\x84\xeb\x7c\xe4\x8b\x8b\x4b\x93\x9f\x3b\x75\xaa\ +\x07\xa6\x72\x3c\x50\xc5\x32\x95\x2e\x4d\x75\xcd\xee\xd0\x5c\xef\ +\xd5\x57\x6c\xd7\x44\xe6\xfd\x88\x72\xd5\xa1\xc7\xbe\x72\xac\x0d\ +\x0c\xdf\x7d\xc2\xb1\x56\x2d\xa1\xba\x39\xe4\x43\x33\x83\xe8\x80\ +\x9c\x35\x1a\x2a\x35\xf7\x56\x5c\xba\xdc\x48\xe5\x4c\x74\x7b\x05\ +\xdd\x42\xe9\x0d\x46\x87\x92\xe1\x1e\x69\x8e\x04\x61\x9a\x93\xb1\ +\x49\xe5\xb1\xe1\x5d\xe8\xea\x0e\x00\x25\x09\x03\x33\x5b\x6b\x3c\ +\xf0\x6d\xad\x34\xde\xe8\x72\xaf\x40\x54\x7a\x3a\xcc\x89\xb1\x38\ +\xd6\xbd\x1f\x3f\x60\x5a\x10\x4e\x2e\xf7\x37\x50\x42\xad\x05\xbb\ +\x98\x54\xbb\xdf\xff\x65\x30\xf0\x8c\x08\x89\x8f\x98\x9c\x81\xbe\ +\xa3\x83\x70\x94\x0a\x2a\xa5\x52\x2f\xaa\xba\x02\x3d\x11\x93\x64\ +\x0f\x32\x92\x1b\x2b\x6d\x02\xe3\x5a\xf4\xc3\xba\xb3\x23\x77\x9c\ +\x8f\xee\xcb\x83\x19\x65\x2c\x1d\x5b\x41\x79\x27\x32\xae\xd9\xf8\ +\xb1\x8c\x4c\x95\x86\xf7\xd5\x95\xdb\xe8\x33\x57\x96\x86\xdf\xaa\ +\x0d\x94\x2f\xb2\xaf\x9a\xfb\x3b\x6a\x36\xce\x4a\xae\x5b\xb5\xce\ +\x22\xdd\x3a\x4b\x58\x69\x9f\x71\xe7\x6c\x68\x31\xd1\x2c\x23\x98\ +\x17\x26\x5c\xa7\xbb\x67\xa6\x6c\x5b\x34\x4d\xf1\xb0\xf1\x83\xc1\ +\xe1\xa0\x0c\x6f\xed\x8c\x49\xcb\xca\xe0\xdc\x5a\x82\x9b\x45\x2c\ +\x18\xf5\x46\xd2\x9f\xa5\xa9\x75\xbf\x8f\xb4\xfd\x16\x13\x50\x85\ +\xf1\x61\x05\x44\x58\x99\x34\x6b\xa7\xe2\xc5\xf3\x76\xe2\x8a\x8d\ +\x63\xaa\x25\x09\xd2\xb8\x40\x2b\xbf\x3f\xae\xb2\xdf\x11\x04\xbc\ +\xff\x34\x88\x53\xf3\x6e\x71\x37\xd6\x58\xac\xac\x5a\x0e\x6c\x67\ +\x86\xb2\xb5\x4a\x68\xae\x6f\xeb\xc0\x57\xe2\x72\xad\x47\x79\x1d\ +\xf5\x4e\x07\xe3\x02\x25\x67\x9b\x34\x5c\xf2\x3b\xc5\x5a\x5e\xaa\ +\xb4\x7e\x79\xbf\xfc\x34\xa7\xfb\xe8\x4e\x28\x72\xfb\xf0\xfa\x13\ +\x74\x33\xb0\x35\x31\x01\x32\x08\x79\x74\x2a\xcf\xc8\xb4\xa5\x4b\ +\xb0\xa0\x83\xee\x7b\xd4\x22\x48\x34\x8a\xc0\xdf\x90\xff\x93\x46\ +\xf8\xec\xc3\xde\x49\x13\x07\x18\x89\xe3\xb8\xc0\xc2\x30\x5b\x29\ +\xe4\x59\x42\x2c\xc7\x9b\xe8\xfd\xfe\xa4\xd3\xef\xba\x93\x5e\xf0\ +\x71\x8a\x3f\x4e\x38\xd9\x33\x37\xa6\x84\x4b\x68\xfb\x99\xdd\xee\ +\xf7\x6c\x32\x9c\x76\x9d\xc9\x14\x33\xea\x03\xa7\x4b\xc5\x08\x72\ +\x39\xd2\x18\xdb\xf8\x6c\xf2\x74\xca\xf3\x2f\x8e\xe2\xf2\x35\x02\ +\x6a\x2c\x86\xe4\x5f\x38\x8a\x62\x54\x71\xff\x13\xc8\xf7\x8e\x73\ +\x8c\x01\xe9\xad\x22\xe3\x29\x32\x3f\x08\x75\x51\x3d\xc1\x99\xc0\ +\x78\x0c\xde\xca\xb8\xef\x98\x27\x09\xe8\xca\x47\x13\x9c\x9a\xcc\ +\x26\xbf\x73\x1c\x44\x5d\xed\x14\x68\x5e\x27\x32\x04\xc4\xe5\x90\ +\xd1\x11\x1d\xed\x98\xf3\xd5\xd0\x8f\x2c\x29\x6f\x1e\x84\x34\xc5\ +\x22\x98\x77\x43\x31\x5a\x8a\x57\x29\xd0\xfd\x73\x90\x42\x99\x68\ +\xf9\x29\xd0\xa5\x1b\x85\x81\x62\xa3\xa5\xa5\x1c\x47\x1c\x7d\x82\ +\xc9\x3f\xbd\x48\x8f\x8f\xe1\x9f\xe7\x9f\x8f\x8f\xa3\xee\xf2\xc5\ +\x9c\xff\xf3\x7c\x61\xa4\x91\x3f\x40\x6b\x93\x4d\xe8\x4a\x9e\xea\ +\x7e\xcf\x36\xa5\x5e\xb5\xdf\xb7\xec\x63\x1d\x95\x63\x19\x9c\x44\ +\xc3\x05\xa5\x78\xc8\x0b\xe8\xb1\xc3\x91\x00\x9a\xe5\x42\x3c\xb6\ +\xf0\x22\xbc\xaf\xc5\xf8\x93\x3f\x7f\x7e\xf7\x16\x9d\x3c\x05\x2a\ +\xee\xdd\xdd\xe2\x39\x10\x7c\xfa\xfc\x9e\x2d\xc3\x81\x66\x4d\x03\ +\xc9\x26\xcf\x3f\xd7\x88\x4d\x60\xcd\x42\x73\x49\x25\xd5\xd5\x46\ +\x4a\x5d\xfb\xe6\x87\xe2\xff\x34\xaf\x0b\x47\xe7\xde\xc1\x4a\x7f\ +\x4b\x8d\x64\xbc\x50\xfe\xc3\x11\xe2\x52\x6e\x31\x3e\x90\xa3\x8e\ +\x2f\xfa\x07\xff\x8e\xdc\x3c\x0e\x97\xe7\xd2\x20\x40\x45\xb1\xd4\ +\x26\x55\xbb\x97\x20\x6b\xfe\x89\x14\x62\xb4\x44\x83\x2e\x32\xef\ +\x29\x75\x62\x54\x9b\x1c\xc3\x7e\xe1\x83\x26\x7c\x87\xf7\xfb\x10\ +\x13\x30\xd0\xe2\x18\x3f\xf5\x87\x3e\xf0\x8d\x8e\x51\xe2\xd6\xf0\ +\xcd\x2d\x8b\x7f\x03\xb6\xd5\x89\x3c\x79\xfa\xa3\x11\x2f\x19\x00\ +\x1f\x59\x6c\x81\x05\xaf\x16\xfc\x4b\x06\x74\x37\xea\x6d\x78\x06\ +\xf6\x30\x66\x0a\xcf\xb0\xdb\x20\x30\x64\x2e\x39\x46\x4d\xfa\xb0\ +\x0f\x98\xd1\x43\xa8\x06\xf6\xb1\xdf\xe3\x17\x58\x7d\xe8\x69\x40\ +\xa6\xdf\x08\x56\x43\x08\x4b\x05\xd7\x3d\x9b\xed\x84\x15\x2a\xcc\ +\xf2\x28\x18\xe0\x76\xbf\x18\xe8\x36\xa7\xc8\x34\x6b\x58\xec\x03\ +\xa6\x2e\x2d\x95\xea\xdc\x9c\x7b\x5a\x26\x27\x14\xf9\x84\x1b\x98\ +\x5f\x5c\x1a\xd7\xdd\xe5\xb5\x30\x02\x5e\x6a\xb9\x63\x36\x43\x37\ +\x8c\x08\xaa\x3b\xa3\x7f\x54\x00\xaa\xd6\x05\x7f\xe7\x60\xa2\xc5\ +\xa0\xab\x07\xc1\x15\x8f\x3a\x1f\x4f\x8c\x26\x53\xdf\xf8\x24\x0c\ +\x1a\x02\xd9\x72\x5e\x26\x53\x2e\x95\x15\x85\x69\x03\xe0\xb9\xa7\ +\x54\xd5\x90\xdf\xba\x84\x01\xb3\x84\xb3\xa8\xa4\x9b\x20\xe9\x30\ +\x4c\xcb\x05\xa1\x37\x2b\xdb\xcd\xb1\xdd\x1c\x55\x7e\x6c\xb4\x08\ +\x44\xe4\xc3\x6c\x32\x9f\x7a\x5a\x94\x0d\xf2\x0d\x60\xcf\x0b\xf2\ +\xa1\x00\xed\x2d\x6a\xd1\x1e\x52\xf1\x99\x24\x7a\x22\x4e\xd9\x8e\ +\x7b\x70\xdc\x71\xa5\x80\x72\x74\x16\xd2\xf3\x81\x3a\x64\xa5\x85\ +\x57\x56\xf6\x57\x59\x41\x0f\x06\xd5\xec\x1e\x4b\x95\xa6\x91\x3b\ +\xae\xb7\x98\xba\x07\x2b\x84\x86\xe0\x14\x40\xd6\xcb\xea\xb9\x44\ +\x64\x97\xa8\x04\x16\xbd\x0b\x0f\x70\xab\xdd\xfc\x1e\x94\x82\x6c\ +\xae\x4f\x1f\xf4\x5f\x5a\x22\xd7\x01\xc4\xbb\x56\xf8\x86\x48\x9d\ +\x18\x62\x20\x00\xb4\x2f\xaa\x9a\x5d\x44\x88\xe1\xfa\x5c\xa9\xb2\ +\x25\xfc\xa5\x8c\xa6\xd0\x10\xfd\x90\x54\x42\xab\x74\xeb\x93\x62\ +\x58\x7c\x4e\x55\x46\x14\x8d\x0b\x10\x47\x68\x6f\x3e\xaf\x39\x00\ +\x47\x51\xb7\xeb\x6a\x6f\x26\x8c\xe2\xe7\xf2\x85\x2e\xfe\x5a\x02\ +\xbe\x9e\x10\x98\x7e\x41\xe9\x0f\x14\x5a\x0e\x8d\x86\x4f\x00\xc4\ +\xac\x13\x79\x43\x5a\x47\xc9\x16\xea\x5c\x83\xf7\x40\xfd\xb7\x62\ +\x58\x06\xf4\xc8\xc1\x48\x53\x50\x69\xdb\xe0\x58\xfe\x18\x42\x42\ +\x6f\x5f\xe8\xed\xbe\x9f\x66\xc4\xc6\x37\x31\x09\x6e\x95\x94\x6c\ +\x40\x63\x12\x33\xa4\x8b\x99\xc6\x24\xe6\x9c\x49\xcc\xa6\x98\x44\ +\x3f\x2f\xdb\x2d\xb0\xdd\x42\x32\x89\x54\x32\x89\xf9\x64\x51\x63\ +\x12\x29\xcf\x5e\x17\x4c\x22\xad\x33\x89\x9d\xf5\x0c\xa6\x55\xbc\ +\xa5\x36\xa4\xa5\x0f\xc6\x58\xa5\xbb\x97\x96\xb8\x4a\xa7\x35\xdf\ +\x01\x41\x84\xcf\x9b\x20\x1e\x66\xfa\xc9\x51\x18\xc2\x47\x79\x00\ +\x31\xf8\x94\x04\xa3\x5f\x83\xe9\x68\x81\x95\xd0\x65\xae\x90\x3a\ +\x5a\x3c\x9f\x8f\x16\x40\xa2\xbb\x94\xa3\x12\xb8\xce\x28\x25\x6c\ +\x55\x62\xc8\xd0\x37\x28\x8e\x1d\x2d\x24\xf5\xd2\x95\xc8\x75\xe3\ +\x6f\x2d\xe0\x93\x45\xf2\x61\xa3\x81\xc7\x1d\x7c\x03\xee\x6c\x1b\ +\xe0\x93\x18\x4a\xfe\x29\x81\x56\x4d\xd0\xa8\x79\xd0\x8e\x8f\xf5\ +\x68\x13\xb7\xee\xad\x2b\x95\x18\x33\x78\xa7\xea\x3a\x6a\xf4\xd5\ +\x6b\xda\x92\xf2\xd7\xa3\xc2\xd9\x89\xfb\x72\x6d\xa6\x13\x5e\x59\ +\x30\x66\x8c\x8b\x56\xac\x1c\xfd\x56\xef\xbd\x6b\xcb\x7b\x2d\xad\ +\x1f\xf1\x94\xd4\x38\x69\xf0\x8f\x8b\x7a\x8f\x26\x71\x7d\xfa\x97\ +\x5e\xab\x45\x4a\xe5\xbb\x00\x7b\x5b\xbe\x7a\xd6\xc1\x3c\x3b\xda\ +\x14\x59\xfe\x52\x5c\xca\x1d\x18\x96\x48\x94\xc5\x74\xaf\x42\xd7\ +\x2b\x98\x79\x9b\xad\x03\x7e\x17\x52\xba\xa3\x54\x9a\xd9\x36\x2f\ +\xb2\x1b\x81\x01\xb3\x50\x6a\x2d\xa5\xa5\xe7\x3e\xa8\x95\xfe\x8a\ +\x1a\xf7\x74\xd2\x4d\x4b\xeb\x01\x9e\x3f\x56\x75\x0f\x22\x73\xb5\ +\x87\xfb\xde\xe9\x7d\xd3\x97\x7f\x7f\x6b\x85\x9e\x07\xb4\xa5\x5d\ +\xb9\x0b\x4a\x7d\xbb\xee\x1f\x4f\x6c\xe7\x7d\x03\xaa\x7d\xae\xa8\ +\x8d\xd5\xe8\x2b\x8b\x12\x17\x92\x1d\x8f\x8e\x0c\xa5\x44\xd2\x53\ +\xbd\x9a\x00\xc7\x5c\x64\xfd\x16\xc0\xa2\x50\x4a\x6a\x37\x1a\x8e\ +\x8c\xaf\x60\x27\x80\xae\xea\xb0\xb3\xa0\x55\xde\x6e\x56\x9e\x5a\ +\x8b\x2a\xc7\x4a\xe5\x48\xba\x6e\x19\xd8\xc2\xf0\x65\xa0\x4a\xbb\ +\x18\x6b\x28\x63\xa7\xdd\xa8\x97\xdc\xef\xb4\x46\x5e\x8a\xf8\x00\ +\x31\x25\x2e\x0d\xcb\x47\x91\xd5\xbb\x5b\xca\x39\x7f\xf0\xb6\xeb\ +\xc4\x88\xd1\x97\x4f\xfa\x1d\xa1\x32\x0c\x58\xb5\xa5\x2e\x00\xab\ +\x98\xab\x0b\xf4\xfd\x5e\xfd\x36\xd6\xa4\x3f\x7e\xd8\x71\x7b\xea\ +\x8e\x21\xaa\x79\xe6\xe5\x93\x91\xf8\xb6\x17\x3e\xdb\x88\x57\x0e\ +\x0f\x69\x2f\xde\x69\x94\x5d\xdc\xd1\x5c\x5e\x66\x23\x4c\xf4\xd3\ +\x02\x92\xa0\xe6\x87\xcc\xc0\x9f\x1e\x7b\x0c\x48\xf4\x94\x24\xf4\ +\x68\x11\x48\x20\x5c\x0e\x91\x41\x53\xf8\x68\x9d\x4e\x9a\xf2\x5c\ +\xd0\x63\x4d\x4a\x21\x36\xa6\x70\xdd\x91\x39\x25\x39\x8d\x8d\x81\ +\x5d\x93\x76\xfb\xb4\xaf\xa0\xaf\x99\xad\xc8\x38\x33\x6f\x12\x8d\ +\x53\x56\xca\x27\xae\x93\x1b\x21\xe3\x56\x76\xc7\x90\xdd\x31\x60\ +\x77\x3c\x31\xb9\x7e\x0f\x6b\x92\xb9\x57\x65\x0d\x6c\xaa\xbf\x91\ +\xe2\xaa\x2c\x39\xd3\x05\x52\xbb\x02\x61\x55\xd6\xe3\x0a\xaa\x8d\ +\xc4\x15\x62\x05\x88\x86\x09\xbd\xc8\x98\x5d\x05\x5e\x1b\xed\x47\ +\x71\xc0\xe8\x91\xb0\xb6\xe5\x32\xa6\x06\x49\xc3\x65\x0c\xab\x5d\ +\xc6\xb4\x2a\xb7\xaf\x0d\xb7\x31\xf6\x09\x82\x01\x0f\xbf\x74\xeb\ +\x10\x95\x8f\x9f\x0c\xeb\x50\x18\xa1\x81\x38\x4e\xed\x0e\xb8\x01\ +\x8a\xd8\x0e\xc5\xb0\x82\x97\x8a\xd2\x71\x54\x43\x67\x3c\x45\x24\ +\x0f\x2f\x7e\x02\xdd\x8f\x3f\xd9\x56\x6a\xd3\x55\xc7\xc6\xc4\x02\ +\x43\xe9\x39\x5d\xd2\xb3\x80\x20\xe7\x9c\xa9\x67\x49\xf6\xd1\x0d\ +\x72\x19\xe6\xd8\x48\x14\xb1\x41\x14\x9a\xd6\x35\x57\xd6\x90\x2b\ +\xcd\x7a\xa3\xf6\xe1\x30\x22\x57\x56\x49\x8f\x8b\x5e\x6b\xe6\x3e\ +\x8f\x10\x15\x5a\xd9\x0c\x34\x0c\x73\x78\xae\xe1\xb0\x8a\xeb\x5a\ +\x4b\x2f\x93\x1b\x6c\xcb\x07\x30\xa1\x92\x0f\x6d\x19\x81\x09\x8c\ +\x0c\x6f\x1e\x1b\xe1\xd7\xab\xa9\xb2\x11\x55\x46\x16\xe9\x03\xe2\ +\x19\xee\xa4\xc5\x86\x6e\x3a\x35\xca\xf5\x5b\xd8\x9c\x15\x0b\x34\ +\x92\xc1\x23\xff\xfd\xc7\xe1\xa0\xf4\x18\x8a\xeb\xb5\xa8\x30\xc6\ +\xfd\x02\xf7\xb7\x6a\xe6\x5b\x24\x6d\x8a\x71\xa2\x7e\x6b\x79\x46\ +\x3a\x99\x4e\x04\xa8\x1a\x87\x05\x2a\xf5\xa3\xf2\x4a\x3c\x5e\xe2\ +\x8b\xa9\x96\xfb\x70\x2c\xef\xb8\x35\x83\x6f\x14\xaa\x4c\x20\x19\ +\x7a\x4e\x87\x69\xbf\x47\x68\x64\xd6\x24\xde\x84\x50\xda\xe4\xb8\ +\x2c\xac\x0d\x56\x79\xe6\x14\x3f\xf5\xc7\x6a\x43\x7e\xb5\xcb\x9f\ +\xef\x1c\x48\x0f\x44\xc5\x22\xa1\x8d\x50\x57\xf6\xfc\xb5\x33\x30\ +\xe5\x6c\x13\x91\xaa\x43\x97\x4e\xc2\xd0\x6b\x6e\x26\xee\xa1\x24\ +\xab\xa9\xe7\x15\x54\x12\x3d\x3c\x95\x8b\x52\x6b\x84\x47\xdb\xf5\ +\xca\x2c\x94\x5a\x03\xe1\x62\x95\x0b\xd5\x01\xc5\x1c\x36\x87\xbf\ +\x11\x69\xe6\x80\xba\xc2\x65\x5e\x93\x9f\x52\xbd\x71\x2b\xa3\x09\ +\xad\x3b\x68\x1a\x4d\xa8\x67\xb5\xe1\xa4\x0a\x57\xbe\x03\x00\xc4\ +\x6b\x4f\xe0\xa8\x59\xc4\xea\xc5\x2c\xf5\x60\x96\xf6\x50\x64\xdb\ +\x11\xa1\xe5\x69\xce\xaf\x51\xf1\x6d\x80\x30\xa0\xc8\x9a\x6e\xc8\ +\x59\x2c\x60\x14\x7e\x0d\xa7\xf8\x4c\x37\xff\xd3\x27\x1c\x51\x29\ +\xc0\x60\x58\xd3\xe5\xcd\x82\xb8\x5e\xa4\x7b\x07\x3d\xfa\x86\xf7\ +\x26\x8f\xf6\x5d\xdd\x35\x7b\xbd\x76\x61\xd6\x0b\xfb\x7c\x80\x86\ +\xc1\x71\x93\x1b\x86\x46\x63\x2b\x2c\x2f\x5a\xb4\xe8\x1c\xde\x33\ +\x22\x15\xf5\x8e\xce\xad\x06\xb8\x16\x32\xbe\x9a\x86\xa8\x0d\x4f\ +\x2e\x6f\xdd\xb9\x50\x4b\x0e\xd5\x1d\x16\x46\xba\x85\x5e\xd1\xa1\ +\x8b\x19\xe9\x96\xd7\x6a\x4a\x0b\x55\x49\x77\x2d\x2f\x4a\x7b\xb0\ +\xdf\x1e\x68\xae\x07\xa2\x5b\x84\x6f\xa5\xb5\x57\x9b\x9e\x44\xae\ +\xd8\x74\x4b\x4e\x86\x1e\x53\x3a\xa2\x2d\x68\x6c\xc5\x63\x70\x0f\ +\x36\x47\x4e\x3d\x60\x4a\x0a\xdb\x4a\xbc\x94\x08\x9b\x97\xae\x05\ +\x7b\xad\xf9\xae\x17\xbe\x73\x5c\xdc\x60\x7c\x4a\x62\x3c\xf0\x01\ +\x47\xa1\xc5\xd3\xdb\xa4\x2b\xea\x20\xce\xdf\x8e\xb2\xc2\x78\x68\ +\x57\x25\x26\x4b\xdf\x17\xce\x57\xe8\x39\x6e\x58\x18\xbd\x66\xef\ +\x52\x9c\xd6\x40\xf0\x22\x81\xb6\x07\x8e\x82\x48\xac\x0c\x73\x20\ +\xfa\x32\x13\xcb\x74\x0a\xb3\xd3\x84\xfa\xa3\x0d\xc6\x65\x8f\x91\ +\xb2\x25\x33\x2d\xdc\xda\xa2\x7b\x15\x70\xf5\x14\x20\xe2\xb3\x02\ +\xb8\x7a\x90\xbd\xbe\xcf\x3a\x06\xee\x1a\x92\x38\xf3\xbd\x63\x12\ +\x85\x09\xfd\x5f\x03\x59\x6d\x1e\x0a\x7c\x2d\x35\xb2\x76\x0c\xc5\ +\xa1\x15\xec\xcf\x84\xa7\x26\x2e\xa8\x31\xd9\x9d\x5e\x8b\x60\xe3\ +\xcc\xf2\x41\xfd\xae\x35\x2c\x29\xb8\x6a\xf9\x45\x35\x08\x6b\xdc\ +\xc7\x90\xb4\x92\x69\x68\x9e\x20\x43\xc4\xea\xf5\xca\xb1\x43\xf9\ +\xdb\x5a\x8a\xdc\x3d\xf7\x80\x3c\x20\x5e\xcb\xb3\x0b\x94\x8a\xe1\ +\x9a\xe5\xda\x94\xa6\x82\x61\x1b\x42\x1e\x6c\xbc\x75\xaf\x54\x51\ +\xf8\xbb\x63\xe4\x4e\xf1\xaa\xc9\xc0\x08\xbe\x94\x14\x7d\x67\x3c\ +\xa8\xd7\xd0\xa7\x39\x92\xd3\x4b\x9a\x81\x1d\xab\x24\x36\xbf\x1a\ +\xa4\x7f\xa7\x10\xab\x09\x2f\xaf\x73\xd7\x2c\xb5\x49\x4a\x8f\x8e\ +\xc1\x10\xf5\x50\x3d\x3a\x12\xb4\xcb\x8d\xd0\x08\xd1\x55\x95\x58\ +\xe4\x97\x3a\xf9\xbf\xca\x73\xb6\xa7\x89\xf7\x22\xae\xed\x7f\x89\ +\x97\x14\x0c\x88\x5d\x15\xec\x50\x29\x47\x3f\x57\x58\x2b\x2d\x65\ +\x16\xf9\xcf\x79\xcc\x5a\xe5\xfd\xf9\xa3\xd2\x0d\x56\xdf\xfc\x1a\ +\xe3\x2c\x47\x68\xe4\x95\xd6\xd6\x86\x07\x49\xb6\xb4\x6c\x51\x95\ +\xdd\x57\xe7\xd3\x38\x7c\xeb\x01\xf3\xe9\x1c\xc0\xad\xe7\x4b\xcb\ +\xe5\xc5\x63\xf5\x6a\x8f\x4c\xc7\xa0\xbf\x44\x22\xff\x82\x03\xff\ +\x42\x5b\x42\xff\xec\x3d\x0a\x2d\xd6\xd6\xa6\x9f\xcc\x04\xf4\xe1\ +\x74\x2d\x90\x66\x59\x85\xf8\x63\x2b\xe6\x1f\xa2\x28\xd7\x21\x29\ +\xf7\x41\xf8\x7e\xc0\x3a\xa4\x0b\xf2\xbe\x85\x34\x1d\x89\x03\xf9\ +\xe3\xcc\xac\x94\x7a\x10\xfd\x51\xe4\x4a\xb3\x51\xca\x4f\x8c\x2d\ +\xd4\x95\xb0\xf1\xd0\xef\x0d\xf5\x18\x9b\x46\x65\xed\xe8\xde\xbc\ +\xc1\xef\x56\xd7\xc6\xb5\x62\xbf\xd6\xdb\x63\xc1\x49\x07\xf5\xa2\ +\x3d\x32\x17\xf7\x24\x15\x7f\x3a\x4b\x53\xa3\x0c\xd5\x20\xaa\x85\ +\x31\xe9\xec\xf5\x49\xd8\xb5\x33\xd8\x27\x61\xaf\x53\xaa\x5d\x79\ +\x38\x0b\x37\xa9\xbc\x19\x33\x30\x43\x7f\x57\x6a\x3c\xf0\x6d\xfa\ +\x8c\xa8\xec\xdd\xa9\x70\xf8\x0c\x7a\xc7\x46\xce\xd8\x93\x50\xa8\ +\x21\x51\x3d\x58\xca\x90\x28\x76\xf8\xa9\xc6\xb6\x80\xd6\x8f\x5e\ +\x01\xa9\x37\xc6\x12\x44\xba\xd7\x93\x50\x68\x81\x66\x8a\x6c\x53\ +\xa8\x69\x99\x9f\xfa\x9f\x4f\x7e\xf7\x28\xb7\x0f\x59\x84\xbd\xde\ +\x0e\xb7\x8d\x3a\xca\x0b\xe4\xea\xe3\x0e\x14\xa1\x38\x93\xcf\x33\ +\xf0\xc0\x5b\x7a\x8d\x41\x19\x38\x95\x67\x1f\x34\x6d\x5d\x93\x95\ +\x65\x2e\xa6\xf9\x3e\x45\x69\x33\x3f\xaf\xf6\x41\x68\x67\x81\xa5\ +\xb4\x21\x1a\xb0\xcc\x73\xfc\xd3\x32\x05\x66\xeb\xd6\xc6\x1a\x4e\ +\xdd\x79\x7d\x86\xa1\x6d\x06\x61\x64\xdf\xbd\x86\x17\xb5\xb1\x9e\ +\x5a\xd7\xf0\xf4\xbb\xd7\x50\x9f\xe1\x99\x75\x0d\xcf\x1a\xd6\x40\ +\xcf\x28\xe0\x8b\x5f\xee\x4e\x74\x33\x1e\xdb\xe8\x72\x21\xb7\xc9\ +\xb6\xf8\xb7\xe7\xe6\xbd\x56\xbd\x85\x7b\x42\xfd\x11\x79\x4f\xd4\ +\xcf\x51\x1d\x86\xf1\x11\x0c\xf0\xe0\x8d\xd8\xef\x1b\x1a\x5b\x57\ +\x3c\x9e\xfb\x8f\x1a\x1d\x3a\xf4\x14\xac\xfe\xbc\xab\xfd\x1e\xcd\ +\xea\x68\xf8\x87\x81\x06\x11\xb6\x6c\x34\x50\x58\x18\x94\x58\x18\ +\x4c\x47\xf5\xbd\x06\x2c\xcc\x1e\x41\xf1\xfb\x7d\x53\x6b\x2b\x6d\ +\x8d\x67\xfe\xe3\xc6\x87\x1e\x3d\x05\xae\x3f\xeb\x6a\xbf\xe5\xdf\ +\x28\xe4\x82\x50\x6d\x7d\x05\xb9\x8f\x35\x3d\xba\x8f\x94\x8b\x3f\ +\x50\x2c\x92\x4c\x6c\x42\xce\xe3\xed\xae\xff\x0f\x4b\x69\x96\x8f\ +\x07\x1e\x07\x27\x6e\x4b\x2a\x2f\x38\xf1\x87\xfb\xc7\x71\xd5\xe7\ +\x4d\x1a\x9c\x99\xdd\x04\x2a\x42\x5a\xf3\x96\x63\x8c\x0f\xe8\x84\ +\xbf\xb1\x59\xb6\x61\xd6\xcc\x58\xa2\x85\x32\x93\x55\x46\x90\xab\ +\xd1\xb4\x71\x31\xff\xf6\x2a\x8d\x96\x18\xf6\x20\x6d\x9e\x2d\xde\ +\xaf\x06\xfa\xc7\xb8\xdb\xd5\x3f\x7d\xd2\x26\xe9\xf8\x78\x73\xa3\ +\xe9\x88\x2c\xb7\xbe\xfd\xfd\xe4\x79\x10\xcc\x64\x4b\xfc\xab\x31\ +\xb5\x70\xe1\x23\x7c\xde\x7a\xc0\xef\xa2\x31\x5f\xb0\xa2\xef\xf2\ +\x89\xca\x5c\x4d\x0a\xac\x27\xc5\x76\x32\x1d\x89\x27\x5f\xb5\xc4\ +\x2b\xcb\x9b\x3e\xf2\x5d\x20\xf3\x6f\xaa\xe8\xf7\x46\x91\x86\x59\ +\xfd\xa6\x7d\x54\x1b\x9e\x6e\x29\xcd\xd7\x6d\x1a\x6f\x2c\x76\xc6\ +\xdf\x96\xa3\xb1\xca\x94\x43\x75\xb9\xad\x8a\xf0\x0f\x1f\x05\xf4\ +\x88\xa4\xcc\x2a\xae\xd4\xc9\x27\xde\x2c\x95\x2d\x47\x5a\xb0\xff\ +\x72\xaa\xa3\x96\xc3\x69\x49\x77\xe4\xe5\xb3\x4d\x76\xf3\x76\x5b\ +\xe4\x29\x26\xd2\x1d\x45\x2a\x93\x4a\xff\x6b\x94\x34\xb1\xba\x50\ +\x03\xda\x64\xa8\x2f\xe1\x5f\x6e\xe4\x35\xda\x9d\x9a\x3e\x1e\x4f\ +\xd5\xc2\x3f\x9a\x57\xad\x10\x38\xa9\x66\xcd\xd5\xe3\xd7\xc4\x5b\ +\x51\x77\x44\xfc\x72\x46\x5e\x1f\x8d\x13\x8a\x6d\xbc\xf2\x65\x29\ +\x97\xa7\x07\xdc\xbf\x5a\xba\x8a\xac\x2d\x56\x2e\xee\x9e\x1c\x75\ +\x1b\x35\xb9\xbb\x88\x47\x32\xdd\x8b\x66\xfe\x67\xc0\xbc\x96\x6d\ +\x66\x24\xc8\x07\x46\x6f\x3c\x26\x6e\xc2\x06\x2e\xc7\xd3\xce\x02\ +\xaf\x8c\x36\x52\x7f\xb5\x09\xce\x89\x8d\x4c\xe6\xb6\xab\xd7\xc3\ +\x41\xfb\xb3\xa1\x75\xa0\xf8\x5f\xb5\x7c\xec\x8c\x72\xab\xbe\x6f\ +\x42\x8a\x6d\x7d\x08\x52\x1b\xf6\xef\x1e\xe8\xf4\x9c\xe0\xfb\x21\ +\xac\x06\x22\x12\x4e\x1e\x11\xa8\xf2\x78\x00\x79\xf4\xca\x77\x6e\ +\xd6\xc3\xa0\xc2\x5c\x63\x83\x8b\xf1\x17\xcf\xc0\x34\xb4\x3f\x89\ +\x56\x6f\x2a\x19\xb9\xf1\xe0\x99\x99\x00\x6c\x3e\x6b\x66\x69\xae\ +\xbd\xd9\xad\xbf\x5c\xe6\xd6\x06\x91\xef\x93\xd5\xdb\x4a\xe6\x3e\ +\x76\x1c\xdf\x32\x52\xe5\x25\x2c\xf3\x61\x27\xca\x98\x27\xef\xfd\ +\x7b\x3c\xe2\xe1\x52\x3a\x83\xaa\x1c\x80\xa4\x19\x3e\xc0\x50\x8f\ +\x2a\x72\x25\x0f\xc0\xbf\x2e\x2a\xc3\xb1\xf9\x5f\x17\x8d\x39\xf2\ +\xd5\x76\x59\xde\x88\xe0\x03\xd6\xc6\x33\x1f\x09\x8f\xa4\xe0\xb2\ +\x42\xd3\x10\x05\xf2\xaf\x26\x83\x3f\xe8\x95\xbf\x6a\xa2\xb7\x56\ +\xa8\x84\x3a\x22\xf3\x47\xa3\xa7\x2a\xc5\x02\x33\x37\x90\xde\x28\ +\xab\xfd\xed\x21\x7c\x27\xa3\xfe\xd6\xac\x68\x66\x7f\x69\x36\x50\ +\x2f\xe7\x68\x04\xd3\xe1\x99\x6c\x04\x4d\x73\xfc\xac\xf4\xb0\x89\ +\x6c\x77\x19\x33\x62\x3c\x1a\x1d\x99\x21\xb5\x49\x67\x32\x75\xcb\ +\xa0\x89\x48\xb9\x22\xbd\xaa\xbf\x01\xdd\xad\x95\x22\x8f\x94\x77\ +\xad\xd7\x69\x24\x72\x7b\x8c\xd0\x46\x2f\x67\xf8\x62\xb9\x1f\x8e\ +\xd5\x5f\xb6\xa5\xc4\x7a\x7c\x4d\x56\x65\xea\x73\x49\x25\x45\xbc\ +\xb7\xfb\x42\xef\x99\xfb\x0e\xbd\xa1\xee\x54\x9e\x5c\xff\x7f\x2f\ +\x2e\x80\x20\ +\x00\x01\x6e\xac\ +\x2f\ +\x2a\x21\x20\x6a\x51\x75\x65\x72\x79\x20\x76\x31\x2e\x37\x2e\x31\ +\x20\x6a\x71\x75\x65\x72\x79\x2e\x63\x6f\x6d\x20\x7c\x20\x6a\x71\ +\x75\x65\x72\x79\x2e\x6f\x72\x67\x2f\x6c\x69\x63\x65\x6e\x73\x65\ +\x20\x2a\x2f\x0a\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\ +\x62\x29\x7b\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x63\x79\x28\x61\ +\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x69\x73\x57\x69\x6e\ +\x64\x6f\x77\x28\x61\x29\x3f\x61\x3a\x61\x2e\x6e\x6f\x64\x65\x54\ +\x79\x70\x65\x3d\x3d\x3d\x39\x3f\x61\x2e\x64\x65\x66\x61\x75\x6c\ +\x74\x56\x69\x65\x77\x7c\x7c\x61\x2e\x70\x61\x72\x65\x6e\x74\x57\ +\x69\x6e\x64\x6f\x77\x3a\x21\x31\x7d\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x20\x63\x76\x28\x61\x29\x7b\x69\x66\x28\x21\x63\x6b\x5b\x61\ +\x5d\x29\x7b\x76\x61\x72\x20\x62\x3d\x63\x2e\x62\x6f\x64\x79\x2c\ +\x64\x3d\x66\x28\x22\x3c\x22\x2b\x61\x2b\x22\x3e\x22\x29\x2e\x61\ +\x70\x70\x65\x6e\x64\x54\x6f\x28\x62\x29\x2c\x65\x3d\x64\x2e\x63\ +\x73\x73\x28\x22\x64\x69\x73\x70\x6c\x61\x79\x22\x29\x3b\x64\x2e\ +\x72\x65\x6d\x6f\x76\x65\x28\x29\x3b\x69\x66\x28\x65\x3d\x3d\x3d\ +\x22\x6e\x6f\x6e\x65\x22\x7c\x7c\x65\x3d\x3d\x3d\x22\x22\x29\x7b\ +\x63\x6c\x7c\x7c\x28\x63\x6c\x3d\x63\x2e\x63\x72\x65\x61\x74\x65\ +\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x69\x66\x72\x61\x6d\x65\x22\ +\x29\x2c\x63\x6c\x2e\x66\x72\x61\x6d\x65\x42\x6f\x72\x64\x65\x72\ +\x3d\x63\x6c\x2e\x77\x69\x64\x74\x68\x3d\x63\x6c\x2e\x68\x65\x69\ +\x67\x68\x74\x3d\x30\x29\x2c\x62\x2e\x61\x70\x70\x65\x6e\x64\x43\ +\x68\x69\x6c\x64\x28\x63\x6c\x29\x3b\x69\x66\x28\x21\x63\x6d\x7c\ +\x7c\x21\x63\x6c\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\ +\x6e\x74\x29\x63\x6d\x3d\x28\x63\x6c\x2e\x63\x6f\x6e\x74\x65\x6e\ +\x74\x57\x69\x6e\x64\x6f\x77\x7c\x7c\x63\x6c\x2e\x63\x6f\x6e\x74\ +\x65\x6e\x74\x44\x6f\x63\x75\x6d\x65\x6e\x74\x29\x2e\x64\x6f\x63\ +\x75\x6d\x65\x6e\x74\x2c\x63\x6d\x2e\x77\x72\x69\x74\x65\x28\x28\ +\x63\x2e\x63\x6f\x6d\x70\x61\x74\x4d\x6f\x64\x65\x3d\x3d\x3d\x22\ +\x43\x53\x53\x31\x43\x6f\x6d\x70\x61\x74\x22\x3f\x22\x3c\x21\x64\ +\x6f\x63\x74\x79\x70\x65\x20\x68\x74\x6d\x6c\x3e\x22\x3a\x22\x22\ +\x29\x2b\x22\x3c\x68\x74\x6d\x6c\x3e\x3c\x62\x6f\x64\x79\x3e\x22\ +\x29\x2c\x63\x6d\x2e\x63\x6c\x6f\x73\x65\x28\x29\x3b\x64\x3d\x63\ +\x6d\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\ +\x61\x29\x2c\x63\x6d\x2e\x62\x6f\x64\x79\x2e\x61\x70\x70\x65\x6e\ +\x64\x43\x68\x69\x6c\x64\x28\x64\x29\x2c\x65\x3d\x66\x2e\x63\x73\ +\x73\x28\x64\x2c\x22\x64\x69\x73\x70\x6c\x61\x79\x22\x29\x2c\x62\ +\x2e\x72\x65\x6d\x6f\x76\x65\x43\x68\x69\x6c\x64\x28\x63\x6c\x29\ +\x7d\x63\x6b\x5b\x61\x5d\x3d\x65\x7d\x72\x65\x74\x75\x72\x6e\x20\ +\x63\x6b\x5b\x61\x5d\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x63\ +\x75\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\x7b\x7d\x3b\ +\x66\x2e\x65\x61\x63\x68\x28\x63\x71\x2e\x63\x6f\x6e\x63\x61\x74\ +\x2e\x61\x70\x70\x6c\x79\x28\x5b\x5d\x2c\x63\x71\x2e\x73\x6c\x69\ +\x63\x65\x28\x30\x2c\x62\x29\x29\x2c\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x29\x7b\x63\x5b\x74\x68\x69\x73\x5d\x3d\x61\x7d\x29\x3b\ +\x72\x65\x74\x75\x72\x6e\x20\x63\x7d\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x20\x63\x74\x28\x29\x7b\x63\x72\x3d\x62\x7d\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x20\x63\x73\x28\x29\x7b\x73\x65\x74\x54\x69\x6d\ +\x65\x6f\x75\x74\x28\x63\x74\x2c\x30\x29\x3b\x72\x65\x74\x75\x72\ +\x6e\x20\x63\x72\x3d\x66\x2e\x6e\x6f\x77\x28\x29\x7d\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x20\x63\x6a\x28\x29\x7b\x74\x72\x79\x7b\x72\ +\x65\x74\x75\x72\x6e\x20\x6e\x65\x77\x20\x61\x2e\x41\x63\x74\x69\ +\x76\x65\x58\x4f\x62\x6a\x65\x63\x74\x28\x22\x4d\x69\x63\x72\x6f\ +\x73\x6f\x66\x74\x2e\x58\x4d\x4c\x48\x54\x54\x50\x22\x29\x7d\x63\ +\x61\x74\x63\x68\x28\x62\x29\x7b\x7d\x7d\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x20\x63\x69\x28\x29\x7b\x74\x72\x79\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x6e\x65\x77\x20\x61\x2e\x58\x4d\x4c\x48\x74\x74\x70\ +\x52\x65\x71\x75\x65\x73\x74\x7d\x63\x61\x74\x63\x68\x28\x62\x29\ +\x7b\x7d\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x63\x63\x28\x61\ +\x2c\x63\x29\x7b\x61\x2e\x64\x61\x74\x61\x46\x69\x6c\x74\x65\x72\ +\x26\x26\x28\x63\x3d\x61\x2e\x64\x61\x74\x61\x46\x69\x6c\x74\x65\ +\x72\x28\x63\x2c\x61\x2e\x64\x61\x74\x61\x54\x79\x70\x65\x29\x29\ +\x3b\x76\x61\x72\x20\x64\x3d\x61\x2e\x64\x61\x74\x61\x54\x79\x70\ +\x65\x73\x2c\x65\x3d\x7b\x7d\x2c\x67\x2c\x68\x2c\x69\x3d\x64\x2e\ +\x6c\x65\x6e\x67\x74\x68\x2c\x6a\x2c\x6b\x3d\x64\x5b\x30\x5d\x2c\ +\x6c\x2c\x6d\x2c\x6e\x2c\x6f\x2c\x70\x3b\x66\x6f\x72\x28\x67\x3d\ +\x31\x3b\x67\x3c\x69\x3b\x67\x2b\x2b\x29\x7b\x69\x66\x28\x67\x3d\ +\x3d\x3d\x31\x29\x66\x6f\x72\x28\x68\x20\x69\x6e\x20\x61\x2e\x63\ +\x6f\x6e\x76\x65\x72\x74\x65\x72\x73\x29\x74\x79\x70\x65\x6f\x66\ +\x20\x68\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x28\x65\ +\x5b\x68\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\ +\x5d\x3d\x61\x2e\x63\x6f\x6e\x76\x65\x72\x74\x65\x72\x73\x5b\x68\ +\x5d\x29\x3b\x6c\x3d\x6b\x2c\x6b\x3d\x64\x5b\x67\x5d\x3b\x69\x66\ +\x28\x6b\x3d\x3d\x3d\x22\x2a\x22\x29\x6b\x3d\x6c\x3b\x65\x6c\x73\ +\x65\x20\x69\x66\x28\x6c\x21\x3d\x3d\x22\x2a\x22\x26\x26\x6c\x21\ +\x3d\x3d\x6b\x29\x7b\x6d\x3d\x6c\x2b\x22\x20\x22\x2b\x6b\x2c\x6e\ +\x3d\x65\x5b\x6d\x5d\x7c\x7c\x65\x5b\x22\x2a\x20\x22\x2b\x6b\x5d\ +\x3b\x69\x66\x28\x21\x6e\x29\x7b\x70\x3d\x62\x3b\x66\x6f\x72\x28\ +\x6f\x20\x69\x6e\x20\x65\x29\x7b\x6a\x3d\x6f\x2e\x73\x70\x6c\x69\ +\x74\x28\x22\x20\x22\x29\x3b\x69\x66\x28\x6a\x5b\x30\x5d\x3d\x3d\ +\x3d\x6c\x7c\x7c\x6a\x5b\x30\x5d\x3d\x3d\x3d\x22\x2a\x22\x29\x7b\ +\x70\x3d\x65\x5b\x6a\x5b\x31\x5d\x2b\x22\x20\x22\x2b\x6b\x5d\x3b\ +\x69\x66\x28\x70\x29\x7b\x6f\x3d\x65\x5b\x6f\x5d\x2c\x6f\x3d\x3d\ +\x3d\x21\x30\x3f\x6e\x3d\x70\x3a\x70\x3d\x3d\x3d\x21\x30\x26\x26\ +\x28\x6e\x3d\x6f\x29\x3b\x62\x72\x65\x61\x6b\x7d\x7d\x7d\x7d\x21\ +\x6e\x26\x26\x21\x70\x26\x26\x66\x2e\x65\x72\x72\x6f\x72\x28\x22\ +\x4e\x6f\x20\x63\x6f\x6e\x76\x65\x72\x73\x69\x6f\x6e\x20\x66\x72\ +\x6f\x6d\x20\x22\x2b\x6d\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x22\ +\x20\x22\x2c\x22\x20\x74\x6f\x20\x22\x29\x29\x2c\x6e\x21\x3d\x3d\ +\x21\x30\x26\x26\x28\x63\x3d\x6e\x3f\x6e\x28\x63\x29\x3a\x70\x28\ +\x6f\x28\x63\x29\x29\x29\x7d\x7d\x72\x65\x74\x75\x72\x6e\x20\x63\ +\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x63\x62\x28\x61\x2c\x63\ +\x2c\x64\x29\x7b\x76\x61\x72\x20\x65\x3d\x61\x2e\x63\x6f\x6e\x74\ +\x65\x6e\x74\x73\x2c\x66\x3d\x61\x2e\x64\x61\x74\x61\x54\x79\x70\ +\x65\x73\x2c\x67\x3d\x61\x2e\x72\x65\x73\x70\x6f\x6e\x73\x65\x46\ +\x69\x65\x6c\x64\x73\x2c\x68\x2c\x69\x2c\x6a\x2c\x6b\x3b\x66\x6f\ +\x72\x28\x69\x20\x69\x6e\x20\x67\x29\x69\x20\x69\x6e\x20\x64\x26\ +\x26\x28\x63\x5b\x67\x5b\x69\x5d\x5d\x3d\x64\x5b\x69\x5d\x29\x3b\ +\x77\x68\x69\x6c\x65\x28\x66\x5b\x30\x5d\x3d\x3d\x3d\x22\x2a\x22\ +\x29\x66\x2e\x73\x68\x69\x66\x74\x28\x29\x2c\x68\x3d\x3d\x3d\x62\ +\x26\x26\x28\x68\x3d\x61\x2e\x6d\x69\x6d\x65\x54\x79\x70\x65\x7c\ +\x7c\x63\x2e\x67\x65\x74\x52\x65\x73\x70\x6f\x6e\x73\x65\x48\x65\ +\x61\x64\x65\x72\x28\x22\x63\x6f\x6e\x74\x65\x6e\x74\x2d\x74\x79\ +\x70\x65\x22\x29\x29\x3b\x69\x66\x28\x68\x29\x66\x6f\x72\x28\x69\ +\x20\x69\x6e\x20\x65\x29\x69\x66\x28\x65\x5b\x69\x5d\x26\x26\x65\ +\x5b\x69\x5d\x2e\x74\x65\x73\x74\x28\x68\x29\x29\x7b\x66\x2e\x75\ +\x6e\x73\x68\x69\x66\x74\x28\x69\x29\x3b\x62\x72\x65\x61\x6b\x7d\ +\x69\x66\x28\x66\x5b\x30\x5d\x69\x6e\x20\x64\x29\x6a\x3d\x66\x5b\ +\x30\x5d\x3b\x65\x6c\x73\x65\x7b\x66\x6f\x72\x28\x69\x20\x69\x6e\ +\x20\x64\x29\x7b\x69\x66\x28\x21\x66\x5b\x30\x5d\x7c\x7c\x61\x2e\ +\x63\x6f\x6e\x76\x65\x72\x74\x65\x72\x73\x5b\x69\x2b\x22\x20\x22\ +\x2b\x66\x5b\x30\x5d\x5d\x29\x7b\x6a\x3d\x69\x3b\x62\x72\x65\x61\ +\x6b\x7d\x6b\x7c\x7c\x28\x6b\x3d\x69\x29\x7d\x6a\x3d\x6a\x7c\x7c\ +\x6b\x7d\x69\x66\x28\x6a\x29\x7b\x6a\x21\x3d\x3d\x66\x5b\x30\x5d\ +\x26\x26\x66\x2e\x75\x6e\x73\x68\x69\x66\x74\x28\x6a\x29\x3b\x72\ +\x65\x74\x75\x72\x6e\x20\x64\x5b\x6a\x5d\x7d\x7d\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x20\x63\x61\x28\x61\x2c\x62\x2c\x63\x2c\x64\x29\ +\x7b\x69\x66\x28\x66\x2e\x69\x73\x41\x72\x72\x61\x79\x28\x62\x29\ +\x29\x66\x2e\x65\x61\x63\x68\x28\x62\x2c\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x62\x2c\x65\x29\x7b\x63\x7c\x7c\x62\x45\x2e\x74\x65\ +\x73\x74\x28\x61\x29\x3f\x64\x28\x61\x2c\x65\x29\x3a\x63\x61\x28\ +\x61\x2b\x22\x5b\x22\x2b\x28\x74\x79\x70\x65\x6f\x66\x20\x65\x3d\ +\x3d\x22\x6f\x62\x6a\x65\x63\x74\x22\x7c\x7c\x66\x2e\x69\x73\x41\ +\x72\x72\x61\x79\x28\x65\x29\x3f\x62\x3a\x22\x22\x29\x2b\x22\x5d\ +\x22\x2c\x65\x2c\x63\x2c\x64\x29\x7d\x29\x3b\x65\x6c\x73\x65\x20\ +\x69\x66\x28\x21\x63\x26\x26\x62\x21\x3d\x6e\x75\x6c\x6c\x26\x26\ +\x74\x79\x70\x65\x6f\x66\x20\x62\x3d\x3d\x22\x6f\x62\x6a\x65\x63\ +\x74\x22\x29\x66\x6f\x72\x28\x76\x61\x72\x20\x65\x20\x69\x6e\x20\ +\x62\x29\x63\x61\x28\x61\x2b\x22\x5b\x22\x2b\x65\x2b\x22\x5d\x22\ +\x2c\x62\x5b\x65\x5d\x2c\x63\x2c\x64\x29\x3b\x65\x6c\x73\x65\x20\ +\x64\x28\x61\x2c\x62\x29\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\ +\x62\x5f\x28\x61\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x2c\x65\x2c\ +\x67\x3d\x66\x2e\x61\x6a\x61\x78\x53\x65\x74\x74\x69\x6e\x67\x73\ +\x2e\x66\x6c\x61\x74\x4f\x70\x74\x69\x6f\x6e\x73\x7c\x7c\x7b\x7d\ +\x3b\x66\x6f\x72\x28\x64\x20\x69\x6e\x20\x63\x29\x63\x5b\x64\x5d\ +\x21\x3d\x3d\x62\x26\x26\x28\x28\x67\x5b\x64\x5d\x3f\x61\x3a\x65\ +\x7c\x7c\x28\x65\x3d\x7b\x7d\x29\x29\x5b\x64\x5d\x3d\x63\x5b\x64\ +\x5d\x29\x3b\x65\x26\x26\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x21\ +\x30\x2c\x61\x2c\x65\x29\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\ +\x62\x24\x28\x61\x2c\x63\x2c\x64\x2c\x65\x2c\x66\x2c\x67\x29\x7b\ +\x66\x3d\x66\x7c\x7c\x63\x2e\x64\x61\x74\x61\x54\x79\x70\x65\x73\ +\x5b\x30\x5d\x2c\x67\x3d\x67\x7c\x7c\x7b\x7d\x2c\x67\x5b\x66\x5d\ +\x3d\x21\x30\x3b\x76\x61\x72\x20\x68\x3d\x61\x5b\x66\x5d\x2c\x69\ +\x3d\x30\x2c\x6a\x3d\x68\x3f\x68\x2e\x6c\x65\x6e\x67\x74\x68\x3a\ +\x30\x2c\x6b\x3d\x61\x3d\x3d\x3d\x62\x54\x2c\x6c\x3b\x66\x6f\x72\ +\x28\x3b\x69\x3c\x6a\x26\x26\x28\x6b\x7c\x7c\x21\x6c\x29\x3b\x69\ +\x2b\x2b\x29\x6c\x3d\x68\x5b\x69\x5d\x28\x63\x2c\x64\x2c\x65\x29\ +\x2c\x74\x79\x70\x65\x6f\x66\x20\x6c\x3d\x3d\x22\x73\x74\x72\x69\ +\x6e\x67\x22\x26\x26\x28\x21\x6b\x7c\x7c\x67\x5b\x6c\x5d\x3f\x6c\ +\x3d\x62\x3a\x28\x63\x2e\x64\x61\x74\x61\x54\x79\x70\x65\x73\x2e\ +\x75\x6e\x73\x68\x69\x66\x74\x28\x6c\x29\x2c\x6c\x3d\x62\x24\x28\ +\x61\x2c\x63\x2c\x64\x2c\x65\x2c\x6c\x2c\x67\x29\x29\x29\x3b\x28\ +\x6b\x7c\x7c\x21\x6c\x29\x26\x26\x21\x67\x5b\x22\x2a\x22\x5d\x26\ +\x26\x28\x6c\x3d\x62\x24\x28\x61\x2c\x63\x2c\x64\x2c\x65\x2c\x22\ +\x2a\x22\x2c\x67\x29\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x6c\x7d\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x62\x5a\x28\x61\x29\x7b\x72\ +\x65\x74\x75\x72\x6e\x20\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\ +\x2c\x63\x29\x7b\x74\x79\x70\x65\x6f\x66\x20\x62\x21\x3d\x22\x73\ +\x74\x72\x69\x6e\x67\x22\x26\x26\x28\x63\x3d\x62\x2c\x62\x3d\x22\ +\x2a\x22\x29\x3b\x69\x66\x28\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x63\x29\x29\x7b\x76\x61\x72\x20\x64\x3d\x62\x2e\ +\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x2e\x73\x70\ +\x6c\x69\x74\x28\x62\x50\x29\x2c\x65\x3d\x30\x2c\x67\x3d\x64\x2e\ +\x6c\x65\x6e\x67\x74\x68\x2c\x68\x2c\x69\x2c\x6a\x3b\x66\x6f\x72\ +\x28\x3b\x65\x3c\x67\x3b\x65\x2b\x2b\x29\x68\x3d\x64\x5b\x65\x5d\ +\x2c\x6a\x3d\x2f\x5e\x5c\x2b\x2f\x2e\x74\x65\x73\x74\x28\x68\x29\ +\x2c\x6a\x26\x26\x28\x68\x3d\x68\x2e\x73\x75\x62\x73\x74\x72\x28\ +\x31\x29\x7c\x7c\x22\x2a\x22\x29\x2c\x69\x3d\x61\x5b\x68\x5d\x3d\ +\x61\x5b\x68\x5d\x7c\x7c\x5b\x5d\x2c\x69\x5b\x6a\x3f\x22\x75\x6e\ +\x73\x68\x69\x66\x74\x22\x3a\x22\x70\x75\x73\x68\x22\x5d\x28\x63\ +\x29\x7d\x7d\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x62\x43\x28\ +\x61\x2c\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x3d\x62\x3d\x3d\ +\x3d\x22\x77\x69\x64\x74\x68\x22\x3f\x61\x2e\x6f\x66\x66\x73\x65\ +\x74\x57\x69\x64\x74\x68\x3a\x61\x2e\x6f\x66\x66\x73\x65\x74\x48\ +\x65\x69\x67\x68\x74\x2c\x65\x3d\x62\x3d\x3d\x3d\x22\x77\x69\x64\ +\x74\x68\x22\x3f\x62\x78\x3a\x62\x79\x2c\x67\x3d\x30\x2c\x68\x3d\ +\x65\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x69\x66\x28\x64\x3e\x30\x29\ +\x7b\x69\x66\x28\x63\x21\x3d\x3d\x22\x62\x6f\x72\x64\x65\x72\x22\ +\x29\x66\x6f\x72\x28\x3b\x67\x3c\x68\x3b\x67\x2b\x2b\x29\x63\x7c\ +\x7c\x28\x64\x2d\x3d\x70\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\x28\ +\x66\x2e\x63\x73\x73\x28\x61\x2c\x22\x70\x61\x64\x64\x69\x6e\x67\ +\x22\x2b\x65\x5b\x67\x5d\x29\x29\x7c\x7c\x30\x29\x2c\x63\x3d\x3d\ +\x3d\x22\x6d\x61\x72\x67\x69\x6e\x22\x3f\x64\x2b\x3d\x70\x61\x72\ +\x73\x65\x46\x6c\x6f\x61\x74\x28\x66\x2e\x63\x73\x73\x28\x61\x2c\ +\x63\x2b\x65\x5b\x67\x5d\x29\x29\x7c\x7c\x30\x3a\x64\x2d\x3d\x70\ +\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\x28\x66\x2e\x63\x73\x73\x28\ +\x61\x2c\x22\x62\x6f\x72\x64\x65\x72\x22\x2b\x65\x5b\x67\x5d\x2b\ +\x22\x57\x69\x64\x74\x68\x22\x29\x29\x7c\x7c\x30\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x64\x2b\x22\x70\x78\x22\x7d\x64\x3d\x62\x7a\x28\ +\x61\x2c\x62\x2c\x62\x29\x3b\x69\x66\x28\x64\x3c\x30\x7c\x7c\x64\ +\x3d\x3d\x6e\x75\x6c\x6c\x29\x64\x3d\x61\x2e\x73\x74\x79\x6c\x65\ +\x5b\x62\x5d\x7c\x7c\x30\x3b\x64\x3d\x70\x61\x72\x73\x65\x46\x6c\ +\x6f\x61\x74\x28\x64\x29\x7c\x7c\x30\x3b\x69\x66\x28\x63\x29\x66\ +\x6f\x72\x28\x3b\x67\x3c\x68\x3b\x67\x2b\x2b\x29\x64\x2b\x3d\x70\ +\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\x28\x66\x2e\x63\x73\x73\x28\ +\x61\x2c\x22\x70\x61\x64\x64\x69\x6e\x67\x22\x2b\x65\x5b\x67\x5d\ +\x29\x29\x7c\x7c\x30\x2c\x63\x21\x3d\x3d\x22\x70\x61\x64\x64\x69\ +\x6e\x67\x22\x26\x26\x28\x64\x2b\x3d\x70\x61\x72\x73\x65\x46\x6c\ +\x6f\x61\x74\x28\x66\x2e\x63\x73\x73\x28\x61\x2c\x22\x62\x6f\x72\ +\x64\x65\x72\x22\x2b\x65\x5b\x67\x5d\x2b\x22\x57\x69\x64\x74\x68\ +\x22\x29\x29\x7c\x7c\x30\x29\x2c\x63\x3d\x3d\x3d\x22\x6d\x61\x72\ +\x67\x69\x6e\x22\x26\x26\x28\x64\x2b\x3d\x70\x61\x72\x73\x65\x46\ +\x6c\x6f\x61\x74\x28\x66\x2e\x63\x73\x73\x28\x61\x2c\x63\x2b\x65\ +\x5b\x67\x5d\x29\x29\x7c\x7c\x30\x29\x3b\x72\x65\x74\x75\x72\x6e\ +\x20\x64\x2b\x22\x70\x78\x22\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x20\x62\x70\x28\x61\x2c\x62\x29\x7b\x62\x2e\x73\x72\x63\x3f\x66\ +\x2e\x61\x6a\x61\x78\x28\x7b\x75\x72\x6c\x3a\x62\x2e\x73\x72\x63\ +\x2c\x61\x73\x79\x6e\x63\x3a\x21\x31\x2c\x64\x61\x74\x61\x54\x79\ +\x70\x65\x3a\x22\x73\x63\x72\x69\x70\x74\x22\x7d\x29\x3a\x66\x2e\ +\x67\x6c\x6f\x62\x61\x6c\x45\x76\x61\x6c\x28\x28\x62\x2e\x74\x65\ +\x78\x74\x7c\x7c\x62\x2e\x74\x65\x78\x74\x43\x6f\x6e\x74\x65\x6e\ +\x74\x7c\x7c\x62\x2e\x69\x6e\x6e\x65\x72\x48\x54\x4d\x4c\x7c\x7c\ +\x22\x22\x29\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x62\x66\x2c\x22\ +\x2f\x2a\x24\x30\x2a\x2f\x22\x29\x29\x2c\x62\x2e\x70\x61\x72\x65\ +\x6e\x74\x4e\x6f\x64\x65\x26\x26\x62\x2e\x70\x61\x72\x65\x6e\x74\ +\x4e\x6f\x64\x65\x2e\x72\x65\x6d\x6f\x76\x65\x43\x68\x69\x6c\x64\ +\x28\x62\x29\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x62\x6f\x28\ +\x61\x29\x7b\x76\x61\x72\x20\x62\x3d\x63\x2e\x63\x72\x65\x61\x74\ +\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x64\x69\x76\x22\x29\x3b\ +\x62\x68\x2e\x61\x70\x70\x65\x6e\x64\x43\x68\x69\x6c\x64\x28\x62\ +\x29\x2c\x62\x2e\x69\x6e\x6e\x65\x72\x48\x54\x4d\x4c\x3d\x61\x2e\ +\x6f\x75\x74\x65\x72\x48\x54\x4d\x4c\x3b\x72\x65\x74\x75\x72\x6e\ +\x20\x62\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x7d\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x20\x62\x6e\x28\x61\x29\x7b\x76\x61\x72\ +\x20\x62\x3d\x28\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x7c\x7c\ +\x22\x22\x29\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\ +\x29\x3b\x62\x3d\x3d\x3d\x22\x69\x6e\x70\x75\x74\x22\x3f\x62\x6d\ +\x28\x61\x29\x3a\x62\x21\x3d\x3d\x22\x73\x63\x72\x69\x70\x74\x22\ +\x26\x26\x74\x79\x70\x65\x6f\x66\x20\x61\x2e\x67\x65\x74\x45\x6c\ +\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x21\ +\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\x65\x64\x22\x26\x26\x66\x2e\ +\x67\x72\x65\x70\x28\x61\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\ +\x74\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\x69\x6e\x70\ +\x75\x74\x22\x29\x2c\x62\x6d\x29\x7d\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x20\x62\x6d\x28\x61\x29\x7b\x69\x66\x28\x61\x2e\x74\x79\x70\ +\x65\x3d\x3d\x3d\x22\x63\x68\x65\x63\x6b\x62\x6f\x78\x22\x7c\x7c\ +\x61\x2e\x74\x79\x70\x65\x3d\x3d\x3d\x22\x72\x61\x64\x69\x6f\x22\ +\x29\x61\x2e\x64\x65\x66\x61\x75\x6c\x74\x43\x68\x65\x63\x6b\x65\ +\x64\x3d\x61\x2e\x63\x68\x65\x63\x6b\x65\x64\x7d\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x20\x62\x6c\x28\x61\x29\x7b\x72\x65\x74\x75\x72\ +\x6e\x20\x74\x79\x70\x65\x6f\x66\x20\x61\x2e\x67\x65\x74\x45\x6c\ +\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x21\ +\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\x65\x64\x22\x3f\x61\x2e\x67\ +\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\ +\x61\x6d\x65\x28\x22\x2a\x22\x29\x3a\x74\x79\x70\x65\x6f\x66\x20\ +\x61\x2e\x71\x75\x65\x72\x79\x53\x65\x6c\x65\x63\x74\x6f\x72\x41\ +\x6c\x6c\x21\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\x65\x64\x22\x3f\ +\x61\x2e\x71\x75\x65\x72\x79\x53\x65\x6c\x65\x63\x74\x6f\x72\x41\ +\x6c\x6c\x28\x22\x2a\x22\x29\x3a\x5b\x5d\x7d\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x20\x62\x6b\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\ +\x63\x3b\x69\x66\x28\x62\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\ +\x3d\x3d\x31\x29\x7b\x62\x2e\x63\x6c\x65\x61\x72\x41\x74\x74\x72\ +\x69\x62\x75\x74\x65\x73\x26\x26\x62\x2e\x63\x6c\x65\x61\x72\x41\ +\x74\x74\x72\x69\x62\x75\x74\x65\x73\x28\x29\x2c\x62\x2e\x6d\x65\ +\x72\x67\x65\x41\x74\x74\x72\x69\x62\x75\x74\x65\x73\x26\x26\x62\ +\x2e\x6d\x65\x72\x67\x65\x41\x74\x74\x72\x69\x62\x75\x74\x65\x73\ +\x28\x61\x29\x2c\x63\x3d\x62\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\ +\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x3b\x69\ +\x66\x28\x63\x3d\x3d\x3d\x22\x6f\x62\x6a\x65\x63\x74\x22\x29\x62\ +\x2e\x6f\x75\x74\x65\x72\x48\x54\x4d\x4c\x3d\x61\x2e\x6f\x75\x74\ +\x65\x72\x48\x54\x4d\x4c\x3b\x65\x6c\x73\x65\x20\x69\x66\x28\x63\ +\x21\x3d\x3d\x22\x69\x6e\x70\x75\x74\x22\x7c\x7c\x61\x2e\x74\x79\ +\x70\x65\x21\x3d\x3d\x22\x63\x68\x65\x63\x6b\x62\x6f\x78\x22\x26\ +\x26\x61\x2e\x74\x79\x70\x65\x21\x3d\x3d\x22\x72\x61\x64\x69\x6f\ +\x22\x29\x7b\x69\x66\x28\x63\x3d\x3d\x3d\x22\x6f\x70\x74\x69\x6f\ +\x6e\x22\x29\x62\x2e\x73\x65\x6c\x65\x63\x74\x65\x64\x3d\x61\x2e\ +\x64\x65\x66\x61\x75\x6c\x74\x53\x65\x6c\x65\x63\x74\x65\x64\x3b\ +\x65\x6c\x73\x65\x20\x69\x66\x28\x63\x3d\x3d\x3d\x22\x69\x6e\x70\ +\x75\x74\x22\x7c\x7c\x63\x3d\x3d\x3d\x22\x74\x65\x78\x74\x61\x72\ +\x65\x61\x22\x29\x62\x2e\x64\x65\x66\x61\x75\x6c\x74\x56\x61\x6c\ +\x75\x65\x3d\x61\x2e\x64\x65\x66\x61\x75\x6c\x74\x56\x61\x6c\x75\ +\x65\x7d\x65\x6c\x73\x65\x20\x61\x2e\x63\x68\x65\x63\x6b\x65\x64\ +\x26\x26\x28\x62\x2e\x64\x65\x66\x61\x75\x6c\x74\x43\x68\x65\x63\ +\x6b\x65\x64\x3d\x62\x2e\x63\x68\x65\x63\x6b\x65\x64\x3d\x61\x2e\ +\x63\x68\x65\x63\x6b\x65\x64\x29\x2c\x62\x2e\x76\x61\x6c\x75\x65\ +\x21\x3d\x3d\x61\x2e\x76\x61\x6c\x75\x65\x26\x26\x28\x62\x2e\x76\ +\x61\x6c\x75\x65\x3d\x61\x2e\x76\x61\x6c\x75\x65\x29\x3b\x62\x2e\ +\x72\x65\x6d\x6f\x76\x65\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\ +\x66\x2e\x65\x78\x70\x61\x6e\x64\x6f\x29\x7d\x7d\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x20\x62\x6a\x28\x61\x2c\x62\x29\x7b\x69\x66\x28\ +\x62\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x26\x26\ +\x21\x21\x66\x2e\x68\x61\x73\x44\x61\x74\x61\x28\x61\x29\x29\x7b\ +\x76\x61\x72\x20\x63\x2c\x64\x2c\x65\x2c\x67\x3d\x66\x2e\x5f\x64\ +\x61\x74\x61\x28\x61\x29\x2c\x68\x3d\x66\x2e\x5f\x64\x61\x74\x61\ +\x28\x62\x2c\x67\x29\x2c\x69\x3d\x67\x2e\x65\x76\x65\x6e\x74\x73\ +\x3b\x69\x66\x28\x69\x29\x7b\x64\x65\x6c\x65\x74\x65\x20\x68\x2e\ +\x68\x61\x6e\x64\x6c\x65\x2c\x68\x2e\x65\x76\x65\x6e\x74\x73\x3d\ +\x7b\x7d\x3b\x66\x6f\x72\x28\x63\x20\x69\x6e\x20\x69\x29\x66\x6f\ +\x72\x28\x64\x3d\x30\x2c\x65\x3d\x69\x5b\x63\x5d\x2e\x6c\x65\x6e\ +\x67\x74\x68\x3b\x64\x3c\x65\x3b\x64\x2b\x2b\x29\x66\x2e\x65\x76\ +\x65\x6e\x74\x2e\x61\x64\x64\x28\x62\x2c\x63\x2b\x28\x69\x5b\x63\ +\x5d\x5b\x64\x5d\x2e\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x3f\x22\ +\x2e\x22\x3a\x22\x22\x29\x2b\x69\x5b\x63\x5d\x5b\x64\x5d\x2e\x6e\ +\x61\x6d\x65\x73\x70\x61\x63\x65\x2c\x69\x5b\x63\x5d\x5b\x64\x5d\ +\x2c\x69\x5b\x63\x5d\x5b\x64\x5d\x2e\x64\x61\x74\x61\x29\x7d\x68\ +\x2e\x64\x61\x74\x61\x26\x26\x28\x68\x2e\x64\x61\x74\x61\x3d\x66\ +\x2e\x65\x78\x74\x65\x6e\x64\x28\x7b\x7d\x2c\x68\x2e\x64\x61\x74\ +\x61\x29\x29\x7d\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x62\x69\ +\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x6e\ +\x6f\x64\x65\x4e\x61\x6d\x65\x28\x61\x2c\x22\x74\x61\x62\x6c\x65\ +\x22\x29\x3f\x61\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\ +\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\x74\x62\x6f\x64\x79\ +\x22\x29\x5b\x30\x5d\x7c\x7c\x61\x2e\x61\x70\x70\x65\x6e\x64\x43\ +\x68\x69\x6c\x64\x28\x61\x2e\x6f\x77\x6e\x65\x72\x44\x6f\x63\x75\ +\x6d\x65\x6e\x74\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\ +\x6e\x74\x28\x22\x74\x62\x6f\x64\x79\x22\x29\x29\x3a\x61\x7d\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x20\x55\x28\x61\x29\x7b\x76\x61\x72\ +\x20\x62\x3d\x56\x2e\x73\x70\x6c\x69\x74\x28\x22\x7c\x22\x29\x2c\ +\x63\x3d\x61\x2e\x63\x72\x65\x61\x74\x65\x44\x6f\x63\x75\x6d\x65\ +\x6e\x74\x46\x72\x61\x67\x6d\x65\x6e\x74\x28\x29\x3b\x69\x66\x28\ +\x63\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x29\ +\x77\x68\x69\x6c\x65\x28\x62\x2e\x6c\x65\x6e\x67\x74\x68\x29\x63\ +\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x62\ +\x2e\x70\x6f\x70\x28\x29\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x63\ +\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x54\x28\x61\x2c\x62\x2c\ +\x63\x29\x7b\x62\x3d\x62\x7c\x7c\x30\x3b\x69\x66\x28\x66\x2e\x69\ +\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\x29\x29\x72\x65\x74\ +\x75\x72\x6e\x20\x66\x2e\x67\x72\x65\x70\x28\x61\x2c\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x2c\x64\x29\x7b\x76\x61\x72\x20\x65\ +\x3d\x21\x21\x62\x2e\x63\x61\x6c\x6c\x28\x61\x2c\x64\x2c\x61\x29\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x65\x3d\x3d\x3d\x63\x7d\x29\x3b\ +\x69\x66\x28\x62\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x29\x72\x65\ +\x74\x75\x72\x6e\x20\x66\x2e\x67\x72\x65\x70\x28\x61\x2c\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x64\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x61\x3d\x3d\x3d\x62\x3d\x3d\x3d\x63\x7d\x29\x3b\x69\ +\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x62\x3d\x3d\x22\x73\x74\x72\ +\x69\x6e\x67\x22\x29\x7b\x76\x61\x72\x20\x64\x3d\x66\x2e\x67\x72\ +\x65\x70\x28\x61\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\ +\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x6e\x6f\x64\x65\x54\x79\ +\x70\x65\x3d\x3d\x3d\x31\x7d\x29\x3b\x69\x66\x28\x4f\x2e\x74\x65\ +\x73\x74\x28\x62\x29\x29\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x66\ +\x69\x6c\x74\x65\x72\x28\x62\x2c\x64\x2c\x21\x63\x29\x3b\x62\x3d\ +\x66\x2e\x66\x69\x6c\x74\x65\x72\x28\x62\x2c\x64\x29\x7d\x72\x65\ +\x74\x75\x72\x6e\x20\x66\x2e\x67\x72\x65\x70\x28\x61\x2c\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x64\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x66\x2e\x69\x6e\x41\x72\x72\x61\x79\x28\x61\x2c\x62\ +\x29\x3e\x3d\x30\x3d\x3d\x3d\x63\x7d\x29\x7d\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x20\x53\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x21\ +\x61\x7c\x7c\x21\x61\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\ +\x7c\x7c\x61\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2e\x6e\ +\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x31\x7d\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x20\x4b\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\ +\x21\x30\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x4a\x28\x29\x7b\ +\x72\x65\x74\x75\x72\x6e\x21\x31\x7d\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x20\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\ +\x3d\x62\x2b\x22\x64\x65\x66\x65\x72\x22\x2c\x65\x3d\x62\x2b\x22\ +\x71\x75\x65\x75\x65\x22\x2c\x67\x3d\x62\x2b\x22\x6d\x61\x72\x6b\ +\x22\x2c\x68\x3d\x66\x2e\x5f\x64\x61\x74\x61\x28\x61\x2c\x64\x29\ +\x3b\x68\x26\x26\x28\x63\x3d\x3d\x3d\x22\x71\x75\x65\x75\x65\x22\ +\x7c\x7c\x21\x66\x2e\x5f\x64\x61\x74\x61\x28\x61\x2c\x65\x29\x29\ +\x26\x26\x28\x63\x3d\x3d\x3d\x22\x6d\x61\x72\x6b\x22\x7c\x7c\x21\ +\x66\x2e\x5f\x64\x61\x74\x61\x28\x61\x2c\x67\x29\x29\x26\x26\x73\ +\x65\x74\x54\x69\x6d\x65\x6f\x75\x74\x28\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x29\x7b\x21\x66\x2e\x5f\x64\x61\x74\x61\x28\x61\x2c\ +\x65\x29\x26\x26\x21\x66\x2e\x5f\x64\x61\x74\x61\x28\x61\x2c\x67\ +\x29\x26\x26\x28\x66\x2e\x72\x65\x6d\x6f\x76\x65\x44\x61\x74\x61\ +\x28\x61\x2c\x64\x2c\x21\x30\x29\x2c\x68\x2e\x66\x69\x72\x65\x28\ +\x29\x29\x7d\x2c\x30\x29\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\ +\x6d\x28\x61\x29\x7b\x66\x6f\x72\x28\x76\x61\x72\x20\x62\x20\x69\ +\x6e\x20\x61\x29\x7b\x69\x66\x28\x62\x3d\x3d\x3d\x22\x64\x61\x74\ +\x61\x22\x26\x26\x66\x2e\x69\x73\x45\x6d\x70\x74\x79\x4f\x62\x6a\ +\x65\x63\x74\x28\x61\x5b\x62\x5d\x29\x29\x63\x6f\x6e\x74\x69\x6e\ +\x75\x65\x3b\x69\x66\x28\x62\x21\x3d\x3d\x22\x74\x6f\x4a\x53\x4f\ +\x4e\x22\x29\x72\x65\x74\x75\x72\x6e\x21\x31\x7d\x72\x65\x74\x75\ +\x72\x6e\x21\x30\x7d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x6c\x28\ +\x61\x2c\x63\x2c\x64\x29\x7b\x69\x66\x28\x64\x3d\x3d\x3d\x62\x26\ +\x26\x61\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x29\ +\x7b\x76\x61\x72\x20\x65\x3d\x22\x64\x61\x74\x61\x2d\x22\x2b\x63\ +\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x6b\x2c\x22\x2d\x24\x31\x22\ +\x29\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x3b\ +\x64\x3d\x61\x2e\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\ +\x28\x65\x29\x3b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x64\x3d\ +\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x29\x7b\x74\x72\x79\x7b\x64\ +\x3d\x64\x3d\x3d\x3d\x22\x74\x72\x75\x65\x22\x3f\x21\x30\x3a\x64\ +\x3d\x3d\x3d\x22\x66\x61\x6c\x73\x65\x22\x3f\x21\x31\x3a\x64\x3d\ +\x3d\x3d\x22\x6e\x75\x6c\x6c\x22\x3f\x6e\x75\x6c\x6c\x3a\x66\x2e\ +\x69\x73\x4e\x75\x6d\x65\x72\x69\x63\x28\x64\x29\x3f\x70\x61\x72\ +\x73\x65\x46\x6c\x6f\x61\x74\x28\x64\x29\x3a\x6a\x2e\x74\x65\x73\ +\x74\x28\x64\x29\x3f\x66\x2e\x70\x61\x72\x73\x65\x4a\x53\x4f\x4e\ +\x28\x64\x29\x3a\x64\x7d\x63\x61\x74\x63\x68\x28\x67\x29\x7b\x7d\ +\x66\x2e\x64\x61\x74\x61\x28\x61\x2c\x63\x2c\x64\x29\x7d\x65\x6c\ +\x73\x65\x20\x64\x3d\x62\x7d\x72\x65\x74\x75\x72\x6e\x20\x64\x7d\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x68\x28\x61\x29\x7b\x76\x61\ +\x72\x20\x62\x3d\x67\x5b\x61\x5d\x3d\x7b\x7d\x2c\x63\x2c\x64\x3b\ +\x61\x3d\x61\x2e\x73\x70\x6c\x69\x74\x28\x2f\x5c\x73\x2b\x2f\x29\ +\x3b\x66\x6f\x72\x28\x63\x3d\x30\x2c\x64\x3d\x61\x2e\x6c\x65\x6e\ +\x67\x74\x68\x3b\x63\x3c\x64\x3b\x63\x2b\x2b\x29\x62\x5b\x61\x5b\ +\x63\x5d\x5d\x3d\x21\x30\x3b\x72\x65\x74\x75\x72\x6e\x20\x62\x7d\ +\x76\x61\x72\x20\x63\x3d\x61\x2e\x64\x6f\x63\x75\x6d\x65\x6e\x74\ +\x2c\x64\x3d\x61\x2e\x6e\x61\x76\x69\x67\x61\x74\x6f\x72\x2c\x65\ +\x3d\x61\x2e\x6c\x6f\x63\x61\x74\x69\x6f\x6e\x2c\x66\x3d\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x20\x4a\x28\x29\x7b\x69\x66\x28\x21\x65\x2e\x69\x73\x52\x65\ +\x61\x64\x79\x29\x7b\x74\x72\x79\x7b\x63\x2e\x64\x6f\x63\x75\x6d\ +\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x2e\x64\x6f\x53\x63\x72\ +\x6f\x6c\x6c\x28\x22\x6c\x65\x66\x74\x22\x29\x7d\x63\x61\x74\x63\ +\x68\x28\x61\x29\x7b\x73\x65\x74\x54\x69\x6d\x65\x6f\x75\x74\x28\ +\x4a\x2c\x31\x29\x3b\x72\x65\x74\x75\x72\x6e\x7d\x65\x2e\x72\x65\ +\x61\x64\x79\x28\x29\x7d\x7d\x76\x61\x72\x20\x65\x3d\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\x72\ +\x6e\x20\x6e\x65\x77\x20\x65\x2e\x66\x6e\x2e\x69\x6e\x69\x74\x28\ +\x61\x2c\x62\x2c\x68\x29\x7d\x2c\x66\x3d\x61\x2e\x6a\x51\x75\x65\ +\x72\x79\x2c\x67\x3d\x61\x2e\x24\x2c\x68\x2c\x69\x3d\x2f\x5e\x28\ +\x3f\x3a\x5b\x5e\x23\x3c\x5d\x2a\x28\x3c\x5b\x5c\x77\x5c\x57\x5d\ +\x2b\x3e\x29\x5b\x5e\x3e\x5d\x2a\x24\x7c\x23\x28\x5b\x5c\x77\x5c\ +\x2d\x5d\x2a\x29\x24\x29\x2f\x2c\x6a\x3d\x2f\x5c\x53\x2f\x2c\x6b\ +\x3d\x2f\x5e\x5c\x73\x2b\x2f\x2c\x6c\x3d\x2f\x5c\x73\x2b\x24\x2f\ +\x2c\x6d\x3d\x2f\x5e\x3c\x28\x5c\x77\x2b\x29\x5c\x73\x2a\x5c\x2f\ +\x3f\x3e\x28\x3f\x3a\x3c\x5c\x2f\x5c\x31\x3e\x29\x3f\x24\x2f\x2c\ +\x6e\x3d\x2f\x5e\x5b\x5c\x5d\x2c\x3a\x7b\x7d\x5c\x73\x5d\x2a\x24\ +\x2f\x2c\x6f\x3d\x2f\x5c\x5c\x28\x3f\x3a\x5b\x22\x5c\x5c\x5c\x2f\ +\x62\x66\x6e\x72\x74\x5d\x7c\x75\x5b\x30\x2d\x39\x61\x2d\x66\x41\ +\x2d\x46\x5d\x7b\x34\x7d\x29\x2f\x67\x2c\x70\x3d\x2f\x22\x5b\x5e\ +\x22\x5c\x5c\x5c\x6e\x5c\x72\x5d\x2a\x22\x7c\x74\x72\x75\x65\x7c\ +\x66\x61\x6c\x73\x65\x7c\x6e\x75\x6c\x6c\x7c\x2d\x3f\x5c\x64\x2b\ +\x28\x3f\x3a\x5c\x2e\x5c\x64\x2a\x29\x3f\x28\x3f\x3a\x5b\x65\x45\ +\x5d\x5b\x2b\x5c\x2d\x5d\x3f\x5c\x64\x2b\x29\x3f\x2f\x67\x2c\x71\ +\x3d\x2f\x28\x3f\x3a\x5e\x7c\x3a\x7c\x2c\x29\x28\x3f\x3a\x5c\x73\ +\x2a\x5c\x5b\x29\x2b\x2f\x67\x2c\x72\x3d\x2f\x28\x77\x65\x62\x6b\ +\x69\x74\x29\x5b\x20\x5c\x2f\x5d\x28\x5b\x5c\x77\x2e\x5d\x2b\x29\ +\x2f\x2c\x73\x3d\x2f\x28\x6f\x70\x65\x72\x61\x29\x28\x3f\x3a\x2e\ +\x2a\x76\x65\x72\x73\x69\x6f\x6e\x29\x3f\x5b\x20\x5c\x2f\x5d\x28\ +\x5b\x5c\x77\x2e\x5d\x2b\x29\x2f\x2c\x74\x3d\x2f\x28\x6d\x73\x69\ +\x65\x29\x20\x28\x5b\x5c\x77\x2e\x5d\x2b\x29\x2f\x2c\x75\x3d\x2f\ +\x28\x6d\x6f\x7a\x69\x6c\x6c\x61\x29\x28\x3f\x3a\x2e\x2a\x3f\x20\ +\x72\x76\x3a\x28\x5b\x5c\x77\x2e\x5d\x2b\x29\x29\x3f\x2f\x2c\x76\ +\x3d\x2f\x2d\x28\x5b\x61\x2d\x7a\x5d\x7c\x5b\x30\x2d\x39\x5d\x29\ +\x2f\x69\x67\x2c\x77\x3d\x2f\x5e\x2d\x6d\x73\x2d\x2f\x2c\x78\x3d\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\ +\x74\x75\x72\x6e\x28\x62\x2b\x22\x22\x29\x2e\x74\x6f\x55\x70\x70\ +\x65\x72\x43\x61\x73\x65\x28\x29\x7d\x2c\x79\x3d\x64\x2e\x75\x73\ +\x65\x72\x41\x67\x65\x6e\x74\x2c\x7a\x2c\x41\x2c\x42\x2c\x43\x3d\ +\x4f\x62\x6a\x65\x63\x74\x2e\x70\x72\x6f\x74\x6f\x74\x79\x70\x65\ +\x2e\x74\x6f\x53\x74\x72\x69\x6e\x67\x2c\x44\x3d\x4f\x62\x6a\x65\ +\x63\x74\x2e\x70\x72\x6f\x74\x6f\x74\x79\x70\x65\x2e\x68\x61\x73\ +\x4f\x77\x6e\x50\x72\x6f\x70\x65\x72\x74\x79\x2c\x45\x3d\x41\x72\ +\x72\x61\x79\x2e\x70\x72\x6f\x74\x6f\x74\x79\x70\x65\x2e\x70\x75\ +\x73\x68\x2c\x46\x3d\x41\x72\x72\x61\x79\x2e\x70\x72\x6f\x74\x6f\ +\x74\x79\x70\x65\x2e\x73\x6c\x69\x63\x65\x2c\x47\x3d\x53\x74\x72\ +\x69\x6e\x67\x2e\x70\x72\x6f\x74\x6f\x74\x79\x70\x65\x2e\x74\x72\ +\x69\x6d\x2c\x48\x3d\x41\x72\x72\x61\x79\x2e\x70\x72\x6f\x74\x6f\ +\x74\x79\x70\x65\x2e\x69\x6e\x64\x65\x78\x4f\x66\x2c\x49\x3d\x7b\ +\x7d\x3b\x65\x2e\x66\x6e\x3d\x65\x2e\x70\x72\x6f\x74\x6f\x74\x79\ +\x70\x65\x3d\x7b\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72\x3a\ +\x65\x2c\x69\x6e\x69\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x2c\x64\x2c\x66\x29\x7b\x76\x61\x72\x20\x67\x2c\x68\x2c\x6a\ +\x2c\x6b\x3b\x69\x66\x28\x21\x61\x29\x72\x65\x74\x75\x72\x6e\x20\ +\x74\x68\x69\x73\x3b\x69\x66\x28\x61\x2e\x6e\x6f\x64\x65\x54\x79\ +\x70\x65\x29\x7b\x74\x68\x69\x73\x2e\x63\x6f\x6e\x74\x65\x78\x74\ +\x3d\x74\x68\x69\x73\x5b\x30\x5d\x3d\x61\x2c\x74\x68\x69\x73\x2e\ +\x6c\x65\x6e\x67\x74\x68\x3d\x31\x3b\x72\x65\x74\x75\x72\x6e\x20\ +\x74\x68\x69\x73\x7d\x69\x66\x28\x61\x3d\x3d\x3d\x22\x62\x6f\x64\ +\x79\x22\x26\x26\x21\x64\x26\x26\x63\x2e\x62\x6f\x64\x79\x29\x7b\ +\x74\x68\x69\x73\x2e\x63\x6f\x6e\x74\x65\x78\x74\x3d\x63\x2c\x74\ +\x68\x69\x73\x5b\x30\x5d\x3d\x63\x2e\x62\x6f\x64\x79\x2c\x74\x68\ +\x69\x73\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x3d\x61\x2c\x74\x68\ +\x69\x73\x2e\x6c\x65\x6e\x67\x74\x68\x3d\x31\x3b\x72\x65\x74\x75\ +\x72\x6e\x20\x74\x68\x69\x73\x7d\x69\x66\x28\x74\x79\x70\x65\x6f\ +\x66\x20\x61\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x29\x7b\x61\ +\x2e\x63\x68\x61\x72\x41\x74\x28\x30\x29\x21\x3d\x3d\x22\x3c\x22\ +\x7c\x7c\x61\x2e\x63\x68\x61\x72\x41\x74\x28\x61\x2e\x6c\x65\x6e\ +\x67\x74\x68\x2d\x31\x29\x21\x3d\x3d\x22\x3e\x22\x7c\x7c\x61\x2e\ +\x6c\x65\x6e\x67\x74\x68\x3c\x33\x3f\x67\x3d\x69\x2e\x65\x78\x65\ +\x63\x28\x61\x29\x3a\x67\x3d\x5b\x6e\x75\x6c\x6c\x2c\x61\x2c\x6e\ +\x75\x6c\x6c\x5d\x3b\x69\x66\x28\x67\x26\x26\x28\x67\x5b\x31\x5d\ +\x7c\x7c\x21\x64\x29\x29\x7b\x69\x66\x28\x67\x5b\x31\x5d\x29\x7b\ +\x64\x3d\x64\x20\x69\x6e\x73\x74\x61\x6e\x63\x65\x6f\x66\x20\x65\ +\x3f\x64\x5b\x30\x5d\x3a\x64\x2c\x6b\x3d\x64\x3f\x64\x2e\x6f\x77\ +\x6e\x65\x72\x44\x6f\x63\x75\x6d\x65\x6e\x74\x7c\x7c\x64\x3a\x63\ +\x2c\x6a\x3d\x6d\x2e\x65\x78\x65\x63\x28\x61\x29\x2c\x6a\x3f\x65\ +\x2e\x69\x73\x50\x6c\x61\x69\x6e\x4f\x62\x6a\x65\x63\x74\x28\x64\ +\x29\x3f\x28\x61\x3d\x5b\x63\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\ +\x65\x6d\x65\x6e\x74\x28\x6a\x5b\x31\x5d\x29\x5d\x2c\x65\x2e\x66\ +\x6e\x2e\x61\x74\x74\x72\x2e\x63\x61\x6c\x6c\x28\x61\x2c\x64\x2c\ +\x21\x30\x29\x29\x3a\x61\x3d\x5b\x6b\x2e\x63\x72\x65\x61\x74\x65\ +\x45\x6c\x65\x6d\x65\x6e\x74\x28\x6a\x5b\x31\x5d\x29\x5d\x3a\x28\ +\x6a\x3d\x65\x2e\x62\x75\x69\x6c\x64\x46\x72\x61\x67\x6d\x65\x6e\ +\x74\x28\x5b\x67\x5b\x31\x5d\x5d\x2c\x5b\x6b\x5d\x29\x2c\x61\x3d\ +\x28\x6a\x2e\x63\x61\x63\x68\x65\x61\x62\x6c\x65\x3f\x65\x2e\x63\ +\x6c\x6f\x6e\x65\x28\x6a\x2e\x66\x72\x61\x67\x6d\x65\x6e\x74\x29\ +\x3a\x6a\x2e\x66\x72\x61\x67\x6d\x65\x6e\x74\x29\x2e\x63\x68\x69\ +\x6c\x64\x4e\x6f\x64\x65\x73\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\ +\x65\x2e\x6d\x65\x72\x67\x65\x28\x74\x68\x69\x73\x2c\x61\x29\x7d\ +\x68\x3d\x63\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x42\x79\ +\x49\x64\x28\x67\x5b\x32\x5d\x29\x3b\x69\x66\x28\x68\x26\x26\x68\ +\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x29\x7b\x69\x66\x28\ +\x68\x2e\x69\x64\x21\x3d\x3d\x67\x5b\x32\x5d\x29\x72\x65\x74\x75\ +\x72\x6e\x20\x66\x2e\x66\x69\x6e\x64\x28\x61\x29\x3b\x74\x68\x69\ +\x73\x2e\x6c\x65\x6e\x67\x74\x68\x3d\x31\x2c\x74\x68\x69\x73\x5b\ +\x30\x5d\x3d\x68\x7d\x74\x68\x69\x73\x2e\x63\x6f\x6e\x74\x65\x78\ +\x74\x3d\x63\x2c\x74\x68\x69\x73\x2e\x73\x65\x6c\x65\x63\x74\x6f\ +\x72\x3d\x61\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\ +\x72\x65\x74\x75\x72\x6e\x21\x64\x7c\x7c\x64\x2e\x6a\x71\x75\x65\ +\x72\x79\x3f\x28\x64\x7c\x7c\x66\x29\x2e\x66\x69\x6e\x64\x28\x61\ +\x29\x3a\x74\x68\x69\x73\x2e\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\ +\x6f\x72\x28\x64\x29\x2e\x66\x69\x6e\x64\x28\x61\x29\x7d\x69\x66\ +\x28\x65\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\ +\x29\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x72\x65\x61\x64\x79\x28\ +\x61\x29\x3b\x61\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x21\x3d\x3d\ +\x62\x26\x26\x28\x74\x68\x69\x73\x2e\x73\x65\x6c\x65\x63\x74\x6f\ +\x72\x3d\x61\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x2c\x74\x68\x69\ +\x73\x2e\x63\x6f\x6e\x74\x65\x78\x74\x3d\x61\x2e\x63\x6f\x6e\x74\ +\x65\x78\x74\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x65\x2e\x6d\x61\ +\x6b\x65\x41\x72\x72\x61\x79\x28\x61\x2c\x74\x68\x69\x73\x29\x7d\ +\x2c\x73\x65\x6c\x65\x63\x74\x6f\x72\x3a\x22\x22\x2c\x6a\x71\x75\ +\x65\x72\x79\x3a\x22\x31\x2e\x37\x2e\x31\x22\x2c\x6c\x65\x6e\x67\ +\x74\x68\x3a\x30\x2c\x73\x69\x7a\x65\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\ +\x2e\x6c\x65\x6e\x67\x74\x68\x7d\x2c\x74\x6f\x41\x72\x72\x61\x79\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x46\x2e\x63\x61\x6c\x6c\x28\x74\x68\x69\x73\x2c\x30\ +\x29\x7d\x2c\x67\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x3d\x3d\x6e\x75\x6c\ +\x6c\x3f\x74\x68\x69\x73\x2e\x74\x6f\x41\x72\x72\x61\x79\x28\x29\ +\x3a\x61\x3c\x30\x3f\x74\x68\x69\x73\x5b\x74\x68\x69\x73\x2e\x6c\ +\x65\x6e\x67\x74\x68\x2b\x61\x5d\x3a\x74\x68\x69\x73\x5b\x61\x5d\ +\x7d\x2c\x70\x75\x73\x68\x53\x74\x61\x63\x6b\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\ +\x64\x3d\x74\x68\x69\x73\x2e\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\ +\x6f\x72\x28\x29\x3b\x65\x2e\x69\x73\x41\x72\x72\x61\x79\x28\x61\ +\x29\x3f\x45\x2e\x61\x70\x70\x6c\x79\x28\x64\x2c\x61\x29\x3a\x65\ +\x2e\x6d\x65\x72\x67\x65\x28\x64\x2c\x61\x29\x2c\x64\x2e\x70\x72\ +\x65\x76\x4f\x62\x6a\x65\x63\x74\x3d\x74\x68\x69\x73\x2c\x64\x2e\ +\x63\x6f\x6e\x74\x65\x78\x74\x3d\x74\x68\x69\x73\x2e\x63\x6f\x6e\ +\x74\x65\x78\x74\x2c\x62\x3d\x3d\x3d\x22\x66\x69\x6e\x64\x22\x3f\ +\x64\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x3d\x74\x68\x69\x73\x2e\ +\x73\x65\x6c\x65\x63\x74\x6f\x72\x2b\x28\x74\x68\x69\x73\x2e\x73\ +\x65\x6c\x65\x63\x74\x6f\x72\x3f\x22\x20\x22\x3a\x22\x22\x29\x2b\ +\x63\x3a\x62\x26\x26\x28\x64\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\ +\x3d\x74\x68\x69\x73\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x2b\x22\ +\x2e\x22\x2b\x62\x2b\x22\x28\x22\x2b\x63\x2b\x22\x29\x22\x29\x3b\ +\x72\x65\x74\x75\x72\x6e\x20\x64\x7d\x2c\x65\x61\x63\x68\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\ +\x75\x72\x6e\x20\x65\x2e\x65\x61\x63\x68\x28\x74\x68\x69\x73\x2c\ +\x61\x2c\x62\x29\x7d\x2c\x72\x65\x61\x64\x79\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x29\x7b\x65\x2e\x62\x69\x6e\x64\x52\x65\ +\x61\x64\x79\x28\x29\x2c\x41\x2e\x61\x64\x64\x28\x61\x29\x3b\x72\ +\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x65\x71\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x61\x3d\x2b\x61\x3b\ +\x72\x65\x74\x75\x72\x6e\x20\x61\x3d\x3d\x3d\x2d\x31\x3f\x74\x68\ +\x69\x73\x2e\x73\x6c\x69\x63\x65\x28\x61\x29\x3a\x74\x68\x69\x73\ +\x2e\x73\x6c\x69\x63\x65\x28\x61\x2c\x61\x2b\x31\x29\x7d\x2c\x66\ +\x69\x72\x73\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\ +\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x71\x28\x30\ +\x29\x7d\x2c\x6c\x61\x73\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\ +\x71\x28\x2d\x31\x29\x7d\x2c\x73\x6c\x69\x63\x65\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x2e\x70\x75\x73\x68\x53\x74\x61\x63\x6b\x28\x46\x2e\ +\x61\x70\x70\x6c\x79\x28\x74\x68\x69\x73\x2c\x61\x72\x67\x75\x6d\ +\x65\x6e\x74\x73\x29\x2c\x22\x73\x6c\x69\x63\x65\x22\x2c\x46\x2e\ +\x63\x61\x6c\x6c\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\x2e\ +\x6a\x6f\x69\x6e\x28\x22\x2c\x22\x29\x29\x7d\x2c\x6d\x61\x70\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x74\x68\x69\x73\x2e\x70\x75\x73\x68\x53\x74\x61\x63\ +\x6b\x28\x65\x2e\x6d\x61\x70\x28\x74\x68\x69\x73\x2c\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x62\x2c\x63\x29\x7b\x72\x65\x74\x75\x72\ +\x6e\x20\x61\x2e\x63\x61\x6c\x6c\x28\x62\x2c\x63\x2c\x62\x29\x7d\ +\x29\x29\x7d\x2c\x65\x6e\x64\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x70\ +\x72\x65\x76\x4f\x62\x6a\x65\x63\x74\x7c\x7c\x74\x68\x69\x73\x2e\ +\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72\x28\x6e\x75\x6c\x6c\ +\x29\x7d\x2c\x70\x75\x73\x68\x3a\x45\x2c\x73\x6f\x72\x74\x3a\x5b\ +\x5d\x2e\x73\x6f\x72\x74\x2c\x73\x70\x6c\x69\x63\x65\x3a\x5b\x5d\ +\x2e\x73\x70\x6c\x69\x63\x65\x7d\x2c\x65\x2e\x66\x6e\x2e\x69\x6e\ +\x69\x74\x2e\x70\x72\x6f\x74\x6f\x74\x79\x70\x65\x3d\x65\x2e\x66\ +\x6e\x2c\x65\x2e\x65\x78\x74\x65\x6e\x64\x3d\x65\x2e\x66\x6e\x2e\ +\x65\x78\x74\x65\x6e\x64\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x29\x7b\x76\x61\x72\x20\x61\x2c\x63\x2c\x64\x2c\x66\x2c\x67\x2c\ +\x68\x2c\x69\x3d\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x5b\x30\x5d\ +\x7c\x7c\x7b\x7d\x2c\x6a\x3d\x31\x2c\x6b\x3d\x61\x72\x67\x75\x6d\ +\x65\x6e\x74\x73\x2e\x6c\x65\x6e\x67\x74\x68\x2c\x6c\x3d\x21\x31\ +\x3b\x74\x79\x70\x65\x6f\x66\x20\x69\x3d\x3d\x22\x62\x6f\x6f\x6c\ +\x65\x61\x6e\x22\x26\x26\x28\x6c\x3d\x69\x2c\x69\x3d\x61\x72\x67\ +\x75\x6d\x65\x6e\x74\x73\x5b\x31\x5d\x7c\x7c\x7b\x7d\x2c\x6a\x3d\ +\x32\x29\x2c\x74\x79\x70\x65\x6f\x66\x20\x69\x21\x3d\x22\x6f\x62\ +\x6a\x65\x63\x74\x22\x26\x26\x21\x65\x2e\x69\x73\x46\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x69\x29\x26\x26\x28\x69\x3d\x7b\x7d\x29\x2c\ +\x6b\x3d\x3d\x3d\x6a\x26\x26\x28\x69\x3d\x74\x68\x69\x73\x2c\x2d\ +\x2d\x6a\x29\x3b\x66\x6f\x72\x28\x3b\x6a\x3c\x6b\x3b\x6a\x2b\x2b\ +\x29\x69\x66\x28\x28\x61\x3d\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\ +\x5b\x6a\x5d\x29\x21\x3d\x6e\x75\x6c\x6c\x29\x66\x6f\x72\x28\x63\ +\x20\x69\x6e\x20\x61\x29\x7b\x64\x3d\x69\x5b\x63\x5d\x2c\x66\x3d\ +\x61\x5b\x63\x5d\x3b\x69\x66\x28\x69\x3d\x3d\x3d\x66\x29\x63\x6f\ +\x6e\x74\x69\x6e\x75\x65\x3b\x6c\x26\x26\x66\x26\x26\x28\x65\x2e\ +\x69\x73\x50\x6c\x61\x69\x6e\x4f\x62\x6a\x65\x63\x74\x28\x66\x29\ +\x7c\x7c\x28\x67\x3d\x65\x2e\x69\x73\x41\x72\x72\x61\x79\x28\x66\ +\x29\x29\x29\x3f\x28\x67\x3f\x28\x67\x3d\x21\x31\x2c\x68\x3d\x64\ +\x26\x26\x65\x2e\x69\x73\x41\x72\x72\x61\x79\x28\x64\x29\x3f\x64\ +\x3a\x5b\x5d\x29\x3a\x68\x3d\x64\x26\x26\x65\x2e\x69\x73\x50\x6c\ +\x61\x69\x6e\x4f\x62\x6a\x65\x63\x74\x28\x64\x29\x3f\x64\x3a\x7b\ +\x7d\x2c\x69\x5b\x63\x5d\x3d\x65\x2e\x65\x78\x74\x65\x6e\x64\x28\ +\x6c\x2c\x68\x2c\x66\x29\x29\x3a\x66\x21\x3d\x3d\x62\x26\x26\x28\ +\x69\x5b\x63\x5d\x3d\x66\x29\x7d\x72\x65\x74\x75\x72\x6e\x20\x69\ +\x7d\x2c\x65\x2e\x65\x78\x74\x65\x6e\x64\x28\x7b\x6e\x6f\x43\x6f\ +\x6e\x66\x6c\x69\x63\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x62\x29\x7b\x61\x2e\x24\x3d\x3d\x3d\x65\x26\x26\x28\x61\x2e\x24\ +\x3d\x67\x29\x2c\x62\x26\x26\x61\x2e\x6a\x51\x75\x65\x72\x79\x3d\ +\x3d\x3d\x65\x26\x26\x28\x61\x2e\x6a\x51\x75\x65\x72\x79\x3d\x66\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x65\x7d\x2c\x69\x73\x52\x65\ +\x61\x64\x79\x3a\x21\x31\x2c\x72\x65\x61\x64\x79\x57\x61\x69\x74\ +\x3a\x31\x2c\x68\x6f\x6c\x64\x52\x65\x61\x64\x79\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x61\x3f\x65\x2e\x72\x65\x61\ +\x64\x79\x57\x61\x69\x74\x2b\x2b\x3a\x65\x2e\x72\x65\x61\x64\x79\ +\x28\x21\x30\x29\x7d\x2c\x72\x65\x61\x64\x79\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x29\x7b\x69\x66\x28\x61\x3d\x3d\x3d\x21\ +\x30\x26\x26\x21\x2d\x2d\x65\x2e\x72\x65\x61\x64\x79\x57\x61\x69\ +\x74\x7c\x7c\x61\x21\x3d\x3d\x21\x30\x26\x26\x21\x65\x2e\x69\x73\ +\x52\x65\x61\x64\x79\x29\x7b\x69\x66\x28\x21\x63\x2e\x62\x6f\x64\ +\x79\x29\x72\x65\x74\x75\x72\x6e\x20\x73\x65\x74\x54\x69\x6d\x65\ +\x6f\x75\x74\x28\x65\x2e\x72\x65\x61\x64\x79\x2c\x31\x29\x3b\x65\ +\x2e\x69\x73\x52\x65\x61\x64\x79\x3d\x21\x30\x3b\x69\x66\x28\x61\ +\x21\x3d\x3d\x21\x30\x26\x26\x2d\x2d\x65\x2e\x72\x65\x61\x64\x79\ +\x57\x61\x69\x74\x3e\x30\x29\x72\x65\x74\x75\x72\x6e\x3b\x41\x2e\ +\x66\x69\x72\x65\x57\x69\x74\x68\x28\x63\x2c\x5b\x65\x5d\x29\x2c\ +\x65\x2e\x66\x6e\x2e\x74\x72\x69\x67\x67\x65\x72\x26\x26\x65\x28\ +\x63\x29\x2e\x74\x72\x69\x67\x67\x65\x72\x28\x22\x72\x65\x61\x64\ +\x79\x22\x29\x2e\x6f\x66\x66\x28\x22\x72\x65\x61\x64\x79\x22\x29\ +\x7d\x7d\x2c\x62\x69\x6e\x64\x52\x65\x61\x64\x79\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x29\x7b\x69\x66\x28\x21\x41\x29\x7b\x41\ +\x3d\x65\x2e\x43\x61\x6c\x6c\x62\x61\x63\x6b\x73\x28\x22\x6f\x6e\ +\x63\x65\x20\x6d\x65\x6d\x6f\x72\x79\x22\x29\x3b\x69\x66\x28\x63\ +\x2e\x72\x65\x61\x64\x79\x53\x74\x61\x74\x65\x3d\x3d\x3d\x22\x63\ +\x6f\x6d\x70\x6c\x65\x74\x65\x22\x29\x72\x65\x74\x75\x72\x6e\x20\ +\x73\x65\x74\x54\x69\x6d\x65\x6f\x75\x74\x28\x65\x2e\x72\x65\x61\ +\x64\x79\x2c\x31\x29\x3b\x69\x66\x28\x63\x2e\x61\x64\x64\x45\x76\ +\x65\x6e\x74\x4c\x69\x73\x74\x65\x6e\x65\x72\x29\x63\x2e\x61\x64\ +\x64\x45\x76\x65\x6e\x74\x4c\x69\x73\x74\x65\x6e\x65\x72\x28\x22\ +\x44\x4f\x4d\x43\x6f\x6e\x74\x65\x6e\x74\x4c\x6f\x61\x64\x65\x64\ +\x22\x2c\x42\x2c\x21\x31\x29\x2c\x61\x2e\x61\x64\x64\x45\x76\x65\ +\x6e\x74\x4c\x69\x73\x74\x65\x6e\x65\x72\x28\x22\x6c\x6f\x61\x64\ +\x22\x2c\x65\x2e\x72\x65\x61\x64\x79\x2c\x21\x31\x29\x3b\x65\x6c\ +\x73\x65\x20\x69\x66\x28\x63\x2e\x61\x74\x74\x61\x63\x68\x45\x76\ +\x65\x6e\x74\x29\x7b\x63\x2e\x61\x74\x74\x61\x63\x68\x45\x76\x65\ +\x6e\x74\x28\x22\x6f\x6e\x72\x65\x61\x64\x79\x73\x74\x61\x74\x65\ +\x63\x68\x61\x6e\x67\x65\x22\x2c\x42\x29\x2c\x61\x2e\x61\x74\x74\ +\x61\x63\x68\x45\x76\x65\x6e\x74\x28\x22\x6f\x6e\x6c\x6f\x61\x64\ +\x22\x2c\x65\x2e\x72\x65\x61\x64\x79\x29\x3b\x76\x61\x72\x20\x62\ +\x3d\x21\x31\x3b\x74\x72\x79\x7b\x62\x3d\x61\x2e\x66\x72\x61\x6d\ +\x65\x45\x6c\x65\x6d\x65\x6e\x74\x3d\x3d\x6e\x75\x6c\x6c\x7d\x63\ +\x61\x74\x63\x68\x28\x64\x29\x7b\x7d\x63\x2e\x64\x6f\x63\x75\x6d\ +\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x2e\x64\x6f\x53\x63\x72\ +\x6f\x6c\x6c\x26\x26\x62\x26\x26\x4a\x28\x29\x7d\x7d\x7d\x2c\x69\ +\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x65\x2e\x74\ +\x79\x70\x65\x28\x61\x29\x3d\x3d\x3d\x22\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x22\x7d\x2c\x69\x73\x41\x72\x72\x61\x79\x3a\x41\x72\x72\ +\x61\x79\x2e\x69\x73\x41\x72\x72\x61\x79\x7c\x7c\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x65\ +\x2e\x74\x79\x70\x65\x28\x61\x29\x3d\x3d\x3d\x22\x61\x72\x72\x61\ +\x79\x22\x7d\x2c\x69\x73\x57\x69\x6e\x64\x6f\x77\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\ +\x61\x26\x26\x74\x79\x70\x65\x6f\x66\x20\x61\x3d\x3d\x22\x6f\x62\ +\x6a\x65\x63\x74\x22\x26\x26\x22\x73\x65\x74\x49\x6e\x74\x65\x72\ +\x76\x61\x6c\x22\x69\x6e\x20\x61\x7d\x2c\x69\x73\x4e\x75\x6d\x65\ +\x72\x69\x63\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\ +\x72\x65\x74\x75\x72\x6e\x21\x69\x73\x4e\x61\x4e\x28\x70\x61\x72\ +\x73\x65\x46\x6c\x6f\x61\x74\x28\x61\x29\x29\x26\x26\x69\x73\x46\ +\x69\x6e\x69\x74\x65\x28\x61\x29\x7d\x2c\x74\x79\x70\x65\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\ +\x6e\x20\x61\x3d\x3d\x6e\x75\x6c\x6c\x3f\x53\x74\x72\x69\x6e\x67\ +\x28\x61\x29\x3a\x49\x5b\x43\x2e\x63\x61\x6c\x6c\x28\x61\x29\x5d\ +\x7c\x7c\x22\x6f\x62\x6a\x65\x63\x74\x22\x7d\x2c\x69\x73\x50\x6c\ +\x61\x69\x6e\x4f\x62\x6a\x65\x63\x74\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x29\x7b\x69\x66\x28\x21\x61\x7c\x7c\x65\x2e\x74\ +\x79\x70\x65\x28\x61\x29\x21\x3d\x3d\x22\x6f\x62\x6a\x65\x63\x74\ +\x22\x7c\x7c\x61\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x7c\x7c\x65\ +\x2e\x69\x73\x57\x69\x6e\x64\x6f\x77\x28\x61\x29\x29\x72\x65\x74\ +\x75\x72\x6e\x21\x31\x3b\x74\x72\x79\x7b\x69\x66\x28\x61\x2e\x63\ +\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72\x26\x26\x21\x44\x2e\x63\ +\x61\x6c\x6c\x28\x61\x2c\x22\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\ +\x6f\x72\x22\x29\x26\x26\x21\x44\x2e\x63\x61\x6c\x6c\x28\x61\x2e\ +\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72\x2e\x70\x72\x6f\x74\ +\x6f\x74\x79\x70\x65\x2c\x22\x69\x73\x50\x72\x6f\x74\x6f\x74\x79\ +\x70\x65\x4f\x66\x22\x29\x29\x72\x65\x74\x75\x72\x6e\x21\x31\x7d\ +\x63\x61\x74\x63\x68\x28\x63\x29\x7b\x72\x65\x74\x75\x72\x6e\x21\ +\x31\x7d\x76\x61\x72\x20\x64\x3b\x66\x6f\x72\x28\x64\x20\x69\x6e\ +\x20\x61\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x64\x3d\x3d\x3d\x62\ +\x7c\x7c\x44\x2e\x63\x61\x6c\x6c\x28\x61\x2c\x64\x29\x7d\x2c\x69\ +\x73\x45\x6d\x70\x74\x79\x4f\x62\x6a\x65\x63\x74\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x66\x6f\x72\x28\x76\x61\x72\ +\x20\x62\x20\x69\x6e\x20\x61\x29\x72\x65\x74\x75\x72\x6e\x21\x31\ +\x3b\x72\x65\x74\x75\x72\x6e\x21\x30\x7d\x2c\x65\x72\x72\x6f\x72\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x74\x68\x72\ +\x6f\x77\x20\x6e\x65\x77\x20\x45\x72\x72\x6f\x72\x28\x61\x29\x7d\ +\x2c\x70\x61\x72\x73\x65\x4a\x53\x4f\x4e\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x62\x29\x7b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\ +\x20\x62\x21\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x7c\x7c\x21\x62\ +\x29\x72\x65\x74\x75\x72\x6e\x20\x6e\x75\x6c\x6c\x3b\x62\x3d\x65\ +\x2e\x74\x72\x69\x6d\x28\x62\x29\x3b\x69\x66\x28\x61\x2e\x4a\x53\ +\x4f\x4e\x26\x26\x61\x2e\x4a\x53\x4f\x4e\x2e\x70\x61\x72\x73\x65\ +\x29\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x4a\x53\x4f\x4e\x2e\x70\ +\x61\x72\x73\x65\x28\x62\x29\x3b\x69\x66\x28\x6e\x2e\x74\x65\x73\ +\x74\x28\x62\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x6f\x2c\x22\x40\ +\x22\x29\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x70\x2c\x22\x5d\x22\ +\x29\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x71\x2c\x22\x22\x29\x29\ +\x29\x72\x65\x74\x75\x72\x6e\x28\x6e\x65\x77\x20\x46\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x22\x72\x65\x74\x75\x72\x6e\x20\x22\x2b\x62\ +\x29\x29\x28\x29\x3b\x65\x2e\x65\x72\x72\x6f\x72\x28\x22\x49\x6e\ +\x76\x61\x6c\x69\x64\x20\x4a\x53\x4f\x4e\x3a\x20\x22\x2b\x62\x29\ +\x7d\x2c\x70\x61\x72\x73\x65\x58\x4d\x4c\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x63\x29\x7b\x76\x61\x72\x20\x64\x2c\x66\x3b\x74\ +\x72\x79\x7b\x61\x2e\x44\x4f\x4d\x50\x61\x72\x73\x65\x72\x3f\x28\ +\x66\x3d\x6e\x65\x77\x20\x44\x4f\x4d\x50\x61\x72\x73\x65\x72\x2c\ +\x64\x3d\x66\x2e\x70\x61\x72\x73\x65\x46\x72\x6f\x6d\x53\x74\x72\ +\x69\x6e\x67\x28\x63\x2c\x22\x74\x65\x78\x74\x2f\x78\x6d\x6c\x22\ +\x29\x29\x3a\x28\x64\x3d\x6e\x65\x77\x20\x41\x63\x74\x69\x76\x65\ +\x58\x4f\x62\x6a\x65\x63\x74\x28\x22\x4d\x69\x63\x72\x6f\x73\x6f\ +\x66\x74\x2e\x58\x4d\x4c\x44\x4f\x4d\x22\x29\x2c\x64\x2e\x61\x73\ +\x79\x6e\x63\x3d\x22\x66\x61\x6c\x73\x65\x22\x2c\x64\x2e\x6c\x6f\ +\x61\x64\x58\x4d\x4c\x28\x63\x29\x29\x7d\x63\x61\x74\x63\x68\x28\ +\x67\x29\x7b\x64\x3d\x62\x7d\x28\x21\x64\x7c\x7c\x21\x64\x2e\x64\ +\x6f\x63\x75\x6d\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x7c\x7c\ +\x64\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\ +\x61\x67\x4e\x61\x6d\x65\x28\x22\x70\x61\x72\x73\x65\x72\x65\x72\ +\x72\x6f\x72\x22\x29\x2e\x6c\x65\x6e\x67\x74\x68\x29\x26\x26\x65\ +\x2e\x65\x72\x72\x6f\x72\x28\x22\x49\x6e\x76\x61\x6c\x69\x64\x20\ +\x58\x4d\x4c\x3a\x20\x22\x2b\x63\x29\x3b\x72\x65\x74\x75\x72\x6e\ +\x20\x64\x7d\x2c\x6e\x6f\x6f\x70\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x29\x7b\x7d\x2c\x67\x6c\x6f\x62\x61\x6c\x45\x76\x61\x6c\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\x29\x7b\x62\x26\x26\ +\x6a\x2e\x74\x65\x73\x74\x28\x62\x29\x26\x26\x28\x61\x2e\x65\x78\ +\x65\x63\x53\x63\x72\x69\x70\x74\x7c\x7c\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x62\x29\x7b\x61\x2e\x65\x76\x61\x6c\x2e\x63\x61\x6c\ +\x6c\x28\x61\x2c\x62\x29\x7d\x29\x28\x62\x29\x7d\x2c\x63\x61\x6d\ +\x65\x6c\x43\x61\x73\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x72\x65\x70\x6c\ +\x61\x63\x65\x28\x77\x2c\x22\x6d\x73\x2d\x22\x29\x2e\x72\x65\x70\ +\x6c\x61\x63\x65\x28\x76\x2c\x78\x29\x7d\x2c\x6e\x6f\x64\x65\x4e\ +\x61\x6d\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\ +\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x6e\x6f\x64\x65\x4e\ +\x61\x6d\x65\x26\x26\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\ +\x74\x6f\x55\x70\x70\x65\x72\x43\x61\x73\x65\x28\x29\x3d\x3d\x3d\ +\x62\x2e\x74\x6f\x55\x70\x70\x65\x72\x43\x61\x73\x65\x28\x29\x7d\ +\x2c\x65\x61\x63\x68\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x63\x2c\x64\x29\x7b\x76\x61\x72\x20\x66\x2c\x67\x3d\x30\x2c\ +\x68\x3d\x61\x2e\x6c\x65\x6e\x67\x74\x68\x2c\x69\x3d\x68\x3d\x3d\ +\x3d\x62\x7c\x7c\x65\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x3b\x69\x66\x28\x64\x29\x7b\x69\x66\x28\x69\x29\x7b\ +\x66\x6f\x72\x28\x66\x20\x69\x6e\x20\x61\x29\x69\x66\x28\x63\x2e\ +\x61\x70\x70\x6c\x79\x28\x61\x5b\x66\x5d\x2c\x64\x29\x3d\x3d\x3d\ +\x21\x31\x29\x62\x72\x65\x61\x6b\x7d\x65\x6c\x73\x65\x20\x66\x6f\ +\x72\x28\x3b\x67\x3c\x68\x3b\x29\x69\x66\x28\x63\x2e\x61\x70\x70\ +\x6c\x79\x28\x61\x5b\x67\x2b\x2b\x5d\x2c\x64\x29\x3d\x3d\x3d\x21\ +\x31\x29\x62\x72\x65\x61\x6b\x7d\x65\x6c\x73\x65\x20\x69\x66\x28\ +\x69\x29\x7b\x66\x6f\x72\x28\x66\x20\x69\x6e\x20\x61\x29\x69\x66\ +\x28\x63\x2e\x63\x61\x6c\x6c\x28\x61\x5b\x66\x5d\x2c\x66\x2c\x61\ +\x5b\x66\x5d\x29\x3d\x3d\x3d\x21\x31\x29\x62\x72\x65\x61\x6b\x7d\ +\x65\x6c\x73\x65\x20\x66\x6f\x72\x28\x3b\x67\x3c\x68\x3b\x29\x69\ +\x66\x28\x63\x2e\x63\x61\x6c\x6c\x28\x61\x5b\x67\x5d\x2c\x67\x2c\ +\x61\x5b\x67\x2b\x2b\x5d\x29\x3d\x3d\x3d\x21\x31\x29\x62\x72\x65\ +\x61\x6b\x3b\x72\x65\x74\x75\x72\x6e\x20\x61\x7d\x2c\x74\x72\x69\ +\x6d\x3a\x47\x3f\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\ +\x72\x65\x74\x75\x72\x6e\x20\x61\x3d\x3d\x6e\x75\x6c\x6c\x3f\x22\ +\x22\x3a\x47\x2e\x63\x61\x6c\x6c\x28\x61\x29\x7d\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\ +\x61\x3d\x3d\x6e\x75\x6c\x6c\x3f\x22\x22\x3a\x28\x61\x2b\x22\x22\ +\x29\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x6b\x2c\x22\x22\x29\x2e\ +\x72\x65\x70\x6c\x61\x63\x65\x28\x6c\x2c\x22\x22\x29\x7d\x2c\x6d\ +\x61\x6b\x65\x41\x72\x72\x61\x79\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\x62\x7c\x7c\ +\x5b\x5d\x3b\x69\x66\x28\x61\x21\x3d\x6e\x75\x6c\x6c\x29\x7b\x76\ +\x61\x72\x20\x64\x3d\x65\x2e\x74\x79\x70\x65\x28\x61\x29\x3b\x61\ +\x2e\x6c\x65\x6e\x67\x74\x68\x3d\x3d\x6e\x75\x6c\x6c\x7c\x7c\x64\ +\x3d\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x7c\x7c\x64\x3d\x3d\ +\x3d\x22\x66\x75\x6e\x63\x74\x69\x6f\x6e\x22\x7c\x7c\x64\x3d\x3d\ +\x3d\x22\x72\x65\x67\x65\x78\x70\x22\x7c\x7c\x65\x2e\x69\x73\x57\ +\x69\x6e\x64\x6f\x77\x28\x61\x29\x3f\x45\x2e\x63\x61\x6c\x6c\x28\ +\x63\x2c\x61\x29\x3a\x65\x2e\x6d\x65\x72\x67\x65\x28\x63\x2c\x61\ +\x29\x7d\x72\x65\x74\x75\x72\x6e\x20\x63\x7d\x2c\x69\x6e\x41\x72\ +\x72\x61\x79\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\ +\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x3b\x69\x66\x28\x62\x29\x7b\ +\x69\x66\x28\x48\x29\x72\x65\x74\x75\x72\x6e\x20\x48\x2e\x63\x61\ +\x6c\x6c\x28\x62\x2c\x61\x2c\x63\x29\x3b\x64\x3d\x62\x2e\x6c\x65\ +\x6e\x67\x74\x68\x2c\x63\x3d\x63\x3f\x63\x3c\x30\x3f\x4d\x61\x74\ +\x68\x2e\x6d\x61\x78\x28\x30\x2c\x64\x2b\x63\x29\x3a\x63\x3a\x30\ +\x3b\x66\x6f\x72\x28\x3b\x63\x3c\x64\x3b\x63\x2b\x2b\x29\x69\x66\ +\x28\x63\x20\x69\x6e\x20\x62\x26\x26\x62\x5b\x63\x5d\x3d\x3d\x3d\ +\x61\x29\x72\x65\x74\x75\x72\x6e\x20\x63\x7d\x72\x65\x74\x75\x72\ +\x6e\x2d\x31\x7d\x2c\x6d\x65\x72\x67\x65\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x3d\x61\ +\x2e\x6c\x65\x6e\x67\x74\x68\x2c\x65\x3d\x30\x3b\x69\x66\x28\x74\ +\x79\x70\x65\x6f\x66\x20\x63\x2e\x6c\x65\x6e\x67\x74\x68\x3d\x3d\ +\x22\x6e\x75\x6d\x62\x65\x72\x22\x29\x66\x6f\x72\x28\x76\x61\x72\ +\x20\x66\x3d\x63\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x65\x3c\x66\x3b\ +\x65\x2b\x2b\x29\x61\x5b\x64\x2b\x2b\x5d\x3d\x63\x5b\x65\x5d\x3b\ +\x65\x6c\x73\x65\x20\x77\x68\x69\x6c\x65\x28\x63\x5b\x65\x5d\x21\ +\x3d\x3d\x62\x29\x61\x5b\x64\x2b\x2b\x5d\x3d\x63\x5b\x65\x2b\x2b\ +\x5d\x3b\x61\x2e\x6c\x65\x6e\x67\x74\x68\x3d\x64\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x61\x7d\x2c\x67\x72\x65\x70\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\ +\x64\x3d\x5b\x5d\x2c\x65\x3b\x63\x3d\x21\x21\x63\x3b\x66\x6f\x72\ +\x28\x76\x61\x72\x20\x66\x3d\x30\x2c\x67\x3d\x61\x2e\x6c\x65\x6e\ +\x67\x74\x68\x3b\x66\x3c\x67\x3b\x66\x2b\x2b\x29\x65\x3d\x21\x21\ +\x62\x28\x61\x5b\x66\x5d\x2c\x66\x29\x2c\x63\x21\x3d\x3d\x65\x26\ +\x26\x64\x2e\x70\x75\x73\x68\x28\x61\x5b\x66\x5d\x29\x3b\x72\x65\ +\x74\x75\x72\x6e\x20\x64\x7d\x2c\x6d\x61\x70\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x63\x2c\x64\x29\x7b\x76\x61\x72\x20\ +\x66\x2c\x67\x2c\x68\x3d\x5b\x5d\x2c\x69\x3d\x30\x2c\x6a\x3d\x61\ +\x2e\x6c\x65\x6e\x67\x74\x68\x2c\x6b\x3d\x61\x20\x69\x6e\x73\x74\ +\x61\x6e\x63\x65\x6f\x66\x20\x65\x7c\x7c\x6a\x21\x3d\x3d\x62\x26\ +\x26\x74\x79\x70\x65\x6f\x66\x20\x6a\x3d\x3d\x22\x6e\x75\x6d\x62\ +\x65\x72\x22\x26\x26\x28\x6a\x3e\x30\x26\x26\x61\x5b\x30\x5d\x26\ +\x26\x61\x5b\x6a\x2d\x31\x5d\x7c\x7c\x6a\x3d\x3d\x3d\x30\x7c\x7c\ +\x65\x2e\x69\x73\x41\x72\x72\x61\x79\x28\x61\x29\x29\x3b\x69\x66\ +\x28\x6b\x29\x66\x6f\x72\x28\x3b\x69\x3c\x6a\x3b\x69\x2b\x2b\x29\ +\x66\x3d\x63\x28\x61\x5b\x69\x5d\x2c\x69\x2c\x64\x29\x2c\x66\x21\ +\x3d\x6e\x75\x6c\x6c\x26\x26\x28\x68\x5b\x68\x2e\x6c\x65\x6e\x67\ +\x74\x68\x5d\x3d\x66\x29\x3b\x65\x6c\x73\x65\x20\x66\x6f\x72\x28\ +\x67\x20\x69\x6e\x20\x61\x29\x66\x3d\x63\x28\x61\x5b\x67\x5d\x2c\ +\x67\x2c\x64\x29\x2c\x66\x21\x3d\x6e\x75\x6c\x6c\x26\x26\x28\x68\ +\x5b\x68\x2e\x6c\x65\x6e\x67\x74\x68\x5d\x3d\x66\x29\x3b\x72\x65\ +\x74\x75\x72\x6e\x20\x68\x2e\x63\x6f\x6e\x63\x61\x74\x2e\x61\x70\ +\x70\x6c\x79\x28\x5b\x5d\x2c\x68\x29\x7d\x2c\x67\x75\x69\x64\x3a\ +\x31\x2c\x70\x72\x6f\x78\x79\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x2c\x63\x29\x7b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\ +\x63\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x29\x7b\x76\x61\x72\ +\x20\x64\x3d\x61\x5b\x63\x5d\x3b\x63\x3d\x61\x2c\x61\x3d\x64\x7d\ +\x69\x66\x28\x21\x65\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x29\x72\x65\x74\x75\x72\x6e\x20\x62\x3b\x76\x61\x72\ +\x20\x66\x3d\x46\x2e\x63\x61\x6c\x6c\x28\x61\x72\x67\x75\x6d\x65\ +\x6e\x74\x73\x2c\x32\x29\x2c\x67\x3d\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x61\x70\x70\ +\x6c\x79\x28\x63\x2c\x66\x2e\x63\x6f\x6e\x63\x61\x74\x28\x46\x2e\ +\x63\x61\x6c\x6c\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\x29\ +\x29\x7d\x3b\x67\x2e\x67\x75\x69\x64\x3d\x61\x2e\x67\x75\x69\x64\ +\x3d\x61\x2e\x67\x75\x69\x64\x7c\x7c\x67\x2e\x67\x75\x69\x64\x7c\ +\x7c\x65\x2e\x67\x75\x69\x64\x2b\x2b\x3b\x72\x65\x74\x75\x72\x6e\ +\x20\x67\x7d\x2c\x61\x63\x63\x65\x73\x73\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x63\x2c\x64\x2c\x66\x2c\x67\x2c\x68\x29\ +\x7b\x76\x61\x72\x20\x69\x3d\x61\x2e\x6c\x65\x6e\x67\x74\x68\x3b\ +\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x63\x3d\x3d\x22\x6f\x62\ +\x6a\x65\x63\x74\x22\x29\x7b\x66\x6f\x72\x28\x76\x61\x72\x20\x6a\ +\x20\x69\x6e\x20\x63\x29\x65\x2e\x61\x63\x63\x65\x73\x73\x28\x61\ +\x2c\x6a\x2c\x63\x5b\x6a\x5d\x2c\x66\x2c\x67\x2c\x64\x29\x3b\x72\ +\x65\x74\x75\x72\x6e\x20\x61\x7d\x69\x66\x28\x64\x21\x3d\x3d\x62\ +\x29\x7b\x66\x3d\x21\x68\x26\x26\x66\x26\x26\x65\x2e\x69\x73\x46\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x64\x29\x3b\x66\x6f\x72\x28\x76\ +\x61\x72\x20\x6b\x3d\x30\x3b\x6b\x3c\x69\x3b\x6b\x2b\x2b\x29\x67\ +\x28\x61\x5b\x6b\x5d\x2c\x63\x2c\x66\x3f\x64\x2e\x63\x61\x6c\x6c\ +\x28\x61\x5b\x6b\x5d\x2c\x6b\x2c\x67\x28\x61\x5b\x6b\x5d\x2c\x63\ +\x29\x29\x3a\x64\x2c\x68\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x61\ +\x7d\x72\x65\x74\x75\x72\x6e\x20\x69\x3f\x67\x28\x61\x5b\x30\x5d\ +\x2c\x63\x29\x3a\x62\x7d\x2c\x6e\x6f\x77\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x28\x6e\x65\x77\ +\x20\x44\x61\x74\x65\x29\x2e\x67\x65\x74\x54\x69\x6d\x65\x28\x29\ +\x7d\x2c\x75\x61\x4d\x61\x74\x63\x68\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x29\x7b\x61\x3d\x61\x2e\x74\x6f\x4c\x6f\x77\x65\ +\x72\x43\x61\x73\x65\x28\x29\x3b\x76\x61\x72\x20\x62\x3d\x72\x2e\ +\x65\x78\x65\x63\x28\x61\x29\x7c\x7c\x73\x2e\x65\x78\x65\x63\x28\ +\x61\x29\x7c\x7c\x74\x2e\x65\x78\x65\x63\x28\x61\x29\x7c\x7c\x61\ +\x2e\x69\x6e\x64\x65\x78\x4f\x66\x28\x22\x63\x6f\x6d\x70\x61\x74\ +\x69\x62\x6c\x65\x22\x29\x3c\x30\x26\x26\x75\x2e\x65\x78\x65\x63\ +\x28\x61\x29\x7c\x7c\x5b\x5d\x3b\x72\x65\x74\x75\x72\x6e\x7b\x62\ +\x72\x6f\x77\x73\x65\x72\x3a\x62\x5b\x31\x5d\x7c\x7c\x22\x22\x2c\ +\x76\x65\x72\x73\x69\x6f\x6e\x3a\x62\x5b\x32\x5d\x7c\x7c\x22\x30\ +\x22\x7d\x7d\x2c\x73\x75\x62\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x29\x7b\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x61\x28\x62\x2c\ +\x63\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x6e\x65\x77\x20\x61\x2e\ +\x66\x6e\x2e\x69\x6e\x69\x74\x28\x62\x2c\x63\x29\x7d\x65\x2e\x65\ +\x78\x74\x65\x6e\x64\x28\x21\x30\x2c\x61\x2c\x74\x68\x69\x73\x29\ +\x2c\x61\x2e\x73\x75\x70\x65\x72\x63\x6c\x61\x73\x73\x3d\x74\x68\ +\x69\x73\x2c\x61\x2e\x66\x6e\x3d\x61\x2e\x70\x72\x6f\x74\x6f\x74\ +\x79\x70\x65\x3d\x74\x68\x69\x73\x28\x29\x2c\x61\x2e\x66\x6e\x2e\ +\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72\x3d\x61\x2c\x61\x2e\ +\x73\x75\x62\x3d\x74\x68\x69\x73\x2e\x73\x75\x62\x2c\x61\x2e\x66\ +\x6e\x2e\x69\x6e\x69\x74\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x64\x2c\x66\x29\x7b\x66\x26\x26\x66\x20\x69\x6e\x73\x74\x61\x6e\ +\x63\x65\x6f\x66\x20\x65\x26\x26\x21\x28\x66\x20\x69\x6e\x73\x74\ +\x61\x6e\x63\x65\x6f\x66\x20\x61\x29\x26\x26\x28\x66\x3d\x61\x28\ +\x66\x29\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x65\x2e\x66\x6e\x2e\ +\x69\x6e\x69\x74\x2e\x63\x61\x6c\x6c\x28\x74\x68\x69\x73\x2c\x64\ +\x2c\x66\x2c\x62\x29\x7d\x2c\x61\x2e\x66\x6e\x2e\x69\x6e\x69\x74\ +\x2e\x70\x72\x6f\x74\x6f\x74\x79\x70\x65\x3d\x61\x2e\x66\x6e\x3b\ +\x76\x61\x72\x20\x62\x3d\x61\x28\x63\x29\x3b\x72\x65\x74\x75\x72\ +\x6e\x20\x61\x7d\x2c\x62\x72\x6f\x77\x73\x65\x72\x3a\x7b\x7d\x7d\ +\x29\x2c\x65\x2e\x65\x61\x63\x68\x28\x22\x42\x6f\x6f\x6c\x65\x61\ +\x6e\x20\x4e\x75\x6d\x62\x65\x72\x20\x53\x74\x72\x69\x6e\x67\x20\ +\x46\x75\x6e\x63\x74\x69\x6f\x6e\x20\x41\x72\x72\x61\x79\x20\x44\ +\x61\x74\x65\x20\x52\x65\x67\x45\x78\x70\x20\x4f\x62\x6a\x65\x63\ +\x74\x22\x2e\x73\x70\x6c\x69\x74\x28\x22\x20\x22\x29\x2c\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x49\x5b\x22\x5b\ +\x6f\x62\x6a\x65\x63\x74\x20\x22\x2b\x62\x2b\x22\x5d\x22\x5d\x3d\ +\x62\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x7d\ +\x29\x2c\x7a\x3d\x65\x2e\x75\x61\x4d\x61\x74\x63\x68\x28\x79\x29\ +\x2c\x7a\x2e\x62\x72\x6f\x77\x73\x65\x72\x26\x26\x28\x65\x2e\x62\ +\x72\x6f\x77\x73\x65\x72\x5b\x7a\x2e\x62\x72\x6f\x77\x73\x65\x72\ +\x5d\x3d\x21\x30\x2c\x65\x2e\x62\x72\x6f\x77\x73\x65\x72\x2e\x76\ +\x65\x72\x73\x69\x6f\x6e\x3d\x7a\x2e\x76\x65\x72\x73\x69\x6f\x6e\ +\x29\x2c\x65\x2e\x62\x72\x6f\x77\x73\x65\x72\x2e\x77\x65\x62\x6b\ +\x69\x74\x26\x26\x28\x65\x2e\x62\x72\x6f\x77\x73\x65\x72\x2e\x73\ +\x61\x66\x61\x72\x69\x3d\x21\x30\x29\x2c\x6a\x2e\x74\x65\x73\x74\ +\x28\x22\xc2\xa0\x22\x29\x26\x26\x28\x6b\x3d\x2f\x5e\x5b\x5c\x73\ +\x5c\x78\x41\x30\x5d\x2b\x2f\x2c\x6c\x3d\x2f\x5b\x5c\x73\x5c\x78\ +\x41\x30\x5d\x2b\x24\x2f\x29\x2c\x68\x3d\x65\x28\x63\x29\x2c\x63\ +\x2e\x61\x64\x64\x45\x76\x65\x6e\x74\x4c\x69\x73\x74\x65\x6e\x65\ +\x72\x3f\x42\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x63\ +\x2e\x72\x65\x6d\x6f\x76\x65\x45\x76\x65\x6e\x74\x4c\x69\x73\x74\ +\x65\x6e\x65\x72\x28\x22\x44\x4f\x4d\x43\x6f\x6e\x74\x65\x6e\x74\ +\x4c\x6f\x61\x64\x65\x64\x22\x2c\x42\x2c\x21\x31\x29\x2c\x65\x2e\ +\x72\x65\x61\x64\x79\x28\x29\x7d\x3a\x63\x2e\x61\x74\x74\x61\x63\ +\x68\x45\x76\x65\x6e\x74\x26\x26\x28\x42\x3d\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x29\x7b\x63\x2e\x72\x65\x61\x64\x79\x53\x74\x61\ +\x74\x65\x3d\x3d\x3d\x22\x63\x6f\x6d\x70\x6c\x65\x74\x65\x22\x26\ +\x26\x28\x63\x2e\x64\x65\x74\x61\x63\x68\x45\x76\x65\x6e\x74\x28\ +\x22\x6f\x6e\x72\x65\x61\x64\x79\x73\x74\x61\x74\x65\x63\x68\x61\ +\x6e\x67\x65\x22\x2c\x42\x29\x2c\x65\x2e\x72\x65\x61\x64\x79\x28\ +\x29\x29\x7d\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x65\x7d\x28\x29\ +\x2c\x67\x3d\x7b\x7d\x3b\x66\x2e\x43\x61\x6c\x6c\x62\x61\x63\x6b\ +\x73\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x61\x3d\ +\x61\x3f\x67\x5b\x61\x5d\x7c\x7c\x68\x28\x61\x29\x3a\x7b\x7d\x3b\ +\x76\x61\x72\x20\x63\x3d\x5b\x5d\x2c\x64\x3d\x5b\x5d\x2c\x65\x2c\ +\x69\x2c\x6a\x2c\x6b\x2c\x6c\x2c\x6d\x3d\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x62\x29\x7b\x76\x61\x72\x20\x64\x2c\x65\x2c\x67\x2c\ +\x68\x2c\x69\x3b\x66\x6f\x72\x28\x64\x3d\x30\x2c\x65\x3d\x62\x2e\ +\x6c\x65\x6e\x67\x74\x68\x3b\x64\x3c\x65\x3b\x64\x2b\x2b\x29\x67\ +\x3d\x62\x5b\x64\x5d\x2c\x68\x3d\x66\x2e\x74\x79\x70\x65\x28\x67\ +\x29\x2c\x68\x3d\x3d\x3d\x22\x61\x72\x72\x61\x79\x22\x3f\x6d\x28\ +\x67\x29\x3a\x68\x3d\x3d\x3d\x22\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x22\x26\x26\x28\x21\x61\x2e\x75\x6e\x69\x71\x75\x65\x7c\x7c\x21\ +\x6f\x2e\x68\x61\x73\x28\x67\x29\x29\x26\x26\x63\x2e\x70\x75\x73\ +\x68\x28\x67\x29\x7d\x2c\x6e\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x62\x2c\x66\x29\x7b\x66\x3d\x66\x7c\x7c\x5b\x5d\x2c\x65\x3d\ +\x21\x61\x2e\x6d\x65\x6d\x6f\x72\x79\x7c\x7c\x5b\x62\x2c\x66\x5d\ +\x2c\x69\x3d\x21\x30\x2c\x6c\x3d\x6a\x7c\x7c\x30\x2c\x6a\x3d\x30\ +\x2c\x6b\x3d\x63\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x66\x6f\x72\x28\ +\x3b\x63\x26\x26\x6c\x3c\x6b\x3b\x6c\x2b\x2b\x29\x69\x66\x28\x63\ +\x5b\x6c\x5d\x2e\x61\x70\x70\x6c\x79\x28\x62\x2c\x66\x29\x3d\x3d\ +\x3d\x21\x31\x26\x26\x61\x2e\x73\x74\x6f\x70\x4f\x6e\x46\x61\x6c\ +\x73\x65\x29\x7b\x65\x3d\x21\x30\x3b\x62\x72\x65\x61\x6b\x7d\x69\ +\x3d\x21\x31\x2c\x63\x26\x26\x28\x61\x2e\x6f\x6e\x63\x65\x3f\x65\ +\x3d\x3d\x3d\x21\x30\x3f\x6f\x2e\x64\x69\x73\x61\x62\x6c\x65\x28\ +\x29\x3a\x63\x3d\x5b\x5d\x3a\x64\x26\x26\x64\x2e\x6c\x65\x6e\x67\ +\x74\x68\x26\x26\x28\x65\x3d\x64\x2e\x73\x68\x69\x66\x74\x28\x29\ +\x2c\x6f\x2e\x66\x69\x72\x65\x57\x69\x74\x68\x28\x65\x5b\x30\x5d\ +\x2c\x65\x5b\x31\x5d\x29\x29\x29\x7d\x2c\x6f\x3d\x7b\x61\x64\x64\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x69\x66\x28\x63\ +\x29\x7b\x76\x61\x72\x20\x61\x3d\x63\x2e\x6c\x65\x6e\x67\x74\x68\ +\x3b\x6d\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\x2c\x69\x3f\ +\x6b\x3d\x63\x2e\x6c\x65\x6e\x67\x74\x68\x3a\x65\x26\x26\x65\x21\ +\x3d\x3d\x21\x30\x26\x26\x28\x6a\x3d\x61\x2c\x6e\x28\x65\x5b\x30\ +\x5d\x2c\x65\x5b\x31\x5d\x29\x29\x7d\x72\x65\x74\x75\x72\x6e\x20\ +\x74\x68\x69\x73\x7d\x2c\x72\x65\x6d\x6f\x76\x65\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x29\x7b\x69\x66\x28\x63\x29\x7b\x76\x61\ +\x72\x20\x62\x3d\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2c\x64\x3d\ +\x30\x2c\x65\x3d\x62\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x66\x6f\x72\ +\x28\x3b\x64\x3c\x65\x3b\x64\x2b\x2b\x29\x66\x6f\x72\x28\x76\x61\ +\x72\x20\x66\x3d\x30\x3b\x66\x3c\x63\x2e\x6c\x65\x6e\x67\x74\x68\ +\x3b\x66\x2b\x2b\x29\x69\x66\x28\x62\x5b\x64\x5d\x3d\x3d\x3d\x63\ +\x5b\x66\x5d\x29\x7b\x69\x26\x26\x66\x3c\x3d\x6b\x26\x26\x28\x6b\ +\x2d\x2d\x2c\x66\x3c\x3d\x6c\x26\x26\x6c\x2d\x2d\x29\x2c\x63\x2e\ +\x73\x70\x6c\x69\x63\x65\x28\x66\x2d\x2d\x2c\x31\x29\x3b\x69\x66\ +\x28\x61\x2e\x75\x6e\x69\x71\x75\x65\x29\x62\x72\x65\x61\x6b\x7d\ +\x7d\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x68\x61\ +\x73\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x69\x66\ +\x28\x63\x29\x7b\x76\x61\x72\x20\x62\x3d\x30\x2c\x64\x3d\x63\x2e\ +\x6c\x65\x6e\x67\x74\x68\x3b\x66\x6f\x72\x28\x3b\x62\x3c\x64\x3b\ +\x62\x2b\x2b\x29\x69\x66\x28\x61\x3d\x3d\x3d\x63\x5b\x62\x5d\x29\ +\x72\x65\x74\x75\x72\x6e\x21\x30\x7d\x72\x65\x74\x75\x72\x6e\x21\ +\x31\x7d\x2c\x65\x6d\x70\x74\x79\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x29\x7b\x63\x3d\x5b\x5d\x3b\x72\x65\x74\x75\x72\x6e\x20\ +\x74\x68\x69\x73\x7d\x2c\x64\x69\x73\x61\x62\x6c\x65\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x63\x3d\x64\x3d\x65\x3d\x62\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x64\x69\ +\x73\x61\x62\x6c\x65\x64\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x29\x7b\x72\x65\x74\x75\x72\x6e\x21\x63\x7d\x2c\x6c\x6f\x63\x6b\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x64\x3d\x62\x2c\ +\x28\x21\x65\x7c\x7c\x65\x3d\x3d\x3d\x21\x30\x29\x26\x26\x6f\x2e\ +\x64\x69\x73\x61\x62\x6c\x65\x28\x29\x3b\x72\x65\x74\x75\x72\x6e\ +\x20\x74\x68\x69\x73\x7d\x2c\x6c\x6f\x63\x6b\x65\x64\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x21\ +\x64\x7d\x2c\x66\x69\x72\x65\x57\x69\x74\x68\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x62\x2c\x63\x29\x7b\x64\x26\x26\x28\x69\x3f\ +\x61\x2e\x6f\x6e\x63\x65\x7c\x7c\x64\x2e\x70\x75\x73\x68\x28\x5b\ +\x62\x2c\x63\x5d\x29\x3a\x28\x21\x61\x2e\x6f\x6e\x63\x65\x7c\x7c\ +\x21\x65\x29\x26\x26\x6e\x28\x62\x2c\x63\x29\x29\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x66\x69\x72\x65\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x6f\x2e\x66\x69\x72\x65\ +\x57\x69\x74\x68\x28\x74\x68\x69\x73\x2c\x61\x72\x67\x75\x6d\x65\ +\x6e\x74\x73\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\ +\x7d\x2c\x66\x69\x72\x65\x64\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x21\x21\x65\x7d\x7d\x3b\x72\ +\x65\x74\x75\x72\x6e\x20\x6f\x7d\x3b\x76\x61\x72\x20\x69\x3d\x5b\ +\x5d\x2e\x73\x6c\x69\x63\x65\x3b\x66\x2e\x65\x78\x74\x65\x6e\x64\ +\x28\x7b\x44\x65\x66\x65\x72\x72\x65\x64\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\x3d\x66\x2e\x43\ +\x61\x6c\x6c\x62\x61\x63\x6b\x73\x28\x22\x6f\x6e\x63\x65\x20\x6d\ +\x65\x6d\x6f\x72\x79\x22\x29\x2c\x63\x3d\x66\x2e\x43\x61\x6c\x6c\ +\x62\x61\x63\x6b\x73\x28\x22\x6f\x6e\x63\x65\x20\x6d\x65\x6d\x6f\ +\x72\x79\x22\x29\x2c\x64\x3d\x66\x2e\x43\x61\x6c\x6c\x62\x61\x63\ +\x6b\x73\x28\x22\x6d\x65\x6d\x6f\x72\x79\x22\x29\x2c\x65\x3d\x22\ +\x70\x65\x6e\x64\x69\x6e\x67\x22\x2c\x67\x3d\x7b\x72\x65\x73\x6f\ +\x6c\x76\x65\x3a\x62\x2c\x72\x65\x6a\x65\x63\x74\x3a\x63\x2c\x6e\ +\x6f\x74\x69\x66\x79\x3a\x64\x7d\x2c\x68\x3d\x7b\x64\x6f\x6e\x65\ +\x3a\x62\x2e\x61\x64\x64\x2c\x66\x61\x69\x6c\x3a\x63\x2e\x61\x64\ +\x64\x2c\x70\x72\x6f\x67\x72\x65\x73\x73\x3a\x64\x2e\x61\x64\x64\ +\x2c\x73\x74\x61\x74\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x65\x7d\x2c\x69\x73\x52\x65\ +\x73\x6f\x6c\x76\x65\x64\x3a\x62\x2e\x66\x69\x72\x65\x64\x2c\x69\ +\x73\x52\x65\x6a\x65\x63\x74\x65\x64\x3a\x63\x2e\x66\x69\x72\x65\ +\x64\x2c\x74\x68\x65\x6e\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x2c\x62\x2c\x63\x29\x7b\x69\x2e\x64\x6f\x6e\x65\x28\x61\x29\ +\x2e\x66\x61\x69\x6c\x28\x62\x29\x2e\x70\x72\x6f\x67\x72\x65\x73\ +\x73\x28\x63\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\ +\x7d\x2c\x61\x6c\x77\x61\x79\x73\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x29\x7b\x69\x2e\x64\x6f\x6e\x65\x2e\x61\x70\x70\x6c\x79\ +\x28\x69\x2c\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\x2e\x66\x61\ +\x69\x6c\x2e\x61\x70\x70\x6c\x79\x28\x69\x2c\x61\x72\x67\x75\x6d\ +\x65\x6e\x74\x73\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\ +\x73\x7d\x2c\x70\x69\x70\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x2c\x62\x2c\x63\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\ +\x2e\x44\x65\x66\x65\x72\x72\x65\x64\x28\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x64\x29\x7b\x66\x2e\x65\x61\x63\x68\x28\x7b\x64\x6f\ +\x6e\x65\x3a\x5b\x61\x2c\x22\x72\x65\x73\x6f\x6c\x76\x65\x22\x5d\ +\x2c\x66\x61\x69\x6c\x3a\x5b\x62\x2c\x22\x72\x65\x6a\x65\x63\x74\ +\x22\x5d\x2c\x70\x72\x6f\x67\x72\x65\x73\x73\x3a\x5b\x63\x2c\x22\ +\x6e\x6f\x74\x69\x66\x79\x22\x5d\x7d\x2c\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\x62\x5b\ +\x30\x5d\x2c\x65\x3d\x62\x5b\x31\x5d\x2c\x67\x3b\x66\x2e\x69\x73\ +\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x63\x29\x3f\x69\x5b\x61\x5d\ +\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x67\x3d\x63\x2e\ +\x61\x70\x70\x6c\x79\x28\x74\x68\x69\x73\x2c\x61\x72\x67\x75\x6d\ +\x65\x6e\x74\x73\x29\x2c\x67\x26\x26\x66\x2e\x69\x73\x46\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x67\x2e\x70\x72\x6f\x6d\x69\x73\x65\x29\ +\x3f\x67\x2e\x70\x72\x6f\x6d\x69\x73\x65\x28\x29\x2e\x74\x68\x65\ +\x6e\x28\x64\x2e\x72\x65\x73\x6f\x6c\x76\x65\x2c\x64\x2e\x72\x65\ +\x6a\x65\x63\x74\x2c\x64\x2e\x6e\x6f\x74\x69\x66\x79\x29\x3a\x64\ +\x5b\x65\x2b\x22\x57\x69\x74\x68\x22\x5d\x28\x74\x68\x69\x73\x3d\ +\x3d\x3d\x69\x3f\x64\x3a\x74\x68\x69\x73\x2c\x5b\x67\x5d\x29\x7d\ +\x29\x3a\x69\x5b\x61\x5d\x28\x64\x5b\x65\x5d\x29\x7d\x29\x7d\x29\ +\x2e\x70\x72\x6f\x6d\x69\x73\x65\x28\x29\x7d\x2c\x70\x72\x6f\x6d\ +\x69\x73\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\ +\x69\x66\x28\x61\x3d\x3d\x6e\x75\x6c\x6c\x29\x61\x3d\x68\x3b\x65\ +\x6c\x73\x65\x20\x66\x6f\x72\x28\x76\x61\x72\x20\x62\x20\x69\x6e\ +\x20\x68\x29\x61\x5b\x62\x5d\x3d\x68\x5b\x62\x5d\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x61\x7d\x7d\x2c\x69\x3d\x68\x2e\x70\x72\x6f\x6d\ +\x69\x73\x65\x28\x7b\x7d\x29\x2c\x6a\x3b\x66\x6f\x72\x28\x6a\x20\ +\x69\x6e\x20\x67\x29\x69\x5b\x6a\x5d\x3d\x67\x5b\x6a\x5d\x2e\x66\ +\x69\x72\x65\x2c\x69\x5b\x6a\x2b\x22\x57\x69\x74\x68\x22\x5d\x3d\ +\x67\x5b\x6a\x5d\x2e\x66\x69\x72\x65\x57\x69\x74\x68\x3b\x69\x2e\ +\x64\x6f\x6e\x65\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\ +\x65\x3d\x22\x72\x65\x73\x6f\x6c\x76\x65\x64\x22\x7d\x2c\x63\x2e\ +\x64\x69\x73\x61\x62\x6c\x65\x2c\x64\x2e\x6c\x6f\x63\x6b\x29\x2e\ +\x66\x61\x69\x6c\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\ +\x65\x3d\x22\x72\x65\x6a\x65\x63\x74\x65\x64\x22\x7d\x2c\x62\x2e\ +\x64\x69\x73\x61\x62\x6c\x65\x2c\x64\x2e\x6c\x6f\x63\x6b\x29\x2c\ +\x61\x26\x26\x61\x2e\x63\x61\x6c\x6c\x28\x69\x2c\x69\x29\x3b\x72\ +\x65\x74\x75\x72\x6e\x20\x69\x7d\x2c\x77\x68\x65\x6e\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x20\x6d\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\x29\x7b\x65\x5b\x61\x5d\x3d\ +\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2e\x6c\x65\x6e\x67\x74\x68\ +\x3e\x31\x3f\x69\x2e\x63\x61\x6c\x6c\x28\x61\x72\x67\x75\x6d\x65\ +\x6e\x74\x73\x2c\x30\x29\x3a\x62\x2c\x6a\x2e\x6e\x6f\x74\x69\x66\ +\x79\x57\x69\x74\x68\x28\x6b\x2c\x65\x29\x7d\x7d\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x20\x6c\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\ +\x20\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x63\x29\x7b\x62\x5b\x61\ +\x5d\x3d\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2e\x6c\x65\x6e\x67\ +\x74\x68\x3e\x31\x3f\x69\x2e\x63\x61\x6c\x6c\x28\x61\x72\x67\x75\ +\x6d\x65\x6e\x74\x73\x2c\x30\x29\x3a\x63\x2c\x2d\x2d\x67\x7c\x7c\ +\x6a\x2e\x72\x65\x73\x6f\x6c\x76\x65\x57\x69\x74\x68\x28\x6a\x2c\ +\x62\x29\x7d\x7d\x76\x61\x72\x20\x62\x3d\x69\x2e\x63\x61\x6c\x6c\ +\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2c\x30\x29\x2c\x63\x3d\ +\x30\x2c\x64\x3d\x62\x2e\x6c\x65\x6e\x67\x74\x68\x2c\x65\x3d\x41\ +\x72\x72\x61\x79\x28\x64\x29\x2c\x67\x3d\x64\x2c\x68\x3d\x64\x2c\ +\x6a\x3d\x64\x3c\x3d\x31\x26\x26\x61\x26\x26\x66\x2e\x69\x73\x46\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2e\x70\x72\x6f\x6d\x69\x73\ +\x65\x29\x3f\x61\x3a\x66\x2e\x44\x65\x66\x65\x72\x72\x65\x64\x28\ +\x29\x2c\x6b\x3d\x6a\x2e\x70\x72\x6f\x6d\x69\x73\x65\x28\x29\x3b\ +\x69\x66\x28\x64\x3e\x31\x29\x7b\x66\x6f\x72\x28\x3b\x63\x3c\x64\ +\x3b\x63\x2b\x2b\x29\x62\x5b\x63\x5d\x26\x26\x62\x5b\x63\x5d\x2e\ +\x70\x72\x6f\x6d\x69\x73\x65\x26\x26\x66\x2e\x69\x73\x46\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x62\x5b\x63\x5d\x2e\x70\x72\x6f\x6d\x69\ +\x73\x65\x29\x3f\x62\x5b\x63\x5d\x2e\x70\x72\x6f\x6d\x69\x73\x65\ +\x28\x29\x2e\x74\x68\x65\x6e\x28\x6c\x28\x63\x29\x2c\x6a\x2e\x72\ +\x65\x6a\x65\x63\x74\x2c\x6d\x28\x63\x29\x29\x3a\x2d\x2d\x67\x3b\ +\x67\x7c\x7c\x6a\x2e\x72\x65\x73\x6f\x6c\x76\x65\x57\x69\x74\x68\ +\x28\x6a\x2c\x62\x29\x7d\x65\x6c\x73\x65\x20\x6a\x21\x3d\x3d\x61\ +\x26\x26\x6a\x2e\x72\x65\x73\x6f\x6c\x76\x65\x57\x69\x74\x68\x28\ +\x6a\x2c\x64\x3f\x5b\x61\x5d\x3a\x5b\x5d\x29\x3b\x72\x65\x74\x75\ +\x72\x6e\x20\x6b\x7d\x7d\x29\x2c\x66\x2e\x73\x75\x70\x70\x6f\x72\ +\x74\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x76\x61\x72\ +\x20\x62\x2c\x64\x2c\x65\x2c\x67\x2c\x68\x2c\x69\x2c\x6a\x2c\x6b\ +\x2c\x6c\x2c\x6d\x2c\x6e\x2c\x6f\x2c\x70\x2c\x71\x3d\x63\x2e\x63\ +\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x64\x69\ +\x76\x22\x29\x2c\x72\x3d\x63\x2e\x64\x6f\x63\x75\x6d\x65\x6e\x74\ +\x45\x6c\x65\x6d\x65\x6e\x74\x3b\x71\x2e\x73\x65\x74\x41\x74\x74\ +\x72\x69\x62\x75\x74\x65\x28\x22\x63\x6c\x61\x73\x73\x4e\x61\x6d\ +\x65\x22\x2c\x22\x74\x22\x29\x2c\x71\x2e\x69\x6e\x6e\x65\x72\x48\ +\x54\x4d\x4c\x3d\x22\x20\x20\x20\x3c\x6c\x69\x6e\x6b\x2f\x3e\x3c\ +\x74\x61\x62\x6c\x65\x3e\x3c\x2f\x74\x61\x62\x6c\x65\x3e\x3c\x61\ +\x20\x68\x72\x65\x66\x3d\x27\x2f\x61\x27\x20\x73\x74\x79\x6c\x65\ +\x3d\x27\x74\x6f\x70\x3a\x31\x70\x78\x3b\x66\x6c\x6f\x61\x74\x3a\ +\x6c\x65\x66\x74\x3b\x6f\x70\x61\x63\x69\x74\x79\x3a\x2e\x35\x35\ +\x3b\x27\x3e\x61\x3c\x2f\x61\x3e\x3c\x69\x6e\x70\x75\x74\x20\x74\ +\x79\x70\x65\x3d\x27\x63\x68\x65\x63\x6b\x62\x6f\x78\x27\x2f\x3e\ +\x22\x2c\x64\x3d\x71\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\ +\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\x2a\x22\x29\x2c\ +\x65\x3d\x71\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\ +\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\x61\x22\x29\x5b\x30\x5d\ +\x3b\x69\x66\x28\x21\x64\x7c\x7c\x21\x64\x2e\x6c\x65\x6e\x67\x74\ +\x68\x7c\x7c\x21\x65\x29\x72\x65\x74\x75\x72\x6e\x7b\x7d\x3b\x67\ +\x3d\x63\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\ +\x28\x22\x73\x65\x6c\x65\x63\x74\x22\x29\x2c\x68\x3d\x67\x2e\x61\ +\x70\x70\x65\x6e\x64\x43\x68\x69\x6c\x64\x28\x63\x2e\x63\x72\x65\ +\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x6f\x70\x74\x69\ +\x6f\x6e\x22\x29\x29\x2c\x69\x3d\x71\x2e\x67\x65\x74\x45\x6c\x65\ +\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\ +\x69\x6e\x70\x75\x74\x22\x29\x5b\x30\x5d\x2c\x62\x3d\x7b\x6c\x65\ +\x61\x64\x69\x6e\x67\x57\x68\x69\x74\x65\x73\x70\x61\x63\x65\x3a\ +\x71\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x2e\x6e\x6f\x64\ +\x65\x54\x79\x70\x65\x3d\x3d\x3d\x33\x2c\x74\x62\x6f\x64\x79\x3a\ +\x21\x71\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\ +\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\x74\x62\x6f\x64\x79\x22\x29\ +\x2e\x6c\x65\x6e\x67\x74\x68\x2c\x68\x74\x6d\x6c\x53\x65\x72\x69\ +\x61\x6c\x69\x7a\x65\x3a\x21\x21\x71\x2e\x67\x65\x74\x45\x6c\x65\ +\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\ +\x6c\x69\x6e\x6b\x22\x29\x2e\x6c\x65\x6e\x67\x74\x68\x2c\x73\x74\ +\x79\x6c\x65\x3a\x2f\x74\x6f\x70\x2f\x2e\x74\x65\x73\x74\x28\x65\ +\x2e\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x73\ +\x74\x79\x6c\x65\x22\x29\x29\x2c\x68\x72\x65\x66\x4e\x6f\x72\x6d\ +\x61\x6c\x69\x7a\x65\x64\x3a\x65\x2e\x67\x65\x74\x41\x74\x74\x72\ +\x69\x62\x75\x74\x65\x28\x22\x68\x72\x65\x66\x22\x29\x3d\x3d\x3d\ +\x22\x2f\x61\x22\x2c\x6f\x70\x61\x63\x69\x74\x79\x3a\x2f\x5e\x30\ +\x2e\x35\x35\x2f\x2e\x74\x65\x73\x74\x28\x65\x2e\x73\x74\x79\x6c\ +\x65\x2e\x6f\x70\x61\x63\x69\x74\x79\x29\x2c\x63\x73\x73\x46\x6c\ +\x6f\x61\x74\x3a\x21\x21\x65\x2e\x73\x74\x79\x6c\x65\x2e\x63\x73\ +\x73\x46\x6c\x6f\x61\x74\x2c\x63\x68\x65\x63\x6b\x4f\x6e\x3a\x69\ +\x2e\x76\x61\x6c\x75\x65\x3d\x3d\x3d\x22\x6f\x6e\x22\x2c\x6f\x70\ +\x74\x53\x65\x6c\x65\x63\x74\x65\x64\x3a\x68\x2e\x73\x65\x6c\x65\ +\x63\x74\x65\x64\x2c\x67\x65\x74\x53\x65\x74\x41\x74\x74\x72\x69\ +\x62\x75\x74\x65\x3a\x71\x2e\x63\x6c\x61\x73\x73\x4e\x61\x6d\x65\ +\x21\x3d\x3d\x22\x74\x22\x2c\x65\x6e\x63\x74\x79\x70\x65\x3a\x21\ +\x21\x63\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\ +\x28\x22\x66\x6f\x72\x6d\x22\x29\x2e\x65\x6e\x63\x74\x79\x70\x65\ +\x2c\x68\x74\x6d\x6c\x35\x43\x6c\x6f\x6e\x65\x3a\x63\x2e\x63\x72\ +\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x6e\x61\x76\ +\x22\x29\x2e\x63\x6c\x6f\x6e\x65\x4e\x6f\x64\x65\x28\x21\x30\x29\ +\x2e\x6f\x75\x74\x65\x72\x48\x54\x4d\x4c\x21\x3d\x3d\x22\x3c\x3a\ +\x6e\x61\x76\x3e\x3c\x2f\x3a\x6e\x61\x76\x3e\x22\x2c\x73\x75\x62\ +\x6d\x69\x74\x42\x75\x62\x62\x6c\x65\x73\x3a\x21\x30\x2c\x63\x68\ +\x61\x6e\x67\x65\x42\x75\x62\x62\x6c\x65\x73\x3a\x21\x30\x2c\x66\ +\x6f\x63\x75\x73\x69\x6e\x42\x75\x62\x62\x6c\x65\x73\x3a\x21\x31\ +\x2c\x64\x65\x6c\x65\x74\x65\x45\x78\x70\x61\x6e\x64\x6f\x3a\x21\ +\x30\x2c\x6e\x6f\x43\x6c\x6f\x6e\x65\x45\x76\x65\x6e\x74\x3a\x21\ +\x30\x2c\x69\x6e\x6c\x69\x6e\x65\x42\x6c\x6f\x63\x6b\x4e\x65\x65\ +\x64\x73\x4c\x61\x79\x6f\x75\x74\x3a\x21\x31\x2c\x73\x68\x72\x69\ +\x6e\x6b\x57\x72\x61\x70\x42\x6c\x6f\x63\x6b\x73\x3a\x21\x31\x2c\ +\x72\x65\x6c\x69\x61\x62\x6c\x65\x4d\x61\x72\x67\x69\x6e\x52\x69\ +\x67\x68\x74\x3a\x21\x30\x7d\x2c\x69\x2e\x63\x68\x65\x63\x6b\x65\ +\x64\x3d\x21\x30\x2c\x62\x2e\x6e\x6f\x43\x6c\x6f\x6e\x65\x43\x68\ +\x65\x63\x6b\x65\x64\x3d\x69\x2e\x63\x6c\x6f\x6e\x65\x4e\x6f\x64\ +\x65\x28\x21\x30\x29\x2e\x63\x68\x65\x63\x6b\x65\x64\x2c\x67\x2e\ +\x64\x69\x73\x61\x62\x6c\x65\x64\x3d\x21\x30\x2c\x62\x2e\x6f\x70\ +\x74\x44\x69\x73\x61\x62\x6c\x65\x64\x3d\x21\x68\x2e\x64\x69\x73\ +\x61\x62\x6c\x65\x64\x3b\x74\x72\x79\x7b\x64\x65\x6c\x65\x74\x65\ +\x20\x71\x2e\x74\x65\x73\x74\x7d\x63\x61\x74\x63\x68\x28\x73\x29\ +\x7b\x62\x2e\x64\x65\x6c\x65\x74\x65\x45\x78\x70\x61\x6e\x64\x6f\ +\x3d\x21\x31\x7d\x21\x71\x2e\x61\x64\x64\x45\x76\x65\x6e\x74\x4c\ +\x69\x73\x74\x65\x6e\x65\x72\x26\x26\x71\x2e\x61\x74\x74\x61\x63\ +\x68\x45\x76\x65\x6e\x74\x26\x26\x71\x2e\x66\x69\x72\x65\x45\x76\ +\x65\x6e\x74\x26\x26\x28\x71\x2e\x61\x74\x74\x61\x63\x68\x45\x76\ +\x65\x6e\x74\x28\x22\x6f\x6e\x63\x6c\x69\x63\x6b\x22\x2c\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x62\x2e\x6e\x6f\x43\x6c\x6f\ +\x6e\x65\x45\x76\x65\x6e\x74\x3d\x21\x31\x7d\x29\x2c\x71\x2e\x63\ +\x6c\x6f\x6e\x65\x4e\x6f\x64\x65\x28\x21\x30\x29\x2e\x66\x69\x72\ +\x65\x45\x76\x65\x6e\x74\x28\x22\x6f\x6e\x63\x6c\x69\x63\x6b\x22\ +\x29\x29\x2c\x69\x3d\x63\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\ +\x6d\x65\x6e\x74\x28\x22\x69\x6e\x70\x75\x74\x22\x29\x2c\x69\x2e\ +\x76\x61\x6c\x75\x65\x3d\x22\x74\x22\x2c\x69\x2e\x73\x65\x74\x41\ +\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x74\x79\x70\x65\x22\x2c\ +\x22\x72\x61\x64\x69\x6f\x22\x29\x2c\x62\x2e\x72\x61\x64\x69\x6f\ +\x56\x61\x6c\x75\x65\x3d\x69\x2e\x76\x61\x6c\x75\x65\x3d\x3d\x3d\ +\x22\x74\x22\x2c\x69\x2e\x73\x65\x74\x41\x74\x74\x72\x69\x62\x75\ +\x74\x65\x28\x22\x63\x68\x65\x63\x6b\x65\x64\x22\x2c\x22\x63\x68\ +\x65\x63\x6b\x65\x64\x22\x29\x2c\x71\x2e\x61\x70\x70\x65\x6e\x64\ +\x43\x68\x69\x6c\x64\x28\x69\x29\x2c\x6b\x3d\x63\x2e\x63\x72\x65\ +\x61\x74\x65\x44\x6f\x63\x75\x6d\x65\x6e\x74\x46\x72\x61\x67\x6d\ +\x65\x6e\x74\x28\x29\x2c\x6b\x2e\x61\x70\x70\x65\x6e\x64\x43\x68\ +\x69\x6c\x64\x28\x71\x2e\x6c\x61\x73\x74\x43\x68\x69\x6c\x64\x29\ +\x2c\x62\x2e\x63\x68\x65\x63\x6b\x43\x6c\x6f\x6e\x65\x3d\x6b\x2e\ +\x63\x6c\x6f\x6e\x65\x4e\x6f\x64\x65\x28\x21\x30\x29\x2e\x63\x6c\ +\x6f\x6e\x65\x4e\x6f\x64\x65\x28\x21\x30\x29\x2e\x6c\x61\x73\x74\ +\x43\x68\x69\x6c\x64\x2e\x63\x68\x65\x63\x6b\x65\x64\x2c\x62\x2e\ +\x61\x70\x70\x65\x6e\x64\x43\x68\x65\x63\x6b\x65\x64\x3d\x69\x2e\ +\x63\x68\x65\x63\x6b\x65\x64\x2c\x6b\x2e\x72\x65\x6d\x6f\x76\x65\ +\x43\x68\x69\x6c\x64\x28\x69\x29\x2c\x6b\x2e\x61\x70\x70\x65\x6e\ +\x64\x43\x68\x69\x6c\x64\x28\x71\x29\x2c\x71\x2e\x69\x6e\x6e\x65\ +\x72\x48\x54\x4d\x4c\x3d\x22\x22\x2c\x61\x2e\x67\x65\x74\x43\x6f\ +\x6d\x70\x75\x74\x65\x64\x53\x74\x79\x6c\x65\x26\x26\x28\x6a\x3d\ +\x63\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\ +\x22\x64\x69\x76\x22\x29\x2c\x6a\x2e\x73\x74\x79\x6c\x65\x2e\x77\ +\x69\x64\x74\x68\x3d\x22\x30\x22\x2c\x6a\x2e\x73\x74\x79\x6c\x65\ +\x2e\x6d\x61\x72\x67\x69\x6e\x52\x69\x67\x68\x74\x3d\x22\x30\x22\ +\x2c\x71\x2e\x73\x74\x79\x6c\x65\x2e\x77\x69\x64\x74\x68\x3d\x22\ +\x32\x70\x78\x22\x2c\x71\x2e\x61\x70\x70\x65\x6e\x64\x43\x68\x69\ +\x6c\x64\x28\x6a\x29\x2c\x62\x2e\x72\x65\x6c\x69\x61\x62\x6c\x65\ +\x4d\x61\x72\x67\x69\x6e\x52\x69\x67\x68\x74\x3d\x28\x70\x61\x72\ +\x73\x65\x49\x6e\x74\x28\x28\x61\x2e\x67\x65\x74\x43\x6f\x6d\x70\ +\x75\x74\x65\x64\x53\x74\x79\x6c\x65\x28\x6a\x2c\x6e\x75\x6c\x6c\ +\x29\x7c\x7c\x7b\x6d\x61\x72\x67\x69\x6e\x52\x69\x67\x68\x74\x3a\ +\x30\x7d\x29\x2e\x6d\x61\x72\x67\x69\x6e\x52\x69\x67\x68\x74\x2c\ +\x31\x30\x29\x7c\x7c\x30\x29\x3d\x3d\x3d\x30\x29\x3b\x69\x66\x28\ +\x71\x2e\x61\x74\x74\x61\x63\x68\x45\x76\x65\x6e\x74\x29\x66\x6f\ +\x72\x28\x6f\x20\x69\x6e\x7b\x73\x75\x62\x6d\x69\x74\x3a\x31\x2c\ +\x63\x68\x61\x6e\x67\x65\x3a\x31\x2c\x66\x6f\x63\x75\x73\x69\x6e\ +\x3a\x31\x7d\x29\x6e\x3d\x22\x6f\x6e\x22\x2b\x6f\x2c\x70\x3d\x6e\ +\x20\x69\x6e\x20\x71\x2c\x70\x7c\x7c\x28\x71\x2e\x73\x65\x74\x41\ +\x74\x74\x72\x69\x62\x75\x74\x65\x28\x6e\x2c\x22\x72\x65\x74\x75\ +\x72\x6e\x3b\x22\x29\x2c\x70\x3d\x74\x79\x70\x65\x6f\x66\x20\x71\ +\x5b\x6e\x5d\x3d\x3d\x22\x66\x75\x6e\x63\x74\x69\x6f\x6e\x22\x29\ +\x2c\x62\x5b\x6f\x2b\x22\x42\x75\x62\x62\x6c\x65\x73\x22\x5d\x3d\ +\x70\x3b\x6b\x2e\x72\x65\x6d\x6f\x76\x65\x43\x68\x69\x6c\x64\x28\ +\x71\x29\x2c\x6b\x3d\x67\x3d\x68\x3d\x6a\x3d\x71\x3d\x69\x3d\x6e\ +\x75\x6c\x6c\x2c\x66\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\ +\x7b\x76\x61\x72\x20\x61\x2c\x64\x2c\x65\x2c\x67\x2c\x68\x2c\x69\ +\x2c\x6a\x2c\x6b\x2c\x6d\x2c\x6e\x2c\x6f\x2c\x72\x3d\x63\x2e\x67\ +\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\ +\x61\x6d\x65\x28\x22\x62\x6f\x64\x79\x22\x29\x5b\x30\x5d\x3b\x21\ +\x72\x7c\x7c\x28\x6a\x3d\x31\x2c\x6b\x3d\x22\x70\x6f\x73\x69\x74\ +\x69\x6f\x6e\x3a\x61\x62\x73\x6f\x6c\x75\x74\x65\x3b\x74\x6f\x70\ +\x3a\x30\x3b\x6c\x65\x66\x74\x3a\x30\x3b\x77\x69\x64\x74\x68\x3a\ +\x31\x70\x78\x3b\x68\x65\x69\x67\x68\x74\x3a\x31\x70\x78\x3b\x6d\ +\x61\x72\x67\x69\x6e\x3a\x30\x3b\x22\x2c\x6d\x3d\x22\x76\x69\x73\ +\x69\x62\x69\x6c\x69\x74\x79\x3a\x68\x69\x64\x64\x65\x6e\x3b\x62\ +\x6f\x72\x64\x65\x72\x3a\x30\x3b\x22\x2c\x6e\x3d\x22\x73\x74\x79\ +\x6c\x65\x3d\x27\x22\x2b\x6b\x2b\x22\x62\x6f\x72\x64\x65\x72\x3a\ +\x35\x70\x78\x20\x73\x6f\x6c\x69\x64\x20\x23\x30\x30\x30\x3b\x70\ +\x61\x64\x64\x69\x6e\x67\x3a\x30\x3b\x27\x22\x2c\x6f\x3d\x22\x3c\ +\x64\x69\x76\x20\x22\x2b\x6e\x2b\x22\x3e\x3c\x64\x69\x76\x3e\x3c\ +\x2f\x64\x69\x76\x3e\x3c\x2f\x64\x69\x76\x3e\x22\x2b\x22\x3c\x74\ +\x61\x62\x6c\x65\x20\x22\x2b\x6e\x2b\x22\x20\x63\x65\x6c\x6c\x70\ +\x61\x64\x64\x69\x6e\x67\x3d\x27\x30\x27\x20\x63\x65\x6c\x6c\x73\ +\x70\x61\x63\x69\x6e\x67\x3d\x27\x30\x27\x3e\x22\x2b\x22\x3c\x74\ +\x72\x3e\x3c\x74\x64\x3e\x3c\x2f\x74\x64\x3e\x3c\x2f\x74\x72\x3e\ +\x3c\x2f\x74\x61\x62\x6c\x65\x3e\x22\x2c\x61\x3d\x63\x2e\x63\x72\ +\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x64\x69\x76\ +\x22\x29\x2c\x61\x2e\x73\x74\x79\x6c\x65\x2e\x63\x73\x73\x54\x65\ +\x78\x74\x3d\x6d\x2b\x22\x77\x69\x64\x74\x68\x3a\x30\x3b\x68\x65\ +\x69\x67\x68\x74\x3a\x30\x3b\x70\x6f\x73\x69\x74\x69\x6f\x6e\x3a\ +\x73\x74\x61\x74\x69\x63\x3b\x74\x6f\x70\x3a\x30\x3b\x6d\x61\x72\ +\x67\x69\x6e\x2d\x74\x6f\x70\x3a\x22\x2b\x6a\x2b\x22\x70\x78\x22\ +\x2c\x72\x2e\x69\x6e\x73\x65\x72\x74\x42\x65\x66\x6f\x72\x65\x28\ +\x61\x2c\x72\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x29\x2c\ +\x71\x3d\x63\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\ +\x74\x28\x22\x64\x69\x76\x22\x29\x2c\x61\x2e\x61\x70\x70\x65\x6e\ +\x64\x43\x68\x69\x6c\x64\x28\x71\x29\x2c\x71\x2e\x69\x6e\x6e\x65\ +\x72\x48\x54\x4d\x4c\x3d\x22\x3c\x74\x61\x62\x6c\x65\x3e\x3c\x74\ +\x72\x3e\x3c\x74\x64\x20\x73\x74\x79\x6c\x65\x3d\x27\x70\x61\x64\ +\x64\x69\x6e\x67\x3a\x30\x3b\x62\x6f\x72\x64\x65\x72\x3a\x30\x3b\ +\x64\x69\x73\x70\x6c\x61\x79\x3a\x6e\x6f\x6e\x65\x27\x3e\x3c\x2f\ +\x74\x64\x3e\x3c\x74\x64\x3e\x74\x3c\x2f\x74\x64\x3e\x3c\x2f\x74\ +\x72\x3e\x3c\x2f\x74\x61\x62\x6c\x65\x3e\x22\x2c\x6c\x3d\x71\x2e\ +\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\ +\x4e\x61\x6d\x65\x28\x22\x74\x64\x22\x29\x2c\x70\x3d\x6c\x5b\x30\ +\x5d\x2e\x6f\x66\x66\x73\x65\x74\x48\x65\x69\x67\x68\x74\x3d\x3d\ +\x3d\x30\x2c\x6c\x5b\x30\x5d\x2e\x73\x74\x79\x6c\x65\x2e\x64\x69\ +\x73\x70\x6c\x61\x79\x3d\x22\x22\x2c\x6c\x5b\x31\x5d\x2e\x73\x74\ +\x79\x6c\x65\x2e\x64\x69\x73\x70\x6c\x61\x79\x3d\x22\x6e\x6f\x6e\ +\x65\x22\x2c\x62\x2e\x72\x65\x6c\x69\x61\x62\x6c\x65\x48\x69\x64\ +\x64\x65\x6e\x4f\x66\x66\x73\x65\x74\x73\x3d\x70\x26\x26\x6c\x5b\ +\x30\x5d\x2e\x6f\x66\x66\x73\x65\x74\x48\x65\x69\x67\x68\x74\x3d\ +\x3d\x3d\x30\x2c\x71\x2e\x69\x6e\x6e\x65\x72\x48\x54\x4d\x4c\x3d\ +\x22\x22\x2c\x71\x2e\x73\x74\x79\x6c\x65\x2e\x77\x69\x64\x74\x68\ +\x3d\x71\x2e\x73\x74\x79\x6c\x65\x2e\x70\x61\x64\x64\x69\x6e\x67\ +\x4c\x65\x66\x74\x3d\x22\x31\x70\x78\x22\x2c\x66\x2e\x62\x6f\x78\ +\x4d\x6f\x64\x65\x6c\x3d\x62\x2e\x62\x6f\x78\x4d\x6f\x64\x65\x6c\ +\x3d\x71\x2e\x6f\x66\x66\x73\x65\x74\x57\x69\x64\x74\x68\x3d\x3d\ +\x3d\x32\x2c\x74\x79\x70\x65\x6f\x66\x20\x71\x2e\x73\x74\x79\x6c\ +\x65\x2e\x7a\x6f\x6f\x6d\x21\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\ +\x65\x64\x22\x26\x26\x28\x71\x2e\x73\x74\x79\x6c\x65\x2e\x64\x69\ +\x73\x70\x6c\x61\x79\x3d\x22\x69\x6e\x6c\x69\x6e\x65\x22\x2c\x71\ +\x2e\x73\x74\x79\x6c\x65\x2e\x7a\x6f\x6f\x6d\x3d\x31\x2c\x62\x2e\ +\x69\x6e\x6c\x69\x6e\x65\x42\x6c\x6f\x63\x6b\x4e\x65\x65\x64\x73\ +\x4c\x61\x79\x6f\x75\x74\x3d\x71\x2e\x6f\x66\x66\x73\x65\x74\x57\ +\x69\x64\x74\x68\x3d\x3d\x3d\x32\x2c\x71\x2e\x73\x74\x79\x6c\x65\ +\x2e\x64\x69\x73\x70\x6c\x61\x79\x3d\x22\x22\x2c\x71\x2e\x69\x6e\ +\x6e\x65\x72\x48\x54\x4d\x4c\x3d\x22\x3c\x64\x69\x76\x20\x73\x74\ +\x79\x6c\x65\x3d\x27\x77\x69\x64\x74\x68\x3a\x34\x70\x78\x3b\x27\ +\x3e\x3c\x2f\x64\x69\x76\x3e\x22\x2c\x62\x2e\x73\x68\x72\x69\x6e\ +\x6b\x57\x72\x61\x70\x42\x6c\x6f\x63\x6b\x73\x3d\x71\x2e\x6f\x66\ +\x66\x73\x65\x74\x57\x69\x64\x74\x68\x21\x3d\x3d\x32\x29\x2c\x71\ +\x2e\x73\x74\x79\x6c\x65\x2e\x63\x73\x73\x54\x65\x78\x74\x3d\x6b\ +\x2b\x6d\x2c\x71\x2e\x69\x6e\x6e\x65\x72\x48\x54\x4d\x4c\x3d\x6f\ +\x2c\x64\x3d\x71\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x2c\ +\x65\x3d\x64\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x2c\x68\ +\x3d\x64\x2e\x6e\x65\x78\x74\x53\x69\x62\x6c\x69\x6e\x67\x2e\x66\ +\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x2e\x66\x69\x72\x73\x74\x43\ +\x68\x69\x6c\x64\x2c\x69\x3d\x7b\x64\x6f\x65\x73\x4e\x6f\x74\x41\ +\x64\x64\x42\x6f\x72\x64\x65\x72\x3a\x65\x2e\x6f\x66\x66\x73\x65\ +\x74\x54\x6f\x70\x21\x3d\x3d\x35\x2c\x64\x6f\x65\x73\x41\x64\x64\ +\x42\x6f\x72\x64\x65\x72\x46\x6f\x72\x54\x61\x62\x6c\x65\x41\x6e\ +\x64\x43\x65\x6c\x6c\x73\x3a\x68\x2e\x6f\x66\x66\x73\x65\x74\x54\ +\x6f\x70\x3d\x3d\x3d\x35\x7d\x2c\x65\x2e\x73\x74\x79\x6c\x65\x2e\ +\x70\x6f\x73\x69\x74\x69\x6f\x6e\x3d\x22\x66\x69\x78\x65\x64\x22\ +\x2c\x65\x2e\x73\x74\x79\x6c\x65\x2e\x74\x6f\x70\x3d\x22\x32\x30\ +\x70\x78\x22\x2c\x69\x2e\x66\x69\x78\x65\x64\x50\x6f\x73\x69\x74\ +\x69\x6f\x6e\x3d\x65\x2e\x6f\x66\x66\x73\x65\x74\x54\x6f\x70\x3d\ +\x3d\x3d\x32\x30\x7c\x7c\x65\x2e\x6f\x66\x66\x73\x65\x74\x54\x6f\ +\x70\x3d\x3d\x3d\x31\x35\x2c\x65\x2e\x73\x74\x79\x6c\x65\x2e\x70\ +\x6f\x73\x69\x74\x69\x6f\x6e\x3d\x65\x2e\x73\x74\x79\x6c\x65\x2e\ +\x74\x6f\x70\x3d\x22\x22\x2c\x64\x2e\x73\x74\x79\x6c\x65\x2e\x6f\ +\x76\x65\x72\x66\x6c\x6f\x77\x3d\x22\x68\x69\x64\x64\x65\x6e\x22\ +\x2c\x64\x2e\x73\x74\x79\x6c\x65\x2e\x70\x6f\x73\x69\x74\x69\x6f\ +\x6e\x3d\x22\x72\x65\x6c\x61\x74\x69\x76\x65\x22\x2c\x69\x2e\x73\ +\x75\x62\x74\x72\x61\x63\x74\x73\x42\x6f\x72\x64\x65\x72\x46\x6f\ +\x72\x4f\x76\x65\x72\x66\x6c\x6f\x77\x4e\x6f\x74\x56\x69\x73\x69\ +\x62\x6c\x65\x3d\x65\x2e\x6f\x66\x66\x73\x65\x74\x54\x6f\x70\x3d\ +\x3d\x3d\x2d\x35\x2c\x69\x2e\x64\x6f\x65\x73\x4e\x6f\x74\x49\x6e\ +\x63\x6c\x75\x64\x65\x4d\x61\x72\x67\x69\x6e\x49\x6e\x42\x6f\x64\ +\x79\x4f\x66\x66\x73\x65\x74\x3d\x72\x2e\x6f\x66\x66\x73\x65\x74\ +\x54\x6f\x70\x21\x3d\x3d\x6a\x2c\x72\x2e\x72\x65\x6d\x6f\x76\x65\ +\x43\x68\x69\x6c\x64\x28\x61\x29\x2c\x71\x3d\x61\x3d\x6e\x75\x6c\ +\x6c\x2c\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x62\x2c\x69\x29\x29\ +\x7d\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x62\x7d\x28\x29\x3b\x76\ +\x61\x72\x20\x6a\x3d\x2f\x5e\x28\x3f\x3a\x5c\x7b\x2e\x2a\x5c\x7d\ +\x7c\x5c\x5b\x2e\x2a\x5c\x5d\x29\x24\x2f\x2c\x6b\x3d\x2f\x28\x5b\ +\x41\x2d\x5a\x5d\x29\x2f\x67\x3b\x66\x2e\x65\x78\x74\x65\x6e\x64\ +\x28\x7b\x63\x61\x63\x68\x65\x3a\x7b\x7d\x2c\x75\x75\x69\x64\x3a\ +\x30\x2c\x65\x78\x70\x61\x6e\x64\x6f\x3a\x22\x6a\x51\x75\x65\x72\ +\x79\x22\x2b\x28\x66\x2e\x66\x6e\x2e\x6a\x71\x75\x65\x72\x79\x2b\ +\x4d\x61\x74\x68\x2e\x72\x61\x6e\x64\x6f\x6d\x28\x29\x29\x2e\x72\ +\x65\x70\x6c\x61\x63\x65\x28\x2f\x5c\x44\x2f\x67\x2c\x22\x22\x29\ +\x2c\x6e\x6f\x44\x61\x74\x61\x3a\x7b\x65\x6d\x62\x65\x64\x3a\x21\ +\x30\x2c\x6f\x62\x6a\x65\x63\x74\x3a\x22\x63\x6c\x73\x69\x64\x3a\ +\x44\x32\x37\x43\x44\x42\x36\x45\x2d\x41\x45\x36\x44\x2d\x31\x31\ +\x63\x66\x2d\x39\x36\x42\x38\x2d\x34\x34\x34\x35\x35\x33\x35\x34\ +\x30\x30\x30\x30\x22\x2c\x61\x70\x70\x6c\x65\x74\x3a\x21\x30\x7d\ +\x2c\x68\x61\x73\x44\x61\x74\x61\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x29\x7b\x61\x3d\x61\x2e\x6e\x6f\x64\x65\x54\x79\x70\ +\x65\x3f\x66\x2e\x63\x61\x63\x68\x65\x5b\x61\x5b\x66\x2e\x65\x78\ +\x70\x61\x6e\x64\x6f\x5d\x5d\x3a\x61\x5b\x66\x2e\x65\x78\x70\x61\ +\x6e\x64\x6f\x5d\x3b\x72\x65\x74\x75\x72\x6e\x21\x21\x61\x26\x26\ +\x21\x6d\x28\x61\x29\x7d\x2c\x64\x61\x74\x61\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x63\x2c\x64\x2c\x65\x29\x7b\x69\x66\ +\x28\x21\x21\x66\x2e\x61\x63\x63\x65\x70\x74\x44\x61\x74\x61\x28\ +\x61\x29\x29\x7b\x76\x61\x72\x20\x67\x2c\x68\x2c\x69\x2c\x6a\x3d\ +\x66\x2e\x65\x78\x70\x61\x6e\x64\x6f\x2c\x6b\x3d\x74\x79\x70\x65\ +\x6f\x66\x20\x63\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x2c\x6c\ +\x3d\x61\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x2c\x6d\x3d\x6c\x3f\ +\x66\x2e\x63\x61\x63\x68\x65\x3a\x61\x2c\x6e\x3d\x6c\x3f\x61\x5b\ +\x6a\x5d\x3a\x61\x5b\x6a\x5d\x26\x26\x6a\x2c\x6f\x3d\x63\x3d\x3d\ +\x3d\x22\x65\x76\x65\x6e\x74\x73\x22\x3b\x69\x66\x28\x28\x21\x6e\ +\x7c\x7c\x21\x6d\x5b\x6e\x5d\x7c\x7c\x21\x6f\x26\x26\x21\x65\x26\ +\x26\x21\x6d\x5b\x6e\x5d\x2e\x64\x61\x74\x61\x29\x26\x26\x6b\x26\ +\x26\x64\x3d\x3d\x3d\x62\x29\x72\x65\x74\x75\x72\x6e\x3b\x6e\x7c\ +\x7c\x28\x6c\x3f\x61\x5b\x6a\x5d\x3d\x6e\x3d\x2b\x2b\x66\x2e\x75\ +\x75\x69\x64\x3a\x6e\x3d\x6a\x29\x2c\x6d\x5b\x6e\x5d\x7c\x7c\x28\ +\x6d\x5b\x6e\x5d\x3d\x7b\x7d\x2c\x6c\x7c\x7c\x28\x6d\x5b\x6e\x5d\ +\x2e\x74\x6f\x4a\x53\x4f\x4e\x3d\x66\x2e\x6e\x6f\x6f\x70\x29\x29\ +\x3b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x63\x3d\x3d\x22\x6f\ +\x62\x6a\x65\x63\x74\x22\x7c\x7c\x74\x79\x70\x65\x6f\x66\x20\x63\ +\x3d\x3d\x22\x66\x75\x6e\x63\x74\x69\x6f\x6e\x22\x29\x65\x3f\x6d\ +\x5b\x6e\x5d\x3d\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x6d\x5b\x6e\ +\x5d\x2c\x63\x29\x3a\x6d\x5b\x6e\x5d\x2e\x64\x61\x74\x61\x3d\x66\ +\x2e\x65\x78\x74\x65\x6e\x64\x28\x6d\x5b\x6e\x5d\x2e\x64\x61\x74\ +\x61\x2c\x63\x29\x3b\x67\x3d\x68\x3d\x6d\x5b\x6e\x5d\x2c\x65\x7c\ +\x7c\x28\x68\x2e\x64\x61\x74\x61\x7c\x7c\x28\x68\x2e\x64\x61\x74\ +\x61\x3d\x7b\x7d\x29\x2c\x68\x3d\x68\x2e\x64\x61\x74\x61\x29\x2c\ +\x64\x21\x3d\x3d\x62\x26\x26\x28\x68\x5b\x66\x2e\x63\x61\x6d\x65\ +\x6c\x43\x61\x73\x65\x28\x63\x29\x5d\x3d\x64\x29\x3b\x69\x66\x28\ +\x6f\x26\x26\x21\x68\x5b\x63\x5d\x29\x72\x65\x74\x75\x72\x6e\x20\ +\x67\x2e\x65\x76\x65\x6e\x74\x73\x3b\x6b\x3f\x28\x69\x3d\x68\x5b\ +\x63\x5d\x2c\x69\x3d\x3d\x6e\x75\x6c\x6c\x26\x26\x28\x69\x3d\x68\ +\x5b\x66\x2e\x63\x61\x6d\x65\x6c\x43\x61\x73\x65\x28\x63\x29\x5d\ +\x29\x29\x3a\x69\x3d\x68\x3b\x72\x65\x74\x75\x72\x6e\x20\x69\x7d\ +\x7d\x2c\x72\x65\x6d\x6f\x76\x65\x44\x61\x74\x61\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x69\x66\x28\ +\x21\x21\x66\x2e\x61\x63\x63\x65\x70\x74\x44\x61\x74\x61\x28\x61\ +\x29\x29\x7b\x76\x61\x72\x20\x64\x2c\x65\x2c\x67\x2c\x68\x3d\x66\ +\x2e\x65\x78\x70\x61\x6e\x64\x6f\x2c\x69\x3d\x61\x2e\x6e\x6f\x64\ +\x65\x54\x79\x70\x65\x2c\x6a\x3d\x69\x3f\x66\x2e\x63\x61\x63\x68\ +\x65\x3a\x61\x2c\x6b\x3d\x69\x3f\x61\x5b\x68\x5d\x3a\x68\x3b\x69\ +\x66\x28\x21\x6a\x5b\x6b\x5d\x29\x72\x65\x74\x75\x72\x6e\x3b\x69\ +\x66\x28\x62\x29\x7b\x64\x3d\x63\x3f\x6a\x5b\x6b\x5d\x3a\x6a\x5b\ +\x6b\x5d\x2e\x64\x61\x74\x61\x3b\x69\x66\x28\x64\x29\x7b\x66\x2e\ +\x69\x73\x41\x72\x72\x61\x79\x28\x62\x29\x7c\x7c\x28\x62\x20\x69\ +\x6e\x20\x64\x3f\x62\x3d\x5b\x62\x5d\x3a\x28\x62\x3d\x66\x2e\x63\ +\x61\x6d\x65\x6c\x43\x61\x73\x65\x28\x62\x29\x2c\x62\x20\x69\x6e\ +\x20\x64\x3f\x62\x3d\x5b\x62\x5d\x3a\x62\x3d\x62\x2e\x73\x70\x6c\ +\x69\x74\x28\x22\x20\x22\x29\x29\x29\x3b\x66\x6f\x72\x28\x65\x3d\ +\x30\x2c\x67\x3d\x62\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x65\x3c\x67\ +\x3b\x65\x2b\x2b\x29\x64\x65\x6c\x65\x74\x65\x20\x64\x5b\x62\x5b\ +\x65\x5d\x5d\x3b\x69\x66\x28\x21\x28\x63\x3f\x6d\x3a\x66\x2e\x69\ +\x73\x45\x6d\x70\x74\x79\x4f\x62\x6a\x65\x63\x74\x29\x28\x64\x29\ +\x29\x72\x65\x74\x75\x72\x6e\x7d\x7d\x69\x66\x28\x21\x63\x29\x7b\ +\x64\x65\x6c\x65\x74\x65\x20\x6a\x5b\x6b\x5d\x2e\x64\x61\x74\x61\ +\x3b\x69\x66\x28\x21\x6d\x28\x6a\x5b\x6b\x5d\x29\x29\x72\x65\x74\ +\x75\x72\x6e\x7d\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\x64\x65\ +\x6c\x65\x74\x65\x45\x78\x70\x61\x6e\x64\x6f\x7c\x7c\x21\x6a\x2e\ +\x73\x65\x74\x49\x6e\x74\x65\x72\x76\x61\x6c\x3f\x64\x65\x6c\x65\ +\x74\x65\x20\x6a\x5b\x6b\x5d\x3a\x6a\x5b\x6b\x5d\x3d\x6e\x75\x6c\ +\x6c\x2c\x69\x26\x26\x28\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\ +\x64\x65\x6c\x65\x74\x65\x45\x78\x70\x61\x6e\x64\x6f\x3f\x64\x65\ +\x6c\x65\x74\x65\x20\x61\x5b\x68\x5d\x3a\x61\x2e\x72\x65\x6d\x6f\ +\x76\x65\x41\x74\x74\x72\x69\x62\x75\x74\x65\x3f\x61\x2e\x72\x65\ +\x6d\x6f\x76\x65\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x68\x29\ +\x3a\x61\x5b\x68\x5d\x3d\x6e\x75\x6c\x6c\x29\x7d\x7d\x2c\x5f\x64\ +\x61\x74\x61\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\ +\x2c\x63\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x64\x61\x74\ +\x61\x28\x61\x2c\x62\x2c\x63\x2c\x21\x30\x29\x7d\x2c\x61\x63\x63\ +\x65\x70\x74\x44\x61\x74\x61\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x7b\x69\x66\x28\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\ +\x65\x29\x7b\x76\x61\x72\x20\x62\x3d\x66\x2e\x6e\x6f\x44\x61\x74\ +\x61\x5b\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\ +\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x5d\x3b\x69\x66\x28\x62\ +\x29\x72\x65\x74\x75\x72\x6e\x20\x62\x21\x3d\x3d\x21\x30\x26\x26\ +\x61\x2e\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\ +\x63\x6c\x61\x73\x73\x69\x64\x22\x29\x3d\x3d\x3d\x62\x7d\x72\x65\ +\x74\x75\x72\x6e\x21\x30\x7d\x7d\x29\x2c\x66\x2e\x66\x6e\x2e\x65\ +\x78\x74\x65\x6e\x64\x28\x7b\x64\x61\x74\x61\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x2c\ +\x65\x2c\x67\x2c\x68\x3d\x6e\x75\x6c\x6c\x3b\x69\x66\x28\x74\x79\ +\x70\x65\x6f\x66\x20\x61\x3d\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\ +\x65\x64\x22\x29\x7b\x69\x66\x28\x74\x68\x69\x73\x2e\x6c\x65\x6e\ +\x67\x74\x68\x29\x7b\x68\x3d\x66\x2e\x64\x61\x74\x61\x28\x74\x68\ +\x69\x73\x5b\x30\x5d\x29\x3b\x69\x66\x28\x74\x68\x69\x73\x5b\x30\ +\x5d\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x26\x26\ +\x21\x66\x2e\x5f\x64\x61\x74\x61\x28\x74\x68\x69\x73\x5b\x30\x5d\ +\x2c\x22\x70\x61\x72\x73\x65\x64\x41\x74\x74\x72\x73\x22\x29\x29\ +\x7b\x65\x3d\x74\x68\x69\x73\x5b\x30\x5d\x2e\x61\x74\x74\x72\x69\ +\x62\x75\x74\x65\x73\x3b\x66\x6f\x72\x28\x76\x61\x72\x20\x69\x3d\ +\x30\x2c\x6a\x3d\x65\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x69\x3c\x6a\ +\x3b\x69\x2b\x2b\x29\x67\x3d\x65\x5b\x69\x5d\x2e\x6e\x61\x6d\x65\ +\x2c\x67\x2e\x69\x6e\x64\x65\x78\x4f\x66\x28\x22\x64\x61\x74\x61\ +\x2d\x22\x29\x3d\x3d\x3d\x30\x26\x26\x28\x67\x3d\x66\x2e\x63\x61\ +\x6d\x65\x6c\x43\x61\x73\x65\x28\x67\x2e\x73\x75\x62\x73\x74\x72\ +\x69\x6e\x67\x28\x35\x29\x29\x2c\x6c\x28\x74\x68\x69\x73\x5b\x30\ +\x5d\x2c\x67\x2c\x68\x5b\x67\x5d\x29\x29\x3b\x66\x2e\x5f\x64\x61\ +\x74\x61\x28\x74\x68\x69\x73\x5b\x30\x5d\x2c\x22\x70\x61\x72\x73\ +\x65\x64\x41\x74\x74\x72\x73\x22\x2c\x21\x30\x29\x7d\x7d\x72\x65\ +\x74\x75\x72\x6e\x20\x68\x7d\x69\x66\x28\x74\x79\x70\x65\x6f\x66\ +\x20\x61\x3d\x3d\x22\x6f\x62\x6a\x65\x63\x74\x22\x29\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x66\x2e\x64\x61\x74\x61\x28\ +\x74\x68\x69\x73\x2c\x61\x29\x7d\x29\x3b\x64\x3d\x61\x2e\x73\x70\ +\x6c\x69\x74\x28\x22\x2e\x22\x29\x2c\x64\x5b\x31\x5d\x3d\x64\x5b\ +\x31\x5d\x3f\x22\x2e\x22\x2b\x64\x5b\x31\x5d\x3a\x22\x22\x3b\x69\ +\x66\x28\x63\x3d\x3d\x3d\x62\x29\x7b\x68\x3d\x74\x68\x69\x73\x2e\ +\x74\x72\x69\x67\x67\x65\x72\x48\x61\x6e\x64\x6c\x65\x72\x28\x22\ +\x67\x65\x74\x44\x61\x74\x61\x22\x2b\x64\x5b\x31\x5d\x2b\x22\x21\ +\x22\x2c\x5b\x64\x5b\x30\x5d\x5d\x29\x2c\x68\x3d\x3d\x3d\x62\x26\ +\x26\x74\x68\x69\x73\x2e\x6c\x65\x6e\x67\x74\x68\x26\x26\x28\x68\ +\x3d\x66\x2e\x64\x61\x74\x61\x28\x74\x68\x69\x73\x5b\x30\x5d\x2c\ +\x61\x29\x2c\x68\x3d\x6c\x28\x74\x68\x69\x73\x5b\x30\x5d\x2c\x61\ +\x2c\x68\x29\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x68\x3d\x3d\x3d\ +\x62\x26\x26\x64\x5b\x31\x5d\x3f\x74\x68\x69\x73\x2e\x64\x61\x74\ +\x61\x28\x64\x5b\x30\x5d\x29\x3a\x68\x7d\x72\x65\x74\x75\x72\x6e\ +\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x29\x7b\x76\x61\x72\x20\x62\x3d\x66\x28\x74\x68\ +\x69\x73\x29\x2c\x65\x3d\x5b\x64\x5b\x30\x5d\x2c\x63\x5d\x3b\x62\ +\x2e\x74\x72\x69\x67\x67\x65\x72\x48\x61\x6e\x64\x6c\x65\x72\x28\ +\x22\x73\x65\x74\x44\x61\x74\x61\x22\x2b\x64\x5b\x31\x5d\x2b\x22\ +\x21\x22\x2c\x65\x29\x2c\x66\x2e\x64\x61\x74\x61\x28\x74\x68\x69\ +\x73\x2c\x61\x2c\x63\x29\x2c\x62\x2e\x74\x72\x69\x67\x67\x65\x72\ +\x48\x61\x6e\x64\x6c\x65\x72\x28\x22\x63\x68\x61\x6e\x67\x65\x44\ +\x61\x74\x61\x22\x2b\x64\x5b\x31\x5d\x2b\x22\x21\x22\x2c\x65\x29\ +\x7d\x29\x7d\x2c\x72\x65\x6d\x6f\x76\x65\x44\x61\x74\x61\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\ +\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x29\x7b\x66\x2e\x72\x65\x6d\x6f\x76\x65\x44\ +\x61\x74\x61\x28\x74\x68\x69\x73\x2c\x61\x29\x7d\x29\x7d\x7d\x29\ +\x2c\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x7b\x5f\x6d\x61\x72\x6b\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x61\ +\x26\x26\x28\x62\x3d\x28\x62\x7c\x7c\x22\x66\x78\x22\x29\x2b\x22\ +\x6d\x61\x72\x6b\x22\x2c\x66\x2e\x5f\x64\x61\x74\x61\x28\x61\x2c\ +\x62\x2c\x28\x66\x2e\x5f\x64\x61\x74\x61\x28\x61\x2c\x62\x29\x7c\ +\x7c\x30\x29\x2b\x31\x29\x29\x7d\x2c\x5f\x75\x6e\x6d\x61\x72\x6b\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\ +\x7b\x61\x21\x3d\x3d\x21\x30\x26\x26\x28\x63\x3d\x62\x2c\x62\x3d\ +\x61\x2c\x61\x3d\x21\x31\x29\x3b\x69\x66\x28\x62\x29\x7b\x63\x3d\ +\x63\x7c\x7c\x22\x66\x78\x22\x3b\x76\x61\x72\x20\x64\x3d\x63\x2b\ +\x22\x6d\x61\x72\x6b\x22\x2c\x65\x3d\x61\x3f\x30\x3a\x28\x66\x2e\ +\x5f\x64\x61\x74\x61\x28\x62\x2c\x64\x29\x7c\x7c\x31\x29\x2d\x31\ +\x3b\x65\x3f\x66\x2e\x5f\x64\x61\x74\x61\x28\x62\x2c\x64\x2c\x65\ +\x29\x3a\x28\x66\x2e\x72\x65\x6d\x6f\x76\x65\x44\x61\x74\x61\x28\ +\x62\x2c\x64\x2c\x21\x30\x29\x2c\x6e\x28\x62\x2c\x63\x2c\x22\x6d\ +\x61\x72\x6b\x22\x29\x29\x7d\x7d\x2c\x71\x75\x65\x75\x65\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x76\ +\x61\x72\x20\x64\x3b\x69\x66\x28\x61\x29\x7b\x62\x3d\x28\x62\x7c\ +\x7c\x22\x66\x78\x22\x29\x2b\x22\x71\x75\x65\x75\x65\x22\x2c\x64\ +\x3d\x66\x2e\x5f\x64\x61\x74\x61\x28\x61\x2c\x62\x29\x2c\x63\x26\ +\x26\x28\x21\x64\x7c\x7c\x66\x2e\x69\x73\x41\x72\x72\x61\x79\x28\ +\x63\x29\x3f\x64\x3d\x66\x2e\x5f\x64\x61\x74\x61\x28\x61\x2c\x62\ +\x2c\x66\x2e\x6d\x61\x6b\x65\x41\x72\x72\x61\x79\x28\x63\x29\x29\ +\x3a\x64\x2e\x70\x75\x73\x68\x28\x63\x29\x29\x3b\x72\x65\x74\x75\ +\x72\x6e\x20\x64\x7c\x7c\x5b\x5d\x7d\x7d\x2c\x64\x65\x71\x75\x65\ +\x75\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\ +\x7b\x62\x3d\x62\x7c\x7c\x22\x66\x78\x22\x3b\x76\x61\x72\x20\x63\ +\x3d\x66\x2e\x71\x75\x65\x75\x65\x28\x61\x2c\x62\x29\x2c\x64\x3d\ +\x63\x2e\x73\x68\x69\x66\x74\x28\x29\x2c\x65\x3d\x7b\x7d\x3b\x64\ +\x3d\x3d\x3d\x22\x69\x6e\x70\x72\x6f\x67\x72\x65\x73\x73\x22\x26\ +\x26\x28\x64\x3d\x63\x2e\x73\x68\x69\x66\x74\x28\x29\x29\x2c\x64\ +\x26\x26\x28\x62\x3d\x3d\x3d\x22\x66\x78\x22\x26\x26\x63\x2e\x75\ +\x6e\x73\x68\x69\x66\x74\x28\x22\x69\x6e\x70\x72\x6f\x67\x72\x65\ +\x73\x73\x22\x29\x2c\x66\x2e\x5f\x64\x61\x74\x61\x28\x61\x2c\x62\ +\x2b\x22\x2e\x72\x75\x6e\x22\x2c\x65\x29\x2c\x64\x2e\x63\x61\x6c\ +\x6c\x28\x61\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x66\ +\x2e\x64\x65\x71\x75\x65\x75\x65\x28\x61\x2c\x62\x29\x7d\x2c\x65\ +\x29\x29\x2c\x63\x2e\x6c\x65\x6e\x67\x74\x68\x7c\x7c\x28\x66\x2e\ +\x72\x65\x6d\x6f\x76\x65\x44\x61\x74\x61\x28\x61\x2c\x62\x2b\x22\ +\x71\x75\x65\x75\x65\x20\x22\x2b\x62\x2b\x22\x2e\x72\x75\x6e\x22\ +\x2c\x21\x30\x29\x2c\x6e\x28\x61\x2c\x62\x2c\x22\x71\x75\x65\x75\ +\x65\x22\x29\x29\x7d\x7d\x29\x2c\x66\x2e\x66\x6e\x2e\x65\x78\x74\ +\x65\x6e\x64\x28\x7b\x71\x75\x65\x75\x65\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x74\x79\x70\x65\x6f\x66\x20\ +\x61\x21\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x28\x63\x3d\ +\x61\x2c\x61\x3d\x22\x66\x78\x22\x29\x3b\x69\x66\x28\x63\x3d\x3d\ +\x3d\x62\x29\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x71\x75\x65\x75\ +\x65\x28\x74\x68\x69\x73\x5b\x30\x5d\x2c\x61\x29\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x76\x61\x72\x20\x62\x3d\x66\ +\x2e\x71\x75\x65\x75\x65\x28\x74\x68\x69\x73\x2c\x61\x2c\x63\x29\ +\x3b\x61\x3d\x3d\x3d\x22\x66\x78\x22\x26\x26\x62\x5b\x30\x5d\x21\ +\x3d\x3d\x22\x69\x6e\x70\x72\x6f\x67\x72\x65\x73\x73\x22\x26\x26\ +\x66\x2e\x64\x65\x71\x75\x65\x75\x65\x28\x74\x68\x69\x73\x2c\x61\ +\x29\x7d\x29\x7d\x2c\x64\x65\x71\x75\x65\x75\x65\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\ +\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x29\x7b\x66\x2e\x64\x65\x71\x75\x65\x75\x65\x28\x74\ +\x68\x69\x73\x2c\x61\x29\x7d\x29\x7d\x2c\x64\x65\x6c\x61\x79\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x61\x3d\ +\x66\x2e\x66\x78\x3f\x66\x2e\x66\x78\x2e\x73\x70\x65\x65\x64\x73\ +\x5b\x61\x5d\x7c\x7c\x61\x3a\x61\x2c\x62\x3d\x62\x7c\x7c\x22\x66\ +\x78\x22\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x71\ +\x75\x65\x75\x65\x28\x62\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x3d\x73\x65\x74\x54\x69\ +\x6d\x65\x6f\x75\x74\x28\x62\x2c\x61\x29\x3b\x63\x2e\x73\x74\x6f\ +\x70\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x63\x6c\x65\ +\x61\x72\x54\x69\x6d\x65\x6f\x75\x74\x28\x64\x29\x7d\x7d\x29\x7d\ +\x2c\x63\x6c\x65\x61\x72\x51\x75\x65\x75\x65\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x2e\x71\x75\x65\x75\x65\x28\x61\x7c\x7c\x22\x66\x78\ +\x22\x2c\x5b\x5d\x29\x7d\x2c\x70\x72\x6f\x6d\x69\x73\x65\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x20\x6d\x28\x29\x7b\x2d\x2d\x68\x7c\x7c\x64\ +\x2e\x72\x65\x73\x6f\x6c\x76\x65\x57\x69\x74\x68\x28\x65\x2c\x5b\ +\x65\x5d\x29\x7d\x74\x79\x70\x65\x6f\x66\x20\x61\x21\x3d\x22\x73\ +\x74\x72\x69\x6e\x67\x22\x26\x26\x28\x63\x3d\x61\x2c\x61\x3d\x62\ +\x29\x2c\x61\x3d\x61\x7c\x7c\x22\x66\x78\x22\x3b\x76\x61\x72\x20\ +\x64\x3d\x66\x2e\x44\x65\x66\x65\x72\x72\x65\x64\x28\x29\x2c\x65\ +\x3d\x74\x68\x69\x73\x2c\x67\x3d\x65\x2e\x6c\x65\x6e\x67\x74\x68\ +\x2c\x68\x3d\x31\x2c\x69\x3d\x61\x2b\x22\x64\x65\x66\x65\x72\x22\ +\x2c\x6a\x3d\x61\x2b\x22\x71\x75\x65\x75\x65\x22\x2c\x6b\x3d\x61\ +\x2b\x22\x6d\x61\x72\x6b\x22\x2c\x6c\x3b\x77\x68\x69\x6c\x65\x28\ +\x67\x2d\x2d\x29\x69\x66\x28\x6c\x3d\x66\x2e\x64\x61\x74\x61\x28\ +\x65\x5b\x67\x5d\x2c\x69\x2c\x62\x2c\x21\x30\x29\x7c\x7c\x28\x66\ +\x2e\x64\x61\x74\x61\x28\x65\x5b\x67\x5d\x2c\x6a\x2c\x62\x2c\x21\ +\x30\x29\x7c\x7c\x66\x2e\x64\x61\x74\x61\x28\x65\x5b\x67\x5d\x2c\ +\x6b\x2c\x62\x2c\x21\x30\x29\x29\x26\x26\x66\x2e\x64\x61\x74\x61\ +\x28\x65\x5b\x67\x5d\x2c\x69\x2c\x66\x2e\x43\x61\x6c\x6c\x62\x61\ +\x63\x6b\x73\x28\x22\x6f\x6e\x63\x65\x20\x6d\x65\x6d\x6f\x72\x79\ +\x22\x29\x2c\x21\x30\x29\x29\x68\x2b\x2b\x2c\x6c\x2e\x61\x64\x64\ +\x28\x6d\x29\x3b\x6d\x28\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x64\ +\x2e\x70\x72\x6f\x6d\x69\x73\x65\x28\x29\x7d\x7d\x29\x3b\x76\x61\ +\x72\x20\x6f\x3d\x2f\x5b\x5c\x6e\x5c\x74\x5c\x72\x5d\x2f\x67\x2c\ +\x70\x3d\x2f\x5c\x73\x2b\x2f\x2c\x71\x3d\x2f\x5c\x72\x2f\x67\x2c\ +\x72\x3d\x2f\x5e\x28\x3f\x3a\x62\x75\x74\x74\x6f\x6e\x7c\x69\x6e\ +\x70\x75\x74\x29\x24\x2f\x69\x2c\x73\x3d\x2f\x5e\x28\x3f\x3a\x62\ +\x75\x74\x74\x6f\x6e\x7c\x69\x6e\x70\x75\x74\x7c\x6f\x62\x6a\x65\ +\x63\x74\x7c\x73\x65\x6c\x65\x63\x74\x7c\x74\x65\x78\x74\x61\x72\ +\x65\x61\x29\x24\x2f\x69\x2c\x74\x3d\x2f\x5e\x61\x28\x3f\x3a\x72\ +\x65\x61\x29\x3f\x24\x2f\x69\x2c\x75\x3d\x2f\x5e\x28\x3f\x3a\x61\ +\x75\x74\x6f\x66\x6f\x63\x75\x73\x7c\x61\x75\x74\x6f\x70\x6c\x61\ +\x79\x7c\x61\x73\x79\x6e\x63\x7c\x63\x68\x65\x63\x6b\x65\x64\x7c\ +\x63\x6f\x6e\x74\x72\x6f\x6c\x73\x7c\x64\x65\x66\x65\x72\x7c\x64\ +\x69\x73\x61\x62\x6c\x65\x64\x7c\x68\x69\x64\x64\x65\x6e\x7c\x6c\ +\x6f\x6f\x70\x7c\x6d\x75\x6c\x74\x69\x70\x6c\x65\x7c\x6f\x70\x65\ +\x6e\x7c\x72\x65\x61\x64\x6f\x6e\x6c\x79\x7c\x72\x65\x71\x75\x69\ +\x72\x65\x64\x7c\x73\x63\x6f\x70\x65\x64\x7c\x73\x65\x6c\x65\x63\ +\x74\x65\x64\x29\x24\x2f\x69\x2c\x76\x3d\x66\x2e\x73\x75\x70\x70\ +\x6f\x72\x74\x2e\x67\x65\x74\x53\x65\x74\x41\x74\x74\x72\x69\x62\ +\x75\x74\x65\x2c\x77\x2c\x78\x2c\x79\x3b\x66\x2e\x66\x6e\x2e\x65\ +\x78\x74\x65\x6e\x64\x28\x7b\x61\x74\x74\x72\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\ +\x20\x66\x2e\x61\x63\x63\x65\x73\x73\x28\x74\x68\x69\x73\x2c\x61\ +\x2c\x62\x2c\x21\x30\x2c\x66\x2e\x61\x74\x74\x72\x29\x7d\x2c\x72\ +\x65\x6d\x6f\x76\x65\x41\x74\x74\x72\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\ +\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x29\x7b\x66\x2e\x72\x65\x6d\x6f\x76\x65\x41\x74\x74\x72\x28\x74\ +\x68\x69\x73\x2c\x61\x29\x7d\x29\x7d\x2c\x70\x72\x6f\x70\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\ +\x75\x72\x6e\x20\x66\x2e\x61\x63\x63\x65\x73\x73\x28\x74\x68\x69\ +\x73\x2c\x61\x2c\x62\x2c\x21\x30\x2c\x66\x2e\x70\x72\x6f\x70\x29\ +\x7d\x2c\x72\x65\x6d\x6f\x76\x65\x50\x72\x6f\x70\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x61\x3d\x66\x2e\x70\x72\x6f\ +\x70\x46\x69\x78\x5b\x61\x5d\x7c\x7c\x61\x3b\x72\x65\x74\x75\x72\ +\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x29\x7b\x74\x72\x79\x7b\x74\x68\x69\x73\x5b\ +\x61\x5d\x3d\x62\x2c\x64\x65\x6c\x65\x74\x65\x20\x74\x68\x69\x73\ +\x5b\x61\x5d\x7d\x63\x61\x74\x63\x68\x28\x63\x29\x7b\x7d\x7d\x29\ +\x7d\x2c\x61\x64\x64\x43\x6c\x61\x73\x73\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\x2c\x63\x2c\x64\ +\x2c\x65\x2c\x67\x2c\x68\x2c\x69\x3b\x69\x66\x28\x66\x2e\x69\x73\ +\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x29\x72\x65\x74\x75\ +\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x62\x29\x7b\x66\x28\x74\x68\x69\x73\x29\ +\x2e\x61\x64\x64\x43\x6c\x61\x73\x73\x28\x61\x2e\x63\x61\x6c\x6c\ +\x28\x74\x68\x69\x73\x2c\x62\x2c\x74\x68\x69\x73\x2e\x63\x6c\x61\ +\x73\x73\x4e\x61\x6d\x65\x29\x29\x7d\x29\x3b\x69\x66\x28\x61\x26\ +\x26\x74\x79\x70\x65\x6f\x66\x20\x61\x3d\x3d\x22\x73\x74\x72\x69\ +\x6e\x67\x22\x29\x7b\x62\x3d\x61\x2e\x73\x70\x6c\x69\x74\x28\x70\ +\x29\x3b\x66\x6f\x72\x28\x63\x3d\x30\x2c\x64\x3d\x74\x68\x69\x73\ +\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x63\x3c\x64\x3b\x63\x2b\x2b\x29\ +\x7b\x65\x3d\x74\x68\x69\x73\x5b\x63\x5d\x3b\x69\x66\x28\x65\x2e\ +\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x29\x69\x66\x28\ +\x21\x65\x2e\x63\x6c\x61\x73\x73\x4e\x61\x6d\x65\x26\x26\x62\x2e\ +\x6c\x65\x6e\x67\x74\x68\x3d\x3d\x3d\x31\x29\x65\x2e\x63\x6c\x61\ +\x73\x73\x4e\x61\x6d\x65\x3d\x61\x3b\x65\x6c\x73\x65\x7b\x67\x3d\ +\x22\x20\x22\x2b\x65\x2e\x63\x6c\x61\x73\x73\x4e\x61\x6d\x65\x2b\ +\x22\x20\x22\x3b\x66\x6f\x72\x28\x68\x3d\x30\x2c\x69\x3d\x62\x2e\ +\x6c\x65\x6e\x67\x74\x68\x3b\x68\x3c\x69\x3b\x68\x2b\x2b\x29\x7e\ +\x67\x2e\x69\x6e\x64\x65\x78\x4f\x66\x28\x22\x20\x22\x2b\x62\x5b\ +\x68\x5d\x2b\x22\x20\x22\x29\x7c\x7c\x28\x67\x2b\x3d\x62\x5b\x68\ +\x5d\x2b\x22\x20\x22\x29\x3b\x65\x2e\x63\x6c\x61\x73\x73\x4e\x61\ +\x6d\x65\x3d\x66\x2e\x74\x72\x69\x6d\x28\x67\x29\x7d\x7d\x7d\x72\ +\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x72\x65\x6d\x6f\ +\x76\x65\x43\x6c\x61\x73\x73\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x7b\x76\x61\x72\x20\x63\x2c\x64\x2c\x65\x2c\x67\x2c\ +\x68\x2c\x69\x2c\x6a\x3b\x69\x66\x28\x66\x2e\x69\x73\x46\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x29\x72\x65\x74\x75\x72\x6e\x20\ +\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x62\x29\x7b\x66\x28\x74\x68\x69\x73\x29\x2e\x72\x65\ +\x6d\x6f\x76\x65\x43\x6c\x61\x73\x73\x28\x61\x2e\x63\x61\x6c\x6c\ +\x28\x74\x68\x69\x73\x2c\x62\x2c\x74\x68\x69\x73\x2e\x63\x6c\x61\ +\x73\x73\x4e\x61\x6d\x65\x29\x29\x7d\x29\x3b\x69\x66\x28\x61\x26\ +\x26\x74\x79\x70\x65\x6f\x66\x20\x61\x3d\x3d\x22\x73\x74\x72\x69\ +\x6e\x67\x22\x7c\x7c\x61\x3d\x3d\x3d\x62\x29\x7b\x63\x3d\x28\x61\ +\x7c\x7c\x22\x22\x29\x2e\x73\x70\x6c\x69\x74\x28\x70\x29\x3b\x66\ +\x6f\x72\x28\x64\x3d\x30\x2c\x65\x3d\x74\x68\x69\x73\x2e\x6c\x65\ +\x6e\x67\x74\x68\x3b\x64\x3c\x65\x3b\x64\x2b\x2b\x29\x7b\x67\x3d\ +\x74\x68\x69\x73\x5b\x64\x5d\x3b\x69\x66\x28\x67\x2e\x6e\x6f\x64\ +\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x26\x26\x67\x2e\x63\x6c\x61\ +\x73\x73\x4e\x61\x6d\x65\x29\x69\x66\x28\x61\x29\x7b\x68\x3d\x28\ +\x22\x20\x22\x2b\x67\x2e\x63\x6c\x61\x73\x73\x4e\x61\x6d\x65\x2b\ +\x22\x20\x22\x29\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x6f\x2c\x22\ +\x20\x22\x29\x3b\x66\x6f\x72\x28\x69\x3d\x30\x2c\x6a\x3d\x63\x2e\ +\x6c\x65\x6e\x67\x74\x68\x3b\x69\x3c\x6a\x3b\x69\x2b\x2b\x29\x68\ +\x3d\x68\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x22\x20\x22\x2b\x63\ +\x5b\x69\x5d\x2b\x22\x20\x22\x2c\x22\x20\x22\x29\x3b\x67\x2e\x63\ +\x6c\x61\x73\x73\x4e\x61\x6d\x65\x3d\x66\x2e\x74\x72\x69\x6d\x28\ +\x68\x29\x7d\x65\x6c\x73\x65\x20\x67\x2e\x63\x6c\x61\x73\x73\x4e\ +\x61\x6d\x65\x3d\x22\x22\x7d\x7d\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x7d\x2c\x74\x6f\x67\x67\x6c\x65\x43\x6c\x61\x73\x73\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\ +\x61\x72\x20\x63\x3d\x74\x79\x70\x65\x6f\x66\x20\x61\x2c\x64\x3d\ +\x74\x79\x70\x65\x6f\x66\x20\x62\x3d\x3d\x22\x62\x6f\x6f\x6c\x65\ +\x61\x6e\x22\x3b\x69\x66\x28\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x29\x72\x65\x74\x75\x72\x6e\x20\x74\x68\ +\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x63\x29\x7b\x66\x28\x74\x68\x69\x73\x29\x2e\x74\x6f\x67\x67\ +\x6c\x65\x43\x6c\x61\x73\x73\x28\x61\x2e\x63\x61\x6c\x6c\x28\x74\ +\x68\x69\x73\x2c\x63\x2c\x74\x68\x69\x73\x2e\x63\x6c\x61\x73\x73\ +\x4e\x61\x6d\x65\x2c\x62\x29\x2c\x62\x29\x7d\x29\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x69\x66\x28\x63\x3d\x3d\x3d\ +\x22\x73\x74\x72\x69\x6e\x67\x22\x29\x7b\x76\x61\x72\x20\x65\x2c\ +\x67\x3d\x30\x2c\x68\x3d\x66\x28\x74\x68\x69\x73\x29\x2c\x69\x3d\ +\x62\x2c\x6a\x3d\x61\x2e\x73\x70\x6c\x69\x74\x28\x70\x29\x3b\x77\ +\x68\x69\x6c\x65\x28\x65\x3d\x6a\x5b\x67\x2b\x2b\x5d\x29\x69\x3d\ +\x64\x3f\x69\x3a\x21\x68\x2e\x68\x61\x73\x43\x6c\x61\x73\x73\x28\ +\x65\x29\x2c\x68\x5b\x69\x3f\x22\x61\x64\x64\x43\x6c\x61\x73\x73\ +\x22\x3a\x22\x72\x65\x6d\x6f\x76\x65\x43\x6c\x61\x73\x73\x22\x5d\ +\x28\x65\x29\x7d\x65\x6c\x73\x65\x20\x69\x66\x28\x63\x3d\x3d\x3d\ +\x22\x75\x6e\x64\x65\x66\x69\x6e\x65\x64\x22\x7c\x7c\x63\x3d\x3d\ +\x3d\x22\x62\x6f\x6f\x6c\x65\x61\x6e\x22\x29\x74\x68\x69\x73\x2e\ +\x63\x6c\x61\x73\x73\x4e\x61\x6d\x65\x26\x26\x66\x2e\x5f\x64\x61\ +\x74\x61\x28\x74\x68\x69\x73\x2c\x22\x5f\x5f\x63\x6c\x61\x73\x73\ +\x4e\x61\x6d\x65\x5f\x5f\x22\x2c\x74\x68\x69\x73\x2e\x63\x6c\x61\ +\x73\x73\x4e\x61\x6d\x65\x29\x2c\x74\x68\x69\x73\x2e\x63\x6c\x61\ +\x73\x73\x4e\x61\x6d\x65\x3d\x74\x68\x69\x73\x2e\x63\x6c\x61\x73\ +\x73\x4e\x61\x6d\x65\x7c\x7c\x61\x3d\x3d\x3d\x21\x31\x3f\x22\x22\ +\x3a\x66\x2e\x5f\x64\x61\x74\x61\x28\x74\x68\x69\x73\x2c\x22\x5f\ +\x5f\x63\x6c\x61\x73\x73\x4e\x61\x6d\x65\x5f\x5f\x22\x29\x7c\x7c\ +\x22\x22\x7d\x29\x7d\x2c\x68\x61\x73\x43\x6c\x61\x73\x73\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\ +\x3d\x22\x20\x22\x2b\x61\x2b\x22\x20\x22\x2c\x63\x3d\x30\x2c\x64\ +\x3d\x74\x68\x69\x73\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x66\x6f\x72\ +\x28\x3b\x63\x3c\x64\x3b\x63\x2b\x2b\x29\x69\x66\x28\x74\x68\x69\ +\x73\x5b\x63\x5d\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\ +\x31\x26\x26\x28\x22\x20\x22\x2b\x74\x68\x69\x73\x5b\x63\x5d\x2e\ +\x63\x6c\x61\x73\x73\x4e\x61\x6d\x65\x2b\x22\x20\x22\x29\x2e\x72\ +\x65\x70\x6c\x61\x63\x65\x28\x6f\x2c\x22\x20\x22\x29\x2e\x69\x6e\ +\x64\x65\x78\x4f\x66\x28\x62\x29\x3e\x2d\x31\x29\x72\x65\x74\x75\ +\x72\x6e\x21\x30\x3b\x72\x65\x74\x75\x72\x6e\x21\x31\x7d\x2c\x76\ +\x61\x6c\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\ +\x61\x72\x20\x63\x2c\x64\x2c\x65\x2c\x67\x3d\x74\x68\x69\x73\x5b\ +\x30\x5d\x3b\x7b\x69\x66\x28\x21\x21\x61\x72\x67\x75\x6d\x65\x6e\ +\x74\x73\x2e\x6c\x65\x6e\x67\x74\x68\x29\x7b\x65\x3d\x66\x2e\x69\ +\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x64\x29\x7b\x76\x61\x72\x20\x67\x3d\ +\x66\x28\x74\x68\x69\x73\x29\x2c\x68\x3b\x69\x66\x28\x74\x68\x69\ +\x73\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x29\x7b\ +\x65\x3f\x68\x3d\x61\x2e\x63\x61\x6c\x6c\x28\x74\x68\x69\x73\x2c\ +\x64\x2c\x67\x2e\x76\x61\x6c\x28\x29\x29\x3a\x68\x3d\x61\x2c\x68\ +\x3d\x3d\x6e\x75\x6c\x6c\x3f\x68\x3d\x22\x22\x3a\x74\x79\x70\x65\ +\x6f\x66\x20\x68\x3d\x3d\x22\x6e\x75\x6d\x62\x65\x72\x22\x3f\x68\ +\x2b\x3d\x22\x22\x3a\x66\x2e\x69\x73\x41\x72\x72\x61\x79\x28\x68\ +\x29\x26\x26\x28\x68\x3d\x66\x2e\x6d\x61\x70\x28\x68\x2c\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\ +\x20\x61\x3d\x3d\x6e\x75\x6c\x6c\x3f\x22\x22\x3a\x61\x2b\x22\x22\ +\x7d\x29\x29\x2c\x63\x3d\x66\x2e\x76\x61\x6c\x48\x6f\x6f\x6b\x73\ +\x5b\x74\x68\x69\x73\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\ +\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x5d\x7c\x7c\x66\ +\x2e\x76\x61\x6c\x48\x6f\x6f\x6b\x73\x5b\x74\x68\x69\x73\x2e\x74\ +\x79\x70\x65\x5d\x3b\x69\x66\x28\x21\x63\x7c\x7c\x21\x28\x22\x73\ +\x65\x74\x22\x69\x6e\x20\x63\x29\x7c\x7c\x63\x2e\x73\x65\x74\x28\ +\x74\x68\x69\x73\x2c\x68\x2c\x22\x76\x61\x6c\x75\x65\x22\x29\x3d\ +\x3d\x3d\x62\x29\x74\x68\x69\x73\x2e\x76\x61\x6c\x75\x65\x3d\x68\ +\x7d\x7d\x29\x7d\x69\x66\x28\x67\x29\x7b\x63\x3d\x66\x2e\x76\x61\ +\x6c\x48\x6f\x6f\x6b\x73\x5b\x67\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\ +\x65\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x5d\ +\x7c\x7c\x66\x2e\x76\x61\x6c\x48\x6f\x6f\x6b\x73\x5b\x67\x2e\x74\ +\x79\x70\x65\x5d\x3b\x69\x66\x28\x63\x26\x26\x22\x67\x65\x74\x22\ +\x69\x6e\x20\x63\x26\x26\x28\x64\x3d\x63\x2e\x67\x65\x74\x28\x67\ +\x2c\x22\x76\x61\x6c\x75\x65\x22\x29\x29\x21\x3d\x3d\x62\x29\x72\ +\x65\x74\x75\x72\x6e\x20\x64\x3b\x64\x3d\x67\x2e\x76\x61\x6c\x75\ +\x65\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x79\x70\x65\x6f\x66\x20\ +\x64\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x3f\x64\x2e\x72\x65\ +\x70\x6c\x61\x63\x65\x28\x71\x2c\x22\x22\x29\x3a\x64\x3d\x3d\x6e\ +\x75\x6c\x6c\x3f\x22\x22\x3a\x64\x7d\x7d\x7d\x7d\x29\x2c\x66\x2e\ +\x65\x78\x74\x65\x6e\x64\x28\x7b\x76\x61\x6c\x48\x6f\x6f\x6b\x73\ +\x3a\x7b\x6f\x70\x74\x69\x6f\x6e\x3a\x7b\x67\x65\x74\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\x3d\ +\x61\x2e\x61\x74\x74\x72\x69\x62\x75\x74\x65\x73\x2e\x76\x61\x6c\ +\x75\x65\x3b\x72\x65\x74\x75\x72\x6e\x21\x62\x7c\x7c\x62\x2e\x73\ +\x70\x65\x63\x69\x66\x69\x65\x64\x3f\x61\x2e\x76\x61\x6c\x75\x65\ +\x3a\x61\x2e\x74\x65\x78\x74\x7d\x7d\x2c\x73\x65\x6c\x65\x63\x74\ +\x3a\x7b\x67\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x29\x7b\x76\x61\x72\x20\x62\x2c\x63\x2c\x64\x2c\x65\x2c\x67\x3d\ +\x61\x2e\x73\x65\x6c\x65\x63\x74\x65\x64\x49\x6e\x64\x65\x78\x2c\ +\x68\x3d\x5b\x5d\x2c\x69\x3d\x61\x2e\x6f\x70\x74\x69\x6f\x6e\x73\ +\x2c\x6a\x3d\x61\x2e\x74\x79\x70\x65\x3d\x3d\x3d\x22\x73\x65\x6c\ +\x65\x63\x74\x2d\x6f\x6e\x65\x22\x3b\x69\x66\x28\x67\x3c\x30\x29\ +\x72\x65\x74\x75\x72\x6e\x20\x6e\x75\x6c\x6c\x3b\x63\x3d\x6a\x3f\ +\x67\x3a\x30\x2c\x64\x3d\x6a\x3f\x67\x2b\x31\x3a\x69\x2e\x6c\x65\ +\x6e\x67\x74\x68\x3b\x66\x6f\x72\x28\x3b\x63\x3c\x64\x3b\x63\x2b\ +\x2b\x29\x7b\x65\x3d\x69\x5b\x63\x5d\x3b\x69\x66\x28\x65\x2e\x73\ +\x65\x6c\x65\x63\x74\x65\x64\x26\x26\x28\x66\x2e\x73\x75\x70\x70\ +\x6f\x72\x74\x2e\x6f\x70\x74\x44\x69\x73\x61\x62\x6c\x65\x64\x3f\ +\x21\x65\x2e\x64\x69\x73\x61\x62\x6c\x65\x64\x3a\x65\x2e\x67\x65\ +\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x64\x69\x73\x61\ +\x62\x6c\x65\x64\x22\x29\x3d\x3d\x3d\x6e\x75\x6c\x6c\x29\x26\x26\ +\x28\x21\x65\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2e\x64\ +\x69\x73\x61\x62\x6c\x65\x64\x7c\x7c\x21\x66\x2e\x6e\x6f\x64\x65\ +\x4e\x61\x6d\x65\x28\x65\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\ +\x65\x2c\x22\x6f\x70\x74\x67\x72\x6f\x75\x70\x22\x29\x29\x29\x7b\ +\x62\x3d\x66\x28\x65\x29\x2e\x76\x61\x6c\x28\x29\x3b\x69\x66\x28\ +\x6a\x29\x72\x65\x74\x75\x72\x6e\x20\x62\x3b\x68\x2e\x70\x75\x73\ +\x68\x28\x62\x29\x7d\x7d\x69\x66\x28\x6a\x26\x26\x21\x68\x2e\x6c\ +\x65\x6e\x67\x74\x68\x26\x26\x69\x2e\x6c\x65\x6e\x67\x74\x68\x29\ +\x72\x65\x74\x75\x72\x6e\x20\x66\x28\x69\x5b\x67\x5d\x29\x2e\x76\ +\x61\x6c\x28\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x68\x7d\x2c\x73\ +\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\ +\x7b\x76\x61\x72\x20\x63\x3d\x66\x2e\x6d\x61\x6b\x65\x41\x72\x72\ +\x61\x79\x28\x62\x29\x3b\x66\x28\x61\x29\x2e\x66\x69\x6e\x64\x28\ +\x22\x6f\x70\x74\x69\x6f\x6e\x22\x29\x2e\x65\x61\x63\x68\x28\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x74\x68\x69\x73\x2e\x73\ +\x65\x6c\x65\x63\x74\x65\x64\x3d\x66\x2e\x69\x6e\x41\x72\x72\x61\ +\x79\x28\x66\x28\x74\x68\x69\x73\x29\x2e\x76\x61\x6c\x28\x29\x2c\ +\x63\x29\x3e\x3d\x30\x7d\x29\x2c\x63\x2e\x6c\x65\x6e\x67\x74\x68\ +\x7c\x7c\x28\x61\x2e\x73\x65\x6c\x65\x63\x74\x65\x64\x49\x6e\x64\ +\x65\x78\x3d\x2d\x31\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x63\x7d\ +\x7d\x7d\x2c\x61\x74\x74\x72\x46\x6e\x3a\x7b\x76\x61\x6c\x3a\x21\ +\x30\x2c\x63\x73\x73\x3a\x21\x30\x2c\x68\x74\x6d\x6c\x3a\x21\x30\ +\x2c\x74\x65\x78\x74\x3a\x21\x30\x2c\x64\x61\x74\x61\x3a\x21\x30\ +\x2c\x77\x69\x64\x74\x68\x3a\x21\x30\x2c\x68\x65\x69\x67\x68\x74\ +\x3a\x21\x30\x2c\x6f\x66\x66\x73\x65\x74\x3a\x21\x30\x7d\x2c\x61\ +\x74\x74\x72\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\ +\x2c\x64\x2c\x65\x29\x7b\x76\x61\x72\x20\x67\x2c\x68\x2c\x69\x2c\ +\x6a\x3d\x61\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3b\x69\x66\x28\ +\x21\x21\x61\x26\x26\x6a\x21\x3d\x3d\x33\x26\x26\x6a\x21\x3d\x3d\ +\x38\x26\x26\x6a\x21\x3d\x3d\x32\x29\x7b\x69\x66\x28\x65\x26\x26\ +\x63\x20\x69\x6e\x20\x66\x2e\x61\x74\x74\x72\x46\x6e\x29\x72\x65\ +\x74\x75\x72\x6e\x20\x66\x28\x61\x29\x5b\x63\x5d\x28\x64\x29\x3b\ +\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x61\x2e\x67\x65\x74\x41\ +\x74\x74\x72\x69\x62\x75\x74\x65\x3d\x3d\x22\x75\x6e\x64\x65\x66\ +\x69\x6e\x65\x64\x22\x29\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x70\ +\x72\x6f\x70\x28\x61\x2c\x63\x2c\x64\x29\x3b\x69\x3d\x6a\x21\x3d\ +\x3d\x31\x7c\x7c\x21\x66\x2e\x69\x73\x58\x4d\x4c\x44\x6f\x63\x28\ +\x61\x29\x2c\x69\x26\x26\x28\x63\x3d\x63\x2e\x74\x6f\x4c\x6f\x77\ +\x65\x72\x43\x61\x73\x65\x28\x29\x2c\x68\x3d\x66\x2e\x61\x74\x74\ +\x72\x48\x6f\x6f\x6b\x73\x5b\x63\x5d\x7c\x7c\x28\x75\x2e\x74\x65\ +\x73\x74\x28\x63\x29\x3f\x78\x3a\x77\x29\x29\x3b\x69\x66\x28\x64\ +\x21\x3d\x3d\x62\x29\x7b\x69\x66\x28\x64\x3d\x3d\x3d\x6e\x75\x6c\ +\x6c\x29\x7b\x66\x2e\x72\x65\x6d\x6f\x76\x65\x41\x74\x74\x72\x28\ +\x61\x2c\x63\x29\x3b\x72\x65\x74\x75\x72\x6e\x7d\x69\x66\x28\x68\ +\x26\x26\x22\x73\x65\x74\x22\x69\x6e\x20\x68\x26\x26\x69\x26\x26\ +\x28\x67\x3d\x68\x2e\x73\x65\x74\x28\x61\x2c\x64\x2c\x63\x29\x29\ +\x21\x3d\x3d\x62\x29\x72\x65\x74\x75\x72\x6e\x20\x67\x3b\x61\x2e\ +\x73\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x63\x2c\x22\ +\x22\x2b\x64\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x64\x7d\x69\x66\ +\x28\x68\x26\x26\x22\x67\x65\x74\x22\x69\x6e\x20\x68\x26\x26\x69\ +\x26\x26\x28\x67\x3d\x68\x2e\x67\x65\x74\x28\x61\x2c\x63\x29\x29\ +\x21\x3d\x3d\x6e\x75\x6c\x6c\x29\x72\x65\x74\x75\x72\x6e\x20\x67\ +\x3b\x67\x3d\x61\x2e\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\ +\x65\x28\x63\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x67\x3d\x3d\x3d\ +\x6e\x75\x6c\x6c\x3f\x62\x3a\x67\x7d\x7d\x2c\x72\x65\x6d\x6f\x76\ +\x65\x41\x74\x74\x72\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x2c\x64\x2c\x65\x2c\x67\x2c\ +\x68\x3d\x30\x3b\x69\x66\x28\x62\x26\x26\x61\x2e\x6e\x6f\x64\x65\ +\x54\x79\x70\x65\x3d\x3d\x3d\x31\x29\x7b\x64\x3d\x62\x2e\x74\x6f\ +\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x2e\x73\x70\x6c\x69\ +\x74\x28\x70\x29\x2c\x67\x3d\x64\x2e\x6c\x65\x6e\x67\x74\x68\x3b\ +\x66\x6f\x72\x28\x3b\x68\x3c\x67\x3b\x68\x2b\x2b\x29\x65\x3d\x64\ +\x5b\x68\x5d\x2c\x65\x26\x26\x28\x63\x3d\x66\x2e\x70\x72\x6f\x70\ +\x46\x69\x78\x5b\x65\x5d\x7c\x7c\x65\x2c\x66\x2e\x61\x74\x74\x72\ +\x28\x61\x2c\x65\x2c\x22\x22\x29\x2c\x61\x2e\x72\x65\x6d\x6f\x76\ +\x65\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x76\x3f\x65\x3a\x63\ +\x29\x2c\x75\x2e\x74\x65\x73\x74\x28\x65\x29\x26\x26\x63\x20\x69\ +\x6e\x20\x61\x26\x26\x28\x61\x5b\x63\x5d\x3d\x21\x31\x29\x29\x7d\ +\x7d\x2c\x61\x74\x74\x72\x48\x6f\x6f\x6b\x73\x3a\x7b\x74\x79\x70\ +\x65\x3a\x7b\x73\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x2c\x62\x29\x7b\x69\x66\x28\x72\x2e\x74\x65\x73\x74\x28\x61\ +\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x29\x26\x26\x61\x2e\x70\x61\ +\x72\x65\x6e\x74\x4e\x6f\x64\x65\x29\x66\x2e\x65\x72\x72\x6f\x72\ +\x28\x22\x74\x79\x70\x65\x20\x70\x72\x6f\x70\x65\x72\x74\x79\x20\ +\x63\x61\x6e\x27\x74\x20\x62\x65\x20\x63\x68\x61\x6e\x67\x65\x64\ +\x22\x29\x3b\x65\x6c\x73\x65\x20\x69\x66\x28\x21\x66\x2e\x73\x75\ +\x70\x70\x6f\x72\x74\x2e\x72\x61\x64\x69\x6f\x56\x61\x6c\x75\x65\ +\x26\x26\x62\x3d\x3d\x3d\x22\x72\x61\x64\x69\x6f\x22\x26\x26\x66\ +\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x28\x61\x2c\x22\x69\x6e\x70\ +\x75\x74\x22\x29\x29\x7b\x76\x61\x72\x20\x63\x3d\x61\x2e\x76\x61\ +\x6c\x75\x65\x3b\x61\x2e\x73\x65\x74\x41\x74\x74\x72\x69\x62\x75\ +\x74\x65\x28\x22\x74\x79\x70\x65\x22\x2c\x62\x29\x2c\x63\x26\x26\ +\x28\x61\x2e\x76\x61\x6c\x75\x65\x3d\x63\x29\x3b\x72\x65\x74\x75\ +\x72\x6e\x20\x62\x7d\x7d\x7d\x2c\x76\x61\x6c\x75\x65\x3a\x7b\x67\ +\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\ +\x7b\x69\x66\x28\x77\x26\x26\x66\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\ +\x65\x28\x61\x2c\x22\x62\x75\x74\x74\x6f\x6e\x22\x29\x29\x72\x65\ +\x74\x75\x72\x6e\x20\x77\x2e\x67\x65\x74\x28\x61\x2c\x62\x29\x3b\ +\x72\x65\x74\x75\x72\x6e\x20\x62\x20\x69\x6e\x20\x61\x3f\x61\x2e\ +\x76\x61\x6c\x75\x65\x3a\x6e\x75\x6c\x6c\x7d\x2c\x73\x65\x74\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\ +\x69\x66\x28\x77\x26\x26\x66\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\ +\x28\x61\x2c\x22\x62\x75\x74\x74\x6f\x6e\x22\x29\x29\x72\x65\x74\ +\x75\x72\x6e\x20\x77\x2e\x73\x65\x74\x28\x61\x2c\x62\x2c\x63\x29\ +\x3b\x61\x2e\x76\x61\x6c\x75\x65\x3d\x62\x7d\x7d\x7d\x2c\x70\x72\ +\x6f\x70\x46\x69\x78\x3a\x7b\x74\x61\x62\x69\x6e\x64\x65\x78\x3a\ +\x22\x74\x61\x62\x49\x6e\x64\x65\x78\x22\x2c\x72\x65\x61\x64\x6f\ +\x6e\x6c\x79\x3a\x22\x72\x65\x61\x64\x4f\x6e\x6c\x79\x22\x2c\x22\ +\x66\x6f\x72\x22\x3a\x22\x68\x74\x6d\x6c\x46\x6f\x72\x22\x2c\x22\ +\x63\x6c\x61\x73\x73\x22\x3a\x22\x63\x6c\x61\x73\x73\x4e\x61\x6d\ +\x65\x22\x2c\x6d\x61\x78\x6c\x65\x6e\x67\x74\x68\x3a\x22\x6d\x61\ +\x78\x4c\x65\x6e\x67\x74\x68\x22\x2c\x63\x65\x6c\x6c\x73\x70\x61\ +\x63\x69\x6e\x67\x3a\x22\x63\x65\x6c\x6c\x53\x70\x61\x63\x69\x6e\ +\x67\x22\x2c\x63\x65\x6c\x6c\x70\x61\x64\x64\x69\x6e\x67\x3a\x22\ +\x63\x65\x6c\x6c\x50\x61\x64\x64\x69\x6e\x67\x22\x2c\x72\x6f\x77\ +\x73\x70\x61\x6e\x3a\x22\x72\x6f\x77\x53\x70\x61\x6e\x22\x2c\x63\ +\x6f\x6c\x73\x70\x61\x6e\x3a\x22\x63\x6f\x6c\x53\x70\x61\x6e\x22\ +\x2c\x75\x73\x65\x6d\x61\x70\x3a\x22\x75\x73\x65\x4d\x61\x70\x22\ +\x2c\x66\x72\x61\x6d\x65\x62\x6f\x72\x64\x65\x72\x3a\x22\x66\x72\ +\x61\x6d\x65\x42\x6f\x72\x64\x65\x72\x22\x2c\x63\x6f\x6e\x74\x65\ +\x6e\x74\x65\x64\x69\x74\x61\x62\x6c\x65\x3a\x22\x63\x6f\x6e\x74\ +\x65\x6e\x74\x45\x64\x69\x74\x61\x62\x6c\x65\x22\x7d\x2c\x70\x72\ +\x6f\x70\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x2c\ +\x64\x29\x7b\x76\x61\x72\x20\x65\x2c\x67\x2c\x68\x2c\x69\x3d\x61\ +\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3b\x69\x66\x28\x21\x21\x61\ +\x26\x26\x69\x21\x3d\x3d\x33\x26\x26\x69\x21\x3d\x3d\x38\x26\x26\ +\x69\x21\x3d\x3d\x32\x29\x7b\x68\x3d\x69\x21\x3d\x3d\x31\x7c\x7c\ +\x21\x66\x2e\x69\x73\x58\x4d\x4c\x44\x6f\x63\x28\x61\x29\x2c\x68\ +\x26\x26\x28\x63\x3d\x66\x2e\x70\x72\x6f\x70\x46\x69\x78\x5b\x63\ +\x5d\x7c\x7c\x63\x2c\x67\x3d\x66\x2e\x70\x72\x6f\x70\x48\x6f\x6f\ +\x6b\x73\x5b\x63\x5d\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x64\x21\ +\x3d\x3d\x62\x3f\x67\x26\x26\x22\x73\x65\x74\x22\x69\x6e\x20\x67\ +\x26\x26\x28\x65\x3d\x67\x2e\x73\x65\x74\x28\x61\x2c\x64\x2c\x63\ +\x29\x29\x21\x3d\x3d\x62\x3f\x65\x3a\x61\x5b\x63\x5d\x3d\x64\x3a\ +\x67\x26\x26\x22\x67\x65\x74\x22\x69\x6e\x20\x67\x26\x26\x28\x65\ +\x3d\x67\x2e\x67\x65\x74\x28\x61\x2c\x63\x29\x29\x21\x3d\x3d\x6e\ +\x75\x6c\x6c\x3f\x65\x3a\x61\x5b\x63\x5d\x7d\x7d\x2c\x70\x72\x6f\ +\x70\x48\x6f\x6f\x6b\x73\x3a\x7b\x74\x61\x62\x49\x6e\x64\x65\x78\ +\x3a\x7b\x67\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x29\x7b\x76\x61\x72\x20\x63\x3d\x61\x2e\x67\x65\x74\x41\x74\x74\ +\x72\x69\x62\x75\x74\x65\x4e\x6f\x64\x65\x28\x22\x74\x61\x62\x69\ +\x6e\x64\x65\x78\x22\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x63\x26\ +\x26\x63\x2e\x73\x70\x65\x63\x69\x66\x69\x65\x64\x3f\x70\x61\x72\ +\x73\x65\x49\x6e\x74\x28\x63\x2e\x76\x61\x6c\x75\x65\x2c\x31\x30\ +\x29\x3a\x73\x2e\x74\x65\x73\x74\x28\x61\x2e\x6e\x6f\x64\x65\x4e\ +\x61\x6d\x65\x29\x7c\x7c\x74\x2e\x74\x65\x73\x74\x28\x61\x2e\x6e\ +\x6f\x64\x65\x4e\x61\x6d\x65\x29\x26\x26\x61\x2e\x68\x72\x65\x66\ +\x3f\x30\x3a\x62\x7d\x7d\x7d\x7d\x29\x2c\x66\x2e\x61\x74\x74\x72\ +\x48\x6f\x6f\x6b\x73\x2e\x74\x61\x62\x69\x6e\x64\x65\x78\x3d\x66\ +\x2e\x70\x72\x6f\x70\x48\x6f\x6f\x6b\x73\x2e\x74\x61\x62\x49\x6e\ +\x64\x65\x78\x2c\x78\x3d\x7b\x67\x65\x74\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x2c\x65\ +\x3d\x66\x2e\x70\x72\x6f\x70\x28\x61\x2c\x63\x29\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x65\x3d\x3d\x3d\x21\x30\x7c\x7c\x74\x79\x70\x65\ +\x6f\x66\x20\x65\x21\x3d\x22\x62\x6f\x6f\x6c\x65\x61\x6e\x22\x26\ +\x26\x28\x64\x3d\x61\x2e\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\ +\x74\x65\x4e\x6f\x64\x65\x28\x63\x29\x29\x26\x26\x64\x2e\x6e\x6f\ +\x64\x65\x56\x61\x6c\x75\x65\x21\x3d\x3d\x21\x31\x3f\x63\x2e\x74\ +\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x3a\x62\x7d\x2c\ +\x73\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\ +\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x3b\x62\x3d\x3d\x3d\x21\x31\ +\x3f\x66\x2e\x72\x65\x6d\x6f\x76\x65\x41\x74\x74\x72\x28\x61\x2c\ +\x63\x29\x3a\x28\x64\x3d\x66\x2e\x70\x72\x6f\x70\x46\x69\x78\x5b\ +\x63\x5d\x7c\x7c\x63\x2c\x64\x20\x69\x6e\x20\x61\x26\x26\x28\x61\ +\x5b\x64\x5d\x3d\x21\x30\x29\x2c\x61\x2e\x73\x65\x74\x41\x74\x74\ +\x72\x69\x62\x75\x74\x65\x28\x63\x2c\x63\x2e\x74\x6f\x4c\x6f\x77\ +\x65\x72\x43\x61\x73\x65\x28\x29\x29\x29\x3b\x72\x65\x74\x75\x72\ +\x6e\x20\x63\x7d\x7d\x2c\x76\x7c\x7c\x28\x79\x3d\x7b\x6e\x61\x6d\ +\x65\x3a\x21\x30\x2c\x69\x64\x3a\x21\x30\x7d\x2c\x77\x3d\x66\x2e\ +\x76\x61\x6c\x48\x6f\x6f\x6b\x73\x2e\x62\x75\x74\x74\x6f\x6e\x3d\ +\x7b\x67\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\ +\x63\x29\x7b\x76\x61\x72\x20\x64\x3b\x64\x3d\x61\x2e\x67\x65\x74\ +\x41\x74\x74\x72\x69\x62\x75\x74\x65\x4e\x6f\x64\x65\x28\x63\x29\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x64\x26\x26\x28\x79\x5b\x63\x5d\ +\x3f\x64\x2e\x6e\x6f\x64\x65\x56\x61\x6c\x75\x65\x21\x3d\x3d\x22\ +\x22\x3a\x64\x2e\x73\x70\x65\x63\x69\x66\x69\x65\x64\x29\x3f\x64\ +\x2e\x6e\x6f\x64\x65\x56\x61\x6c\x75\x65\x3a\x62\x7d\x2c\x73\x65\ +\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x64\ +\x29\x7b\x76\x61\x72\x20\x65\x3d\x61\x2e\x67\x65\x74\x41\x74\x74\ +\x72\x69\x62\x75\x74\x65\x4e\x6f\x64\x65\x28\x64\x29\x3b\x65\x7c\ +\x7c\x28\x65\x3d\x63\x2e\x63\x72\x65\x61\x74\x65\x41\x74\x74\x72\ +\x69\x62\x75\x74\x65\x28\x64\x29\x2c\x61\x2e\x73\x65\x74\x41\x74\ +\x74\x72\x69\x62\x75\x74\x65\x4e\x6f\x64\x65\x28\x65\x29\x29\x3b\ +\x72\x65\x74\x75\x72\x6e\x20\x65\x2e\x6e\x6f\x64\x65\x56\x61\x6c\ +\x75\x65\x3d\x62\x2b\x22\x22\x7d\x7d\x2c\x66\x2e\x61\x74\x74\x72\ +\x48\x6f\x6f\x6b\x73\x2e\x74\x61\x62\x69\x6e\x64\x65\x78\x2e\x73\ +\x65\x74\x3d\x77\x2e\x73\x65\x74\x2c\x66\x2e\x65\x61\x63\x68\x28\ +\x5b\x22\x77\x69\x64\x74\x68\x22\x2c\x22\x68\x65\x69\x67\x68\x74\ +\x22\x5d\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\ +\x7b\x66\x2e\x61\x74\x74\x72\x48\x6f\x6f\x6b\x73\x5b\x62\x5d\x3d\ +\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x66\x2e\x61\x74\x74\x72\x48\ +\x6f\x6f\x6b\x73\x5b\x62\x5d\x2c\x7b\x73\x65\x74\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x69\x66\x28\x63\x3d\ +\x3d\x3d\x22\x22\x29\x7b\x61\x2e\x73\x65\x74\x41\x74\x74\x72\x69\ +\x62\x75\x74\x65\x28\x62\x2c\x22\x61\x75\x74\x6f\x22\x29\x3b\x72\ +\x65\x74\x75\x72\x6e\x20\x63\x7d\x7d\x7d\x29\x7d\x29\x2c\x66\x2e\ +\x61\x74\x74\x72\x48\x6f\x6f\x6b\x73\x2e\x63\x6f\x6e\x74\x65\x6e\ +\x74\x65\x64\x69\x74\x61\x62\x6c\x65\x3d\x7b\x67\x65\x74\x3a\x77\ +\x2e\x67\x65\x74\x2c\x73\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x62\x3d\x3d\x3d\x22\x22\x26\ +\x26\x28\x62\x3d\x22\x66\x61\x6c\x73\x65\x22\x29\x2c\x77\x2e\x73\ +\x65\x74\x28\x61\x2c\x62\x2c\x63\x29\x7d\x7d\x29\x2c\x66\x2e\x73\ +\x75\x70\x70\x6f\x72\x74\x2e\x68\x72\x65\x66\x4e\x6f\x72\x6d\x61\ +\x6c\x69\x7a\x65\x64\x7c\x7c\x66\x2e\x65\x61\x63\x68\x28\x5b\x22\ +\x68\x72\x65\x66\x22\x2c\x22\x73\x72\x63\x22\x2c\x22\x77\x69\x64\ +\x74\x68\x22\x2c\x22\x68\x65\x69\x67\x68\x74\x22\x5d\x2c\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x66\x2e\x61\x74\ +\x74\x72\x48\x6f\x6f\x6b\x73\x5b\x63\x5d\x3d\x66\x2e\x65\x78\x74\ +\x65\x6e\x64\x28\x66\x2e\x61\x74\x74\x72\x48\x6f\x6f\x6b\x73\x5b\ +\x63\x5d\x2c\x7b\x67\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x7b\x76\x61\x72\x20\x64\x3d\x61\x2e\x67\x65\x74\x41\ +\x74\x74\x72\x69\x62\x75\x74\x65\x28\x63\x2c\x32\x29\x3b\x72\x65\ +\x74\x75\x72\x6e\x20\x64\x3d\x3d\x3d\x6e\x75\x6c\x6c\x3f\x62\x3a\ +\x64\x7d\x7d\x29\x7d\x29\x2c\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\ +\x2e\x73\x74\x79\x6c\x65\x7c\x7c\x28\x66\x2e\x61\x74\x74\x72\x48\ +\x6f\x6f\x6b\x73\x2e\x73\x74\x79\x6c\x65\x3d\x7b\x67\x65\x74\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x61\x2e\x73\x74\x79\x6c\x65\x2e\x63\x73\x73\x54\x65\ +\x78\x74\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\ +\x7c\x7c\x62\x7d\x2c\x73\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\ +\x73\x74\x79\x6c\x65\x2e\x63\x73\x73\x54\x65\x78\x74\x3d\x22\x22\ +\x2b\x62\x7d\x7d\x29\x2c\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\ +\x6f\x70\x74\x53\x65\x6c\x65\x63\x74\x65\x64\x7c\x7c\x28\x66\x2e\ +\x70\x72\x6f\x70\x48\x6f\x6f\x6b\x73\x2e\x73\x65\x6c\x65\x63\x74\ +\x65\x64\x3d\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x66\x2e\x70\x72\ +\x6f\x70\x48\x6f\x6f\x6b\x73\x2e\x73\x65\x6c\x65\x63\x74\x65\x64\ +\x2c\x7b\x67\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x29\x7b\x76\x61\x72\x20\x62\x3d\x61\x2e\x70\x61\x72\x65\x6e\x74\ +\x4e\x6f\x64\x65\x3b\x62\x26\x26\x28\x62\x2e\x73\x65\x6c\x65\x63\ +\x74\x65\x64\x49\x6e\x64\x65\x78\x2c\x62\x2e\x70\x61\x72\x65\x6e\ +\x74\x4e\x6f\x64\x65\x26\x26\x62\x2e\x70\x61\x72\x65\x6e\x74\x4e\ +\x6f\x64\x65\x2e\x73\x65\x6c\x65\x63\x74\x65\x64\x49\x6e\x64\x65\ +\x78\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x6e\x75\x6c\x6c\x7d\x7d\ +\x29\x29\x2c\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\x65\x6e\x63\ +\x74\x79\x70\x65\x7c\x7c\x28\x66\x2e\x70\x72\x6f\x70\x46\x69\x78\ +\x2e\x65\x6e\x63\x74\x79\x70\x65\x3d\x22\x65\x6e\x63\x6f\x64\x69\ +\x6e\x67\x22\x29\x2c\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\x63\ +\x68\x65\x63\x6b\x4f\x6e\x7c\x7c\x66\x2e\x65\x61\x63\x68\x28\x5b\ +\x22\x72\x61\x64\x69\x6f\x22\x2c\x22\x63\x68\x65\x63\x6b\x62\x6f\ +\x78\x22\x5d\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x66\ +\x2e\x76\x61\x6c\x48\x6f\x6f\x6b\x73\x5b\x74\x68\x69\x73\x5d\x3d\ +\x7b\x67\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\ +\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x67\x65\x74\x41\x74\x74\ +\x72\x69\x62\x75\x74\x65\x28\x22\x76\x61\x6c\x75\x65\x22\x29\x3d\ +\x3d\x3d\x6e\x75\x6c\x6c\x3f\x22\x6f\x6e\x22\x3a\x61\x2e\x76\x61\ +\x6c\x75\x65\x7d\x7d\x7d\x29\x2c\x66\x2e\x65\x61\x63\x68\x28\x5b\ +\x22\x72\x61\x64\x69\x6f\x22\x2c\x22\x63\x68\x65\x63\x6b\x62\x6f\ +\x78\x22\x5d\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x66\ +\x2e\x76\x61\x6c\x48\x6f\x6f\x6b\x73\x5b\x74\x68\x69\x73\x5d\x3d\ +\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x66\x2e\x76\x61\x6c\x48\x6f\ +\x6f\x6b\x73\x5b\x74\x68\x69\x73\x5d\x2c\x7b\x73\x65\x74\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x69\x66\x28\ +\x66\x2e\x69\x73\x41\x72\x72\x61\x79\x28\x62\x29\x29\x72\x65\x74\ +\x75\x72\x6e\x20\x61\x2e\x63\x68\x65\x63\x6b\x65\x64\x3d\x66\x2e\ +\x69\x6e\x41\x72\x72\x61\x79\x28\x66\x28\x61\x29\x2e\x76\x61\x6c\ +\x28\x29\x2c\x62\x29\x3e\x3d\x30\x7d\x7d\x29\x7d\x29\x3b\x76\x61\ +\x72\x20\x7a\x3d\x2f\x5e\x28\x3f\x3a\x74\x65\x78\x74\x61\x72\x65\ +\x61\x7c\x69\x6e\x70\x75\x74\x7c\x73\x65\x6c\x65\x63\x74\x29\x24\ +\x2f\x69\x2c\x41\x3d\x2f\x5e\x28\x5b\x5e\x5c\x2e\x5d\x2a\x29\x3f\ +\x28\x3f\x3a\x5c\x2e\x28\x2e\x2b\x29\x29\x3f\x24\x2f\x2c\x42\x3d\ +\x2f\x5c\x62\x68\x6f\x76\x65\x72\x28\x5c\x2e\x5c\x53\x2b\x29\x3f\ +\x5c\x62\x2f\x2c\x43\x3d\x2f\x5e\x6b\x65\x79\x2f\x2c\x44\x3d\x2f\ +\x5e\x28\x3f\x3a\x6d\x6f\x75\x73\x65\x7c\x63\x6f\x6e\x74\x65\x78\ +\x74\x6d\x65\x6e\x75\x29\x7c\x63\x6c\x69\x63\x6b\x2f\x2c\x45\x3d\ +\x2f\x5e\x28\x3f\x3a\x66\x6f\x63\x75\x73\x69\x6e\x66\x6f\x63\x75\ +\x73\x7c\x66\x6f\x63\x75\x73\x6f\x75\x74\x62\x6c\x75\x72\x29\x24\ +\x2f\x2c\x46\x3d\x2f\x5e\x28\x5c\x77\x2a\x29\x28\x3f\x3a\x23\x28\ +\x5b\x5c\x77\x5c\x2d\x5d\x2b\x29\x29\x3f\x28\x3f\x3a\x5c\x2e\x28\ +\x5b\x5c\x77\x5c\x2d\x5d\x2b\x29\x29\x3f\x24\x2f\x2c\x47\x3d\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\ +\x3d\x46\x2e\x65\x78\x65\x63\x28\x61\x29\x3b\x62\x26\x26\x28\x62\ +\x5b\x31\x5d\x3d\x28\x62\x5b\x31\x5d\x7c\x7c\x22\x22\x29\x2e\x74\ +\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x2c\x62\x5b\x33\ +\x5d\x3d\x62\x5b\x33\x5d\x26\x26\x6e\x65\x77\x20\x52\x65\x67\x45\ +\x78\x70\x28\x22\x28\x3f\x3a\x5e\x7c\x5c\x5c\x73\x29\x22\x2b\x62\ +\x5b\x33\x5d\x2b\x22\x28\x3f\x3a\x5c\x5c\x73\x7c\x24\x29\x22\x29\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x62\x7d\x2c\x48\x3d\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\ +\x63\x3d\x61\x2e\x61\x74\x74\x72\x69\x62\x75\x74\x65\x73\x7c\x7c\ +\x7b\x7d\x3b\x72\x65\x74\x75\x72\x6e\x28\x21\x62\x5b\x31\x5d\x7c\ +\x7c\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\x6f\ +\x77\x65\x72\x43\x61\x73\x65\x28\x29\x3d\x3d\x3d\x62\x5b\x31\x5d\ +\x29\x26\x26\x28\x21\x62\x5b\x32\x5d\x7c\x7c\x28\x63\x2e\x69\x64\ +\x7c\x7c\x7b\x7d\x29\x2e\x76\x61\x6c\x75\x65\x3d\x3d\x3d\x62\x5b\ +\x32\x5d\x29\x26\x26\x28\x21\x62\x5b\x33\x5d\x7c\x7c\x62\x5b\x33\ +\x5d\x2e\x74\x65\x73\x74\x28\x28\x63\x5b\x22\x63\x6c\x61\x73\x73\ +\x22\x5d\x7c\x7c\x7b\x7d\x29\x2e\x76\x61\x6c\x75\x65\x29\x29\x7d\ +\x2c\x49\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\ +\x65\x74\x75\x72\x6e\x20\x66\x2e\x65\x76\x65\x6e\x74\x2e\x73\x70\ +\x65\x63\x69\x61\x6c\x2e\x68\x6f\x76\x65\x72\x3f\x61\x3a\x61\x2e\ +\x72\x65\x70\x6c\x61\x63\x65\x28\x42\x2c\x22\x6d\x6f\x75\x73\x65\ +\x65\x6e\x74\x65\x72\x24\x31\x20\x6d\x6f\x75\x73\x65\x6c\x65\x61\ +\x76\x65\x24\x31\x22\x29\x7d\x3b\x0a\x66\x2e\x65\x76\x65\x6e\x74\ +\x3d\x7b\x61\x64\x64\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x63\x2c\x64\x2c\x65\x2c\x67\x29\x7b\x76\x61\x72\x20\x68\x2c\ +\x69\x2c\x6a\x2c\x6b\x2c\x6c\x2c\x6d\x2c\x6e\x2c\x6f\x2c\x70\x2c\ +\x71\x2c\x72\x2c\x73\x3b\x69\x66\x28\x21\x28\x61\x2e\x6e\x6f\x64\ +\x65\x54\x79\x70\x65\x3d\x3d\x3d\x33\x7c\x7c\x61\x2e\x6e\x6f\x64\ +\x65\x54\x79\x70\x65\x3d\x3d\x3d\x38\x7c\x7c\x21\x63\x7c\x7c\x21\ +\x64\x7c\x7c\x21\x28\x68\x3d\x66\x2e\x5f\x64\x61\x74\x61\x28\x61\ +\x29\x29\x29\x29\x7b\x64\x2e\x68\x61\x6e\x64\x6c\x65\x72\x26\x26\ +\x28\x70\x3d\x64\x2c\x64\x3d\x70\x2e\x68\x61\x6e\x64\x6c\x65\x72\ +\x29\x2c\x64\x2e\x67\x75\x69\x64\x7c\x7c\x28\x64\x2e\x67\x75\x69\ +\x64\x3d\x66\x2e\x67\x75\x69\x64\x2b\x2b\x29\x2c\x6a\x3d\x68\x2e\ +\x65\x76\x65\x6e\x74\x73\x2c\x6a\x7c\x7c\x28\x68\x2e\x65\x76\x65\ +\x6e\x74\x73\x3d\x6a\x3d\x7b\x7d\x29\x2c\x69\x3d\x68\x2e\x68\x61\ +\x6e\x64\x6c\x65\x2c\x69\x7c\x7c\x28\x68\x2e\x68\x61\x6e\x64\x6c\ +\x65\x3d\x69\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\ +\x72\x65\x74\x75\x72\x6e\x20\x74\x79\x70\x65\x6f\x66\x20\x66\x21\ +\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\x65\x64\x22\x26\x26\x28\x21\ +\x61\x7c\x7c\x66\x2e\x65\x76\x65\x6e\x74\x2e\x74\x72\x69\x67\x67\ +\x65\x72\x65\x64\x21\x3d\x3d\x61\x2e\x74\x79\x70\x65\x29\x3f\x66\ +\x2e\x65\x76\x65\x6e\x74\x2e\x64\x69\x73\x70\x61\x74\x63\x68\x2e\ +\x61\x70\x70\x6c\x79\x28\x69\x2e\x65\x6c\x65\x6d\x2c\x61\x72\x67\ +\x75\x6d\x65\x6e\x74\x73\x29\x3a\x62\x7d\x2c\x69\x2e\x65\x6c\x65\ +\x6d\x3d\x61\x29\x2c\x63\x3d\x66\x2e\x74\x72\x69\x6d\x28\x49\x28\ +\x63\x29\x29\x2e\x73\x70\x6c\x69\x74\x28\x22\x20\x22\x29\x3b\x66\ +\x6f\x72\x28\x6b\x3d\x30\x3b\x6b\x3c\x63\x2e\x6c\x65\x6e\x67\x74\ +\x68\x3b\x6b\x2b\x2b\x29\x7b\x6c\x3d\x41\x2e\x65\x78\x65\x63\x28\ +\x63\x5b\x6b\x5d\x29\x7c\x7c\x5b\x5d\x2c\x6d\x3d\x6c\x5b\x31\x5d\ +\x2c\x6e\x3d\x28\x6c\x5b\x32\x5d\x7c\x7c\x22\x22\x29\x2e\x73\x70\ +\x6c\x69\x74\x28\x22\x2e\x22\x29\x2e\x73\x6f\x72\x74\x28\x29\x2c\ +\x73\x3d\x66\x2e\x65\x76\x65\x6e\x74\x2e\x73\x70\x65\x63\x69\x61\ +\x6c\x5b\x6d\x5d\x7c\x7c\x7b\x7d\x2c\x6d\x3d\x28\x67\x3f\x73\x2e\ +\x64\x65\x6c\x65\x67\x61\x74\x65\x54\x79\x70\x65\x3a\x73\x2e\x62\ +\x69\x6e\x64\x54\x79\x70\x65\x29\x7c\x7c\x6d\x2c\x73\x3d\x66\x2e\ +\x65\x76\x65\x6e\x74\x2e\x73\x70\x65\x63\x69\x61\x6c\x5b\x6d\x5d\ +\x7c\x7c\x7b\x7d\x2c\x6f\x3d\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\ +\x7b\x74\x79\x70\x65\x3a\x6d\x2c\x6f\x72\x69\x67\x54\x79\x70\x65\ +\x3a\x6c\x5b\x31\x5d\x2c\x64\x61\x74\x61\x3a\x65\x2c\x68\x61\x6e\ +\x64\x6c\x65\x72\x3a\x64\x2c\x67\x75\x69\x64\x3a\x64\x2e\x67\x75\ +\x69\x64\x2c\x73\x65\x6c\x65\x63\x74\x6f\x72\x3a\x67\x2c\x71\x75\ +\x69\x63\x6b\x3a\x47\x28\x67\x29\x2c\x6e\x61\x6d\x65\x73\x70\x61\ +\x63\x65\x3a\x6e\x2e\x6a\x6f\x69\x6e\x28\x22\x2e\x22\x29\x7d\x2c\ +\x70\x29\x2c\x72\x3d\x6a\x5b\x6d\x5d\x3b\x69\x66\x28\x21\x72\x29\ +\x7b\x72\x3d\x6a\x5b\x6d\x5d\x3d\x5b\x5d\x2c\x72\x2e\x64\x65\x6c\ +\x65\x67\x61\x74\x65\x43\x6f\x75\x6e\x74\x3d\x30\x3b\x69\x66\x28\ +\x21\x73\x2e\x73\x65\x74\x75\x70\x7c\x7c\x73\x2e\x73\x65\x74\x75\ +\x70\x2e\x63\x61\x6c\x6c\x28\x61\x2c\x65\x2c\x6e\x2c\x69\x29\x3d\ +\x3d\x3d\x21\x31\x29\x61\x2e\x61\x64\x64\x45\x76\x65\x6e\x74\x4c\ +\x69\x73\x74\x65\x6e\x65\x72\x3f\x61\x2e\x61\x64\x64\x45\x76\x65\ +\x6e\x74\x4c\x69\x73\x74\x65\x6e\x65\x72\x28\x6d\x2c\x69\x2c\x21\ +\x31\x29\x3a\x61\x2e\x61\x74\x74\x61\x63\x68\x45\x76\x65\x6e\x74\ +\x26\x26\x61\x2e\x61\x74\x74\x61\x63\x68\x45\x76\x65\x6e\x74\x28\ +\x22\x6f\x6e\x22\x2b\x6d\x2c\x69\x29\x7d\x73\x2e\x61\x64\x64\x26\ +\x26\x28\x73\x2e\x61\x64\x64\x2e\x63\x61\x6c\x6c\x28\x61\x2c\x6f\ +\x29\x2c\x6f\x2e\x68\x61\x6e\x64\x6c\x65\x72\x2e\x67\x75\x69\x64\ +\x7c\x7c\x28\x6f\x2e\x68\x61\x6e\x64\x6c\x65\x72\x2e\x67\x75\x69\ +\x64\x3d\x64\x2e\x67\x75\x69\x64\x29\x29\x2c\x67\x3f\x72\x2e\x73\ +\x70\x6c\x69\x63\x65\x28\x72\x2e\x64\x65\x6c\x65\x67\x61\x74\x65\ +\x43\x6f\x75\x6e\x74\x2b\x2b\x2c\x30\x2c\x6f\x29\x3a\x72\x2e\x70\ +\x75\x73\x68\x28\x6f\x29\x2c\x66\x2e\x65\x76\x65\x6e\x74\x2e\x67\ +\x6c\x6f\x62\x61\x6c\x5b\x6d\x5d\x3d\x21\x30\x7d\x61\x3d\x6e\x75\ +\x6c\x6c\x7d\x7d\x2c\x67\x6c\x6f\x62\x61\x6c\x3a\x7b\x7d\x2c\x72\ +\x65\x6d\x6f\x76\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x62\x2c\x63\x2c\x64\x2c\x65\x29\x7b\x76\x61\x72\x20\x67\x3d\ +\x66\x2e\x68\x61\x73\x44\x61\x74\x61\x28\x61\x29\x26\x26\x66\x2e\ +\x5f\x64\x61\x74\x61\x28\x61\x29\x2c\x68\x2c\x69\x2c\x6a\x2c\x6b\ +\x2c\x6c\x2c\x6d\x2c\x6e\x2c\x6f\x2c\x70\x2c\x71\x2c\x72\x2c\x73\ +\x3b\x69\x66\x28\x21\x21\x67\x26\x26\x21\x21\x28\x6f\x3d\x67\x2e\ +\x65\x76\x65\x6e\x74\x73\x29\x29\x7b\x62\x3d\x66\x2e\x74\x72\x69\ +\x6d\x28\x49\x28\x62\x7c\x7c\x22\x22\x29\x29\x2e\x73\x70\x6c\x69\ +\x74\x28\x22\x20\x22\x29\x3b\x66\x6f\x72\x28\x68\x3d\x30\x3b\x68\ +\x3c\x62\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x68\x2b\x2b\x29\x7b\x69\ +\x3d\x41\x2e\x65\x78\x65\x63\x28\x62\x5b\x68\x5d\x29\x7c\x7c\x5b\ +\x5d\x2c\x6a\x3d\x6b\x3d\x69\x5b\x31\x5d\x2c\x6c\x3d\x69\x5b\x32\ +\x5d\x3b\x69\x66\x28\x21\x6a\x29\x7b\x66\x6f\x72\x28\x6a\x20\x69\ +\x6e\x20\x6f\x29\x66\x2e\x65\x76\x65\x6e\x74\x2e\x72\x65\x6d\x6f\ +\x76\x65\x28\x61\x2c\x6a\x2b\x62\x5b\x68\x5d\x2c\x63\x2c\x64\x2c\ +\x21\x30\x29\x3b\x63\x6f\x6e\x74\x69\x6e\x75\x65\x7d\x70\x3d\x66\ +\x2e\x65\x76\x65\x6e\x74\x2e\x73\x70\x65\x63\x69\x61\x6c\x5b\x6a\ +\x5d\x7c\x7c\x7b\x7d\x2c\x6a\x3d\x28\x64\x3f\x70\x2e\x64\x65\x6c\ +\x65\x67\x61\x74\x65\x54\x79\x70\x65\x3a\x70\x2e\x62\x69\x6e\x64\ +\x54\x79\x70\x65\x29\x7c\x7c\x6a\x2c\x72\x3d\x6f\x5b\x6a\x5d\x7c\ +\x7c\x5b\x5d\x2c\x6d\x3d\x72\x2e\x6c\x65\x6e\x67\x74\x68\x2c\x6c\ +\x3d\x6c\x3f\x6e\x65\x77\x20\x52\x65\x67\x45\x78\x70\x28\x22\x28\ +\x5e\x7c\x5c\x5c\x2e\x29\x22\x2b\x6c\x2e\x73\x70\x6c\x69\x74\x28\ +\x22\x2e\x22\x29\x2e\x73\x6f\x72\x74\x28\x29\x2e\x6a\x6f\x69\x6e\ +\x28\x22\x5c\x5c\x2e\x28\x3f\x3a\x2e\x2a\x5c\x5c\x2e\x29\x3f\x22\ +\x29\x2b\x22\x28\x5c\x5c\x2e\x7c\x24\x29\x22\x29\x3a\x6e\x75\x6c\ +\x6c\x3b\x66\x6f\x72\x28\x6e\x3d\x30\x3b\x6e\x3c\x72\x2e\x6c\x65\ +\x6e\x67\x74\x68\x3b\x6e\x2b\x2b\x29\x73\x3d\x72\x5b\x6e\x5d\x2c\ +\x28\x65\x7c\x7c\x6b\x3d\x3d\x3d\x73\x2e\x6f\x72\x69\x67\x54\x79\ +\x70\x65\x29\x26\x26\x28\x21\x63\x7c\x7c\x63\x2e\x67\x75\x69\x64\ +\x3d\x3d\x3d\x73\x2e\x67\x75\x69\x64\x29\x26\x26\x28\x21\x6c\x7c\ +\x7c\x6c\x2e\x74\x65\x73\x74\x28\x73\x2e\x6e\x61\x6d\x65\x73\x70\ +\x61\x63\x65\x29\x29\x26\x26\x28\x21\x64\x7c\x7c\x64\x3d\x3d\x3d\ +\x73\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x7c\x7c\x64\x3d\x3d\x3d\ +\x22\x2a\x2a\x22\x26\x26\x73\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\ +\x29\x26\x26\x28\x72\x2e\x73\x70\x6c\x69\x63\x65\x28\x6e\x2d\x2d\ +\x2c\x31\x29\x2c\x73\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x26\x26\ +\x72\x2e\x64\x65\x6c\x65\x67\x61\x74\x65\x43\x6f\x75\x6e\x74\x2d\ +\x2d\x2c\x70\x2e\x72\x65\x6d\x6f\x76\x65\x26\x26\x70\x2e\x72\x65\ +\x6d\x6f\x76\x65\x2e\x63\x61\x6c\x6c\x28\x61\x2c\x73\x29\x29\x3b\ +\x72\x2e\x6c\x65\x6e\x67\x74\x68\x3d\x3d\x3d\x30\x26\x26\x6d\x21\ +\x3d\x3d\x72\x2e\x6c\x65\x6e\x67\x74\x68\x26\x26\x28\x28\x21\x70\ +\x2e\x74\x65\x61\x72\x64\x6f\x77\x6e\x7c\x7c\x70\x2e\x74\x65\x61\ +\x72\x64\x6f\x77\x6e\x2e\x63\x61\x6c\x6c\x28\x61\x2c\x6c\x29\x3d\ +\x3d\x3d\x21\x31\x29\x26\x26\x66\x2e\x72\x65\x6d\x6f\x76\x65\x45\ +\x76\x65\x6e\x74\x28\x61\x2c\x6a\x2c\x67\x2e\x68\x61\x6e\x64\x6c\ +\x65\x29\x2c\x64\x65\x6c\x65\x74\x65\x20\x6f\x5b\x6a\x5d\x29\x7d\ +\x66\x2e\x69\x73\x45\x6d\x70\x74\x79\x4f\x62\x6a\x65\x63\x74\x28\ +\x6f\x29\x26\x26\x28\x71\x3d\x67\x2e\x68\x61\x6e\x64\x6c\x65\x2c\ +\x71\x26\x26\x28\x71\x2e\x65\x6c\x65\x6d\x3d\x6e\x75\x6c\x6c\x29\ +\x2c\x66\x2e\x72\x65\x6d\x6f\x76\x65\x44\x61\x74\x61\x28\x61\x2c\ +\x5b\x22\x65\x76\x65\x6e\x74\x73\x22\x2c\x22\x68\x61\x6e\x64\x6c\ +\x65\x22\x5d\x2c\x21\x30\x29\x29\x7d\x7d\x2c\x63\x75\x73\x74\x6f\ +\x6d\x45\x76\x65\x6e\x74\x3a\x7b\x67\x65\x74\x44\x61\x74\x61\x3a\ +\x21\x30\x2c\x73\x65\x74\x44\x61\x74\x61\x3a\x21\x30\x2c\x63\x68\ +\x61\x6e\x67\x65\x44\x61\x74\x61\x3a\x21\x30\x7d\x2c\x74\x72\x69\ +\x67\x67\x65\x72\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x63\x2c\ +\x64\x2c\x65\x2c\x67\x29\x7b\x69\x66\x28\x21\x65\x7c\x7c\x65\x2e\ +\x6e\x6f\x64\x65\x54\x79\x70\x65\x21\x3d\x3d\x33\x26\x26\x65\x2e\ +\x6e\x6f\x64\x65\x54\x79\x70\x65\x21\x3d\x3d\x38\x29\x7b\x76\x61\ +\x72\x20\x68\x3d\x63\x2e\x74\x79\x70\x65\x7c\x7c\x63\x2c\x69\x3d\ +\x5b\x5d\x2c\x6a\x2c\x6b\x2c\x6c\x2c\x6d\x2c\x6e\x2c\x6f\x2c\x70\ +\x2c\x71\x2c\x72\x2c\x73\x3b\x69\x66\x28\x45\x2e\x74\x65\x73\x74\ +\x28\x68\x2b\x66\x2e\x65\x76\x65\x6e\x74\x2e\x74\x72\x69\x67\x67\ +\x65\x72\x65\x64\x29\x29\x72\x65\x74\x75\x72\x6e\x3b\x68\x2e\x69\ +\x6e\x64\x65\x78\x4f\x66\x28\x22\x21\x22\x29\x3e\x3d\x30\x26\x26\ +\x28\x68\x3d\x68\x2e\x73\x6c\x69\x63\x65\x28\x30\x2c\x2d\x31\x29\ +\x2c\x6b\x3d\x21\x30\x29\x2c\x68\x2e\x69\x6e\x64\x65\x78\x4f\x66\ +\x28\x22\x2e\x22\x29\x3e\x3d\x30\x26\x26\x28\x69\x3d\x68\x2e\x73\ +\x70\x6c\x69\x74\x28\x22\x2e\x22\x29\x2c\x68\x3d\x69\x2e\x73\x68\ +\x69\x66\x74\x28\x29\x2c\x69\x2e\x73\x6f\x72\x74\x28\x29\x29\x3b\ +\x69\x66\x28\x28\x21\x65\x7c\x7c\x66\x2e\x65\x76\x65\x6e\x74\x2e\ +\x63\x75\x73\x74\x6f\x6d\x45\x76\x65\x6e\x74\x5b\x68\x5d\x29\x26\ +\x26\x21\x66\x2e\x65\x76\x65\x6e\x74\x2e\x67\x6c\x6f\x62\x61\x6c\ +\x5b\x68\x5d\x29\x72\x65\x74\x75\x72\x6e\x3b\x63\x3d\x74\x79\x70\ +\x65\x6f\x66\x20\x63\x3d\x3d\x22\x6f\x62\x6a\x65\x63\x74\x22\x3f\ +\x63\x5b\x66\x2e\x65\x78\x70\x61\x6e\x64\x6f\x5d\x3f\x63\x3a\x6e\ +\x65\x77\x20\x66\x2e\x45\x76\x65\x6e\x74\x28\x68\x2c\x63\x29\x3a\ +\x6e\x65\x77\x20\x66\x2e\x45\x76\x65\x6e\x74\x28\x68\x29\x2c\x63\ +\x2e\x74\x79\x70\x65\x3d\x68\x2c\x63\x2e\x69\x73\x54\x72\x69\x67\ +\x67\x65\x72\x3d\x21\x30\x2c\x63\x2e\x65\x78\x63\x6c\x75\x73\x69\ +\x76\x65\x3d\x6b\x2c\x63\x2e\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\ +\x3d\x69\x2e\x6a\x6f\x69\x6e\x28\x22\x2e\x22\x29\x2c\x63\x2e\x6e\ +\x61\x6d\x65\x73\x70\x61\x63\x65\x5f\x72\x65\x3d\x63\x2e\x6e\x61\ +\x6d\x65\x73\x70\x61\x63\x65\x3f\x6e\x65\x77\x20\x52\x65\x67\x45\ +\x78\x70\x28\x22\x28\x5e\x7c\x5c\x5c\x2e\x29\x22\x2b\x69\x2e\x6a\ +\x6f\x69\x6e\x28\x22\x5c\x5c\x2e\x28\x3f\x3a\x2e\x2a\x5c\x5c\x2e\ +\x29\x3f\x22\x29\x2b\x22\x28\x5c\x5c\x2e\x7c\x24\x29\x22\x29\x3a\ +\x6e\x75\x6c\x6c\x2c\x6f\x3d\x68\x2e\x69\x6e\x64\x65\x78\x4f\x66\ +\x28\x22\x3a\x22\x29\x3c\x30\x3f\x22\x6f\x6e\x22\x2b\x68\x3a\x22\ +\x22\x3b\x69\x66\x28\x21\x65\x29\x7b\x6a\x3d\x66\x2e\x63\x61\x63\ +\x68\x65\x3b\x66\x6f\x72\x28\x6c\x20\x69\x6e\x20\x6a\x29\x6a\x5b\ +\x6c\x5d\x2e\x65\x76\x65\x6e\x74\x73\x26\x26\x6a\x5b\x6c\x5d\x2e\ +\x65\x76\x65\x6e\x74\x73\x5b\x68\x5d\x26\x26\x66\x2e\x65\x76\x65\ +\x6e\x74\x2e\x74\x72\x69\x67\x67\x65\x72\x28\x63\x2c\x64\x2c\x6a\ +\x5b\x6c\x5d\x2e\x68\x61\x6e\x64\x6c\x65\x2e\x65\x6c\x65\x6d\x2c\ +\x21\x30\x29\x3b\x72\x65\x74\x75\x72\x6e\x7d\x63\x2e\x72\x65\x73\ +\x75\x6c\x74\x3d\x62\x2c\x63\x2e\x74\x61\x72\x67\x65\x74\x7c\x7c\ +\x28\x63\x2e\x74\x61\x72\x67\x65\x74\x3d\x65\x29\x2c\x64\x3d\x64\ +\x21\x3d\x6e\x75\x6c\x6c\x3f\x66\x2e\x6d\x61\x6b\x65\x41\x72\x72\ +\x61\x79\x28\x64\x29\x3a\x5b\x5d\x2c\x64\x2e\x75\x6e\x73\x68\x69\ +\x66\x74\x28\x63\x29\x2c\x70\x3d\x66\x2e\x65\x76\x65\x6e\x74\x2e\ +\x73\x70\x65\x63\x69\x61\x6c\x5b\x68\x5d\x7c\x7c\x7b\x7d\x3b\x69\ +\x66\x28\x70\x2e\x74\x72\x69\x67\x67\x65\x72\x26\x26\x70\x2e\x74\ +\x72\x69\x67\x67\x65\x72\x2e\x61\x70\x70\x6c\x79\x28\x65\x2c\x64\ +\x29\x3d\x3d\x3d\x21\x31\x29\x72\x65\x74\x75\x72\x6e\x3b\x72\x3d\ +\x5b\x5b\x65\x2c\x70\x2e\x62\x69\x6e\x64\x54\x79\x70\x65\x7c\x7c\ +\x68\x5d\x5d\x3b\x69\x66\x28\x21\x67\x26\x26\x21\x70\x2e\x6e\x6f\ +\x42\x75\x62\x62\x6c\x65\x26\x26\x21\x66\x2e\x69\x73\x57\x69\x6e\ +\x64\x6f\x77\x28\x65\x29\x29\x7b\x73\x3d\x70\x2e\x64\x65\x6c\x65\ +\x67\x61\x74\x65\x54\x79\x70\x65\x7c\x7c\x68\x2c\x6d\x3d\x45\x2e\ +\x74\x65\x73\x74\x28\x73\x2b\x68\x29\x3f\x65\x3a\x65\x2e\x70\x61\ +\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2c\x6e\x3d\x6e\x75\x6c\x6c\x3b\ +\x66\x6f\x72\x28\x3b\x6d\x3b\x6d\x3d\x6d\x2e\x70\x61\x72\x65\x6e\ +\x74\x4e\x6f\x64\x65\x29\x72\x2e\x70\x75\x73\x68\x28\x5b\x6d\x2c\ +\x73\x5d\x29\x2c\x6e\x3d\x6d\x3b\x6e\x26\x26\x6e\x3d\x3d\x3d\x65\ +\x2e\x6f\x77\x6e\x65\x72\x44\x6f\x63\x75\x6d\x65\x6e\x74\x26\x26\ +\x72\x2e\x70\x75\x73\x68\x28\x5b\x6e\x2e\x64\x65\x66\x61\x75\x6c\ +\x74\x56\x69\x65\x77\x7c\x7c\x6e\x2e\x70\x61\x72\x65\x6e\x74\x57\ +\x69\x6e\x64\x6f\x77\x7c\x7c\x61\x2c\x73\x5d\x29\x7d\x66\x6f\x72\ +\x28\x6c\x3d\x30\x3b\x6c\x3c\x72\x2e\x6c\x65\x6e\x67\x74\x68\x26\ +\x26\x21\x63\x2e\x69\x73\x50\x72\x6f\x70\x61\x67\x61\x74\x69\x6f\ +\x6e\x53\x74\x6f\x70\x70\x65\x64\x28\x29\x3b\x6c\x2b\x2b\x29\x6d\ +\x3d\x72\x5b\x6c\x5d\x5b\x30\x5d\x2c\x63\x2e\x74\x79\x70\x65\x3d\ +\x72\x5b\x6c\x5d\x5b\x31\x5d\x2c\x71\x3d\x28\x66\x2e\x5f\x64\x61\ +\x74\x61\x28\x6d\x2c\x22\x65\x76\x65\x6e\x74\x73\x22\x29\x7c\x7c\ +\x7b\x7d\x29\x5b\x63\x2e\x74\x79\x70\x65\x5d\x26\x26\x66\x2e\x5f\ +\x64\x61\x74\x61\x28\x6d\x2c\x22\x68\x61\x6e\x64\x6c\x65\x22\x29\ +\x2c\x71\x26\x26\x71\x2e\x61\x70\x70\x6c\x79\x28\x6d\x2c\x64\x29\ +\x2c\x71\x3d\x6f\x26\x26\x6d\x5b\x6f\x5d\x2c\x71\x26\x26\x66\x2e\ +\x61\x63\x63\x65\x70\x74\x44\x61\x74\x61\x28\x6d\x29\x26\x26\x71\ +\x2e\x61\x70\x70\x6c\x79\x28\x6d\x2c\x64\x29\x3d\x3d\x3d\x21\x31\ +\x26\x26\x63\x2e\x70\x72\x65\x76\x65\x6e\x74\x44\x65\x66\x61\x75\ +\x6c\x74\x28\x29\x3b\x63\x2e\x74\x79\x70\x65\x3d\x68\x2c\x21\x67\ +\x26\x26\x21\x63\x2e\x69\x73\x44\x65\x66\x61\x75\x6c\x74\x50\x72\ +\x65\x76\x65\x6e\x74\x65\x64\x28\x29\x26\x26\x28\x21\x70\x2e\x5f\ +\x64\x65\x66\x61\x75\x6c\x74\x7c\x7c\x70\x2e\x5f\x64\x65\x66\x61\ +\x75\x6c\x74\x2e\x61\x70\x70\x6c\x79\x28\x65\x2e\x6f\x77\x6e\x65\ +\x72\x44\x6f\x63\x75\x6d\x65\x6e\x74\x2c\x64\x29\x3d\x3d\x3d\x21\ +\x31\x29\x26\x26\x28\x68\x21\x3d\x3d\x22\x63\x6c\x69\x63\x6b\x22\ +\x7c\x7c\x21\x66\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x28\x65\x2c\ +\x22\x61\x22\x29\x29\x26\x26\x66\x2e\x61\x63\x63\x65\x70\x74\x44\ +\x61\x74\x61\x28\x65\x29\x26\x26\x6f\x26\x26\x65\x5b\x68\x5d\x26\ +\x26\x28\x68\x21\x3d\x3d\x22\x66\x6f\x63\x75\x73\x22\x26\x26\x68\ +\x21\x3d\x3d\x22\x62\x6c\x75\x72\x22\x7c\x7c\x63\x2e\x74\x61\x72\ +\x67\x65\x74\x2e\x6f\x66\x66\x73\x65\x74\x57\x69\x64\x74\x68\x21\ +\x3d\x3d\x30\x29\x26\x26\x21\x66\x2e\x69\x73\x57\x69\x6e\x64\x6f\ +\x77\x28\x65\x29\x26\x26\x28\x6e\x3d\x65\x5b\x6f\x5d\x2c\x6e\x26\ +\x26\x28\x65\x5b\x6f\x5d\x3d\x6e\x75\x6c\x6c\x29\x2c\x66\x2e\x65\ +\x76\x65\x6e\x74\x2e\x74\x72\x69\x67\x67\x65\x72\x65\x64\x3d\x68\ +\x2c\x65\x5b\x68\x5d\x28\x29\x2c\x66\x2e\x65\x76\x65\x6e\x74\x2e\ +\x74\x72\x69\x67\x67\x65\x72\x65\x64\x3d\x62\x2c\x6e\x26\x26\x28\ +\x65\x5b\x6f\x5d\x3d\x6e\x29\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\ +\x63\x2e\x72\x65\x73\x75\x6c\x74\x7d\x7d\x2c\x64\x69\x73\x70\x61\ +\x74\x63\x68\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x63\x29\x7b\ +\x63\x3d\x66\x2e\x65\x76\x65\x6e\x74\x2e\x66\x69\x78\x28\x63\x7c\ +\x7c\x61\x2e\x65\x76\x65\x6e\x74\x29\x3b\x76\x61\x72\x20\x64\x3d\ +\x28\x66\x2e\x5f\x64\x61\x74\x61\x28\x74\x68\x69\x73\x2c\x22\x65\ +\x76\x65\x6e\x74\x73\x22\x29\x7c\x7c\x7b\x7d\x29\x5b\x63\x2e\x74\ +\x79\x70\x65\x5d\x7c\x7c\x5b\x5d\x2c\x65\x3d\x64\x2e\x64\x65\x6c\ +\x65\x67\x61\x74\x65\x43\x6f\x75\x6e\x74\x2c\x67\x3d\x5b\x5d\x2e\ +\x73\x6c\x69\x63\x65\x2e\x63\x61\x6c\x6c\x28\x61\x72\x67\x75\x6d\ +\x65\x6e\x74\x73\x2c\x30\x29\x2c\x68\x3d\x21\x63\x2e\x65\x78\x63\ +\x6c\x75\x73\x69\x76\x65\x26\x26\x21\x63\x2e\x6e\x61\x6d\x65\x73\ +\x70\x61\x63\x65\x2c\x69\x3d\x5b\x5d\x2c\x6a\x2c\x6b\x2c\x6c\x2c\ +\x6d\x2c\x6e\x2c\x6f\x2c\x70\x2c\x71\x2c\x72\x2c\x73\x2c\x74\x3b\ +\x67\x5b\x30\x5d\x3d\x63\x2c\x63\x2e\x64\x65\x6c\x65\x67\x61\x74\ +\x65\x54\x61\x72\x67\x65\x74\x3d\x74\x68\x69\x73\x3b\x69\x66\x28\ +\x65\x26\x26\x21\x63\x2e\x74\x61\x72\x67\x65\x74\x2e\x64\x69\x73\ +\x61\x62\x6c\x65\x64\x26\x26\x28\x21\x63\x2e\x62\x75\x74\x74\x6f\ +\x6e\x7c\x7c\x63\x2e\x74\x79\x70\x65\x21\x3d\x3d\x22\x63\x6c\x69\ +\x63\x6b\x22\x29\x29\x7b\x6d\x3d\x66\x28\x74\x68\x69\x73\x29\x2c\ +\x6d\x2e\x63\x6f\x6e\x74\x65\x78\x74\x3d\x74\x68\x69\x73\x2e\x6f\ +\x77\x6e\x65\x72\x44\x6f\x63\x75\x6d\x65\x6e\x74\x7c\x7c\x74\x68\ +\x69\x73\x3b\x66\x6f\x72\x28\x6c\x3d\x63\x2e\x74\x61\x72\x67\x65\ +\x74\x3b\x6c\x21\x3d\x74\x68\x69\x73\x3b\x6c\x3d\x6c\x2e\x70\x61\ +\x72\x65\x6e\x74\x4e\x6f\x64\x65\x7c\x7c\x74\x68\x69\x73\x29\x7b\ +\x6f\x3d\x7b\x7d\x2c\x71\x3d\x5b\x5d\x2c\x6d\x5b\x30\x5d\x3d\x6c\ +\x3b\x66\x6f\x72\x28\x6a\x3d\x30\x3b\x6a\x3c\x65\x3b\x6a\x2b\x2b\ +\x29\x72\x3d\x64\x5b\x6a\x5d\x2c\x73\x3d\x72\x2e\x73\x65\x6c\x65\ +\x63\x74\x6f\x72\x2c\x6f\x5b\x73\x5d\x3d\x3d\x3d\x62\x26\x26\x28\ +\x6f\x5b\x73\x5d\x3d\x72\x2e\x71\x75\x69\x63\x6b\x3f\x48\x28\x6c\ +\x2c\x72\x2e\x71\x75\x69\x63\x6b\x29\x3a\x6d\x2e\x69\x73\x28\x73\ +\x29\x29\x2c\x6f\x5b\x73\x5d\x26\x26\x71\x2e\x70\x75\x73\x68\x28\ +\x72\x29\x3b\x71\x2e\x6c\x65\x6e\x67\x74\x68\x26\x26\x69\x2e\x70\ +\x75\x73\x68\x28\x7b\x65\x6c\x65\x6d\x3a\x6c\x2c\x6d\x61\x74\x63\ +\x68\x65\x73\x3a\x71\x7d\x29\x7d\x7d\x64\x2e\x6c\x65\x6e\x67\x74\ +\x68\x3e\x65\x26\x26\x69\x2e\x70\x75\x73\x68\x28\x7b\x65\x6c\x65\ +\x6d\x3a\x74\x68\x69\x73\x2c\x6d\x61\x74\x63\x68\x65\x73\x3a\x64\ +\x2e\x73\x6c\x69\x63\x65\x28\x65\x29\x7d\x29\x3b\x66\x6f\x72\x28\ +\x6a\x3d\x30\x3b\x6a\x3c\x69\x2e\x6c\x65\x6e\x67\x74\x68\x26\x26\ +\x21\x63\x2e\x69\x73\x50\x72\x6f\x70\x61\x67\x61\x74\x69\x6f\x6e\ +\x53\x74\x6f\x70\x70\x65\x64\x28\x29\x3b\x6a\x2b\x2b\x29\x7b\x70\ +\x3d\x69\x5b\x6a\x5d\x2c\x63\x2e\x63\x75\x72\x72\x65\x6e\x74\x54\ +\x61\x72\x67\x65\x74\x3d\x70\x2e\x65\x6c\x65\x6d\x3b\x66\x6f\x72\ +\x28\x6b\x3d\x30\x3b\x6b\x3c\x70\x2e\x6d\x61\x74\x63\x68\x65\x73\ +\x2e\x6c\x65\x6e\x67\x74\x68\x26\x26\x21\x63\x2e\x69\x73\x49\x6d\ +\x6d\x65\x64\x69\x61\x74\x65\x50\x72\x6f\x70\x61\x67\x61\x74\x69\ +\x6f\x6e\x53\x74\x6f\x70\x70\x65\x64\x28\x29\x3b\x6b\x2b\x2b\x29\ +\x7b\x72\x3d\x70\x2e\x6d\x61\x74\x63\x68\x65\x73\x5b\x6b\x5d\x3b\ +\x69\x66\x28\x68\x7c\x7c\x21\x63\x2e\x6e\x61\x6d\x65\x73\x70\x61\ +\x63\x65\x26\x26\x21\x72\x2e\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\ +\x7c\x7c\x63\x2e\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x5f\x72\x65\ +\x26\x26\x63\x2e\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x5f\x72\x65\ +\x2e\x74\x65\x73\x74\x28\x72\x2e\x6e\x61\x6d\x65\x73\x70\x61\x63\ +\x65\x29\x29\x63\x2e\x64\x61\x74\x61\x3d\x72\x2e\x64\x61\x74\x61\ +\x2c\x63\x2e\x68\x61\x6e\x64\x6c\x65\x4f\x62\x6a\x3d\x72\x2c\x6e\ +\x3d\x28\x28\x66\x2e\x65\x76\x65\x6e\x74\x2e\x73\x70\x65\x63\x69\ +\x61\x6c\x5b\x72\x2e\x6f\x72\x69\x67\x54\x79\x70\x65\x5d\x7c\x7c\ +\x7b\x7d\x29\x2e\x68\x61\x6e\x64\x6c\x65\x7c\x7c\x72\x2e\x68\x61\ +\x6e\x64\x6c\x65\x72\x29\x2e\x61\x70\x70\x6c\x79\x28\x70\x2e\x65\ +\x6c\x65\x6d\x2c\x67\x29\x2c\x6e\x21\x3d\x3d\x62\x26\x26\x28\x63\ +\x2e\x72\x65\x73\x75\x6c\x74\x3d\x6e\x2c\x6e\x3d\x3d\x3d\x21\x31\ +\x26\x26\x28\x63\x2e\x70\x72\x65\x76\x65\x6e\x74\x44\x65\x66\x61\ +\x75\x6c\x74\x28\x29\x2c\x63\x2e\x73\x74\x6f\x70\x50\x72\x6f\x70\ +\x61\x67\x61\x74\x69\x6f\x6e\x28\x29\x29\x29\x7d\x7d\x72\x65\x74\ +\x75\x72\x6e\x20\x63\x2e\x72\x65\x73\x75\x6c\x74\x7d\x2c\x70\x72\ +\x6f\x70\x73\x3a\x22\x61\x74\x74\x72\x43\x68\x61\x6e\x67\x65\x20\ +\x61\x74\x74\x72\x4e\x61\x6d\x65\x20\x72\x65\x6c\x61\x74\x65\x64\ +\x4e\x6f\x64\x65\x20\x73\x72\x63\x45\x6c\x65\x6d\x65\x6e\x74\x20\ +\x61\x6c\x74\x4b\x65\x79\x20\x62\x75\x62\x62\x6c\x65\x73\x20\x63\ +\x61\x6e\x63\x65\x6c\x61\x62\x6c\x65\x20\x63\x74\x72\x6c\x4b\x65\ +\x79\x20\x63\x75\x72\x72\x65\x6e\x74\x54\x61\x72\x67\x65\x74\x20\ +\x65\x76\x65\x6e\x74\x50\x68\x61\x73\x65\x20\x6d\x65\x74\x61\x4b\ +\x65\x79\x20\x72\x65\x6c\x61\x74\x65\x64\x54\x61\x72\x67\x65\x74\ +\x20\x73\x68\x69\x66\x74\x4b\x65\x79\x20\x74\x61\x72\x67\x65\x74\ +\x20\x74\x69\x6d\x65\x53\x74\x61\x6d\x70\x20\x76\x69\x65\x77\x20\ +\x77\x68\x69\x63\x68\x22\x2e\x73\x70\x6c\x69\x74\x28\x22\x20\x22\ +\x29\x2c\x66\x69\x78\x48\x6f\x6f\x6b\x73\x3a\x7b\x7d\x2c\x6b\x65\ +\x79\x48\x6f\x6f\x6b\x73\x3a\x7b\x70\x72\x6f\x70\x73\x3a\x22\x63\ +\x68\x61\x72\x20\x63\x68\x61\x72\x43\x6f\x64\x65\x20\x6b\x65\x79\ +\x20\x6b\x65\x79\x43\x6f\x64\x65\x22\x2e\x73\x70\x6c\x69\x74\x28\ +\x22\x20\x22\x29\x2c\x66\x69\x6c\x74\x65\x72\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x61\x2e\x77\x68\x69\x63\ +\x68\x3d\x3d\x6e\x75\x6c\x6c\x26\x26\x28\x61\x2e\x77\x68\x69\x63\ +\x68\x3d\x62\x2e\x63\x68\x61\x72\x43\x6f\x64\x65\x21\x3d\x6e\x75\ +\x6c\x6c\x3f\x62\x2e\x63\x68\x61\x72\x43\x6f\x64\x65\x3a\x62\x2e\ +\x6b\x65\x79\x43\x6f\x64\x65\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\ +\x61\x7d\x7d\x2c\x6d\x6f\x75\x73\x65\x48\x6f\x6f\x6b\x73\x3a\x7b\ +\x70\x72\x6f\x70\x73\x3a\x22\x62\x75\x74\x74\x6f\x6e\x20\x62\x75\ +\x74\x74\x6f\x6e\x73\x20\x63\x6c\x69\x65\x6e\x74\x58\x20\x63\x6c\ +\x69\x65\x6e\x74\x59\x20\x66\x72\x6f\x6d\x45\x6c\x65\x6d\x65\x6e\ +\x74\x20\x6f\x66\x66\x73\x65\x74\x58\x20\x6f\x66\x66\x73\x65\x74\ +\x59\x20\x70\x61\x67\x65\x58\x20\x70\x61\x67\x65\x59\x20\x73\x63\ +\x72\x65\x65\x6e\x58\x20\x73\x63\x72\x65\x65\x6e\x59\x20\x74\x6f\ +\x45\x6c\x65\x6d\x65\x6e\x74\x22\x2e\x73\x70\x6c\x69\x74\x28\x22\ +\x20\x22\x29\x2c\x66\x69\x6c\x74\x65\x72\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x64\x29\x7b\x76\x61\x72\x20\x65\x2c\x66\ +\x2c\x67\x2c\x68\x3d\x64\x2e\x62\x75\x74\x74\x6f\x6e\x2c\x69\x3d\ +\x64\x2e\x66\x72\x6f\x6d\x45\x6c\x65\x6d\x65\x6e\x74\x3b\x61\x2e\ +\x70\x61\x67\x65\x58\x3d\x3d\x6e\x75\x6c\x6c\x26\x26\x64\x2e\x63\ +\x6c\x69\x65\x6e\x74\x58\x21\x3d\x6e\x75\x6c\x6c\x26\x26\x28\x65\ +\x3d\x61\x2e\x74\x61\x72\x67\x65\x74\x2e\x6f\x77\x6e\x65\x72\x44\ +\x6f\x63\x75\x6d\x65\x6e\x74\x7c\x7c\x63\x2c\x66\x3d\x65\x2e\x64\ +\x6f\x63\x75\x6d\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x2c\x67\ +\x3d\x65\x2e\x62\x6f\x64\x79\x2c\x61\x2e\x70\x61\x67\x65\x58\x3d\ +\x64\x2e\x63\x6c\x69\x65\x6e\x74\x58\x2b\x28\x66\x26\x26\x66\x2e\ +\x73\x63\x72\x6f\x6c\x6c\x4c\x65\x66\x74\x7c\x7c\x67\x26\x26\x67\ +\x2e\x73\x63\x72\x6f\x6c\x6c\x4c\x65\x66\x74\x7c\x7c\x30\x29\x2d\ +\x28\x66\x26\x26\x66\x2e\x63\x6c\x69\x65\x6e\x74\x4c\x65\x66\x74\ +\x7c\x7c\x67\x26\x26\x67\x2e\x63\x6c\x69\x65\x6e\x74\x4c\x65\x66\ +\x74\x7c\x7c\x30\x29\x2c\x61\x2e\x70\x61\x67\x65\x59\x3d\x64\x2e\ +\x63\x6c\x69\x65\x6e\x74\x59\x2b\x28\x66\x26\x26\x66\x2e\x73\x63\ +\x72\x6f\x6c\x6c\x54\x6f\x70\x7c\x7c\x67\x26\x26\x67\x2e\x73\x63\ +\x72\x6f\x6c\x6c\x54\x6f\x70\x7c\x7c\x30\x29\x2d\x28\x66\x26\x26\ +\x66\x2e\x63\x6c\x69\x65\x6e\x74\x54\x6f\x70\x7c\x7c\x67\x26\x26\ +\x67\x2e\x63\x6c\x69\x65\x6e\x74\x54\x6f\x70\x7c\x7c\x30\x29\x29\ +\x2c\x21\x61\x2e\x72\x65\x6c\x61\x74\x65\x64\x54\x61\x72\x67\x65\ +\x74\x26\x26\x69\x26\x26\x28\x61\x2e\x72\x65\x6c\x61\x74\x65\x64\ +\x54\x61\x72\x67\x65\x74\x3d\x69\x3d\x3d\x3d\x61\x2e\x74\x61\x72\ +\x67\x65\x74\x3f\x64\x2e\x74\x6f\x45\x6c\x65\x6d\x65\x6e\x74\x3a\ +\x69\x29\x2c\x21\x61\x2e\x77\x68\x69\x63\x68\x26\x26\x68\x21\x3d\ +\x3d\x62\x26\x26\x28\x61\x2e\x77\x68\x69\x63\x68\x3d\x68\x26\x31\ +\x3f\x31\x3a\x68\x26\x32\x3f\x33\x3a\x68\x26\x34\x3f\x32\x3a\x30\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x61\x7d\x7d\x2c\x66\x69\x78\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x69\x66\x28\ +\x61\x5b\x66\x2e\x65\x78\x70\x61\x6e\x64\x6f\x5d\x29\x72\x65\x74\ +\x75\x72\x6e\x20\x61\x3b\x76\x61\x72\x20\x64\x2c\x65\x2c\x67\x3d\ +\x61\x2c\x68\x3d\x66\x2e\x65\x76\x65\x6e\x74\x2e\x66\x69\x78\x48\ +\x6f\x6f\x6b\x73\x5b\x61\x2e\x74\x79\x70\x65\x5d\x7c\x7c\x7b\x7d\ +\x2c\x69\x3d\x68\x2e\x70\x72\x6f\x70\x73\x3f\x74\x68\x69\x73\x2e\ +\x70\x72\x6f\x70\x73\x2e\x63\x6f\x6e\x63\x61\x74\x28\x68\x2e\x70\ +\x72\x6f\x70\x73\x29\x3a\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x73\ +\x3b\x61\x3d\x66\x2e\x45\x76\x65\x6e\x74\x28\x67\x29\x3b\x66\x6f\ +\x72\x28\x64\x3d\x69\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x64\x3b\x29\ +\x65\x3d\x69\x5b\x2d\x2d\x64\x5d\x2c\x61\x5b\x65\x5d\x3d\x67\x5b\ +\x65\x5d\x3b\x61\x2e\x74\x61\x72\x67\x65\x74\x7c\x7c\x28\x61\x2e\ +\x74\x61\x72\x67\x65\x74\x3d\x67\x2e\x73\x72\x63\x45\x6c\x65\x6d\ +\x65\x6e\x74\x7c\x7c\x63\x29\x2c\x61\x2e\x74\x61\x72\x67\x65\x74\ +\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x33\x26\x26\x28\ +\x61\x2e\x74\x61\x72\x67\x65\x74\x3d\x61\x2e\x74\x61\x72\x67\x65\ +\x74\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x29\x2c\x61\x2e\ +\x6d\x65\x74\x61\x4b\x65\x79\x3d\x3d\x3d\x62\x26\x26\x28\x61\x2e\ +\x6d\x65\x74\x61\x4b\x65\x79\x3d\x61\x2e\x63\x74\x72\x6c\x4b\x65\ +\x79\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x68\x2e\x66\x69\x6c\x74\ +\x65\x72\x3f\x68\x2e\x66\x69\x6c\x74\x65\x72\x28\x61\x2c\x67\x29\ +\x3a\x61\x7d\x2c\x73\x70\x65\x63\x69\x61\x6c\x3a\x7b\x72\x65\x61\ +\x64\x79\x3a\x7b\x73\x65\x74\x75\x70\x3a\x66\x2e\x62\x69\x6e\x64\ +\x52\x65\x61\x64\x79\x7d\x2c\x6c\x6f\x61\x64\x3a\x7b\x6e\x6f\x42\ +\x75\x62\x62\x6c\x65\x3a\x21\x30\x7d\x2c\x66\x6f\x63\x75\x73\x3a\ +\x7b\x64\x65\x6c\x65\x67\x61\x74\x65\x54\x79\x70\x65\x3a\x22\x66\ +\x6f\x63\x75\x73\x69\x6e\x22\x7d\x2c\x62\x6c\x75\x72\x3a\x7b\x64\ +\x65\x6c\x65\x67\x61\x74\x65\x54\x79\x70\x65\x3a\x22\x66\x6f\x63\ +\x75\x73\x6f\x75\x74\x22\x7d\x2c\x62\x65\x66\x6f\x72\x65\x75\x6e\ +\x6c\x6f\x61\x64\x3a\x7b\x73\x65\x74\x75\x70\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x66\x2e\x69\x73\ +\x57\x69\x6e\x64\x6f\x77\x28\x74\x68\x69\x73\x29\x26\x26\x28\x74\ +\x68\x69\x73\x2e\x6f\x6e\x62\x65\x66\x6f\x72\x65\x75\x6e\x6c\x6f\ +\x61\x64\x3d\x63\x29\x7d\x2c\x74\x65\x61\x72\x64\x6f\x77\x6e\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x74\x68\ +\x69\x73\x2e\x6f\x6e\x62\x65\x66\x6f\x72\x65\x75\x6e\x6c\x6f\x61\ +\x64\x3d\x3d\x3d\x62\x26\x26\x28\x74\x68\x69\x73\x2e\x6f\x6e\x62\ +\x65\x66\x6f\x72\x65\x75\x6e\x6c\x6f\x61\x64\x3d\x6e\x75\x6c\x6c\ +\x29\x7d\x7d\x7d\x2c\x73\x69\x6d\x75\x6c\x61\x74\x65\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\x64\x29\x7b\ +\x76\x61\x72\x20\x65\x3d\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x6e\ +\x65\x77\x20\x66\x2e\x45\x76\x65\x6e\x74\x2c\x63\x2c\x7b\x74\x79\ +\x70\x65\x3a\x61\x2c\x69\x73\x53\x69\x6d\x75\x6c\x61\x74\x65\x64\ +\x3a\x21\x30\x2c\x6f\x72\x69\x67\x69\x6e\x61\x6c\x45\x76\x65\x6e\ +\x74\x3a\x7b\x7d\x7d\x29\x3b\x64\x3f\x66\x2e\x65\x76\x65\x6e\x74\ +\x2e\x74\x72\x69\x67\x67\x65\x72\x28\x65\x2c\x6e\x75\x6c\x6c\x2c\ +\x62\x29\x3a\x66\x2e\x65\x76\x65\x6e\x74\x2e\x64\x69\x73\x70\x61\ +\x74\x63\x68\x2e\x63\x61\x6c\x6c\x28\x62\x2c\x65\x29\x2c\x65\x2e\ +\x69\x73\x44\x65\x66\x61\x75\x6c\x74\x50\x72\x65\x76\x65\x6e\x74\ +\x65\x64\x28\x29\x26\x26\x63\x2e\x70\x72\x65\x76\x65\x6e\x74\x44\ +\x65\x66\x61\x75\x6c\x74\x28\x29\x7d\x7d\x2c\x66\x2e\x65\x76\x65\ +\x6e\x74\x2e\x68\x61\x6e\x64\x6c\x65\x3d\x66\x2e\x65\x76\x65\x6e\ +\x74\x2e\x64\x69\x73\x70\x61\x74\x63\x68\x2c\x66\x2e\x72\x65\x6d\ +\x6f\x76\x65\x45\x76\x65\x6e\x74\x3d\x63\x2e\x72\x65\x6d\x6f\x76\ +\x65\x45\x76\x65\x6e\x74\x4c\x69\x73\x74\x65\x6e\x65\x72\x3f\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x61\ +\x2e\x72\x65\x6d\x6f\x76\x65\x45\x76\x65\x6e\x74\x4c\x69\x73\x74\ +\x65\x6e\x65\x72\x26\x26\x61\x2e\x72\x65\x6d\x6f\x76\x65\x45\x76\ +\x65\x6e\x74\x4c\x69\x73\x74\x65\x6e\x65\x72\x28\x62\x2c\x63\x2c\ +\x21\x31\x29\x7d\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\ +\x62\x2c\x63\x29\x7b\x61\x2e\x64\x65\x74\x61\x63\x68\x45\x76\x65\ +\x6e\x74\x26\x26\x61\x2e\x64\x65\x74\x61\x63\x68\x45\x76\x65\x6e\ +\x74\x28\x22\x6f\x6e\x22\x2b\x62\x2c\x63\x29\x7d\x2c\x66\x2e\x45\ +\x76\x65\x6e\x74\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\ +\x62\x29\x7b\x69\x66\x28\x21\x28\x74\x68\x69\x73\x20\x69\x6e\x73\ +\x74\x61\x6e\x63\x65\x6f\x66\x20\x66\x2e\x45\x76\x65\x6e\x74\x29\ +\x29\x72\x65\x74\x75\x72\x6e\x20\x6e\x65\x77\x20\x66\x2e\x45\x76\ +\x65\x6e\x74\x28\x61\x2c\x62\x29\x3b\x61\x26\x26\x61\x2e\x74\x79\ +\x70\x65\x3f\x28\x74\x68\x69\x73\x2e\x6f\x72\x69\x67\x69\x6e\x61\ +\x6c\x45\x76\x65\x6e\x74\x3d\x61\x2c\x74\x68\x69\x73\x2e\x74\x79\ +\x70\x65\x3d\x61\x2e\x74\x79\x70\x65\x2c\x74\x68\x69\x73\x2e\x69\ +\x73\x44\x65\x66\x61\x75\x6c\x74\x50\x72\x65\x76\x65\x6e\x74\x65\ +\x64\x3d\x61\x2e\x64\x65\x66\x61\x75\x6c\x74\x50\x72\x65\x76\x65\ +\x6e\x74\x65\x64\x7c\x7c\x61\x2e\x72\x65\x74\x75\x72\x6e\x56\x61\ +\x6c\x75\x65\x3d\x3d\x3d\x21\x31\x7c\x7c\x61\x2e\x67\x65\x74\x50\ +\x72\x65\x76\x65\x6e\x74\x44\x65\x66\x61\x75\x6c\x74\x26\x26\x61\ +\x2e\x67\x65\x74\x50\x72\x65\x76\x65\x6e\x74\x44\x65\x66\x61\x75\ +\x6c\x74\x28\x29\x3f\x4b\x3a\x4a\x29\x3a\x74\x68\x69\x73\x2e\x74\ +\x79\x70\x65\x3d\x61\x2c\x62\x26\x26\x66\x2e\x65\x78\x74\x65\x6e\ +\x64\x28\x74\x68\x69\x73\x2c\x62\x29\x2c\x74\x68\x69\x73\x2e\x74\ +\x69\x6d\x65\x53\x74\x61\x6d\x70\x3d\x61\x26\x26\x61\x2e\x74\x69\ +\x6d\x65\x53\x74\x61\x6d\x70\x7c\x7c\x66\x2e\x6e\x6f\x77\x28\x29\ +\x2c\x74\x68\x69\x73\x5b\x66\x2e\x65\x78\x70\x61\x6e\x64\x6f\x5d\ +\x3d\x21\x30\x7d\x2c\x66\x2e\x45\x76\x65\x6e\x74\x2e\x70\x72\x6f\ +\x74\x6f\x74\x79\x70\x65\x3d\x7b\x70\x72\x65\x76\x65\x6e\x74\x44\ +\x65\x66\x61\x75\x6c\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x29\x7b\x74\x68\x69\x73\x2e\x69\x73\x44\x65\x66\x61\x75\x6c\x74\ +\x50\x72\x65\x76\x65\x6e\x74\x65\x64\x3d\x4b\x3b\x76\x61\x72\x20\ +\x61\x3d\x74\x68\x69\x73\x2e\x6f\x72\x69\x67\x69\x6e\x61\x6c\x45\ +\x76\x65\x6e\x74\x3b\x21\x61\x7c\x7c\x28\x61\x2e\x70\x72\x65\x76\ +\x65\x6e\x74\x44\x65\x66\x61\x75\x6c\x74\x3f\x61\x2e\x70\x72\x65\ +\x76\x65\x6e\x74\x44\x65\x66\x61\x75\x6c\x74\x28\x29\x3a\x61\x2e\ +\x72\x65\x74\x75\x72\x6e\x56\x61\x6c\x75\x65\x3d\x21\x31\x29\x7d\ +\x2c\x73\x74\x6f\x70\x50\x72\x6f\x70\x61\x67\x61\x74\x69\x6f\x6e\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x74\x68\x69\x73\ +\x2e\x69\x73\x50\x72\x6f\x70\x61\x67\x61\x74\x69\x6f\x6e\x53\x74\ +\x6f\x70\x70\x65\x64\x3d\x4b\x3b\x76\x61\x72\x20\x61\x3d\x74\x68\ +\x69\x73\x2e\x6f\x72\x69\x67\x69\x6e\x61\x6c\x45\x76\x65\x6e\x74\ +\x3b\x21\x61\x7c\x7c\x28\x61\x2e\x73\x74\x6f\x70\x50\x72\x6f\x70\ +\x61\x67\x61\x74\x69\x6f\x6e\x26\x26\x61\x2e\x73\x74\x6f\x70\x50\ +\x72\x6f\x70\x61\x67\x61\x74\x69\x6f\x6e\x28\x29\x2c\x61\x2e\x63\ +\x61\x6e\x63\x65\x6c\x42\x75\x62\x62\x6c\x65\x3d\x21\x30\x29\x7d\ +\x2c\x73\x74\x6f\x70\x49\x6d\x6d\x65\x64\x69\x61\x74\x65\x50\x72\ +\x6f\x70\x61\x67\x61\x74\x69\x6f\x6e\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x29\x7b\x74\x68\x69\x73\x2e\x69\x73\x49\x6d\x6d\x65\ +\x64\x69\x61\x74\x65\x50\x72\x6f\x70\x61\x67\x61\x74\x69\x6f\x6e\ +\x53\x74\x6f\x70\x70\x65\x64\x3d\x4b\x2c\x74\x68\x69\x73\x2e\x73\ +\x74\x6f\x70\x50\x72\x6f\x70\x61\x67\x61\x74\x69\x6f\x6e\x28\x29\ +\x7d\x2c\x69\x73\x44\x65\x66\x61\x75\x6c\x74\x50\x72\x65\x76\x65\ +\x6e\x74\x65\x64\x3a\x4a\x2c\x69\x73\x50\x72\x6f\x70\x61\x67\x61\ +\x74\x69\x6f\x6e\x53\x74\x6f\x70\x70\x65\x64\x3a\x4a\x2c\x69\x73\ +\x49\x6d\x6d\x65\x64\x69\x61\x74\x65\x50\x72\x6f\x70\x61\x67\x61\ +\x74\x69\x6f\x6e\x53\x74\x6f\x70\x70\x65\x64\x3a\x4a\x7d\x2c\x66\ +\x2e\x65\x61\x63\x68\x28\x7b\x6d\x6f\x75\x73\x65\x65\x6e\x74\x65\ +\x72\x3a\x22\x6d\x6f\x75\x73\x65\x6f\x76\x65\x72\x22\x2c\x6d\x6f\ +\x75\x73\x65\x6c\x65\x61\x76\x65\x3a\x22\x6d\x6f\x75\x73\x65\x6f\ +\x75\x74\x22\x7d\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\ +\x62\x29\x7b\x66\x2e\x65\x76\x65\x6e\x74\x2e\x73\x70\x65\x63\x69\ +\x61\x6c\x5b\x61\x5d\x3d\x7b\x64\x65\x6c\x65\x67\x61\x74\x65\x54\ +\x79\x70\x65\x3a\x62\x2c\x62\x69\x6e\x64\x54\x79\x70\x65\x3a\x62\ +\x2c\x68\x61\x6e\x64\x6c\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x7b\x76\x61\x72\x20\x63\x3d\x74\x68\x69\x73\x2c\x64\ +\x3d\x61\x2e\x72\x65\x6c\x61\x74\x65\x64\x54\x61\x72\x67\x65\x74\ +\x2c\x65\x3d\x61\x2e\x68\x61\x6e\x64\x6c\x65\x4f\x62\x6a\x2c\x67\ +\x3d\x65\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x2c\x68\x3b\x69\x66\ +\x28\x21\x64\x7c\x7c\x64\x21\x3d\x3d\x63\x26\x26\x21\x66\x2e\x63\ +\x6f\x6e\x74\x61\x69\x6e\x73\x28\x63\x2c\x64\x29\x29\x61\x2e\x74\ +\x79\x70\x65\x3d\x65\x2e\x6f\x72\x69\x67\x54\x79\x70\x65\x2c\x68\ +\x3d\x65\x2e\x68\x61\x6e\x64\x6c\x65\x72\x2e\x61\x70\x70\x6c\x79\ +\x28\x74\x68\x69\x73\x2c\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\ +\x2c\x61\x2e\x74\x79\x70\x65\x3d\x62\x3b\x72\x65\x74\x75\x72\x6e\ +\x20\x68\x7d\x7d\x7d\x29\x2c\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\ +\x2e\x73\x75\x62\x6d\x69\x74\x42\x75\x62\x62\x6c\x65\x73\x7c\x7c\ +\x28\x66\x2e\x65\x76\x65\x6e\x74\x2e\x73\x70\x65\x63\x69\x61\x6c\ +\x2e\x73\x75\x62\x6d\x69\x74\x3d\x7b\x73\x65\x74\x75\x70\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x69\x66\x28\x66\x2e\x6e\ +\x6f\x64\x65\x4e\x61\x6d\x65\x28\x74\x68\x69\x73\x2c\x22\x66\x6f\ +\x72\x6d\x22\x29\x29\x72\x65\x74\x75\x72\x6e\x21\x31\x3b\x66\x2e\ +\x65\x76\x65\x6e\x74\x2e\x61\x64\x64\x28\x74\x68\x69\x73\x2c\x22\ +\x63\x6c\x69\x63\x6b\x2e\x5f\x73\x75\x62\x6d\x69\x74\x20\x6b\x65\ +\x79\x70\x72\x65\x73\x73\x2e\x5f\x73\x75\x62\x6d\x69\x74\x22\x2c\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\ +\x63\x3d\x61\x2e\x74\x61\x72\x67\x65\x74\x2c\x64\x3d\x66\x2e\x6e\ +\x6f\x64\x65\x4e\x61\x6d\x65\x28\x63\x2c\x22\x69\x6e\x70\x75\x74\ +\x22\x29\x7c\x7c\x66\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x28\x63\ +\x2c\x22\x62\x75\x74\x74\x6f\x6e\x22\x29\x3f\x63\x2e\x66\x6f\x72\ +\x6d\x3a\x62\x3b\x64\x26\x26\x21\x64\x2e\x5f\x73\x75\x62\x6d\x69\ +\x74\x5f\x61\x74\x74\x61\x63\x68\x65\x64\x26\x26\x28\x66\x2e\x65\ +\x76\x65\x6e\x74\x2e\x61\x64\x64\x28\x64\x2c\x22\x73\x75\x62\x6d\ +\x69\x74\x2e\x5f\x73\x75\x62\x6d\x69\x74\x22\x2c\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x29\x7b\x74\x68\x69\x73\x2e\x70\x61\x72\ +\x65\x6e\x74\x4e\x6f\x64\x65\x26\x26\x21\x61\x2e\x69\x73\x54\x72\ +\x69\x67\x67\x65\x72\x26\x26\x66\x2e\x65\x76\x65\x6e\x74\x2e\x73\ +\x69\x6d\x75\x6c\x61\x74\x65\x28\x22\x73\x75\x62\x6d\x69\x74\x22\ +\x2c\x74\x68\x69\x73\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\ +\x2c\x61\x2c\x21\x30\x29\x7d\x29\x2c\x64\x2e\x5f\x73\x75\x62\x6d\ +\x69\x74\x5f\x61\x74\x74\x61\x63\x68\x65\x64\x3d\x21\x30\x29\x7d\ +\x29\x7d\x2c\x74\x65\x61\x72\x64\x6f\x77\x6e\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x29\x7b\x69\x66\x28\x66\x2e\x6e\x6f\x64\x65\ +\x4e\x61\x6d\x65\x28\x74\x68\x69\x73\x2c\x22\x66\x6f\x72\x6d\x22\ +\x29\x29\x72\x65\x74\x75\x72\x6e\x21\x31\x3b\x66\x2e\x65\x76\x65\ +\x6e\x74\x2e\x72\x65\x6d\x6f\x76\x65\x28\x74\x68\x69\x73\x2c\x22\ +\x2e\x5f\x73\x75\x62\x6d\x69\x74\x22\x29\x7d\x7d\x29\x2c\x66\x2e\ +\x73\x75\x70\x70\x6f\x72\x74\x2e\x63\x68\x61\x6e\x67\x65\x42\x75\ +\x62\x62\x6c\x65\x73\x7c\x7c\x28\x66\x2e\x65\x76\x65\x6e\x74\x2e\ +\x73\x70\x65\x63\x69\x61\x6c\x2e\x63\x68\x61\x6e\x67\x65\x3d\x7b\ +\x73\x65\x74\x75\x70\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\ +\x7b\x69\x66\x28\x7a\x2e\x74\x65\x73\x74\x28\x74\x68\x69\x73\x2e\ +\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x29\x29\x7b\x69\x66\x28\x74\x68\ +\x69\x73\x2e\x74\x79\x70\x65\x3d\x3d\x3d\x22\x63\x68\x65\x63\x6b\ +\x62\x6f\x78\x22\x7c\x7c\x74\x68\x69\x73\x2e\x74\x79\x70\x65\x3d\ +\x3d\x3d\x22\x72\x61\x64\x69\x6f\x22\x29\x66\x2e\x65\x76\x65\x6e\ +\x74\x2e\x61\x64\x64\x28\x74\x68\x69\x73\x2c\x22\x70\x72\x6f\x70\ +\x65\x72\x74\x79\x63\x68\x61\x6e\x67\x65\x2e\x5f\x63\x68\x61\x6e\ +\x67\x65\x22\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\ +\x61\x2e\x6f\x72\x69\x67\x69\x6e\x61\x6c\x45\x76\x65\x6e\x74\x2e\ +\x70\x72\x6f\x70\x65\x72\x74\x79\x4e\x61\x6d\x65\x3d\x3d\x3d\x22\ +\x63\x68\x65\x63\x6b\x65\x64\x22\x26\x26\x28\x74\x68\x69\x73\x2e\ +\x5f\x6a\x75\x73\x74\x5f\x63\x68\x61\x6e\x67\x65\x64\x3d\x21\x30\ +\x29\x7d\x29\x2c\x66\x2e\x65\x76\x65\x6e\x74\x2e\x61\x64\x64\x28\ +\x74\x68\x69\x73\x2c\x22\x63\x6c\x69\x63\x6b\x2e\x5f\x63\x68\x61\ +\x6e\x67\x65\x22\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\ +\x7b\x74\x68\x69\x73\x2e\x5f\x6a\x75\x73\x74\x5f\x63\x68\x61\x6e\ +\x67\x65\x64\x26\x26\x21\x61\x2e\x69\x73\x54\x72\x69\x67\x67\x65\ +\x72\x26\x26\x28\x74\x68\x69\x73\x2e\x5f\x6a\x75\x73\x74\x5f\x63\ +\x68\x61\x6e\x67\x65\x64\x3d\x21\x31\x2c\x66\x2e\x65\x76\x65\x6e\ +\x74\x2e\x73\x69\x6d\x75\x6c\x61\x74\x65\x28\x22\x63\x68\x61\x6e\ +\x67\x65\x22\x2c\x74\x68\x69\x73\x2c\x61\x2c\x21\x30\x29\x29\x7d\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x21\x31\x7d\x66\x2e\x65\x76\x65\ +\x6e\x74\x2e\x61\x64\x64\x28\x74\x68\x69\x73\x2c\x22\x62\x65\x66\ +\x6f\x72\x65\x61\x63\x74\x69\x76\x61\x74\x65\x2e\x5f\x63\x68\x61\ +\x6e\x67\x65\x22\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\ +\x7b\x76\x61\x72\x20\x62\x3d\x61\x2e\x74\x61\x72\x67\x65\x74\x3b\ +\x7a\x2e\x74\x65\x73\x74\x28\x62\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\ +\x65\x29\x26\x26\x21\x62\x2e\x5f\x63\x68\x61\x6e\x67\x65\x5f\x61\ +\x74\x74\x61\x63\x68\x65\x64\x26\x26\x28\x66\x2e\x65\x76\x65\x6e\ +\x74\x2e\x61\x64\x64\x28\x62\x2c\x22\x63\x68\x61\x6e\x67\x65\x2e\ +\x5f\x63\x68\x61\x6e\x67\x65\x22\x2c\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x29\x7b\x74\x68\x69\x73\x2e\x70\x61\x72\x65\x6e\x74\ +\x4e\x6f\x64\x65\x26\x26\x21\x61\x2e\x69\x73\x53\x69\x6d\x75\x6c\ +\x61\x74\x65\x64\x26\x26\x21\x61\x2e\x69\x73\x54\x72\x69\x67\x67\ +\x65\x72\x26\x26\x66\x2e\x65\x76\x65\x6e\x74\x2e\x73\x69\x6d\x75\ +\x6c\x61\x74\x65\x28\x22\x63\x68\x61\x6e\x67\x65\x22\x2c\x74\x68\ +\x69\x73\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2c\x61\x2c\ +\x21\x30\x29\x7d\x29\x2c\x62\x2e\x5f\x63\x68\x61\x6e\x67\x65\x5f\ +\x61\x74\x74\x61\x63\x68\x65\x64\x3d\x21\x30\x29\x7d\x29\x7d\x2c\ +\x68\x61\x6e\x64\x6c\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x29\x7b\x76\x61\x72\x20\x62\x3d\x61\x2e\x74\x61\x72\x67\x65\ +\x74\x3b\x69\x66\x28\x74\x68\x69\x73\x21\x3d\x3d\x62\x7c\x7c\x61\ +\x2e\x69\x73\x53\x69\x6d\x75\x6c\x61\x74\x65\x64\x7c\x7c\x61\x2e\ +\x69\x73\x54\x72\x69\x67\x67\x65\x72\x7c\x7c\x62\x2e\x74\x79\x70\ +\x65\x21\x3d\x3d\x22\x72\x61\x64\x69\x6f\x22\x26\x26\x62\x2e\x74\ +\x79\x70\x65\x21\x3d\x3d\x22\x63\x68\x65\x63\x6b\x62\x6f\x78\x22\ +\x29\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x68\x61\x6e\x64\x6c\x65\ +\x4f\x62\x6a\x2e\x68\x61\x6e\x64\x6c\x65\x72\x2e\x61\x70\x70\x6c\ +\x79\x28\x74\x68\x69\x73\x2c\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\ +\x29\x7d\x2c\x74\x65\x61\x72\x64\x6f\x77\x6e\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x29\x7b\x66\x2e\x65\x76\x65\x6e\x74\x2e\x72\ +\x65\x6d\x6f\x76\x65\x28\x74\x68\x69\x73\x2c\x22\x2e\x5f\x63\x68\ +\x61\x6e\x67\x65\x22\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x7a\x2e\ +\x74\x65\x73\x74\x28\x74\x68\x69\x73\x2e\x6e\x6f\x64\x65\x4e\x61\ +\x6d\x65\x29\x7d\x7d\x29\x2c\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\ +\x2e\x66\x6f\x63\x75\x73\x69\x6e\x42\x75\x62\x62\x6c\x65\x73\x7c\ +\x7c\x66\x2e\x65\x61\x63\x68\x28\x7b\x66\x6f\x63\x75\x73\x3a\x22\ +\x66\x6f\x63\x75\x73\x69\x6e\x22\x2c\x62\x6c\x75\x72\x3a\x22\x66\ +\x6f\x63\x75\x73\x6f\x75\x74\x22\x7d\x2c\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x64\x3d\x30\x2c\ +\x65\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x66\x2e\ +\x65\x76\x65\x6e\x74\x2e\x73\x69\x6d\x75\x6c\x61\x74\x65\x28\x62\ +\x2c\x61\x2e\x74\x61\x72\x67\x65\x74\x2c\x66\x2e\x65\x76\x65\x6e\ +\x74\x2e\x66\x69\x78\x28\x61\x29\x2c\x21\x30\x29\x7d\x3b\x66\x2e\ +\x65\x76\x65\x6e\x74\x2e\x73\x70\x65\x63\x69\x61\x6c\x5b\x62\x5d\ +\x3d\x7b\x73\x65\x74\x75\x70\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x29\x7b\x64\x2b\x2b\x3d\x3d\x3d\x30\x26\x26\x63\x2e\x61\x64\ +\x64\x45\x76\x65\x6e\x74\x4c\x69\x73\x74\x65\x6e\x65\x72\x28\x61\ +\x2c\x65\x2c\x21\x30\x29\x7d\x2c\x74\x65\x61\x72\x64\x6f\x77\x6e\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x2d\x2d\x64\x3d\ +\x3d\x3d\x30\x26\x26\x63\x2e\x72\x65\x6d\x6f\x76\x65\x45\x76\x65\ +\x6e\x74\x4c\x69\x73\x74\x65\x6e\x65\x72\x28\x61\x2c\x65\x2c\x21\ +\x30\x29\x7d\x7d\x7d\x29\x2c\x66\x2e\x66\x6e\x2e\x65\x78\x74\x65\ +\x6e\x64\x28\x7b\x6f\x6e\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x2c\x63\x2c\x64\x2c\x65\x2c\x67\x29\x7b\x76\x61\x72\x20\x68\ +\x2c\x69\x3b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x61\x3d\x3d\ +\x22\x6f\x62\x6a\x65\x63\x74\x22\x29\x7b\x74\x79\x70\x65\x6f\x66\ +\x20\x63\x21\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x28\x64\ +\x3d\x63\x2c\x63\x3d\x62\x29\x3b\x66\x6f\x72\x28\x69\x20\x69\x6e\ +\x20\x61\x29\x74\x68\x69\x73\x2e\x6f\x6e\x28\x69\x2c\x63\x2c\x64\ +\x2c\x61\x5b\x69\x5d\x2c\x67\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\ +\x74\x68\x69\x73\x7d\x64\x3d\x3d\x6e\x75\x6c\x6c\x26\x26\x65\x3d\ +\x3d\x6e\x75\x6c\x6c\x3f\x28\x65\x3d\x63\x2c\x64\x3d\x63\x3d\x62\ +\x29\x3a\x65\x3d\x3d\x6e\x75\x6c\x6c\x26\x26\x28\x74\x79\x70\x65\ +\x6f\x66\x20\x63\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x3f\x28\ +\x65\x3d\x64\x2c\x64\x3d\x62\x29\x3a\x28\x65\x3d\x64\x2c\x64\x3d\ +\x63\x2c\x63\x3d\x62\x29\x29\x3b\x69\x66\x28\x65\x3d\x3d\x3d\x21\ +\x31\x29\x65\x3d\x4a\x3b\x65\x6c\x73\x65\x20\x69\x66\x28\x21\x65\ +\x29\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x3b\x67\x3d\x3d\ +\x3d\x31\x26\x26\x28\x68\x3d\x65\x2c\x65\x3d\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x66\x28\x29\x2e\x6f\x66\x66\x28\x61\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x68\x2e\x61\x70\x70\x6c\x79\ +\x28\x74\x68\x69\x73\x2c\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\ +\x7d\x2c\x65\x2e\x67\x75\x69\x64\x3d\x68\x2e\x67\x75\x69\x64\x7c\ +\x7c\x28\x68\x2e\x67\x75\x69\x64\x3d\x66\x2e\x67\x75\x69\x64\x2b\ +\x2b\x29\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\ +\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\ +\x66\x2e\x65\x76\x65\x6e\x74\x2e\x61\x64\x64\x28\x74\x68\x69\x73\ +\x2c\x61\x2c\x65\x2c\x64\x2c\x63\x29\x7d\x29\x7d\x2c\x6f\x6e\x65\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\ +\x64\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x6f\ +\x6e\x2e\x63\x61\x6c\x6c\x28\x74\x68\x69\x73\x2c\x61\x2c\x62\x2c\ +\x63\x2c\x64\x2c\x31\x29\x7d\x2c\x6f\x66\x66\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x63\x2c\x64\x29\x7b\x69\x66\x28\x61\ +\x26\x26\x61\x2e\x70\x72\x65\x76\x65\x6e\x74\x44\x65\x66\x61\x75\ +\x6c\x74\x26\x26\x61\x2e\x68\x61\x6e\x64\x6c\x65\x4f\x62\x6a\x29\ +\x7b\x76\x61\x72\x20\x65\x3d\x61\x2e\x68\x61\x6e\x64\x6c\x65\x4f\ +\x62\x6a\x3b\x66\x28\x61\x2e\x64\x65\x6c\x65\x67\x61\x74\x65\x54\ +\x61\x72\x67\x65\x74\x29\x2e\x6f\x66\x66\x28\x65\x2e\x6e\x61\x6d\ +\x65\x73\x70\x61\x63\x65\x3f\x65\x2e\x74\x79\x70\x65\x2b\x22\x2e\ +\x22\x2b\x65\x2e\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x3a\x65\x2e\ +\x74\x79\x70\x65\x2c\x65\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x2c\ +\x65\x2e\x68\x61\x6e\x64\x6c\x65\x72\x29\x3b\x72\x65\x74\x75\x72\ +\x6e\x20\x74\x68\x69\x73\x7d\x69\x66\x28\x74\x79\x70\x65\x6f\x66\ +\x20\x61\x3d\x3d\x22\x6f\x62\x6a\x65\x63\x74\x22\x29\x7b\x66\x6f\ +\x72\x28\x76\x61\x72\x20\x67\x20\x69\x6e\x20\x61\x29\x74\x68\x69\ +\x73\x2e\x6f\x66\x66\x28\x67\x2c\x63\x2c\x61\x5b\x67\x5d\x29\x3b\ +\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\x69\x66\x28\x63\ +\x3d\x3d\x3d\x21\x31\x7c\x7c\x74\x79\x70\x65\x6f\x66\x20\x63\x3d\ +\x3d\x22\x66\x75\x6e\x63\x74\x69\x6f\x6e\x22\x29\x64\x3d\x63\x2c\ +\x63\x3d\x62\x3b\x64\x3d\x3d\x3d\x21\x31\x26\x26\x28\x64\x3d\x4a\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\ +\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x66\x2e\ +\x65\x76\x65\x6e\x74\x2e\x72\x65\x6d\x6f\x76\x65\x28\x74\x68\x69\ +\x73\x2c\x61\x2c\x64\x2c\x63\x29\x7d\x29\x7d\x2c\x62\x69\x6e\x64\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\ +\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x6f\x6e\x28\ +\x61\x2c\x6e\x75\x6c\x6c\x2c\x62\x2c\x63\x29\x7d\x2c\x75\x6e\x62\ +\x69\x6e\x64\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\ +\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x6f\x66\ +\x66\x28\x61\x2c\x6e\x75\x6c\x6c\x2c\x62\x29\x7d\x2c\x6c\x69\x76\ +\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\ +\x29\x7b\x66\x28\x74\x68\x69\x73\x2e\x63\x6f\x6e\x74\x65\x78\x74\ +\x29\x2e\x6f\x6e\x28\x61\x2c\x74\x68\x69\x73\x2e\x73\x65\x6c\x65\ +\x63\x74\x6f\x72\x2c\x62\x2c\x63\x29\x3b\x72\x65\x74\x75\x72\x6e\ +\x20\x74\x68\x69\x73\x7d\x2c\x64\x69\x65\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x66\x28\x74\x68\x69\x73\x2e\ +\x63\x6f\x6e\x74\x65\x78\x74\x29\x2e\x6f\x66\x66\x28\x61\x2c\x74\ +\x68\x69\x73\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x7c\x7c\x22\x2a\ +\x2a\x22\x2c\x62\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\ +\x73\x7d\x2c\x64\x65\x6c\x65\x67\x61\x74\x65\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\x64\x29\x7b\x72\x65\ +\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x6f\x6e\x28\x62\x2c\x61\ +\x2c\x63\x2c\x64\x29\x7d\x2c\x75\x6e\x64\x65\x6c\x65\x67\x61\x74\ +\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\ +\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x72\x67\x75\x6d\x65\x6e\ +\x74\x73\x2e\x6c\x65\x6e\x67\x74\x68\x3d\x3d\x31\x3f\x74\x68\x69\ +\x73\x2e\x6f\x66\x66\x28\x61\x2c\x22\x2a\x2a\x22\x29\x3a\x74\x68\ +\x69\x73\x2e\x6f\x66\x66\x28\x62\x2c\x61\x2c\x63\x29\x7d\x2c\x74\ +\x72\x69\x67\x67\x65\x72\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\ +\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\ +\x7b\x66\x2e\x65\x76\x65\x6e\x74\x2e\x74\x72\x69\x67\x67\x65\x72\ +\x28\x61\x2c\x62\x2c\x74\x68\x69\x73\x29\x7d\x29\x7d\x2c\x74\x72\ +\x69\x67\x67\x65\x72\x48\x61\x6e\x64\x6c\x65\x72\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x69\x66\x28\x74\x68\ +\x69\x73\x5b\x30\x5d\x29\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x65\ +\x76\x65\x6e\x74\x2e\x74\x72\x69\x67\x67\x65\x72\x28\x61\x2c\x62\ +\x2c\x74\x68\x69\x73\x5b\x30\x5d\x2c\x21\x30\x29\x7d\x2c\x74\x6f\ +\x67\x67\x6c\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\ +\x7b\x76\x61\x72\x20\x62\x3d\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\ +\x2c\x63\x3d\x61\x2e\x67\x75\x69\x64\x7c\x7c\x66\x2e\x67\x75\x69\ +\x64\x2b\x2b\x2c\x64\x3d\x30\x2c\x65\x3d\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x63\x29\x7b\x76\x61\x72\x20\x65\x3d\x28\x66\x2e\x5f\ +\x64\x61\x74\x61\x28\x74\x68\x69\x73\x2c\x22\x6c\x61\x73\x74\x54\ +\x6f\x67\x67\x6c\x65\x22\x2b\x61\x2e\x67\x75\x69\x64\x29\x7c\x7c\ +\x30\x29\x25\x64\x3b\x66\x2e\x5f\x64\x61\x74\x61\x28\x74\x68\x69\ +\x73\x2c\x22\x6c\x61\x73\x74\x54\x6f\x67\x67\x6c\x65\x22\x2b\x61\ +\x2e\x67\x75\x69\x64\x2c\x65\x2b\x31\x29\x2c\x63\x2e\x70\x72\x65\ +\x76\x65\x6e\x74\x44\x65\x66\x61\x75\x6c\x74\x28\x29\x3b\x72\x65\ +\x74\x75\x72\x6e\x20\x62\x5b\x65\x5d\x2e\x61\x70\x70\x6c\x79\x28\ +\x74\x68\x69\x73\x2c\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\x7c\ +\x7c\x21\x31\x7d\x3b\x65\x2e\x67\x75\x69\x64\x3d\x63\x3b\x77\x68\ +\x69\x6c\x65\x28\x64\x3c\x62\x2e\x6c\x65\x6e\x67\x74\x68\x29\x62\ +\x5b\x64\x2b\x2b\x5d\x2e\x67\x75\x69\x64\x3d\x63\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x63\x6c\x69\x63\x6b\x28\x65\ +\x29\x7d\x2c\x68\x6f\x76\x65\x72\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\ +\x69\x73\x2e\x6d\x6f\x75\x73\x65\x65\x6e\x74\x65\x72\x28\x61\x29\ +\x2e\x6d\x6f\x75\x73\x65\x6c\x65\x61\x76\x65\x28\x62\x7c\x7c\x61\ +\x29\x7d\x7d\x29\x2c\x66\x2e\x65\x61\x63\x68\x28\x22\x62\x6c\x75\ +\x72\x20\x66\x6f\x63\x75\x73\x20\x66\x6f\x63\x75\x73\x69\x6e\x20\ +\x66\x6f\x63\x75\x73\x6f\x75\x74\x20\x6c\x6f\x61\x64\x20\x72\x65\ +\x73\x69\x7a\x65\x20\x73\x63\x72\x6f\x6c\x6c\x20\x75\x6e\x6c\x6f\ +\x61\x64\x20\x63\x6c\x69\x63\x6b\x20\x64\x62\x6c\x63\x6c\x69\x63\ +\x6b\x20\x6d\x6f\x75\x73\x65\x64\x6f\x77\x6e\x20\x6d\x6f\x75\x73\ +\x65\x75\x70\x20\x6d\x6f\x75\x73\x65\x6d\x6f\x76\x65\x20\x6d\x6f\ +\x75\x73\x65\x6f\x76\x65\x72\x20\x6d\x6f\x75\x73\x65\x6f\x75\x74\ +\x20\x6d\x6f\x75\x73\x65\x65\x6e\x74\x65\x72\x20\x6d\x6f\x75\x73\ +\x65\x6c\x65\x61\x76\x65\x20\x63\x68\x61\x6e\x67\x65\x20\x73\x65\ +\x6c\x65\x63\x74\x20\x73\x75\x62\x6d\x69\x74\x20\x6b\x65\x79\x64\ +\x6f\x77\x6e\x20\x6b\x65\x79\x70\x72\x65\x73\x73\x20\x6b\x65\x79\ +\x75\x70\x20\x65\x72\x72\x6f\x72\x20\x63\x6f\x6e\x74\x65\x78\x74\ +\x6d\x65\x6e\x75\x22\x2e\x73\x70\x6c\x69\x74\x28\x22\x20\x22\x29\ +\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x66\ +\x2e\x66\x6e\x5b\x62\x5d\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x2c\x63\x29\x7b\x63\x3d\x3d\x6e\x75\x6c\x6c\x26\x26\x28\x63\ +\x3d\x61\x2c\x61\x3d\x6e\x75\x6c\x6c\x29\x3b\x72\x65\x74\x75\x72\ +\x6e\x20\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2e\x6c\x65\x6e\x67\ +\x74\x68\x3e\x30\x3f\x74\x68\x69\x73\x2e\x6f\x6e\x28\x62\x2c\x6e\ +\x75\x6c\x6c\x2c\x61\x2c\x63\x29\x3a\x74\x68\x69\x73\x2e\x74\x72\ +\x69\x67\x67\x65\x72\x28\x62\x29\x7d\x2c\x66\x2e\x61\x74\x74\x72\ +\x46\x6e\x26\x26\x28\x66\x2e\x61\x74\x74\x72\x46\x6e\x5b\x62\x5d\ +\x3d\x21\x30\x29\x2c\x43\x2e\x74\x65\x73\x74\x28\x62\x29\x26\x26\ +\x28\x66\x2e\x65\x76\x65\x6e\x74\x2e\x66\x69\x78\x48\x6f\x6f\x6b\ +\x73\x5b\x62\x5d\x3d\x66\x2e\x65\x76\x65\x6e\x74\x2e\x6b\x65\x79\ +\x48\x6f\x6f\x6b\x73\x29\x2c\x44\x2e\x74\x65\x73\x74\x28\x62\x29\ +\x26\x26\x28\x66\x2e\x65\x76\x65\x6e\x74\x2e\x66\x69\x78\x48\x6f\ +\x6f\x6b\x73\x5b\x62\x5d\x3d\x66\x2e\x65\x76\x65\x6e\x74\x2e\x6d\ +\x6f\x75\x73\x65\x48\x6f\x6f\x6b\x73\x29\x7d\x29\x2c\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x29\x7b\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x20\x78\x28\x61\x2c\x62\x2c\x63\x2c\x65\x2c\x66\x2c\x67\x29\x7b\ +\x66\x6f\x72\x28\x76\x61\x72\x20\x68\x3d\x30\x2c\x69\x3d\x65\x2e\ +\x6c\x65\x6e\x67\x74\x68\x3b\x68\x3c\x69\x3b\x68\x2b\x2b\x29\x7b\ +\x76\x61\x72\x20\x6a\x3d\x65\x5b\x68\x5d\x3b\x69\x66\x28\x6a\x29\ +\x7b\x76\x61\x72\x20\x6b\x3d\x21\x31\x3b\x6a\x3d\x6a\x5b\x61\x5d\ +\x3b\x77\x68\x69\x6c\x65\x28\x6a\x29\x7b\x69\x66\x28\x6a\x5b\x64\ +\x5d\x3d\x3d\x3d\x63\x29\x7b\x6b\x3d\x65\x5b\x6a\x2e\x73\x69\x7a\ +\x73\x65\x74\x5d\x3b\x62\x72\x65\x61\x6b\x7d\x69\x66\x28\x6a\x2e\ +\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x29\x7b\x67\x7c\ +\x7c\x28\x6a\x5b\x64\x5d\x3d\x63\x2c\x6a\x2e\x73\x69\x7a\x73\x65\ +\x74\x3d\x68\x29\x3b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x62\ +\x21\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x29\x7b\x69\x66\x28\x6a\ +\x3d\x3d\x3d\x62\x29\x7b\x6b\x3d\x21\x30\x3b\x62\x72\x65\x61\x6b\ +\x7d\x7d\x65\x6c\x73\x65\x20\x69\x66\x28\x6d\x2e\x66\x69\x6c\x74\ +\x65\x72\x28\x62\x2c\x5b\x6a\x5d\x29\x2e\x6c\x65\x6e\x67\x74\x68\ +\x3e\x30\x29\x7b\x6b\x3d\x6a\x3b\x62\x72\x65\x61\x6b\x7d\x7d\x6a\ +\x3d\x6a\x5b\x61\x5d\x7d\x65\x5b\x68\x5d\x3d\x6b\x7d\x7d\x7d\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x20\x77\x28\x61\x2c\x62\x2c\x63\x2c\ +\x65\x2c\x66\x2c\x67\x29\x7b\x66\x6f\x72\x28\x76\x61\x72\x20\x68\ +\x3d\x30\x2c\x69\x3d\x65\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x68\x3c\ +\x69\x3b\x68\x2b\x2b\x29\x7b\x76\x61\x72\x20\x6a\x3d\x65\x5b\x68\ +\x5d\x3b\x69\x66\x28\x6a\x29\x7b\x76\x61\x72\x20\x6b\x3d\x21\x31\ +\x3b\x6a\x3d\x6a\x5b\x61\x5d\x3b\x77\x68\x69\x6c\x65\x28\x6a\x29\ +\x7b\x69\x66\x28\x6a\x5b\x64\x5d\x3d\x3d\x3d\x63\x29\x7b\x6b\x3d\ +\x65\x5b\x6a\x2e\x73\x69\x7a\x73\x65\x74\x5d\x3b\x62\x72\x65\x61\ +\x6b\x7d\x6a\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\ +\x26\x26\x21\x67\x26\x26\x28\x6a\x5b\x64\x5d\x3d\x63\x2c\x6a\x2e\ +\x73\x69\x7a\x73\x65\x74\x3d\x68\x29\x3b\x69\x66\x28\x6a\x2e\x6e\ +\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\ +\x61\x73\x65\x28\x29\x3d\x3d\x3d\x62\x29\x7b\x6b\x3d\x6a\x3b\x62\ +\x72\x65\x61\x6b\x7d\x6a\x3d\x6a\x5b\x61\x5d\x7d\x65\x5b\x68\x5d\ +\x3d\x6b\x7d\x7d\x7d\x76\x61\x72\x20\x61\x3d\x2f\x28\x28\x3f\x3a\ +\x5c\x28\x28\x3f\x3a\x5c\x28\x5b\x5e\x28\x29\x5d\x2b\x5c\x29\x7c\ +\x5b\x5e\x28\x29\x5d\x2b\x29\x2b\x5c\x29\x7c\x5c\x5b\x28\x3f\x3a\ +\x5c\x5b\x5b\x5e\x5c\x5b\x5c\x5d\x5d\x2a\x5c\x5d\x7c\x5b\x27\x22\ +\x5d\x5b\x5e\x27\x22\x5d\x2a\x5b\x27\x22\x5d\x7c\x5b\x5e\x5c\x5b\ +\x5c\x5d\x27\x22\x5d\x2b\x29\x2b\x5c\x5d\x7c\x5c\x5c\x2e\x7c\x5b\ +\x5e\x20\x3e\x2b\x7e\x2c\x28\x5c\x5b\x5c\x5c\x5d\x2b\x29\x2b\x7c\ +\x5b\x3e\x2b\x7e\x5d\x29\x28\x5c\x73\x2a\x2c\x5c\x73\x2a\x29\x3f\ +\x28\x28\x3f\x3a\x2e\x7c\x5c\x72\x7c\x5c\x6e\x29\x2a\x29\x2f\x67\ +\x2c\x64\x3d\x22\x73\x69\x7a\x63\x61\x63\x68\x65\x22\x2b\x28\x4d\ +\x61\x74\x68\x2e\x72\x61\x6e\x64\x6f\x6d\x28\x29\x2b\x22\x22\x29\ +\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x22\x2e\x22\x2c\x22\x22\x29\ +\x2c\x65\x3d\x30\x2c\x67\x3d\x4f\x62\x6a\x65\x63\x74\x2e\x70\x72\ +\x6f\x74\x6f\x74\x79\x70\x65\x2e\x74\x6f\x53\x74\x72\x69\x6e\x67\ +\x2c\x68\x3d\x21\x31\x2c\x69\x3d\x21\x30\x2c\x6a\x3d\x2f\x5c\x5c\ +\x2f\x67\x2c\x6b\x3d\x2f\x5c\x72\x5c\x6e\x2f\x67\x2c\x6c\x3d\x2f\ +\x5c\x57\x2f\x3b\x5b\x30\x2c\x30\x5d\x2e\x73\x6f\x72\x74\x28\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x69\x3d\x21\x31\x3b\x72\ +\x65\x74\x75\x72\x6e\x20\x30\x7d\x29\x3b\x76\x61\x72\x20\x6d\x3d\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\x2c\x64\x2c\x65\x2c\x66\ +\x29\x7b\x65\x3d\x65\x7c\x7c\x5b\x5d\x2c\x64\x3d\x64\x7c\x7c\x63\ +\x3b\x76\x61\x72\x20\x68\x3d\x64\x3b\x69\x66\x28\x64\x2e\x6e\x6f\ +\x64\x65\x54\x79\x70\x65\x21\x3d\x3d\x31\x26\x26\x64\x2e\x6e\x6f\ +\x64\x65\x54\x79\x70\x65\x21\x3d\x3d\x39\x29\x72\x65\x74\x75\x72\ +\x6e\x5b\x5d\x3b\x69\x66\x28\x21\x62\x7c\x7c\x74\x79\x70\x65\x6f\ +\x66\x20\x62\x21\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x29\x72\x65\ +\x74\x75\x72\x6e\x20\x65\x3b\x76\x61\x72\x20\x69\x2c\x6a\x2c\x6b\ +\x2c\x6c\x2c\x6e\x2c\x71\x2c\x72\x2c\x74\x2c\x75\x3d\x21\x30\x2c\ +\x76\x3d\x6d\x2e\x69\x73\x58\x4d\x4c\x28\x64\x29\x2c\x77\x3d\x5b\ +\x5d\x2c\x78\x3d\x62\x3b\x64\x6f\x7b\x61\x2e\x65\x78\x65\x63\x28\ +\x22\x22\x29\x2c\x69\x3d\x61\x2e\x65\x78\x65\x63\x28\x78\x29\x3b\ +\x69\x66\x28\x69\x29\x7b\x78\x3d\x69\x5b\x33\x5d\x2c\x77\x2e\x70\ +\x75\x73\x68\x28\x69\x5b\x31\x5d\x29\x3b\x69\x66\x28\x69\x5b\x32\ +\x5d\x29\x7b\x6c\x3d\x69\x5b\x33\x5d\x3b\x62\x72\x65\x61\x6b\x7d\ +\x7d\x7d\x77\x68\x69\x6c\x65\x28\x69\x29\x3b\x69\x66\x28\x77\x2e\ +\x6c\x65\x6e\x67\x74\x68\x3e\x31\x26\x26\x70\x2e\x65\x78\x65\x63\ +\x28\x62\x29\x29\x69\x66\x28\x77\x2e\x6c\x65\x6e\x67\x74\x68\x3d\ +\x3d\x3d\x32\x26\x26\x6f\x2e\x72\x65\x6c\x61\x74\x69\x76\x65\x5b\ +\x77\x5b\x30\x5d\x5d\x29\x6a\x3d\x79\x28\x77\x5b\x30\x5d\x2b\x77\ +\x5b\x31\x5d\x2c\x64\x2c\x66\x29\x3b\x65\x6c\x73\x65\x7b\x6a\x3d\ +\x6f\x2e\x72\x65\x6c\x61\x74\x69\x76\x65\x5b\x77\x5b\x30\x5d\x5d\ +\x3f\x5b\x64\x5d\x3a\x6d\x28\x77\x2e\x73\x68\x69\x66\x74\x28\x29\ +\x2c\x64\x29\x3b\x77\x68\x69\x6c\x65\x28\x77\x2e\x6c\x65\x6e\x67\ +\x74\x68\x29\x62\x3d\x77\x2e\x73\x68\x69\x66\x74\x28\x29\x2c\x6f\ +\x2e\x72\x65\x6c\x61\x74\x69\x76\x65\x5b\x62\x5d\x26\x26\x28\x62\ +\x2b\x3d\x77\x2e\x73\x68\x69\x66\x74\x28\x29\x29\x2c\x6a\x3d\x79\ +\x28\x62\x2c\x6a\x2c\x66\x29\x7d\x65\x6c\x73\x65\x7b\x21\x66\x26\ +\x26\x77\x2e\x6c\x65\x6e\x67\x74\x68\x3e\x31\x26\x26\x64\x2e\x6e\ +\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x39\x26\x26\x21\x76\x26\ +\x26\x6f\x2e\x6d\x61\x74\x63\x68\x2e\x49\x44\x2e\x74\x65\x73\x74\ +\x28\x77\x5b\x30\x5d\x29\x26\x26\x21\x6f\x2e\x6d\x61\x74\x63\x68\ +\x2e\x49\x44\x2e\x74\x65\x73\x74\x28\x77\x5b\x77\x2e\x6c\x65\x6e\ +\x67\x74\x68\x2d\x31\x5d\x29\x26\x26\x28\x6e\x3d\x6d\x2e\x66\x69\ +\x6e\x64\x28\x77\x2e\x73\x68\x69\x66\x74\x28\x29\x2c\x64\x2c\x76\ +\x29\x2c\x64\x3d\x6e\x2e\x65\x78\x70\x72\x3f\x6d\x2e\x66\x69\x6c\ +\x74\x65\x72\x28\x6e\x2e\x65\x78\x70\x72\x2c\x6e\x2e\x73\x65\x74\ +\x29\x5b\x30\x5d\x3a\x6e\x2e\x73\x65\x74\x5b\x30\x5d\x29\x3b\x69\ +\x66\x28\x64\x29\x7b\x6e\x3d\x66\x3f\x7b\x65\x78\x70\x72\x3a\x77\ +\x2e\x70\x6f\x70\x28\x29\x2c\x73\x65\x74\x3a\x73\x28\x66\x29\x7d\ +\x3a\x6d\x2e\x66\x69\x6e\x64\x28\x77\x2e\x70\x6f\x70\x28\x29\x2c\ +\x77\x2e\x6c\x65\x6e\x67\x74\x68\x3d\x3d\x3d\x31\x26\x26\x28\x77\ +\x5b\x30\x5d\x3d\x3d\x3d\x22\x7e\x22\x7c\x7c\x77\x5b\x30\x5d\x3d\ +\x3d\x3d\x22\x2b\x22\x29\x26\x26\x64\x2e\x70\x61\x72\x65\x6e\x74\ +\x4e\x6f\x64\x65\x3f\x64\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\ +\x65\x3a\x64\x2c\x76\x29\x2c\x6a\x3d\x6e\x2e\x65\x78\x70\x72\x3f\ +\x6d\x2e\x66\x69\x6c\x74\x65\x72\x28\x6e\x2e\x65\x78\x70\x72\x2c\ +\x6e\x2e\x73\x65\x74\x29\x3a\x6e\x2e\x73\x65\x74\x2c\x77\x2e\x6c\ +\x65\x6e\x67\x74\x68\x3e\x30\x3f\x6b\x3d\x73\x28\x6a\x29\x3a\x75\ +\x3d\x21\x31\x3b\x77\x68\x69\x6c\x65\x28\x77\x2e\x6c\x65\x6e\x67\ +\x74\x68\x29\x71\x3d\x77\x2e\x70\x6f\x70\x28\x29\x2c\x72\x3d\x71\ +\x2c\x6f\x2e\x72\x65\x6c\x61\x74\x69\x76\x65\x5b\x71\x5d\x3f\x72\ +\x3d\x77\x2e\x70\x6f\x70\x28\x29\x3a\x71\x3d\x22\x22\x2c\x72\x3d\ +\x3d\x6e\x75\x6c\x6c\x26\x26\x28\x72\x3d\x64\x29\x2c\x6f\x2e\x72\ +\x65\x6c\x61\x74\x69\x76\x65\x5b\x71\x5d\x28\x6b\x2c\x72\x2c\x76\ +\x29\x7d\x65\x6c\x73\x65\x20\x6b\x3d\x77\x3d\x5b\x5d\x7d\x6b\x7c\ +\x7c\x28\x6b\x3d\x6a\x29\x2c\x6b\x7c\x7c\x6d\x2e\x65\x72\x72\x6f\ +\x72\x28\x71\x7c\x7c\x62\x29\x3b\x69\x66\x28\x67\x2e\x63\x61\x6c\ +\x6c\x28\x6b\x29\x3d\x3d\x3d\x22\x5b\x6f\x62\x6a\x65\x63\x74\x20\ +\x41\x72\x72\x61\x79\x5d\x22\x29\x69\x66\x28\x21\x75\x29\x65\x2e\ +\x70\x75\x73\x68\x2e\x61\x70\x70\x6c\x79\x28\x65\x2c\x6b\x29\x3b\ +\x65\x6c\x73\x65\x20\x69\x66\x28\x64\x26\x26\x64\x2e\x6e\x6f\x64\ +\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x29\x66\x6f\x72\x28\x74\x3d\ +\x30\x3b\x6b\x5b\x74\x5d\x21\x3d\x6e\x75\x6c\x6c\x3b\x74\x2b\x2b\ +\x29\x6b\x5b\x74\x5d\x26\x26\x28\x6b\x5b\x74\x5d\x3d\x3d\x3d\x21\ +\x30\x7c\x7c\x6b\x5b\x74\x5d\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\ +\x3d\x3d\x3d\x31\x26\x26\x6d\x2e\x63\x6f\x6e\x74\x61\x69\x6e\x73\ +\x28\x64\x2c\x6b\x5b\x74\x5d\x29\x29\x26\x26\x65\x2e\x70\x75\x73\ +\x68\x28\x6a\x5b\x74\x5d\x29\x3b\x65\x6c\x73\x65\x20\x66\x6f\x72\ +\x28\x74\x3d\x30\x3b\x6b\x5b\x74\x5d\x21\x3d\x6e\x75\x6c\x6c\x3b\ +\x74\x2b\x2b\x29\x6b\x5b\x74\x5d\x26\x26\x6b\x5b\x74\x5d\x2e\x6e\ +\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x26\x26\x65\x2e\x70\ +\x75\x73\x68\x28\x6a\x5b\x74\x5d\x29\x3b\x65\x6c\x73\x65\x20\x73\ +\x28\x6b\x2c\x65\x29\x3b\x6c\x26\x26\x28\x6d\x28\x6c\x2c\x68\x2c\ +\x65\x2c\x66\x29\x2c\x6d\x2e\x75\x6e\x69\x71\x75\x65\x53\x6f\x72\ +\x74\x28\x65\x29\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x65\x7d\x3b\ +\x6d\x2e\x75\x6e\x69\x71\x75\x65\x53\x6f\x72\x74\x3d\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x69\x66\x28\x75\x29\x7b\x68\ +\x3d\x69\x2c\x61\x2e\x73\x6f\x72\x74\x28\x75\x29\x3b\x69\x66\x28\ +\x68\x29\x66\x6f\x72\x28\x76\x61\x72\x20\x62\x3d\x31\x3b\x62\x3c\ +\x61\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x62\x2b\x2b\x29\x61\x5b\x62\ +\x5d\x3d\x3d\x3d\x61\x5b\x62\x2d\x31\x5d\x26\x26\x61\x2e\x73\x70\ +\x6c\x69\x63\x65\x28\x62\x2d\x2d\x2c\x31\x29\x7d\x72\x65\x74\x75\ +\x72\x6e\x20\x61\x7d\x2c\x6d\x2e\x6d\x61\x74\x63\x68\x65\x73\x3d\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\ +\x74\x75\x72\x6e\x20\x6d\x28\x61\x2c\x6e\x75\x6c\x6c\x2c\x6e\x75\ +\x6c\x6c\x2c\x62\x29\x7d\x2c\x6d\x2e\x6d\x61\x74\x63\x68\x65\x73\ +\x53\x65\x6c\x65\x63\x74\x6f\x72\x3d\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x6d\x28\ +\x62\x2c\x6e\x75\x6c\x6c\x2c\x6e\x75\x6c\x6c\x2c\x5b\x61\x5d\x29\ +\x2e\x6c\x65\x6e\x67\x74\x68\x3e\x30\x7d\x2c\x6d\x2e\x66\x69\x6e\ +\x64\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\ +\x29\x7b\x76\x61\x72\x20\x64\x2c\x65\x2c\x66\x2c\x67\x2c\x68\x2c\ +\x69\x3b\x69\x66\x28\x21\x61\x29\x72\x65\x74\x75\x72\x6e\x5b\x5d\ +\x3b\x66\x6f\x72\x28\x65\x3d\x30\x2c\x66\x3d\x6f\x2e\x6f\x72\x64\ +\x65\x72\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x65\x3c\x66\x3b\x65\x2b\ +\x2b\x29\x7b\x68\x3d\x6f\x2e\x6f\x72\x64\x65\x72\x5b\x65\x5d\x3b\ +\x69\x66\x28\x67\x3d\x6f\x2e\x6c\x65\x66\x74\x4d\x61\x74\x63\x68\ +\x5b\x68\x5d\x2e\x65\x78\x65\x63\x28\x61\x29\x29\x7b\x69\x3d\x67\ +\x5b\x31\x5d\x2c\x67\x2e\x73\x70\x6c\x69\x63\x65\x28\x31\x2c\x31\ +\x29\x3b\x69\x66\x28\x69\x2e\x73\x75\x62\x73\x74\x72\x28\x69\x2e\ +\x6c\x65\x6e\x67\x74\x68\x2d\x31\x29\x21\x3d\x3d\x22\x5c\x5c\x22\ +\x29\x7b\x67\x5b\x31\x5d\x3d\x28\x67\x5b\x31\x5d\x7c\x7c\x22\x22\ +\x29\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x6a\x2c\x22\x22\x29\x2c\ +\x64\x3d\x6f\x2e\x66\x69\x6e\x64\x5b\x68\x5d\x28\x67\x2c\x62\x2c\ +\x63\x29\x3b\x69\x66\x28\x64\x21\x3d\x6e\x75\x6c\x6c\x29\x7b\x61\ +\x3d\x61\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x6f\x2e\x6d\x61\x74\ +\x63\x68\x5b\x68\x5d\x2c\x22\x22\x29\x3b\x62\x72\x65\x61\x6b\x7d\ +\x7d\x7d\x7d\x64\x7c\x7c\x28\x64\x3d\x74\x79\x70\x65\x6f\x66\x20\ +\x62\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\ +\x61\x67\x4e\x61\x6d\x65\x21\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\ +\x65\x64\x22\x3f\x62\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\ +\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\x2a\x22\x29\x3a\ +\x5b\x5d\x29\x3b\x72\x65\x74\x75\x72\x6e\x7b\x73\x65\x74\x3a\x64\ +\x2c\x65\x78\x70\x72\x3a\x61\x7d\x7d\x2c\x6d\x2e\x66\x69\x6c\x74\ +\x65\x72\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x2c\ +\x64\x2c\x65\x29\x7b\x76\x61\x72\x20\x66\x2c\x67\x2c\x68\x2c\x69\ +\x2c\x6a\x2c\x6b\x2c\x6c\x2c\x6e\x2c\x70\x2c\x71\x3d\x61\x2c\x72\ +\x3d\x5b\x5d\x2c\x73\x3d\x63\x2c\x74\x3d\x63\x26\x26\x63\x5b\x30\ +\x5d\x26\x26\x6d\x2e\x69\x73\x58\x4d\x4c\x28\x63\x5b\x30\x5d\x29\ +\x3b\x77\x68\x69\x6c\x65\x28\x61\x26\x26\x63\x2e\x6c\x65\x6e\x67\ +\x74\x68\x29\x7b\x66\x6f\x72\x28\x68\x20\x69\x6e\x20\x6f\x2e\x66\ +\x69\x6c\x74\x65\x72\x29\x69\x66\x28\x28\x66\x3d\x6f\x2e\x6c\x65\ +\x66\x74\x4d\x61\x74\x63\x68\x5b\x68\x5d\x2e\x65\x78\x65\x63\x28\ +\x61\x29\x29\x21\x3d\x6e\x75\x6c\x6c\x26\x26\x66\x5b\x32\x5d\x29\ +\x7b\x6b\x3d\x6f\x2e\x66\x69\x6c\x74\x65\x72\x5b\x68\x5d\x2c\x6c\ +\x3d\x66\x5b\x31\x5d\x2c\x67\x3d\x21\x31\x2c\x66\x2e\x73\x70\x6c\ +\x69\x63\x65\x28\x31\x2c\x31\x29\x3b\x69\x66\x28\x6c\x2e\x73\x75\ +\x62\x73\x74\x72\x28\x6c\x2e\x6c\x65\x6e\x67\x74\x68\x2d\x31\x29\ +\x3d\x3d\x3d\x22\x5c\x5c\x22\x29\x63\x6f\x6e\x74\x69\x6e\x75\x65\ +\x3b\x73\x3d\x3d\x3d\x72\x26\x26\x28\x72\x3d\x5b\x5d\x29\x3b\x69\ +\x66\x28\x6f\x2e\x70\x72\x65\x46\x69\x6c\x74\x65\x72\x5b\x68\x5d\ +\x29\x7b\x66\x3d\x6f\x2e\x70\x72\x65\x46\x69\x6c\x74\x65\x72\x5b\ +\x68\x5d\x28\x66\x2c\x73\x2c\x64\x2c\x72\x2c\x65\x2c\x74\x29\x3b\ +\x69\x66\x28\x21\x66\x29\x67\x3d\x69\x3d\x21\x30\x3b\x65\x6c\x73\ +\x65\x20\x69\x66\x28\x66\x3d\x3d\x3d\x21\x30\x29\x63\x6f\x6e\x74\ +\x69\x6e\x75\x65\x7d\x69\x66\x28\x66\x29\x66\x6f\x72\x28\x6e\x3d\ +\x30\x3b\x28\x6a\x3d\x73\x5b\x6e\x5d\x29\x21\x3d\x6e\x75\x6c\x6c\ +\x3b\x6e\x2b\x2b\x29\x6a\x26\x26\x28\x69\x3d\x6b\x28\x6a\x2c\x66\ +\x2c\x6e\x2c\x73\x29\x2c\x70\x3d\x65\x5e\x69\x2c\x64\x26\x26\x69\ +\x21\x3d\x6e\x75\x6c\x6c\x3f\x70\x3f\x67\x3d\x21\x30\x3a\x73\x5b\ +\x6e\x5d\x3d\x21\x31\x3a\x70\x26\x26\x28\x72\x2e\x70\x75\x73\x68\ +\x28\x6a\x29\x2c\x67\x3d\x21\x30\x29\x29\x3b\x69\x66\x28\x69\x21\ +\x3d\x3d\x62\x29\x7b\x64\x7c\x7c\x28\x73\x3d\x72\x29\x2c\x61\x3d\ +\x61\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x6f\x2e\x6d\x61\x74\x63\ +\x68\x5b\x68\x5d\x2c\x22\x22\x29\x3b\x69\x66\x28\x21\x67\x29\x72\ +\x65\x74\x75\x72\x6e\x5b\x5d\x3b\x62\x72\x65\x61\x6b\x7d\x7d\x69\ +\x66\x28\x61\x3d\x3d\x3d\x71\x29\x69\x66\x28\x67\x3d\x3d\x6e\x75\ +\x6c\x6c\x29\x6d\x2e\x65\x72\x72\x6f\x72\x28\x61\x29\x3b\x65\x6c\ +\x73\x65\x20\x62\x72\x65\x61\x6b\x3b\x71\x3d\x61\x7d\x72\x65\x74\ +\x75\x72\x6e\x20\x73\x7d\x2c\x6d\x2e\x65\x72\x72\x6f\x72\x3d\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x74\x68\x72\x6f\x77\ +\x20\x6e\x65\x77\x20\x45\x72\x72\x6f\x72\x28\x22\x53\x79\x6e\x74\ +\x61\x78\x20\x65\x72\x72\x6f\x72\x2c\x20\x75\x6e\x72\x65\x63\x6f\ +\x67\x6e\x69\x7a\x65\x64\x20\x65\x78\x70\x72\x65\x73\x73\x69\x6f\ +\x6e\x3a\x20\x22\x2b\x61\x29\x7d\x3b\x76\x61\x72\x20\x6e\x3d\x6d\ +\x2e\x67\x65\x74\x54\x65\x78\x74\x3d\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\x2c\x63\x2c\x64\x3d\x61\ +\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x2c\x65\x3d\x22\x22\x3b\x69\ +\x66\x28\x64\x29\x7b\x69\x66\x28\x64\x3d\x3d\x3d\x31\x7c\x7c\x64\ +\x3d\x3d\x3d\x39\x29\x7b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\ +\x61\x2e\x74\x65\x78\x74\x43\x6f\x6e\x74\x65\x6e\x74\x3d\x3d\x22\ +\x73\x74\x72\x69\x6e\x67\x22\x29\x72\x65\x74\x75\x72\x6e\x20\x61\ +\x2e\x74\x65\x78\x74\x43\x6f\x6e\x74\x65\x6e\x74\x3b\x69\x66\x28\ +\x74\x79\x70\x65\x6f\x66\x20\x61\x2e\x69\x6e\x6e\x65\x72\x54\x65\ +\x78\x74\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x29\x72\x65\x74\ +\x75\x72\x6e\x20\x61\x2e\x69\x6e\x6e\x65\x72\x54\x65\x78\x74\x2e\ +\x72\x65\x70\x6c\x61\x63\x65\x28\x6b\x2c\x22\x22\x29\x3b\x66\x6f\ +\x72\x28\x61\x3d\x61\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\ +\x3b\x61\x3b\x61\x3d\x61\x2e\x6e\x65\x78\x74\x53\x69\x62\x6c\x69\ +\x6e\x67\x29\x65\x2b\x3d\x6e\x28\x61\x29\x7d\x65\x6c\x73\x65\x20\ +\x69\x66\x28\x64\x3d\x3d\x3d\x33\x7c\x7c\x64\x3d\x3d\x3d\x34\x29\ +\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x6e\x6f\x64\x65\x56\x61\x6c\ +\x75\x65\x7d\x65\x6c\x73\x65\x20\x66\x6f\x72\x28\x62\x3d\x30\x3b\ +\x63\x3d\x61\x5b\x62\x5d\x3b\x62\x2b\x2b\x29\x63\x2e\x6e\x6f\x64\ +\x65\x54\x79\x70\x65\x21\x3d\x3d\x38\x26\x26\x28\x65\x2b\x3d\x6e\ +\x28\x63\x29\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x65\x7d\x2c\x6f\ +\x3d\x6d\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x73\x3d\x7b\x6f\x72\ +\x64\x65\x72\x3a\x5b\x22\x49\x44\x22\x2c\x22\x4e\x41\x4d\x45\x22\ +\x2c\x22\x54\x41\x47\x22\x5d\x2c\x6d\x61\x74\x63\x68\x3a\x7b\x49\ +\x44\x3a\x2f\x23\x28\x28\x3f\x3a\x5b\x5c\x77\x5c\x75\x30\x30\x63\ +\x30\x2d\x5c\x75\x46\x46\x46\x46\x5c\x2d\x5d\x7c\x5c\x5c\x2e\x29\ +\x2b\x29\x2f\x2c\x43\x4c\x41\x53\x53\x3a\x2f\x5c\x2e\x28\x28\x3f\ +\x3a\x5b\x5c\x77\x5c\x75\x30\x30\x63\x30\x2d\x5c\x75\x46\x46\x46\ +\x46\x5c\x2d\x5d\x7c\x5c\x5c\x2e\x29\x2b\x29\x2f\x2c\x4e\x41\x4d\ +\x45\x3a\x2f\x5c\x5b\x6e\x61\x6d\x65\x3d\x5b\x27\x22\x5d\x2a\x28\ +\x28\x3f\x3a\x5b\x5c\x77\x5c\x75\x30\x30\x63\x30\x2d\x5c\x75\x46\ +\x46\x46\x46\x5c\x2d\x5d\x7c\x5c\x5c\x2e\x29\x2b\x29\x5b\x27\x22\ +\x5d\x2a\x5c\x5d\x2f\x2c\x41\x54\x54\x52\x3a\x2f\x5c\x5b\x5c\x73\ +\x2a\x28\x28\x3f\x3a\x5b\x5c\x77\x5c\x75\x30\x30\x63\x30\x2d\x5c\ +\x75\x46\x46\x46\x46\x5c\x2d\x5d\x7c\x5c\x5c\x2e\x29\x2b\x29\x5c\ +\x73\x2a\x28\x3f\x3a\x28\x5c\x53\x3f\x3d\x29\x5c\x73\x2a\x28\x3f\ +\x3a\x28\x5b\x27\x22\x5d\x29\x28\x2e\x2a\x3f\x29\x5c\x33\x7c\x28\ +\x23\x3f\x28\x3f\x3a\x5b\x5c\x77\x5c\x75\x30\x30\x63\x30\x2d\x5c\ +\x75\x46\x46\x46\x46\x5c\x2d\x5d\x7c\x5c\x5c\x2e\x29\x2a\x29\x7c\ +\x29\x7c\x29\x5c\x73\x2a\x5c\x5d\x2f\x2c\x54\x41\x47\x3a\x2f\x5e\ +\x28\x28\x3f\x3a\x5b\x5c\x77\x5c\x75\x30\x30\x63\x30\x2d\x5c\x75\ +\x46\x46\x46\x46\x5c\x2a\x5c\x2d\x5d\x7c\x5c\x5c\x2e\x29\x2b\x29\ +\x2f\x2c\x43\x48\x49\x4c\x44\x3a\x2f\x3a\x28\x6f\x6e\x6c\x79\x7c\ +\x6e\x74\x68\x7c\x6c\x61\x73\x74\x7c\x66\x69\x72\x73\x74\x29\x2d\ +\x63\x68\x69\x6c\x64\x28\x3f\x3a\x5c\x28\x5c\x73\x2a\x28\x65\x76\ +\x65\x6e\x7c\x6f\x64\x64\x7c\x28\x3f\x3a\x5b\x2b\x5c\x2d\x5d\x3f\ +\x5c\x64\x2b\x7c\x28\x3f\x3a\x5b\x2b\x5c\x2d\x5d\x3f\x5c\x64\x2a\ +\x29\x3f\x6e\x5c\x73\x2a\x28\x3f\x3a\x5b\x2b\x5c\x2d\x5d\x5c\x73\ +\x2a\x5c\x64\x2b\x29\x3f\x29\x29\x5c\x73\x2a\x5c\x29\x29\x3f\x2f\ +\x2c\x50\x4f\x53\x3a\x2f\x3a\x28\x6e\x74\x68\x7c\x65\x71\x7c\x67\ +\x74\x7c\x6c\x74\x7c\x66\x69\x72\x73\x74\x7c\x6c\x61\x73\x74\x7c\ +\x65\x76\x65\x6e\x7c\x6f\x64\x64\x29\x28\x3f\x3a\x5c\x28\x28\x5c\ +\x64\x2a\x29\x5c\x29\x29\x3f\x28\x3f\x3d\x5b\x5e\x5c\x2d\x5d\x7c\ +\x24\x29\x2f\x2c\x50\x53\x45\x55\x44\x4f\x3a\x2f\x3a\x28\x28\x3f\ +\x3a\x5b\x5c\x77\x5c\x75\x30\x30\x63\x30\x2d\x5c\x75\x46\x46\x46\ +\x46\x5c\x2d\x5d\x7c\x5c\x5c\x2e\x29\x2b\x29\x28\x3f\x3a\x5c\x28\ +\x28\x5b\x27\x22\x5d\x3f\x29\x28\x28\x3f\x3a\x5c\x28\x5b\x5e\x5c\ +\x29\x5d\x2b\x5c\x29\x7c\x5b\x5e\x5c\x28\x5c\x29\x5d\x2a\x29\x2b\ +\x29\x5c\x32\x5c\x29\x29\x3f\x2f\x7d\x2c\x6c\x65\x66\x74\x4d\x61\ +\x74\x63\x68\x3a\x7b\x7d\x2c\x61\x74\x74\x72\x4d\x61\x70\x3a\x7b\ +\x22\x63\x6c\x61\x73\x73\x22\x3a\x22\x63\x6c\x61\x73\x73\x4e\x61\ +\x6d\x65\x22\x2c\x22\x66\x6f\x72\x22\x3a\x22\x68\x74\x6d\x6c\x46\ +\x6f\x72\x22\x7d\x2c\x61\x74\x74\x72\x48\x61\x6e\x64\x6c\x65\x3a\ +\x7b\x68\x72\x65\x66\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x67\x65\x74\x41\x74\ +\x74\x72\x69\x62\x75\x74\x65\x28\x22\x68\x72\x65\x66\x22\x29\x7d\ +\x2c\x74\x79\x70\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x67\x65\x74\x41\x74\ +\x74\x72\x69\x62\x75\x74\x65\x28\x22\x74\x79\x70\x65\x22\x29\x7d\ +\x7d\x2c\x72\x65\x6c\x61\x74\x69\x76\x65\x3a\x7b\x22\x2b\x22\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\ +\x72\x20\x63\x3d\x74\x79\x70\x65\x6f\x66\x20\x62\x3d\x3d\x22\x73\ +\x74\x72\x69\x6e\x67\x22\x2c\x64\x3d\x63\x26\x26\x21\x6c\x2e\x74\ +\x65\x73\x74\x28\x62\x29\x2c\x65\x3d\x63\x26\x26\x21\x64\x3b\x64\ +\x26\x26\x28\x62\x3d\x62\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\ +\x73\x65\x28\x29\x29\x3b\x66\x6f\x72\x28\x76\x61\x72\x20\x66\x3d\ +\x30\x2c\x67\x3d\x61\x2e\x6c\x65\x6e\x67\x74\x68\x2c\x68\x3b\x66\ +\x3c\x67\x3b\x66\x2b\x2b\x29\x69\x66\x28\x68\x3d\x61\x5b\x66\x5d\ +\x29\x7b\x77\x68\x69\x6c\x65\x28\x28\x68\x3d\x68\x2e\x70\x72\x65\ +\x76\x69\x6f\x75\x73\x53\x69\x62\x6c\x69\x6e\x67\x29\x26\x26\x68\ +\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x21\x3d\x3d\x31\x29\x3b\x61\ +\x5b\x66\x5d\x3d\x65\x7c\x7c\x68\x26\x26\x68\x2e\x6e\x6f\x64\x65\ +\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\ +\x28\x29\x3d\x3d\x3d\x62\x3f\x68\x7c\x7c\x21\x31\x3a\x68\x3d\x3d\ +\x3d\x62\x7d\x65\x26\x26\x6d\x2e\x66\x69\x6c\x74\x65\x72\x28\x62\ +\x2c\x61\x2c\x21\x30\x29\x7d\x2c\x22\x3e\x22\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x2c\ +\x64\x3d\x74\x79\x70\x65\x6f\x66\x20\x62\x3d\x3d\x22\x73\x74\x72\ +\x69\x6e\x67\x22\x2c\x65\x3d\x30\x2c\x66\x3d\x61\x2e\x6c\x65\x6e\ +\x67\x74\x68\x3b\x69\x66\x28\x64\x26\x26\x21\x6c\x2e\x74\x65\x73\ +\x74\x28\x62\x29\x29\x7b\x62\x3d\x62\x2e\x74\x6f\x4c\x6f\x77\x65\ +\x72\x43\x61\x73\x65\x28\x29\x3b\x66\x6f\x72\x28\x3b\x65\x3c\x66\ +\x3b\x65\x2b\x2b\x29\x7b\x63\x3d\x61\x5b\x65\x5d\x3b\x69\x66\x28\ +\x63\x29\x7b\x76\x61\x72\x20\x67\x3d\x63\x2e\x70\x61\x72\x65\x6e\ +\x74\x4e\x6f\x64\x65\x3b\x61\x5b\x65\x5d\x3d\x67\x2e\x6e\x6f\x64\ +\x65\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\ +\x65\x28\x29\x3d\x3d\x3d\x62\x3f\x67\x3a\x21\x31\x7d\x7d\x7d\x65\ +\x6c\x73\x65\x7b\x66\x6f\x72\x28\x3b\x65\x3c\x66\x3b\x65\x2b\x2b\ +\x29\x63\x3d\x61\x5b\x65\x5d\x2c\x63\x26\x26\x28\x61\x5b\x65\x5d\ +\x3d\x64\x3f\x63\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x3a\ +\x63\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x3d\x3d\x3d\x62\ +\x29\x3b\x64\x26\x26\x6d\x2e\x66\x69\x6c\x74\x65\x72\x28\x62\x2c\ +\x61\x2c\x21\x30\x29\x7d\x7d\x2c\x22\x22\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\ +\x2c\x66\x3d\x65\x2b\x2b\x2c\x67\x3d\x78\x3b\x74\x79\x70\x65\x6f\ +\x66\x20\x62\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x21\ +\x6c\x2e\x74\x65\x73\x74\x28\x62\x29\x26\x26\x28\x62\x3d\x62\x2e\ +\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x2c\x64\x3d\ +\x62\x2c\x67\x3d\x77\x29\x2c\x67\x28\x22\x70\x61\x72\x65\x6e\x74\ +\x4e\x6f\x64\x65\x22\x2c\x62\x2c\x66\x2c\x61\x2c\x64\x2c\x63\x29\ +\x7d\x2c\x22\x7e\x22\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x2c\x66\x3d\x65\x2b\ +\x2b\x2c\x67\x3d\x78\x3b\x74\x79\x70\x65\x6f\x66\x20\x62\x3d\x3d\ +\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x21\x6c\x2e\x74\x65\x73\ +\x74\x28\x62\x29\x26\x26\x28\x62\x3d\x62\x2e\x74\x6f\x4c\x6f\x77\ +\x65\x72\x43\x61\x73\x65\x28\x29\x2c\x64\x3d\x62\x2c\x67\x3d\x77\ +\x29\x2c\x67\x28\x22\x70\x72\x65\x76\x69\x6f\x75\x73\x53\x69\x62\ +\x6c\x69\x6e\x67\x22\x2c\x62\x2c\x66\x2c\x61\x2c\x64\x2c\x63\x29\ +\x7d\x7d\x2c\x66\x69\x6e\x64\x3a\x7b\x49\x44\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x69\x66\x28\x74\ +\x79\x70\x65\x6f\x66\x20\x62\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\ +\x6e\x74\x42\x79\x49\x64\x21\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\ +\x65\x64\x22\x26\x26\x21\x63\x29\x7b\x76\x61\x72\x20\x64\x3d\x62\ +\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x42\x79\x49\x64\x28\ +\x61\x5b\x31\x5d\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x64\x26\x26\ +\x64\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x3f\x5b\x64\x5d\ +\x3a\x5b\x5d\x7d\x7d\x2c\x4e\x41\x4d\x45\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x69\x66\x28\x74\x79\x70\x65\ +\x6f\x66\x20\x62\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\ +\x42\x79\x4e\x61\x6d\x65\x21\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\ +\x65\x64\x22\x29\x7b\x76\x61\x72\x20\x63\x3d\x5b\x5d\x2c\x64\x3d\ +\x62\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x4e\ +\x61\x6d\x65\x28\x61\x5b\x31\x5d\x29\x3b\x66\x6f\x72\x28\x76\x61\ +\x72\x20\x65\x3d\x30\x2c\x66\x3d\x64\x2e\x6c\x65\x6e\x67\x74\x68\ +\x3b\x65\x3c\x66\x3b\x65\x2b\x2b\x29\x64\x5b\x65\x5d\x2e\x67\x65\ +\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x6e\x61\x6d\x65\ +\x22\x29\x3d\x3d\x3d\x61\x5b\x31\x5d\x26\x26\x63\x2e\x70\x75\x73\ +\x68\x28\x64\x5b\x65\x5d\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x63\ +\x2e\x6c\x65\x6e\x67\x74\x68\x3d\x3d\x3d\x30\x3f\x6e\x75\x6c\x6c\ +\x3a\x63\x7d\x7d\x2c\x54\x41\x47\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x2c\x62\x29\x7b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\ +\x20\x62\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\ +\x54\x61\x67\x4e\x61\x6d\x65\x21\x3d\x22\x75\x6e\x64\x65\x66\x69\ +\x6e\x65\x64\x22\x29\x72\x65\x74\x75\x72\x6e\x20\x62\x2e\x67\x65\ +\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\x61\ +\x6d\x65\x28\x61\x5b\x31\x5d\x29\x7d\x7d\x2c\x70\x72\x65\x46\x69\ +\x6c\x74\x65\x72\x3a\x7b\x43\x4c\x41\x53\x53\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\x64\x2c\x65\x2c\x66\ +\x29\x7b\x61\x3d\x22\x20\x22\x2b\x61\x5b\x31\x5d\x2e\x72\x65\x70\ +\x6c\x61\x63\x65\x28\x6a\x2c\x22\x22\x29\x2b\x22\x20\x22\x3b\x69\ +\x66\x28\x66\x29\x72\x65\x74\x75\x72\x6e\x20\x61\x3b\x66\x6f\x72\ +\x28\x76\x61\x72\x20\x67\x3d\x30\x2c\x68\x3b\x28\x68\x3d\x62\x5b\ +\x67\x5d\x29\x21\x3d\x6e\x75\x6c\x6c\x3b\x67\x2b\x2b\x29\x68\x26\ +\x26\x28\x65\x5e\x28\x68\x2e\x63\x6c\x61\x73\x73\x4e\x61\x6d\x65\ +\x26\x26\x28\x22\x20\x22\x2b\x68\x2e\x63\x6c\x61\x73\x73\x4e\x61\ +\x6d\x65\x2b\x22\x20\x22\x29\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\ +\x2f\x5b\x5c\x74\x5c\x6e\x5c\x72\x5d\x2f\x67\x2c\x22\x20\x22\x29\ +\x2e\x69\x6e\x64\x65\x78\x4f\x66\x28\x61\x29\x3e\x3d\x30\x29\x3f\ +\x63\x7c\x7c\x64\x2e\x70\x75\x73\x68\x28\x68\x29\x3a\x63\x26\x26\ +\x28\x62\x5b\x67\x5d\x3d\x21\x31\x29\x29\x3b\x72\x65\x74\x75\x72\ +\x6e\x21\x31\x7d\x2c\x49\x44\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x5b\x31\x5d\x2e\ +\x72\x65\x70\x6c\x61\x63\x65\x28\x6a\x2c\x22\x22\x29\x7d\x2c\x54\ +\x41\x47\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\ +\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x5b\x31\x5d\x2e\x72\x65\x70\ +\x6c\x61\x63\x65\x28\x6a\x2c\x22\x22\x29\x2e\x74\x6f\x4c\x6f\x77\ +\x65\x72\x43\x61\x73\x65\x28\x29\x7d\x2c\x43\x48\x49\x4c\x44\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x69\x66\x28\x61\ +\x5b\x31\x5d\x3d\x3d\x3d\x22\x6e\x74\x68\x22\x29\x7b\x61\x5b\x32\ +\x5d\x7c\x7c\x6d\x2e\x65\x72\x72\x6f\x72\x28\x61\x5b\x30\x5d\x29\ +\x2c\x61\x5b\x32\x5d\x3d\x61\x5b\x32\x5d\x2e\x72\x65\x70\x6c\x61\ +\x63\x65\x28\x2f\x5e\x5c\x2b\x7c\x5c\x73\x2a\x2f\x67\x2c\x22\x22\ +\x29\x3b\x76\x61\x72\x20\x62\x3d\x2f\x28\x2d\x3f\x29\x28\x5c\x64\ +\x2a\x29\x28\x3f\x3a\x6e\x28\x5b\x2b\x5c\x2d\x5d\x3f\x5c\x64\x2a\ +\x29\x29\x3f\x2f\x2e\x65\x78\x65\x63\x28\x61\x5b\x32\x5d\x3d\x3d\ +\x3d\x22\x65\x76\x65\x6e\x22\x26\x26\x22\x32\x6e\x22\x7c\x7c\x61\ +\x5b\x32\x5d\x3d\x3d\x3d\x22\x6f\x64\x64\x22\x26\x26\x22\x32\x6e\ +\x2b\x31\x22\x7c\x7c\x21\x2f\x5c\x44\x2f\x2e\x74\x65\x73\x74\x28\ +\x61\x5b\x32\x5d\x29\x26\x26\x22\x30\x6e\x2b\x22\x2b\x61\x5b\x32\ +\x5d\x7c\x7c\x61\x5b\x32\x5d\x29\x3b\x61\x5b\x32\x5d\x3d\x62\x5b\ +\x31\x5d\x2b\x28\x62\x5b\x32\x5d\x7c\x7c\x31\x29\x2d\x30\x2c\x61\ +\x5b\x33\x5d\x3d\x62\x5b\x33\x5d\x2d\x30\x7d\x65\x6c\x73\x65\x20\ +\x61\x5b\x32\x5d\x26\x26\x6d\x2e\x65\x72\x72\x6f\x72\x28\x61\x5b\ +\x30\x5d\x29\x3b\x61\x5b\x30\x5d\x3d\x65\x2b\x2b\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x61\x7d\x2c\x41\x54\x54\x52\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\x64\x2c\x65\x2c\x66\ +\x29\x7b\x76\x61\x72\x20\x67\x3d\x61\x5b\x31\x5d\x3d\x61\x5b\x31\ +\x5d\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x6a\x2c\x22\x22\x29\x3b\ +\x21\x66\x26\x26\x6f\x2e\x61\x74\x74\x72\x4d\x61\x70\x5b\x67\x5d\ +\x26\x26\x28\x61\x5b\x31\x5d\x3d\x6f\x2e\x61\x74\x74\x72\x4d\x61\ +\x70\x5b\x67\x5d\x29\x2c\x61\x5b\x34\x5d\x3d\x28\x61\x5b\x34\x5d\ +\x7c\x7c\x61\x5b\x35\x5d\x7c\x7c\x22\x22\x29\x2e\x72\x65\x70\x6c\ +\x61\x63\x65\x28\x6a\x2c\x22\x22\x29\x2c\x61\x5b\x32\x5d\x3d\x3d\ +\x3d\x22\x7e\x3d\x22\x26\x26\x28\x61\x5b\x34\x5d\x3d\x22\x20\x22\ +\x2b\x61\x5b\x34\x5d\x2b\x22\x20\x22\x29\x3b\x72\x65\x74\x75\x72\ +\x6e\x20\x61\x7d\x2c\x50\x53\x45\x55\x44\x4f\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x62\x2c\x63\x2c\x64\x2c\x65\x2c\x66\x29\x7b\ +\x69\x66\x28\x62\x5b\x31\x5d\x3d\x3d\x3d\x22\x6e\x6f\x74\x22\x29\ +\x69\x66\x28\x28\x61\x2e\x65\x78\x65\x63\x28\x62\x5b\x33\x5d\x29\ +\x7c\x7c\x22\x22\x29\x2e\x6c\x65\x6e\x67\x74\x68\x3e\x31\x7c\x7c\ +\x2f\x5e\x5c\x77\x2f\x2e\x74\x65\x73\x74\x28\x62\x5b\x33\x5d\x29\ +\x29\x62\x5b\x33\x5d\x3d\x6d\x28\x62\x5b\x33\x5d\x2c\x6e\x75\x6c\ +\x6c\x2c\x6e\x75\x6c\x6c\x2c\x63\x29\x3b\x65\x6c\x73\x65\x7b\x76\ +\x61\x72\x20\x67\x3d\x6d\x2e\x66\x69\x6c\x74\x65\x72\x28\x62\x5b\ +\x33\x5d\x2c\x63\x2c\x64\x2c\x21\x30\x5e\x66\x29\x3b\x64\x7c\x7c\ +\x65\x2e\x70\x75\x73\x68\x2e\x61\x70\x70\x6c\x79\x28\x65\x2c\x67\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x21\x31\x7d\x65\x6c\x73\x65\x20\ +\x69\x66\x28\x6f\x2e\x6d\x61\x74\x63\x68\x2e\x50\x4f\x53\x2e\x74\ +\x65\x73\x74\x28\x62\x5b\x30\x5d\x29\x7c\x7c\x6f\x2e\x6d\x61\x74\ +\x63\x68\x2e\x43\x48\x49\x4c\x44\x2e\x74\x65\x73\x74\x28\x62\x5b\ +\x30\x5d\x29\x29\x72\x65\x74\x75\x72\x6e\x21\x30\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x62\x7d\x2c\x50\x4f\x53\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x61\x2e\x75\x6e\x73\x68\x69\x66\x74\ +\x28\x21\x30\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x61\x7d\x7d\x2c\ +\x66\x69\x6c\x74\x65\x72\x73\x3a\x7b\x65\x6e\x61\x62\x6c\x65\x64\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\ +\x75\x72\x6e\x20\x61\x2e\x64\x69\x73\x61\x62\x6c\x65\x64\x3d\x3d\ +\x3d\x21\x31\x26\x26\x61\x2e\x74\x79\x70\x65\x21\x3d\x3d\x22\x68\ +\x69\x64\x64\x65\x6e\x22\x7d\x2c\x64\x69\x73\x61\x62\x6c\x65\x64\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\ +\x75\x72\x6e\x20\x61\x2e\x64\x69\x73\x61\x62\x6c\x65\x64\x3d\x3d\ +\x3d\x21\x30\x7d\x2c\x63\x68\x65\x63\x6b\x65\x64\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\ +\x61\x2e\x63\x68\x65\x63\x6b\x65\x64\x3d\x3d\x3d\x21\x30\x7d\x2c\ +\x73\x65\x6c\x65\x63\x74\x65\x64\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x29\x7b\x61\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\ +\x65\x26\x26\x61\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2e\ +\x73\x65\x6c\x65\x63\x74\x65\x64\x49\x6e\x64\x65\x78\x3b\x72\x65\ +\x74\x75\x72\x6e\x20\x61\x2e\x73\x65\x6c\x65\x63\x74\x65\x64\x3d\ +\x3d\x3d\x21\x30\x7d\x2c\x70\x61\x72\x65\x6e\x74\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x21\ +\x21\x61\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x7d\x2c\x65\ +\x6d\x70\x74\x79\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\ +\x7b\x72\x65\x74\x75\x72\x6e\x21\x61\x2e\x66\x69\x72\x73\x74\x43\ +\x68\x69\x6c\x64\x7d\x2c\x68\x61\x73\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x72\x65\x74\x75\x72\x6e\ +\x21\x21\x6d\x28\x63\x5b\x33\x5d\x2c\x61\x29\x2e\x6c\x65\x6e\x67\ +\x74\x68\x7d\x2c\x68\x65\x61\x64\x65\x72\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x2f\x68\x5c\ +\x64\x2f\x69\x2e\x74\x65\x73\x74\x28\x61\x2e\x6e\x6f\x64\x65\x4e\ +\x61\x6d\x65\x29\x7d\x2c\x74\x65\x78\x74\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\x3d\x61\x2e\x67\ +\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x74\x79\x70\ +\x65\x22\x29\x2c\x63\x3d\x61\x2e\x74\x79\x70\x65\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\ +\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x3d\x3d\x3d\x22\ +\x69\x6e\x70\x75\x74\x22\x26\x26\x22\x74\x65\x78\x74\x22\x3d\x3d\ +\x3d\x63\x26\x26\x28\x62\x3d\x3d\x3d\x63\x7c\x7c\x62\x3d\x3d\x3d\ +\x6e\x75\x6c\x6c\x29\x7d\x2c\x72\x61\x64\x69\x6f\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\ +\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\x6f\x77\ +\x65\x72\x43\x61\x73\x65\x28\x29\x3d\x3d\x3d\x22\x69\x6e\x70\x75\ +\x74\x22\x26\x26\x22\x72\x61\x64\x69\x6f\x22\x3d\x3d\x3d\x61\x2e\ +\x74\x79\x70\x65\x7d\x2c\x63\x68\x65\x63\x6b\x62\x6f\x78\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\ +\x6e\x20\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\ +\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x3d\x3d\x3d\x22\x69\x6e\ +\x70\x75\x74\x22\x26\x26\x22\x63\x68\x65\x63\x6b\x62\x6f\x78\x22\ +\x3d\x3d\x3d\x61\x2e\x74\x79\x70\x65\x7d\x2c\x66\x69\x6c\x65\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\ +\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x3d\x3d\x3d\x22\x69\ +\x6e\x70\x75\x74\x22\x26\x26\x22\x66\x69\x6c\x65\x22\x3d\x3d\x3d\ +\x61\x2e\x74\x79\x70\x65\x7d\x2c\x70\x61\x73\x73\x77\x6f\x72\x64\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\ +\x75\x72\x6e\x20\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\ +\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x3d\x3d\x3d\x22\ +\x69\x6e\x70\x75\x74\x22\x26\x26\x22\x70\x61\x73\x73\x77\x6f\x72\ +\x64\x22\x3d\x3d\x3d\x61\x2e\x74\x79\x70\x65\x7d\x2c\x73\x75\x62\ +\x6d\x69\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\ +\x76\x61\x72\x20\x62\x3d\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\ +\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x3b\x72\ +\x65\x74\x75\x72\x6e\x28\x62\x3d\x3d\x3d\x22\x69\x6e\x70\x75\x74\ +\x22\x7c\x7c\x62\x3d\x3d\x3d\x22\x62\x75\x74\x74\x6f\x6e\x22\x29\ +\x26\x26\x22\x73\x75\x62\x6d\x69\x74\x22\x3d\x3d\x3d\x61\x2e\x74\ +\x79\x70\x65\x7d\x2c\x69\x6d\x61\x67\x65\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\ +\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\x6f\x77\x65\x72\ +\x43\x61\x73\x65\x28\x29\x3d\x3d\x3d\x22\x69\x6e\x70\x75\x74\x22\ +\x26\x26\x22\x69\x6d\x61\x67\x65\x22\x3d\x3d\x3d\x61\x2e\x74\x79\ +\x70\x65\x7d\x2c\x72\x65\x73\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\x3d\x61\x2e\x6e\x6f\ +\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\ +\x73\x65\x28\x29\x3b\x72\x65\x74\x75\x72\x6e\x28\x62\x3d\x3d\x3d\ +\x22\x69\x6e\x70\x75\x74\x22\x7c\x7c\x62\x3d\x3d\x3d\x22\x62\x75\ +\x74\x74\x6f\x6e\x22\x29\x26\x26\x22\x72\x65\x73\x65\x74\x22\x3d\ +\x3d\x3d\x61\x2e\x74\x79\x70\x65\x7d\x2c\x62\x75\x74\x74\x6f\x6e\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\ +\x20\x62\x3d\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\ +\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x3b\x72\x65\x74\x75\ +\x72\x6e\x20\x62\x3d\x3d\x3d\x22\x69\x6e\x70\x75\x74\x22\x26\x26\ +\x22\x62\x75\x74\x74\x6f\x6e\x22\x3d\x3d\x3d\x61\x2e\x74\x79\x70\ +\x65\x7c\x7c\x62\x3d\x3d\x3d\x22\x62\x75\x74\x74\x6f\x6e\x22\x7d\ +\x2c\x69\x6e\x70\x75\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x2f\x69\x6e\x70\x75\x74\x7c\ +\x73\x65\x6c\x65\x63\x74\x7c\x74\x65\x78\x74\x61\x72\x65\x61\x7c\ +\x62\x75\x74\x74\x6f\x6e\x2f\x69\x2e\x74\x65\x73\x74\x28\x61\x2e\ +\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x29\x7d\x2c\x66\x6f\x63\x75\x73\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\ +\x75\x72\x6e\x20\x61\x3d\x3d\x3d\x61\x2e\x6f\x77\x6e\x65\x72\x44\ +\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x61\x63\x74\x69\x76\x65\x45\x6c\ +\x65\x6d\x65\x6e\x74\x7d\x7d\x2c\x73\x65\x74\x46\x69\x6c\x74\x65\ +\x72\x73\x3a\x7b\x66\x69\x72\x73\x74\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x62\ +\x3d\x3d\x3d\x30\x7d\x2c\x6c\x61\x73\x74\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\x64\x29\x7b\x72\x65\x74\ +\x75\x72\x6e\x20\x62\x3d\x3d\x3d\x64\x2e\x6c\x65\x6e\x67\x74\x68\ +\x2d\x31\x7d\x2c\x65\x76\x65\x6e\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x62\x25\ +\x32\x3d\x3d\x3d\x30\x7d\x2c\x6f\x64\x64\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\ +\x62\x25\x32\x3d\x3d\x3d\x31\x7d\x2c\x6c\x74\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x62\x3c\x63\x5b\x33\x5d\x2d\x30\x7d\x2c\x67\x74\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\ +\x72\x65\x74\x75\x72\x6e\x20\x62\x3e\x63\x5b\x33\x5d\x2d\x30\x7d\ +\x2c\x6e\x74\x68\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\ +\x62\x2c\x63\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x63\x5b\x33\x5d\ +\x2d\x30\x3d\x3d\x3d\x62\x7d\x2c\x65\x71\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x72\x65\x74\x75\x72\ +\x6e\x20\x63\x5b\x33\x5d\x2d\x30\x3d\x3d\x3d\x62\x7d\x7d\x2c\x66\ +\x69\x6c\x74\x65\x72\x3a\x7b\x50\x53\x45\x55\x44\x4f\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\x64\x29\x7b\ +\x76\x61\x72\x20\x65\x3d\x62\x5b\x31\x5d\x2c\x66\x3d\x6f\x2e\x66\ +\x69\x6c\x74\x65\x72\x73\x5b\x65\x5d\x3b\x69\x66\x28\x66\x29\x72\ +\x65\x74\x75\x72\x6e\x20\x66\x28\x61\x2c\x63\x2c\x62\x2c\x64\x29\ +\x3b\x69\x66\x28\x65\x3d\x3d\x3d\x22\x63\x6f\x6e\x74\x61\x69\x6e\ +\x73\x22\x29\x72\x65\x74\x75\x72\x6e\x28\x61\x2e\x74\x65\x78\x74\ +\x43\x6f\x6e\x74\x65\x6e\x74\x7c\x7c\x61\x2e\x69\x6e\x6e\x65\x72\ +\x54\x65\x78\x74\x7c\x7c\x6e\x28\x5b\x61\x5d\x29\x7c\x7c\x22\x22\ +\x29\x2e\x69\x6e\x64\x65\x78\x4f\x66\x28\x62\x5b\x33\x5d\x29\x3e\ +\x3d\x30\x3b\x69\x66\x28\x65\x3d\x3d\x3d\x22\x6e\x6f\x74\x22\x29\ +\x7b\x76\x61\x72\x20\x67\x3d\x62\x5b\x33\x5d\x3b\x66\x6f\x72\x28\ +\x76\x61\x72\x20\x68\x3d\x30\x2c\x69\x3d\x67\x2e\x6c\x65\x6e\x67\ +\x74\x68\x3b\x68\x3c\x69\x3b\x68\x2b\x2b\x29\x69\x66\x28\x67\x5b\ +\x68\x5d\x3d\x3d\x3d\x61\x29\x72\x65\x74\x75\x72\x6e\x21\x31\x3b\ +\x72\x65\x74\x75\x72\x6e\x21\x30\x7d\x6d\x2e\x65\x72\x72\x6f\x72\ +\x28\x65\x29\x7d\x2c\x43\x48\x49\x4c\x44\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x2c\x65\ +\x2c\x66\x2c\x67\x2c\x68\x2c\x69\x2c\x6a\x2c\x6b\x3d\x62\x5b\x31\ +\x5d\x2c\x6c\x3d\x61\x3b\x73\x77\x69\x74\x63\x68\x28\x6b\x29\x7b\ +\x63\x61\x73\x65\x22\x6f\x6e\x6c\x79\x22\x3a\x63\x61\x73\x65\x22\ +\x66\x69\x72\x73\x74\x22\x3a\x77\x68\x69\x6c\x65\x28\x6c\x3d\x6c\ +\x2e\x70\x72\x65\x76\x69\x6f\x75\x73\x53\x69\x62\x6c\x69\x6e\x67\ +\x29\x69\x66\x28\x6c\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\ +\x3d\x31\x29\x72\x65\x74\x75\x72\x6e\x21\x31\x3b\x69\x66\x28\x6b\ +\x3d\x3d\x3d\x22\x66\x69\x72\x73\x74\x22\x29\x72\x65\x74\x75\x72\ +\x6e\x21\x30\x3b\x6c\x3d\x61\x3b\x63\x61\x73\x65\x22\x6c\x61\x73\ +\x74\x22\x3a\x77\x68\x69\x6c\x65\x28\x6c\x3d\x6c\x2e\x6e\x65\x78\ +\x74\x53\x69\x62\x6c\x69\x6e\x67\x29\x69\x66\x28\x6c\x2e\x6e\x6f\ +\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x29\x72\x65\x74\x75\x72\ +\x6e\x21\x31\x3b\x72\x65\x74\x75\x72\x6e\x21\x30\x3b\x63\x61\x73\ +\x65\x22\x6e\x74\x68\x22\x3a\x63\x3d\x62\x5b\x32\x5d\x2c\x65\x3d\ +\x62\x5b\x33\x5d\x3b\x69\x66\x28\x63\x3d\x3d\x3d\x31\x26\x26\x65\ +\x3d\x3d\x3d\x30\x29\x72\x65\x74\x75\x72\x6e\x21\x30\x3b\x66\x3d\ +\x62\x5b\x30\x5d\x2c\x67\x3d\x61\x2e\x70\x61\x72\x65\x6e\x74\x4e\ +\x6f\x64\x65\x3b\x69\x66\x28\x67\x26\x26\x28\x67\x5b\x64\x5d\x21\ +\x3d\x3d\x66\x7c\x7c\x21\x61\x2e\x6e\x6f\x64\x65\x49\x6e\x64\x65\ +\x78\x29\x29\x7b\x69\x3d\x30\x3b\x66\x6f\x72\x28\x6c\x3d\x67\x2e\ +\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x3b\x6c\x3b\x6c\x3d\x6c\ +\x2e\x6e\x65\x78\x74\x53\x69\x62\x6c\x69\x6e\x67\x29\x6c\x2e\x6e\ +\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x26\x26\x28\x6c\x2e\ +\x6e\x6f\x64\x65\x49\x6e\x64\x65\x78\x3d\x2b\x2b\x69\x29\x3b\x67\ +\x5b\x64\x5d\x3d\x66\x7d\x6a\x3d\x61\x2e\x6e\x6f\x64\x65\x49\x6e\ +\x64\x65\x78\x2d\x65\x3b\x72\x65\x74\x75\x72\x6e\x20\x63\x3d\x3d\ +\x3d\x30\x3f\x6a\x3d\x3d\x3d\x30\x3a\x6a\x25\x63\x3d\x3d\x3d\x30\ +\x26\x26\x6a\x2f\x63\x3e\x3d\x30\x7d\x7d\x2c\x49\x44\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x61\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\ +\x31\x26\x26\x61\x2e\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\ +\x65\x28\x22\x69\x64\x22\x29\x3d\x3d\x3d\x62\x7d\x2c\x54\x41\x47\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\ +\x65\x74\x75\x72\x6e\x20\x62\x3d\x3d\x3d\x22\x2a\x22\x26\x26\x61\ +\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x7c\x7c\x21\ +\x21\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x26\x26\x61\x2e\x6e\ +\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\ +\x61\x73\x65\x28\x29\x3d\x3d\x3d\x62\x7d\x2c\x43\x4c\x41\x53\x53\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\ +\x65\x74\x75\x72\x6e\x28\x22\x20\x22\x2b\x28\x61\x2e\x63\x6c\x61\ +\x73\x73\x4e\x61\x6d\x65\x7c\x7c\x61\x2e\x67\x65\x74\x41\x74\x74\ +\x72\x69\x62\x75\x74\x65\x28\x22\x63\x6c\x61\x73\x73\x22\x29\x29\ +\x2b\x22\x20\x22\x29\x2e\x69\x6e\x64\x65\x78\x4f\x66\x28\x62\x29\ +\x3e\x2d\x31\x7d\x2c\x41\x54\x54\x52\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\x62\x5b\ +\x31\x5d\x2c\x64\x3d\x6d\x2e\x61\x74\x74\x72\x3f\x6d\x2e\x61\x74\ +\x74\x72\x28\x61\x2c\x63\x29\x3a\x6f\x2e\x61\x74\x74\x72\x48\x61\ +\x6e\x64\x6c\x65\x5b\x63\x5d\x3f\x6f\x2e\x61\x74\x74\x72\x48\x61\ +\x6e\x64\x6c\x65\x5b\x63\x5d\x28\x61\x29\x3a\x61\x5b\x63\x5d\x21\ +\x3d\x6e\x75\x6c\x6c\x3f\x61\x5b\x63\x5d\x3a\x61\x2e\x67\x65\x74\ +\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x63\x29\x2c\x65\x3d\x64\ +\x2b\x22\x22\x2c\x66\x3d\x62\x5b\x32\x5d\x2c\x67\x3d\x62\x5b\x34\ +\x5d\x3b\x72\x65\x74\x75\x72\x6e\x20\x64\x3d\x3d\x6e\x75\x6c\x6c\ +\x3f\x66\x3d\x3d\x3d\x22\x21\x3d\x22\x3a\x21\x66\x26\x26\x6d\x2e\ +\x61\x74\x74\x72\x3f\x64\x21\x3d\x6e\x75\x6c\x6c\x3a\x66\x3d\x3d\ +\x3d\x22\x3d\x22\x3f\x65\x3d\x3d\x3d\x67\x3a\x66\x3d\x3d\x3d\x22\ +\x2a\x3d\x22\x3f\x65\x2e\x69\x6e\x64\x65\x78\x4f\x66\x28\x67\x29\ +\x3e\x3d\x30\x3a\x66\x3d\x3d\x3d\x22\x7e\x3d\x22\x3f\x28\x22\x20\ +\x22\x2b\x65\x2b\x22\x20\x22\x29\x2e\x69\x6e\x64\x65\x78\x4f\x66\ +\x28\x67\x29\x3e\x3d\x30\x3a\x67\x3f\x66\x3d\x3d\x3d\x22\x21\x3d\ +\x22\x3f\x65\x21\x3d\x3d\x67\x3a\x66\x3d\x3d\x3d\x22\x5e\x3d\x22\ +\x3f\x65\x2e\x69\x6e\x64\x65\x78\x4f\x66\x28\x67\x29\x3d\x3d\x3d\ +\x30\x3a\x66\x3d\x3d\x3d\x22\x24\x3d\x22\x3f\x65\x2e\x73\x75\x62\ +\x73\x74\x72\x28\x65\x2e\x6c\x65\x6e\x67\x74\x68\x2d\x67\x2e\x6c\ +\x65\x6e\x67\x74\x68\x29\x3d\x3d\x3d\x67\x3a\x66\x3d\x3d\x3d\x22\ +\x7c\x3d\x22\x3f\x65\x3d\x3d\x3d\x67\x7c\x7c\x65\x2e\x73\x75\x62\ +\x73\x74\x72\x28\x30\x2c\x67\x2e\x6c\x65\x6e\x67\x74\x68\x2b\x31\ +\x29\x3d\x3d\x3d\x67\x2b\x22\x2d\x22\x3a\x21\x31\x3a\x65\x26\x26\ +\x64\x21\x3d\x3d\x21\x31\x7d\x2c\x50\x4f\x53\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\x64\x29\x7b\x76\x61\ +\x72\x20\x65\x3d\x62\x5b\x32\x5d\x2c\x66\x3d\x6f\x2e\x73\x65\x74\ +\x46\x69\x6c\x74\x65\x72\x73\x5b\x65\x5d\x3b\x69\x66\x28\x66\x29\ +\x72\x65\x74\x75\x72\x6e\x20\x66\x28\x61\x2c\x63\x2c\x62\x2c\x64\ +\x29\x7d\x7d\x7d\x2c\x70\x3d\x6f\x2e\x6d\x61\x74\x63\x68\x2e\x50\ +\x4f\x53\x2c\x71\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\ +\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\x22\x5c\x5c\x22\x2b\x28\x62\ +\x2d\x30\x2b\x31\x29\x7d\x3b\x66\x6f\x72\x28\x76\x61\x72\x20\x72\ +\x20\x69\x6e\x20\x6f\x2e\x6d\x61\x74\x63\x68\x29\x6f\x2e\x6d\x61\ +\x74\x63\x68\x5b\x72\x5d\x3d\x6e\x65\x77\x20\x52\x65\x67\x45\x78\ +\x70\x28\x6f\x2e\x6d\x61\x74\x63\x68\x5b\x72\x5d\x2e\x73\x6f\x75\ +\x72\x63\x65\x2b\x2f\x28\x3f\x21\x5b\x5e\x5c\x5b\x5d\x2a\x5c\x5d\ +\x29\x28\x3f\x21\x5b\x5e\x5c\x28\x5d\x2a\x5c\x29\x29\x2f\x2e\x73\ +\x6f\x75\x72\x63\x65\x29\x2c\x6f\x2e\x6c\x65\x66\x74\x4d\x61\x74\ +\x63\x68\x5b\x72\x5d\x3d\x6e\x65\x77\x20\x52\x65\x67\x45\x78\x70\ +\x28\x2f\x28\x5e\x28\x3f\x3a\x2e\x7c\x5c\x72\x7c\x5c\x6e\x29\x2a\ +\x3f\x29\x2f\x2e\x73\x6f\x75\x72\x63\x65\x2b\x6f\x2e\x6d\x61\x74\ +\x63\x68\x5b\x72\x5d\x2e\x73\x6f\x75\x72\x63\x65\x2e\x72\x65\x70\ +\x6c\x61\x63\x65\x28\x2f\x5c\x5c\x28\x5c\x64\x2b\x29\x2f\x67\x2c\ +\x71\x29\x29\x3b\x76\x61\x72\x20\x73\x3d\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x61\x3d\x41\x72\x72\x61\x79\x2e\ +\x70\x72\x6f\x74\x6f\x74\x79\x70\x65\x2e\x73\x6c\x69\x63\x65\x2e\ +\x63\x61\x6c\x6c\x28\x61\x2c\x30\x29\x3b\x69\x66\x28\x62\x29\x7b\ +\x62\x2e\x70\x75\x73\x68\x2e\x61\x70\x70\x6c\x79\x28\x62\x2c\x61\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x62\x7d\x72\x65\x74\x75\x72\ +\x6e\x20\x61\x7d\x3b\x74\x72\x79\x7b\x41\x72\x72\x61\x79\x2e\x70\ +\x72\x6f\x74\x6f\x74\x79\x70\x65\x2e\x73\x6c\x69\x63\x65\x2e\x63\ +\x61\x6c\x6c\x28\x63\x2e\x64\x6f\x63\x75\x6d\x65\x6e\x74\x45\x6c\ +\x65\x6d\x65\x6e\x74\x2e\x63\x68\x69\x6c\x64\x4e\x6f\x64\x65\x73\ +\x2c\x30\x29\x5b\x30\x5d\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x7d\ +\x63\x61\x74\x63\x68\x28\x74\x29\x7b\x73\x3d\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\x30\ +\x2c\x64\x3d\x62\x7c\x7c\x5b\x5d\x3b\x69\x66\x28\x67\x2e\x63\x61\ +\x6c\x6c\x28\x61\x29\x3d\x3d\x3d\x22\x5b\x6f\x62\x6a\x65\x63\x74\ +\x20\x41\x72\x72\x61\x79\x5d\x22\x29\x41\x72\x72\x61\x79\x2e\x70\ +\x72\x6f\x74\x6f\x74\x79\x70\x65\x2e\x70\x75\x73\x68\x2e\x61\x70\ +\x70\x6c\x79\x28\x64\x2c\x61\x29\x3b\x65\x6c\x73\x65\x20\x69\x66\ +\x28\x74\x79\x70\x65\x6f\x66\x20\x61\x2e\x6c\x65\x6e\x67\x74\x68\ +\x3d\x3d\x22\x6e\x75\x6d\x62\x65\x72\x22\x29\x66\x6f\x72\x28\x76\ +\x61\x72\x20\x65\x3d\x61\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x63\x3c\ +\x65\x3b\x63\x2b\x2b\x29\x64\x2e\x70\x75\x73\x68\x28\x61\x5b\x63\ +\x5d\x29\x3b\x65\x6c\x73\x65\x20\x66\x6f\x72\x28\x3b\x61\x5b\x63\ +\x5d\x3b\x63\x2b\x2b\x29\x64\x2e\x70\x75\x73\x68\x28\x61\x5b\x63\ +\x5d\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x64\x7d\x7d\x76\x61\x72\ +\x20\x75\x2c\x76\x3b\x63\x2e\x64\x6f\x63\x75\x6d\x65\x6e\x74\x45\ +\x6c\x65\x6d\x65\x6e\x74\x2e\x63\x6f\x6d\x70\x61\x72\x65\x44\x6f\ +\x63\x75\x6d\x65\x6e\x74\x50\x6f\x73\x69\x74\x69\x6f\x6e\x3f\x75\ +\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x69\ +\x66\x28\x61\x3d\x3d\x3d\x62\x29\x7b\x68\x3d\x21\x30\x3b\x72\x65\ +\x74\x75\x72\x6e\x20\x30\x7d\x69\x66\x28\x21\x61\x2e\x63\x6f\x6d\ +\x70\x61\x72\x65\x44\x6f\x63\x75\x6d\x65\x6e\x74\x50\x6f\x73\x69\ +\x74\x69\x6f\x6e\x7c\x7c\x21\x62\x2e\x63\x6f\x6d\x70\x61\x72\x65\ +\x44\x6f\x63\x75\x6d\x65\x6e\x74\x50\x6f\x73\x69\x74\x69\x6f\x6e\ +\x29\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x63\x6f\x6d\x70\x61\x72\ +\x65\x44\x6f\x63\x75\x6d\x65\x6e\x74\x50\x6f\x73\x69\x74\x69\x6f\ +\x6e\x3f\x2d\x31\x3a\x31\x3b\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\ +\x63\x6f\x6d\x70\x61\x72\x65\x44\x6f\x63\x75\x6d\x65\x6e\x74\x50\ +\x6f\x73\x69\x74\x69\x6f\x6e\x28\x62\x29\x26\x34\x3f\x2d\x31\x3a\ +\x31\x7d\x3a\x28\x75\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x62\x29\x7b\x69\x66\x28\x61\x3d\x3d\x3d\x62\x29\x7b\x68\x3d\ +\x21\x30\x3b\x72\x65\x74\x75\x72\x6e\x20\x30\x7d\x69\x66\x28\x61\ +\x2e\x73\x6f\x75\x72\x63\x65\x49\x6e\x64\x65\x78\x26\x26\x62\x2e\ +\x73\x6f\x75\x72\x63\x65\x49\x6e\x64\x65\x78\x29\x72\x65\x74\x75\ +\x72\x6e\x20\x61\x2e\x73\x6f\x75\x72\x63\x65\x49\x6e\x64\x65\x78\ +\x2d\x62\x2e\x73\x6f\x75\x72\x63\x65\x49\x6e\x64\x65\x78\x3b\x76\ +\x61\x72\x20\x63\x2c\x64\x2c\x65\x3d\x5b\x5d\x2c\x66\x3d\x5b\x5d\ +\x2c\x67\x3d\x61\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2c\ +\x69\x3d\x62\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2c\x6a\ +\x3d\x67\x3b\x69\x66\x28\x67\x3d\x3d\x3d\x69\x29\x72\x65\x74\x75\ +\x72\x6e\x20\x76\x28\x61\x2c\x62\x29\x3b\x69\x66\x28\x21\x67\x29\ +\x72\x65\x74\x75\x72\x6e\x2d\x31\x3b\x69\x66\x28\x21\x69\x29\x72\ +\x65\x74\x75\x72\x6e\x20\x31\x3b\x77\x68\x69\x6c\x65\x28\x6a\x29\ +\x65\x2e\x75\x6e\x73\x68\x69\x66\x74\x28\x6a\x29\x2c\x6a\x3d\x6a\ +\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x3b\x6a\x3d\x69\x3b\ +\x77\x68\x69\x6c\x65\x28\x6a\x29\x66\x2e\x75\x6e\x73\x68\x69\x66\ +\x74\x28\x6a\x29\x2c\x6a\x3d\x6a\x2e\x70\x61\x72\x65\x6e\x74\x4e\ +\x6f\x64\x65\x3b\x63\x3d\x65\x2e\x6c\x65\x6e\x67\x74\x68\x2c\x64\ +\x3d\x66\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x66\x6f\x72\x28\x76\x61\ +\x72\x20\x6b\x3d\x30\x3b\x6b\x3c\x63\x26\x26\x6b\x3c\x64\x3b\x6b\ +\x2b\x2b\x29\x69\x66\x28\x65\x5b\x6b\x5d\x21\x3d\x3d\x66\x5b\x6b\ +\x5d\x29\x72\x65\x74\x75\x72\x6e\x20\x76\x28\x65\x5b\x6b\x5d\x2c\ +\x66\x5b\x6b\x5d\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x6b\x3d\x3d\ +\x3d\x63\x3f\x76\x28\x61\x2c\x66\x5b\x6b\x5d\x2c\x2d\x31\x29\x3a\ +\x76\x28\x65\x5b\x6b\x5d\x2c\x62\x2c\x31\x29\x7d\x2c\x76\x3d\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x69\ +\x66\x28\x61\x3d\x3d\x3d\x62\x29\x72\x65\x74\x75\x72\x6e\x20\x63\ +\x3b\x76\x61\x72\x20\x64\x3d\x61\x2e\x6e\x65\x78\x74\x53\x69\x62\ +\x6c\x69\x6e\x67\x3b\x77\x68\x69\x6c\x65\x28\x64\x29\x7b\x69\x66\ +\x28\x64\x3d\x3d\x3d\x62\x29\x72\x65\x74\x75\x72\x6e\x2d\x31\x3b\ +\x64\x3d\x64\x2e\x6e\x65\x78\x74\x53\x69\x62\x6c\x69\x6e\x67\x7d\ +\x72\x65\x74\x75\x72\x6e\x20\x31\x7d\x29\x2c\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x29\x7b\x76\x61\x72\x20\x61\x3d\x63\x2e\x63\x72\ +\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x64\x69\x76\ +\x22\x29\x2c\x64\x3d\x22\x73\x63\x72\x69\x70\x74\x22\x2b\x28\x6e\ +\x65\x77\x20\x44\x61\x74\x65\x29\x2e\x67\x65\x74\x54\x69\x6d\x65\ +\x28\x29\x2c\x65\x3d\x63\x2e\x64\x6f\x63\x75\x6d\x65\x6e\x74\x45\ +\x6c\x65\x6d\x65\x6e\x74\x3b\x61\x2e\x69\x6e\x6e\x65\x72\x48\x54\ +\x4d\x4c\x3d\x22\x3c\x61\x20\x6e\x61\x6d\x65\x3d\x27\x22\x2b\x64\ +\x2b\x22\x27\x2f\x3e\x22\x2c\x65\x2e\x69\x6e\x73\x65\x72\x74\x42\ +\x65\x66\x6f\x72\x65\x28\x61\x2c\x65\x2e\x66\x69\x72\x73\x74\x43\ +\x68\x69\x6c\x64\x29\x2c\x63\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\ +\x6e\x74\x42\x79\x49\x64\x28\x64\x29\x26\x26\x28\x6f\x2e\x66\x69\ +\x6e\x64\x2e\x49\x44\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x63\x2c\x64\x29\x7b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\ +\x63\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x42\x79\x49\x64\ +\x21\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\x65\x64\x22\x26\x26\x21\ +\x64\x29\x7b\x76\x61\x72\x20\x65\x3d\x63\x2e\x67\x65\x74\x45\x6c\ +\x65\x6d\x65\x6e\x74\x42\x79\x49\x64\x28\x61\x5b\x31\x5d\x29\x3b\ +\x72\x65\x74\x75\x72\x6e\x20\x65\x3f\x65\x2e\x69\x64\x3d\x3d\x3d\ +\x61\x5b\x31\x5d\x7c\x7c\x74\x79\x70\x65\x6f\x66\x20\x65\x2e\x67\ +\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x4e\x6f\x64\x65\x21\ +\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\x65\x64\x22\x26\x26\x65\x2e\ +\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x4e\x6f\x64\x65\ +\x28\x22\x69\x64\x22\x29\x2e\x6e\x6f\x64\x65\x56\x61\x6c\x75\x65\ +\x3d\x3d\x3d\x61\x5b\x31\x5d\x3f\x5b\x65\x5d\x3a\x62\x3a\x5b\x5d\ +\x7d\x7d\x2c\x6f\x2e\x66\x69\x6c\x74\x65\x72\x2e\x49\x44\x3d\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\ +\x20\x63\x3d\x74\x79\x70\x65\x6f\x66\x20\x61\x2e\x67\x65\x74\x41\ +\x74\x74\x72\x69\x62\x75\x74\x65\x4e\x6f\x64\x65\x21\x3d\x22\x75\ +\x6e\x64\x65\x66\x69\x6e\x65\x64\x22\x26\x26\x61\x2e\x67\x65\x74\ +\x41\x74\x74\x72\x69\x62\x75\x74\x65\x4e\x6f\x64\x65\x28\x22\x69\ +\x64\x22\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x61\x2e\x6e\x6f\x64\ +\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x26\x26\x63\x26\x26\x63\x2e\ +\x6e\x6f\x64\x65\x56\x61\x6c\x75\x65\x3d\x3d\x3d\x62\x7d\x29\x2c\ +\x65\x2e\x72\x65\x6d\x6f\x76\x65\x43\x68\x69\x6c\x64\x28\x61\x29\ +\x2c\x65\x3d\x61\x3d\x6e\x75\x6c\x6c\x7d\x28\x29\x2c\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x29\x7b\x76\x61\x72\x20\x61\x3d\x63\x2e\ +\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x64\ +\x69\x76\x22\x29\x3b\x61\x2e\x61\x70\x70\x65\x6e\x64\x43\x68\x69\ +\x6c\x64\x28\x63\x2e\x63\x72\x65\x61\x74\x65\x43\x6f\x6d\x6d\x65\ +\x6e\x74\x28\x22\x22\x29\x29\x2c\x61\x2e\x67\x65\x74\x45\x6c\x65\ +\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\ +\x2a\x22\x29\x2e\x6c\x65\x6e\x67\x74\x68\x3e\x30\x26\x26\x28\x6f\ +\x2e\x66\x69\x6e\x64\x2e\x54\x41\x47\x3d\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\x62\x2e\ +\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\ +\x4e\x61\x6d\x65\x28\x61\x5b\x31\x5d\x29\x3b\x69\x66\x28\x61\x5b\ +\x31\x5d\x3d\x3d\x3d\x22\x2a\x22\x29\x7b\x76\x61\x72\x20\x64\x3d\ +\x5b\x5d\x3b\x66\x6f\x72\x28\x76\x61\x72\x20\x65\x3d\x30\x3b\x63\ +\x5b\x65\x5d\x3b\x65\x2b\x2b\x29\x63\x5b\x65\x5d\x2e\x6e\x6f\x64\ +\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x26\x26\x64\x2e\x70\x75\x73\ +\x68\x28\x63\x5b\x65\x5d\x29\x3b\x63\x3d\x64\x7d\x72\x65\x74\x75\ +\x72\x6e\x20\x63\x7d\x29\x2c\x61\x2e\x69\x6e\x6e\x65\x72\x48\x54\ +\x4d\x4c\x3d\x22\x3c\x61\x20\x68\x72\x65\x66\x3d\x27\x23\x27\x3e\ +\x3c\x2f\x61\x3e\x22\x2c\x61\x2e\x66\x69\x72\x73\x74\x43\x68\x69\ +\x6c\x64\x26\x26\x74\x79\x70\x65\x6f\x66\x20\x61\x2e\x66\x69\x72\ +\x73\x74\x43\x68\x69\x6c\x64\x2e\x67\x65\x74\x41\x74\x74\x72\x69\ +\x62\x75\x74\x65\x21\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\x65\x64\ +\x22\x26\x26\x61\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x2e\ +\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x68\x72\ +\x65\x66\x22\x29\x21\x3d\x3d\x22\x23\x22\x26\x26\x28\x6f\x2e\x61\ +\x74\x74\x72\x48\x61\x6e\x64\x6c\x65\x2e\x68\x72\x65\x66\x3d\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\ +\x6e\x20\x61\x2e\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\ +\x28\x22\x68\x72\x65\x66\x22\x2c\x32\x29\x7d\x29\x2c\x61\x3d\x6e\ +\x75\x6c\x6c\x7d\x28\x29\x2c\x63\x2e\x71\x75\x65\x72\x79\x53\x65\ +\x6c\x65\x63\x74\x6f\x72\x41\x6c\x6c\x26\x26\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x29\x7b\x76\x61\x72\x20\x61\x3d\x6d\x2c\x62\x3d\ +\x63\x2e\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\ +\x22\x64\x69\x76\x22\x29\x2c\x64\x3d\x22\x5f\x5f\x73\x69\x7a\x7a\ +\x6c\x65\x5f\x5f\x22\x3b\x62\x2e\x69\x6e\x6e\x65\x72\x48\x54\x4d\ +\x4c\x3d\x22\x3c\x70\x20\x63\x6c\x61\x73\x73\x3d\x27\x54\x45\x53\ +\x54\x27\x3e\x3c\x2f\x70\x3e\x22\x3b\x69\x66\x28\x21\x62\x2e\x71\ +\x75\x65\x72\x79\x53\x65\x6c\x65\x63\x74\x6f\x72\x41\x6c\x6c\x7c\ +\x7c\x62\x2e\x71\x75\x65\x72\x79\x53\x65\x6c\x65\x63\x74\x6f\x72\ +\x41\x6c\x6c\x28\x22\x2e\x54\x45\x53\x54\x22\x29\x2e\x6c\x65\x6e\ +\x67\x74\x68\x21\x3d\x3d\x30\x29\x7b\x6d\x3d\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x62\x2c\x65\x2c\x66\x2c\x67\x29\x7b\x65\x3d\x65\ +\x7c\x7c\x63\x3b\x69\x66\x28\x21\x67\x26\x26\x21\x6d\x2e\x69\x73\ +\x58\x4d\x4c\x28\x65\x29\x29\x7b\x76\x61\x72\x20\x68\x3d\x2f\x5e\ +\x28\x5c\x77\x2b\x24\x29\x7c\x5e\x5c\x2e\x28\x5b\x5c\x77\x5c\x2d\ +\x5d\x2b\x24\x29\x7c\x5e\x23\x28\x5b\x5c\x77\x5c\x2d\x5d\x2b\x24\ +\x29\x2f\x2e\x65\x78\x65\x63\x28\x62\x29\x3b\x69\x66\x28\x68\x26\ +\x26\x28\x65\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\ +\x7c\x7c\x65\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x39\ +\x29\x29\x7b\x69\x66\x28\x68\x5b\x31\x5d\x29\x72\x65\x74\x75\x72\ +\x6e\x20\x73\x28\x65\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\ +\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x62\x29\x2c\x66\x29\ +\x3b\x69\x66\x28\x68\x5b\x32\x5d\x26\x26\x6f\x2e\x66\x69\x6e\x64\ +\x2e\x43\x4c\x41\x53\x53\x26\x26\x65\x2e\x67\x65\x74\x45\x6c\x65\ +\x6d\x65\x6e\x74\x73\x42\x79\x43\x6c\x61\x73\x73\x4e\x61\x6d\x65\ +\x29\x72\x65\x74\x75\x72\x6e\x20\x73\x28\x65\x2e\x67\x65\x74\x45\ +\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x43\x6c\x61\x73\x73\x4e\x61\ +\x6d\x65\x28\x68\x5b\x32\x5d\x29\x2c\x66\x29\x7d\x69\x66\x28\x65\ +\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x39\x29\x7b\x69\ +\x66\x28\x62\x3d\x3d\x3d\x22\x62\x6f\x64\x79\x22\x26\x26\x65\x2e\ +\x62\x6f\x64\x79\x29\x72\x65\x74\x75\x72\x6e\x20\x73\x28\x5b\x65\ +\x2e\x62\x6f\x64\x79\x5d\x2c\x66\x29\x3b\x69\x66\x28\x68\x26\x26\ +\x68\x5b\x33\x5d\x29\x7b\x76\x61\x72\x20\x69\x3d\x65\x2e\x67\x65\ +\x74\x45\x6c\x65\x6d\x65\x6e\x74\x42\x79\x49\x64\x28\x68\x5b\x33\ +\x5d\x29\x3b\x69\x66\x28\x21\x69\x7c\x7c\x21\x69\x2e\x70\x61\x72\ +\x65\x6e\x74\x4e\x6f\x64\x65\x29\x72\x65\x74\x75\x72\x6e\x20\x73\ +\x28\x5b\x5d\x2c\x66\x29\x3b\x69\x66\x28\x69\x2e\x69\x64\x3d\x3d\ +\x3d\x68\x5b\x33\x5d\x29\x72\x65\x74\x75\x72\x6e\x20\x73\x28\x5b\ +\x69\x5d\x2c\x66\x29\x7d\x74\x72\x79\x7b\x72\x65\x74\x75\x72\x6e\ +\x20\x73\x28\x65\x2e\x71\x75\x65\x72\x79\x53\x65\x6c\x65\x63\x74\ +\x6f\x72\x41\x6c\x6c\x28\x62\x29\x2c\x66\x29\x7d\x63\x61\x74\x63\ +\x68\x28\x6a\x29\x7b\x7d\x7d\x65\x6c\x73\x65\x20\x69\x66\x28\x65\ +\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x26\x26\x65\ +\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\x6f\x77\x65\ +\x72\x43\x61\x73\x65\x28\x29\x21\x3d\x3d\x22\x6f\x62\x6a\x65\x63\ +\x74\x22\x29\x7b\x76\x61\x72\x20\x6b\x3d\x65\x2c\x6c\x3d\x65\x2e\ +\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x69\x64\ +\x22\x29\x2c\x6e\x3d\x6c\x7c\x7c\x64\x2c\x70\x3d\x65\x2e\x70\x61\ +\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2c\x71\x3d\x2f\x5e\x5c\x73\x2a\ +\x5b\x2b\x7e\x5d\x2f\x2e\x74\x65\x73\x74\x28\x62\x29\x3b\x6c\x3f\ +\x6e\x3d\x6e\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x2f\x27\x2f\x67\ +\x2c\x22\x5c\x5c\x24\x26\x22\x29\x3a\x65\x2e\x73\x65\x74\x41\x74\ +\x74\x72\x69\x62\x75\x74\x65\x28\x22\x69\x64\x22\x2c\x6e\x29\x2c\ +\x71\x26\x26\x70\x26\x26\x28\x65\x3d\x65\x2e\x70\x61\x72\x65\x6e\ +\x74\x4e\x6f\x64\x65\x29\x3b\x74\x72\x79\x7b\x69\x66\x28\x21\x71\ +\x7c\x7c\x70\x29\x72\x65\x74\x75\x72\x6e\x20\x73\x28\x65\x2e\x71\ +\x75\x65\x72\x79\x53\x65\x6c\x65\x63\x74\x6f\x72\x41\x6c\x6c\x28\ +\x22\x5b\x69\x64\x3d\x27\x22\x2b\x6e\x2b\x22\x27\x5d\x20\x22\x2b\ +\x62\x29\x2c\x66\x29\x7d\x63\x61\x74\x63\x68\x28\x72\x29\x7b\x7d\ +\x66\x69\x6e\x61\x6c\x6c\x79\x7b\x6c\x7c\x7c\x6b\x2e\x72\x65\x6d\ +\x6f\x76\x65\x41\x74\x74\x72\x69\x62\x75\x74\x65\x28\x22\x69\x64\ +\x22\x29\x7d\x7d\x7d\x72\x65\x74\x75\x72\x6e\x20\x61\x28\x62\x2c\ +\x65\x2c\x66\x2c\x67\x29\x7d\x3b\x66\x6f\x72\x28\x76\x61\x72\x20\ +\x65\x20\x69\x6e\x20\x61\x29\x6d\x5b\x65\x5d\x3d\x61\x5b\x65\x5d\ +\x3b\x62\x3d\x6e\x75\x6c\x6c\x7d\x7d\x28\x29\x2c\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x29\x7b\x76\x61\x72\x20\x61\x3d\x63\x2e\x64\ +\x6f\x63\x75\x6d\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x2c\x62\ +\x3d\x61\x2e\x6d\x61\x74\x63\x68\x65\x73\x53\x65\x6c\x65\x63\x74\ +\x6f\x72\x7c\x7c\x61\x2e\x6d\x6f\x7a\x4d\x61\x74\x63\x68\x65\x73\ +\x53\x65\x6c\x65\x63\x74\x6f\x72\x7c\x7c\x61\x2e\x77\x65\x62\x6b\ +\x69\x74\x4d\x61\x74\x63\x68\x65\x73\x53\x65\x6c\x65\x63\x74\x6f\ +\x72\x7c\x7c\x61\x2e\x6d\x73\x4d\x61\x74\x63\x68\x65\x73\x53\x65\ +\x6c\x65\x63\x74\x6f\x72\x3b\x69\x66\x28\x62\x29\x7b\x76\x61\x72\ +\x20\x64\x3d\x21\x62\x2e\x63\x61\x6c\x6c\x28\x63\x2e\x63\x72\x65\ +\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x64\x69\x76\x22\ +\x29\x2c\x22\x64\x69\x76\x22\x29\x2c\x65\x3d\x21\x31\x3b\x74\x72\ +\x79\x7b\x62\x2e\x63\x61\x6c\x6c\x28\x63\x2e\x64\x6f\x63\x75\x6d\ +\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x2c\x22\x5b\x74\x65\x73\ +\x74\x21\x3d\x27\x27\x5d\x3a\x73\x69\x7a\x7a\x6c\x65\x22\x29\x7d\ +\x63\x61\x74\x63\x68\x28\x66\x29\x7b\x65\x3d\x21\x30\x7d\x6d\x2e\ +\x6d\x61\x74\x63\x68\x65\x73\x53\x65\x6c\x65\x63\x74\x6f\x72\x3d\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x63\x3d\ +\x63\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x2f\x5c\x3d\x5c\x73\x2a\ +\x28\x5b\x5e\x27\x22\x5c\x5d\x5d\x2a\x29\x5c\x73\x2a\x5c\x5d\x2f\ +\x67\x2c\x22\x3d\x27\x24\x31\x27\x5d\x22\x29\x3b\x69\x66\x28\x21\ +\x6d\x2e\x69\x73\x58\x4d\x4c\x28\x61\x29\x29\x74\x72\x79\x7b\x69\ +\x66\x28\x65\x7c\x7c\x21\x6f\x2e\x6d\x61\x74\x63\x68\x2e\x50\x53\ +\x45\x55\x44\x4f\x2e\x74\x65\x73\x74\x28\x63\x29\x26\x26\x21\x2f\ +\x21\x3d\x2f\x2e\x74\x65\x73\x74\x28\x63\x29\x29\x7b\x76\x61\x72\ +\x20\x66\x3d\x62\x2e\x63\x61\x6c\x6c\x28\x61\x2c\x63\x29\x3b\x69\ +\x66\x28\x66\x7c\x7c\x21\x64\x7c\x7c\x61\x2e\x64\x6f\x63\x75\x6d\ +\x65\x6e\x74\x26\x26\x61\x2e\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\ +\x6e\x6f\x64\x65\x54\x79\x70\x65\x21\x3d\x3d\x31\x31\x29\x72\x65\ +\x74\x75\x72\x6e\x20\x66\x7d\x7d\x63\x61\x74\x63\x68\x28\x67\x29\ +\x7b\x7d\x72\x65\x74\x75\x72\x6e\x20\x6d\x28\x63\x2c\x6e\x75\x6c\ +\x6c\x2c\x6e\x75\x6c\x6c\x2c\x5b\x61\x5d\x29\x2e\x6c\x65\x6e\x67\ +\x74\x68\x3e\x30\x7d\x7d\x7d\x28\x29\x2c\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x29\x7b\x76\x61\x72\x20\x61\x3d\x63\x2e\x63\x72\x65\ +\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x64\x69\x76\x22\ +\x29\x3b\x61\x2e\x69\x6e\x6e\x65\x72\x48\x54\x4d\x4c\x3d\x22\x3c\ +\x64\x69\x76\x20\x63\x6c\x61\x73\x73\x3d\x27\x74\x65\x73\x74\x20\ +\x65\x27\x3e\x3c\x2f\x64\x69\x76\x3e\x3c\x64\x69\x76\x20\x63\x6c\ +\x61\x73\x73\x3d\x27\x74\x65\x73\x74\x27\x3e\x3c\x2f\x64\x69\x76\ +\x3e\x22\x3b\x69\x66\x28\x21\x21\x61\x2e\x67\x65\x74\x45\x6c\x65\ +\x6d\x65\x6e\x74\x73\x42\x79\x43\x6c\x61\x73\x73\x4e\x61\x6d\x65\ +\x26\x26\x61\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\ +\x79\x43\x6c\x61\x73\x73\x4e\x61\x6d\x65\x28\x22\x65\x22\x29\x2e\ +\x6c\x65\x6e\x67\x74\x68\x21\x3d\x3d\x30\x29\x7b\x61\x2e\x6c\x61\ +\x73\x74\x43\x68\x69\x6c\x64\x2e\x63\x6c\x61\x73\x73\x4e\x61\x6d\ +\x65\x3d\x22\x65\x22\x3b\x69\x66\x28\x61\x2e\x67\x65\x74\x45\x6c\ +\x65\x6d\x65\x6e\x74\x73\x42\x79\x43\x6c\x61\x73\x73\x4e\x61\x6d\ +\x65\x28\x22\x65\x22\x29\x2e\x6c\x65\x6e\x67\x74\x68\x3d\x3d\x3d\ +\x31\x29\x72\x65\x74\x75\x72\x6e\x3b\x6f\x2e\x6f\x72\x64\x65\x72\ +\x2e\x73\x70\x6c\x69\x63\x65\x28\x31\x2c\x30\x2c\x22\x43\x4c\x41\ +\x53\x53\x22\x29\x2c\x6f\x2e\x66\x69\x6e\x64\x2e\x43\x4c\x41\x53\ +\x53\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\ +\x29\x7b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x62\x2e\x67\x65\ +\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x43\x6c\x61\x73\x73\ +\x4e\x61\x6d\x65\x21\x3d\x22\x75\x6e\x64\x65\x66\x69\x6e\x65\x64\ +\x22\x26\x26\x21\x63\x29\x72\x65\x74\x75\x72\x6e\x20\x62\x2e\x67\ +\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x43\x6c\x61\x73\ +\x73\x4e\x61\x6d\x65\x28\x61\x5b\x31\x5d\x29\x7d\x2c\x61\x3d\x6e\ +\x75\x6c\x6c\x7d\x7d\x28\x29\x2c\x63\x2e\x64\x6f\x63\x75\x6d\x65\ +\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x2e\x63\x6f\x6e\x74\x61\x69\ +\x6e\x73\x3f\x6d\x2e\x63\x6f\x6e\x74\x61\x69\x6e\x73\x3d\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x61\x21\x3d\x3d\x62\x26\x26\x28\x61\x2e\x63\x6f\x6e\ +\x74\x61\x69\x6e\x73\x3f\x61\x2e\x63\x6f\x6e\x74\x61\x69\x6e\x73\ +\x28\x62\x29\x3a\x21\x30\x29\x7d\x3a\x63\x2e\x64\x6f\x63\x75\x6d\ +\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x2e\x63\x6f\x6d\x70\x61\ +\x72\x65\x44\x6f\x63\x75\x6d\x65\x6e\x74\x50\x6f\x73\x69\x74\x69\ +\x6f\x6e\x3f\x6d\x2e\x63\x6f\x6e\x74\x61\x69\x6e\x73\x3d\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x21\x21\x28\x61\x2e\x63\x6f\x6d\x70\x61\x72\x65\x44\x6f\ +\x63\x75\x6d\x65\x6e\x74\x50\x6f\x73\x69\x74\x69\x6f\x6e\x28\x62\ +\x29\x26\x31\x36\x29\x7d\x3a\x6d\x2e\x63\x6f\x6e\x74\x61\x69\x6e\ +\x73\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\ +\x75\x72\x6e\x21\x31\x7d\x2c\x6d\x2e\x69\x73\x58\x4d\x4c\x3d\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\ +\x3d\x28\x61\x3f\x61\x2e\x6f\x77\x6e\x65\x72\x44\x6f\x63\x75\x6d\ +\x65\x6e\x74\x7c\x7c\x61\x3a\x30\x29\x2e\x64\x6f\x63\x75\x6d\x65\ +\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x3b\x72\x65\x74\x75\x72\x6e\ +\x20\x62\x3f\x62\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x21\x3d\x3d\ +\x22\x48\x54\x4d\x4c\x22\x3a\x21\x31\x7d\x3b\x76\x61\x72\x20\x79\ +\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\ +\x7b\x76\x61\x72\x20\x64\x2c\x65\x3d\x5b\x5d\x2c\x66\x3d\x22\x22\ +\x2c\x67\x3d\x62\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3f\x5b\x62\ +\x5d\x3a\x62\x3b\x77\x68\x69\x6c\x65\x28\x64\x3d\x6f\x2e\x6d\x61\ +\x74\x63\x68\x2e\x50\x53\x45\x55\x44\x4f\x2e\x65\x78\x65\x63\x28\ +\x61\x29\x29\x66\x2b\x3d\x64\x5b\x30\x5d\x2c\x61\x3d\x61\x2e\x72\ +\x65\x70\x6c\x61\x63\x65\x28\x6f\x2e\x6d\x61\x74\x63\x68\x2e\x50\ +\x53\x45\x55\x44\x4f\x2c\x22\x22\x29\x3b\x61\x3d\x6f\x2e\x72\x65\ +\x6c\x61\x74\x69\x76\x65\x5b\x61\x5d\x3f\x61\x2b\x22\x2a\x22\x3a\ +\x61\x3b\x66\x6f\x72\x28\x76\x61\x72\x20\x68\x3d\x30\x2c\x69\x3d\ +\x67\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x68\x3c\x69\x3b\x68\x2b\x2b\ +\x29\x6d\x28\x61\x2c\x67\x5b\x68\x5d\x2c\x65\x2c\x63\x29\x3b\x72\ +\x65\x74\x75\x72\x6e\x20\x6d\x2e\x66\x69\x6c\x74\x65\x72\x28\x66\ +\x2c\x65\x29\x7d\x3b\x6d\x2e\x61\x74\x74\x72\x3d\x66\x2e\x61\x74\ +\x74\x72\x2c\x6d\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x73\x2e\x61\ +\x74\x74\x72\x4d\x61\x70\x3d\x7b\x7d\x2c\x66\x2e\x66\x69\x6e\x64\ +\x3d\x6d\x2c\x66\x2e\x65\x78\x70\x72\x3d\x6d\x2e\x73\x65\x6c\x65\ +\x63\x74\x6f\x72\x73\x2c\x66\x2e\x65\x78\x70\x72\x5b\x22\x3a\x22\ +\x5d\x3d\x66\x2e\x65\x78\x70\x72\x2e\x66\x69\x6c\x74\x65\x72\x73\ +\x2c\x66\x2e\x75\x6e\x69\x71\x75\x65\x3d\x6d\x2e\x75\x6e\x69\x71\ +\x75\x65\x53\x6f\x72\x74\x2c\x66\x2e\x74\x65\x78\x74\x3d\x6d\x2e\ +\x67\x65\x74\x54\x65\x78\x74\x2c\x66\x2e\x69\x73\x58\x4d\x4c\x44\ +\x6f\x63\x3d\x6d\x2e\x69\x73\x58\x4d\x4c\x2c\x66\x2e\x63\x6f\x6e\ +\x74\x61\x69\x6e\x73\x3d\x6d\x2e\x63\x6f\x6e\x74\x61\x69\x6e\x73\ +\x7d\x28\x29\x3b\x76\x61\x72\x20\x4c\x3d\x2f\x55\x6e\x74\x69\x6c\ +\x24\x2f\x2c\x4d\x3d\x2f\x5e\x28\x3f\x3a\x70\x61\x72\x65\x6e\x74\ +\x73\x7c\x70\x72\x65\x76\x55\x6e\x74\x69\x6c\x7c\x70\x72\x65\x76\ +\x41\x6c\x6c\x29\x2f\x2c\x4e\x3d\x2f\x2c\x2f\x2c\x4f\x3d\x2f\x5e\ +\x2e\x5b\x5e\x3a\x23\x5c\x5b\x5c\x2e\x2c\x5d\x2a\x24\x2f\x2c\x50\ +\x3d\x41\x72\x72\x61\x79\x2e\x70\x72\x6f\x74\x6f\x74\x79\x70\x65\ +\x2e\x73\x6c\x69\x63\x65\x2c\x51\x3d\x66\x2e\x65\x78\x70\x72\x2e\ +\x6d\x61\x74\x63\x68\x2e\x50\x4f\x53\x2c\x52\x3d\x7b\x63\x68\x69\ +\x6c\x64\x72\x65\x6e\x3a\x21\x30\x2c\x63\x6f\x6e\x74\x65\x6e\x74\ +\x73\x3a\x21\x30\x2c\x6e\x65\x78\x74\x3a\x21\x30\x2c\x70\x72\x65\ +\x76\x3a\x21\x30\x7d\x3b\x66\x2e\x66\x6e\x2e\x65\x78\x74\x65\x6e\ +\x64\x28\x7b\x66\x69\x6e\x64\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x7b\x76\x61\x72\x20\x62\x3d\x74\x68\x69\x73\x2c\x63\ +\x2c\x64\x3b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x61\x21\x3d\ +\x22\x73\x74\x72\x69\x6e\x67\x22\x29\x72\x65\x74\x75\x72\x6e\x20\ +\x66\x28\x61\x29\x2e\x66\x69\x6c\x74\x65\x72\x28\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x29\x7b\x66\x6f\x72\x28\x63\x3d\x30\x2c\x64\ +\x3d\x62\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x63\x3c\x64\x3b\x63\x2b\ +\x2b\x29\x69\x66\x28\x66\x2e\x63\x6f\x6e\x74\x61\x69\x6e\x73\x28\ +\x62\x5b\x63\x5d\x2c\x74\x68\x69\x73\x29\x29\x72\x65\x74\x75\x72\ +\x6e\x21\x30\x7d\x29\x3b\x76\x61\x72\x20\x65\x3d\x74\x68\x69\x73\ +\x2e\x70\x75\x73\x68\x53\x74\x61\x63\x6b\x28\x22\x22\x2c\x22\x66\ +\x69\x6e\x64\x22\x2c\x61\x29\x2c\x67\x2c\x68\x2c\x69\x3b\x66\x6f\ +\x72\x28\x63\x3d\x30\x2c\x64\x3d\x74\x68\x69\x73\x2e\x6c\x65\x6e\ +\x67\x74\x68\x3b\x63\x3c\x64\x3b\x63\x2b\x2b\x29\x7b\x67\x3d\x65\ +\x2e\x6c\x65\x6e\x67\x74\x68\x2c\x66\x2e\x66\x69\x6e\x64\x28\x61\ +\x2c\x74\x68\x69\x73\x5b\x63\x5d\x2c\x65\x29\x3b\x69\x66\x28\x63\ +\x3e\x30\x29\x66\x6f\x72\x28\x68\x3d\x67\x3b\x68\x3c\x65\x2e\x6c\ +\x65\x6e\x67\x74\x68\x3b\x68\x2b\x2b\x29\x66\x6f\x72\x28\x69\x3d\ +\x30\x3b\x69\x3c\x67\x3b\x69\x2b\x2b\x29\x69\x66\x28\x65\x5b\x69\ +\x5d\x3d\x3d\x3d\x65\x5b\x68\x5d\x29\x7b\x65\x2e\x73\x70\x6c\x69\ +\x63\x65\x28\x68\x2d\x2d\x2c\x31\x29\x3b\x62\x72\x65\x61\x6b\x7d\ +\x7d\x72\x65\x74\x75\x72\x6e\x20\x65\x7d\x2c\x68\x61\x73\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\ +\x3d\x66\x28\x61\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\ +\x73\x2e\x66\x69\x6c\x74\x65\x72\x28\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x29\x7b\x66\x6f\x72\x28\x76\x61\x72\x20\x61\x3d\x30\x2c\ +\x63\x3d\x62\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x61\x3c\x63\x3b\x61\ +\x2b\x2b\x29\x69\x66\x28\x66\x2e\x63\x6f\x6e\x74\x61\x69\x6e\x73\ +\x28\x74\x68\x69\x73\x2c\x62\x5b\x61\x5d\x29\x29\x72\x65\x74\x75\ +\x72\x6e\x21\x30\x7d\x29\x7d\x2c\x6e\x6f\x74\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x2e\x70\x75\x73\x68\x53\x74\x61\x63\x6b\x28\x54\x28\ +\x74\x68\x69\x73\x2c\x61\x2c\x21\x31\x29\x2c\x22\x6e\x6f\x74\x22\ +\x2c\x61\x29\x7d\x2c\x66\x69\x6c\x74\x65\x72\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x2e\x70\x75\x73\x68\x53\x74\x61\x63\x6b\x28\x54\x28\ +\x74\x68\x69\x73\x2c\x61\x2c\x21\x30\x29\x2c\x22\x66\x69\x6c\x74\ +\x65\x72\x22\x2c\x61\x29\x7d\x2c\x69\x73\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x21\x21\x61\ +\x26\x26\x28\x74\x79\x70\x65\x6f\x66\x20\x61\x3d\x3d\x22\x73\x74\ +\x72\x69\x6e\x67\x22\x3f\x51\x2e\x74\x65\x73\x74\x28\x61\x29\x3f\ +\x66\x28\x61\x2c\x74\x68\x69\x73\x2e\x63\x6f\x6e\x74\x65\x78\x74\ +\x29\x2e\x69\x6e\x64\x65\x78\x28\x74\x68\x69\x73\x5b\x30\x5d\x29\ +\x3e\x3d\x30\x3a\x66\x2e\x66\x69\x6c\x74\x65\x72\x28\x61\x2c\x74\ +\x68\x69\x73\x29\x2e\x6c\x65\x6e\x67\x74\x68\x3e\x30\x3a\x74\x68\ +\x69\x73\x2e\x66\x69\x6c\x74\x65\x72\x28\x61\x29\x2e\x6c\x65\x6e\ +\x67\x74\x68\x3e\x30\x29\x7d\x2c\x63\x6c\x6f\x73\x65\x73\x74\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\ +\x72\x20\x63\x3d\x5b\x5d\x2c\x64\x2c\x65\x2c\x67\x3d\x74\x68\x69\ +\x73\x5b\x30\x5d\x3b\x69\x66\x28\x66\x2e\x69\x73\x41\x72\x72\x61\ +\x79\x28\x61\x29\x29\x7b\x76\x61\x72\x20\x68\x3d\x31\x3b\x77\x68\ +\x69\x6c\x65\x28\x67\x26\x26\x67\x2e\x6f\x77\x6e\x65\x72\x44\x6f\ +\x63\x75\x6d\x65\x6e\x74\x26\x26\x67\x21\x3d\x3d\x62\x29\x7b\x66\ +\x6f\x72\x28\x64\x3d\x30\x3b\x64\x3c\x61\x2e\x6c\x65\x6e\x67\x74\ +\x68\x3b\x64\x2b\x2b\x29\x66\x28\x67\x29\x2e\x69\x73\x28\x61\x5b\ +\x64\x5d\x29\x26\x26\x63\x2e\x70\x75\x73\x68\x28\x7b\x73\x65\x6c\ +\x65\x63\x74\x6f\x72\x3a\x61\x5b\x64\x5d\x2c\x65\x6c\x65\x6d\x3a\ +\x67\x2c\x6c\x65\x76\x65\x6c\x3a\x68\x7d\x29\x3b\x67\x3d\x67\x2e\ +\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2c\x68\x2b\x2b\x7d\x72\ +\x65\x74\x75\x72\x6e\x20\x63\x7d\x76\x61\x72\x20\x69\x3d\x51\x2e\ +\x74\x65\x73\x74\x28\x61\x29\x7c\x7c\x74\x79\x70\x65\x6f\x66\x20\ +\x61\x21\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x3f\x66\x28\x61\x2c\ +\x62\x7c\x7c\x74\x68\x69\x73\x2e\x63\x6f\x6e\x74\x65\x78\x74\x29\ +\x3a\x30\x3b\x66\x6f\x72\x28\x64\x3d\x30\x2c\x65\x3d\x74\x68\x69\ +\x73\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x64\x3c\x65\x3b\x64\x2b\x2b\ +\x29\x7b\x67\x3d\x74\x68\x69\x73\x5b\x64\x5d\x3b\x77\x68\x69\x6c\ +\x65\x28\x67\x29\x7b\x69\x66\x28\x69\x3f\x69\x2e\x69\x6e\x64\x65\ +\x78\x28\x67\x29\x3e\x2d\x31\x3a\x66\x2e\x66\x69\x6e\x64\x2e\x6d\ +\x61\x74\x63\x68\x65\x73\x53\x65\x6c\x65\x63\x74\x6f\x72\x28\x67\ +\x2c\x61\x29\x29\x7b\x63\x2e\x70\x75\x73\x68\x28\x67\x29\x3b\x62\ +\x72\x65\x61\x6b\x7d\x67\x3d\x67\x2e\x70\x61\x72\x65\x6e\x74\x4e\ +\x6f\x64\x65\x3b\x69\x66\x28\x21\x67\x7c\x7c\x21\x67\x2e\x6f\x77\ +\x6e\x65\x72\x44\x6f\x63\x75\x6d\x65\x6e\x74\x7c\x7c\x67\x3d\x3d\ +\x3d\x62\x7c\x7c\x67\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\ +\x3d\x31\x31\x29\x62\x72\x65\x61\x6b\x7d\x7d\x63\x3d\x63\x2e\x6c\ +\x65\x6e\x67\x74\x68\x3e\x31\x3f\x66\x2e\x75\x6e\x69\x71\x75\x65\ +\x28\x63\x29\x3a\x63\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\ +\x73\x2e\x70\x75\x73\x68\x53\x74\x61\x63\x6b\x28\x63\x2c\x22\x63\ +\x6c\x6f\x73\x65\x73\x74\x22\x2c\x61\x29\x7d\x2c\x69\x6e\x64\x65\ +\x78\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x69\x66\ +\x28\x21\x61\x29\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x5b\ +\x30\x5d\x26\x26\x74\x68\x69\x73\x5b\x30\x5d\x2e\x70\x61\x72\x65\ +\x6e\x74\x4e\x6f\x64\x65\x3f\x74\x68\x69\x73\x2e\x70\x72\x65\x76\ +\x41\x6c\x6c\x28\x29\x2e\x6c\x65\x6e\x67\x74\x68\x3a\x2d\x31\x3b\ +\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x61\x3d\x3d\x22\x73\x74\ +\x72\x69\x6e\x67\x22\x29\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x69\ +\x6e\x41\x72\x72\x61\x79\x28\x74\x68\x69\x73\x5b\x30\x5d\x2c\x66\ +\x28\x61\x29\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x69\x6e\ +\x41\x72\x72\x61\x79\x28\x61\x2e\x6a\x71\x75\x65\x72\x79\x3f\x61\ +\x5b\x30\x5d\x3a\x61\x2c\x74\x68\x69\x73\x29\x7d\x2c\x61\x64\x64\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\ +\x61\x72\x20\x63\x3d\x74\x79\x70\x65\x6f\x66\x20\x61\x3d\x3d\x22\ +\x73\x74\x72\x69\x6e\x67\x22\x3f\x66\x28\x61\x2c\x62\x29\x3a\x66\ +\x2e\x6d\x61\x6b\x65\x41\x72\x72\x61\x79\x28\x61\x26\x26\x61\x2e\ +\x6e\x6f\x64\x65\x54\x79\x70\x65\x3f\x5b\x61\x5d\x3a\x61\x29\x2c\ +\x64\x3d\x66\x2e\x6d\x65\x72\x67\x65\x28\x74\x68\x69\x73\x2e\x67\ +\x65\x74\x28\x29\x2c\x63\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x2e\x70\x75\x73\x68\x53\x74\x61\x63\x6b\x28\x53\x28\ +\x63\x5b\x30\x5d\x29\x7c\x7c\x53\x28\x64\x5b\x30\x5d\x29\x3f\x64\ +\x3a\x66\x2e\x75\x6e\x69\x71\x75\x65\x28\x64\x29\x29\x7d\x2c\x61\ +\x6e\x64\x53\x65\x6c\x66\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x61\x64\ +\x64\x28\x74\x68\x69\x73\x2e\x70\x72\x65\x76\x4f\x62\x6a\x65\x63\ +\x74\x29\x7d\x7d\x29\x2c\x66\x2e\x65\x61\x63\x68\x28\x7b\x70\x61\ +\x72\x65\x6e\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\ +\x7b\x76\x61\x72\x20\x62\x3d\x61\x2e\x70\x61\x72\x65\x6e\x74\x4e\ +\x6f\x64\x65\x3b\x72\x65\x74\x75\x72\x6e\x20\x62\x26\x26\x62\x2e\ +\x6e\x6f\x64\x65\x54\x79\x70\x65\x21\x3d\x3d\x31\x31\x3f\x62\x3a\ +\x6e\x75\x6c\x6c\x7d\x2c\x70\x61\x72\x65\x6e\x74\x73\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\ +\x20\x66\x2e\x64\x69\x72\x28\x61\x2c\x22\x70\x61\x72\x65\x6e\x74\ +\x4e\x6f\x64\x65\x22\x29\x7d\x2c\x70\x61\x72\x65\x6e\x74\x73\x55\ +\x6e\x74\x69\x6c\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\ +\x62\x2c\x63\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x64\x69\ +\x72\x28\x61\x2c\x22\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x22\ +\x2c\x63\x29\x7d\x2c\x6e\x65\x78\x74\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x6e\ +\x74\x68\x28\x61\x2c\x32\x2c\x22\x6e\x65\x78\x74\x53\x69\x62\x6c\ +\x69\x6e\x67\x22\x29\x7d\x2c\x70\x72\x65\x76\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\ +\x2e\x6e\x74\x68\x28\x61\x2c\x32\x2c\x22\x70\x72\x65\x76\x69\x6f\ +\x75\x73\x53\x69\x62\x6c\x69\x6e\x67\x22\x29\x7d\x2c\x6e\x65\x78\ +\x74\x41\x6c\x6c\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\ +\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x64\x69\x72\x28\x61\x2c\ +\x22\x6e\x65\x78\x74\x53\x69\x62\x6c\x69\x6e\x67\x22\x29\x7d\x2c\ +\x70\x72\x65\x76\x41\x6c\x6c\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x64\x69\x72\ +\x28\x61\x2c\x22\x70\x72\x65\x76\x69\x6f\x75\x73\x53\x69\x62\x6c\ +\x69\x6e\x67\x22\x29\x7d\x2c\x6e\x65\x78\x74\x55\x6e\x74\x69\x6c\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\ +\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x64\x69\x72\x28\x61\x2c\ +\x22\x6e\x65\x78\x74\x53\x69\x62\x6c\x69\x6e\x67\x22\x2c\x63\x29\ +\x7d\x2c\x70\x72\x65\x76\x55\x6e\x74\x69\x6c\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x66\x2e\x64\x69\x72\x28\x61\x2c\x22\x70\x72\x65\x76\ +\x69\x6f\x75\x73\x53\x69\x62\x6c\x69\x6e\x67\x22\x2c\x63\x29\x7d\ +\x2c\x73\x69\x62\x6c\x69\x6e\x67\x73\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x73\ +\x69\x62\x6c\x69\x6e\x67\x28\x61\x2e\x70\x61\x72\x65\x6e\x74\x4e\ +\x6f\x64\x65\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x2c\x61\ +\x29\x7d\x2c\x63\x68\x69\x6c\x64\x72\x65\x6e\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\ +\x2e\x73\x69\x62\x6c\x69\x6e\x67\x28\x61\x2e\x66\x69\x72\x73\x74\ +\x43\x68\x69\x6c\x64\x29\x7d\x2c\x63\x6f\x6e\x74\x65\x6e\x74\x73\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\ +\x75\x72\x6e\x20\x66\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x28\x61\ +\x2c\x22\x69\x66\x72\x61\x6d\x65\x22\x29\x3f\x61\x2e\x63\x6f\x6e\ +\x74\x65\x6e\x74\x44\x6f\x63\x75\x6d\x65\x6e\x74\x7c\x7c\x61\x2e\ +\x63\x6f\x6e\x74\x65\x6e\x74\x57\x69\x6e\x64\x6f\x77\x2e\x64\x6f\ +\x63\x75\x6d\x65\x6e\x74\x3a\x66\x2e\x6d\x61\x6b\x65\x41\x72\x72\ +\x61\x79\x28\x61\x2e\x63\x68\x69\x6c\x64\x4e\x6f\x64\x65\x73\x29\ +\x7d\x7d\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\ +\x7b\x66\x2e\x66\x6e\x5b\x61\x5d\x3d\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x63\x2c\x64\x29\x7b\x76\x61\x72\x20\x65\x3d\x66\x2e\x6d\ +\x61\x70\x28\x74\x68\x69\x73\x2c\x62\x2c\x63\x29\x3b\x4c\x2e\x74\ +\x65\x73\x74\x28\x61\x29\x7c\x7c\x28\x64\x3d\x63\x29\x2c\x64\x26\ +\x26\x74\x79\x70\x65\x6f\x66\x20\x64\x3d\x3d\x22\x73\x74\x72\x69\ +\x6e\x67\x22\x26\x26\x28\x65\x3d\x66\x2e\x66\x69\x6c\x74\x65\x72\ +\x28\x64\x2c\x65\x29\x29\x2c\x65\x3d\x74\x68\x69\x73\x2e\x6c\x65\ +\x6e\x67\x74\x68\x3e\x31\x26\x26\x21\x52\x5b\x61\x5d\x3f\x66\x2e\ +\x75\x6e\x69\x71\x75\x65\x28\x65\x29\x3a\x65\x2c\x28\x74\x68\x69\ +\x73\x2e\x6c\x65\x6e\x67\x74\x68\x3e\x31\x7c\x7c\x4e\x2e\x74\x65\ +\x73\x74\x28\x64\x29\x29\x26\x26\x4d\x2e\x74\x65\x73\x74\x28\x61\ +\x29\x26\x26\x28\x65\x3d\x65\x2e\x72\x65\x76\x65\x72\x73\x65\x28\ +\x29\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x70\ +\x75\x73\x68\x53\x74\x61\x63\x6b\x28\x65\x2c\x61\x2c\x50\x2e\x63\ +\x61\x6c\x6c\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\x2e\x6a\ +\x6f\x69\x6e\x28\x22\x2c\x22\x29\x29\x7d\x7d\x29\x2c\x66\x2e\x65\ +\x78\x74\x65\x6e\x64\x28\x7b\x66\x69\x6c\x74\x65\x72\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x63\x26\ +\x26\x28\x61\x3d\x22\x3a\x6e\x6f\x74\x28\x22\x2b\x61\x2b\x22\x29\ +\x22\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x62\x2e\x6c\x65\x6e\x67\ +\x74\x68\x3d\x3d\x3d\x31\x3f\x66\x2e\x66\x69\x6e\x64\x2e\x6d\x61\ +\x74\x63\x68\x65\x73\x53\x65\x6c\x65\x63\x74\x6f\x72\x28\x62\x5b\ +\x30\x5d\x2c\x61\x29\x3f\x5b\x62\x5b\x30\x5d\x5d\x3a\x5b\x5d\x3a\ +\x66\x2e\x66\x69\x6e\x64\x2e\x6d\x61\x74\x63\x68\x65\x73\x28\x61\ +\x2c\x62\x29\x7d\x2c\x64\x69\x72\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x2c\x63\x2c\x64\x29\x7b\x76\x61\x72\x20\x65\x3d\x5b\ +\x5d\x2c\x67\x3d\x61\x5b\x63\x5d\x3b\x77\x68\x69\x6c\x65\x28\x67\ +\x26\x26\x67\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x21\x3d\x3d\x39\ +\x26\x26\x28\x64\x3d\x3d\x3d\x62\x7c\x7c\x67\x2e\x6e\x6f\x64\x65\ +\x54\x79\x70\x65\x21\x3d\x3d\x31\x7c\x7c\x21\x66\x28\x67\x29\x2e\ +\x69\x73\x28\x64\x29\x29\x29\x67\x2e\x6e\x6f\x64\x65\x54\x79\x70\ +\x65\x3d\x3d\x3d\x31\x26\x26\x65\x2e\x70\x75\x73\x68\x28\x67\x29\ +\x2c\x67\x3d\x67\x5b\x63\x5d\x3b\x72\x65\x74\x75\x72\x6e\x20\x65\ +\x7d\x2c\x6e\x74\x68\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x62\x2c\x63\x2c\x64\x29\x7b\x62\x3d\x62\x7c\x7c\x31\x3b\x76\ +\x61\x72\x20\x65\x3d\x30\x3b\x66\x6f\x72\x28\x3b\x61\x3b\x61\x3d\ +\x61\x5b\x63\x5d\x29\x69\x66\x28\x61\x2e\x6e\x6f\x64\x65\x54\x79\ +\x70\x65\x3d\x3d\x3d\x31\x26\x26\x2b\x2b\x65\x3d\x3d\x3d\x62\x29\ +\x62\x72\x65\x61\x6b\x3b\x72\x65\x74\x75\x72\x6e\x20\x61\x7d\x2c\ +\x73\x69\x62\x6c\x69\x6e\x67\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\x5b\x5d\x3b\x66\ +\x6f\x72\x28\x3b\x61\x3b\x61\x3d\x61\x2e\x6e\x65\x78\x74\x53\x69\ +\x62\x6c\x69\x6e\x67\x29\x61\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\ +\x3d\x3d\x3d\x31\x26\x26\x61\x21\x3d\x3d\x62\x26\x26\x63\x2e\x70\ +\x75\x73\x68\x28\x61\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x63\x7d\ +\x7d\x29\x3b\x76\x61\x72\x20\x56\x3d\x22\x61\x62\x62\x72\x7c\x61\ +\x72\x74\x69\x63\x6c\x65\x7c\x61\x73\x69\x64\x65\x7c\x61\x75\x64\ +\x69\x6f\x7c\x63\x61\x6e\x76\x61\x73\x7c\x64\x61\x74\x61\x6c\x69\ +\x73\x74\x7c\x64\x65\x74\x61\x69\x6c\x73\x7c\x66\x69\x67\x63\x61\ +\x70\x74\x69\x6f\x6e\x7c\x66\x69\x67\x75\x72\x65\x7c\x66\x6f\x6f\ +\x74\x65\x72\x7c\x68\x65\x61\x64\x65\x72\x7c\x68\x67\x72\x6f\x75\ +\x70\x7c\x6d\x61\x72\x6b\x7c\x6d\x65\x74\x65\x72\x7c\x6e\x61\x76\ +\x7c\x6f\x75\x74\x70\x75\x74\x7c\x70\x72\x6f\x67\x72\x65\x73\x73\ +\x7c\x73\x65\x63\x74\x69\x6f\x6e\x7c\x73\x75\x6d\x6d\x61\x72\x79\ +\x7c\x74\x69\x6d\x65\x7c\x76\x69\x64\x65\x6f\x22\x2c\x57\x3d\x2f\ +\x20\x6a\x51\x75\x65\x72\x79\x5c\x64\x2b\x3d\x22\x28\x3f\x3a\x5c\ +\x64\x2b\x7c\x6e\x75\x6c\x6c\x29\x22\x2f\x67\x2c\x58\x3d\x2f\x5e\ +\x5c\x73\x2b\x2f\x2c\x59\x3d\x2f\x3c\x28\x3f\x21\x61\x72\x65\x61\ +\x7c\x62\x72\x7c\x63\x6f\x6c\x7c\x65\x6d\x62\x65\x64\x7c\x68\x72\ +\x7c\x69\x6d\x67\x7c\x69\x6e\x70\x75\x74\x7c\x6c\x69\x6e\x6b\x7c\ +\x6d\x65\x74\x61\x7c\x70\x61\x72\x61\x6d\x29\x28\x28\x5b\x5c\x77\ +\x3a\x5d\x2b\x29\x5b\x5e\x3e\x5d\x2a\x29\x5c\x2f\x3e\x2f\x69\x67\ +\x2c\x5a\x3d\x2f\x3c\x28\x5b\x5c\x77\x3a\x5d\x2b\x29\x2f\x2c\x24\ +\x3d\x2f\x3c\x74\x62\x6f\x64\x79\x2f\x69\x2c\x5f\x3d\x2f\x3c\x7c\ +\x26\x23\x3f\x5c\x77\x2b\x3b\x2f\x2c\x62\x61\x3d\x2f\x3c\x28\x3f\ +\x3a\x73\x63\x72\x69\x70\x74\x7c\x73\x74\x79\x6c\x65\x29\x2f\x69\ +\x2c\x62\x62\x3d\x2f\x3c\x28\x3f\x3a\x73\x63\x72\x69\x70\x74\x7c\ +\x6f\x62\x6a\x65\x63\x74\x7c\x65\x6d\x62\x65\x64\x7c\x6f\x70\x74\ +\x69\x6f\x6e\x7c\x73\x74\x79\x6c\x65\x29\x2f\x69\x2c\x62\x63\x3d\ +\x6e\x65\x77\x20\x52\x65\x67\x45\x78\x70\x28\x22\x3c\x28\x3f\x3a\ +\x22\x2b\x56\x2b\x22\x29\x22\x2c\x22\x69\x22\x29\x2c\x62\x64\x3d\ +\x2f\x63\x68\x65\x63\x6b\x65\x64\x5c\x73\x2a\x28\x3f\x3a\x5b\x5e\ +\x3d\x5d\x7c\x3d\x5c\x73\x2a\x2e\x63\x68\x65\x63\x6b\x65\x64\x2e\ +\x29\x2f\x69\x2c\x62\x65\x3d\x2f\x5c\x2f\x28\x6a\x61\x76\x61\x7c\ +\x65\x63\x6d\x61\x29\x73\x63\x72\x69\x70\x74\x2f\x69\x2c\x62\x66\ +\x3d\x2f\x5e\x5c\x73\x2a\x3c\x21\x28\x3f\x3a\x5c\x5b\x43\x44\x41\ +\x54\x41\x5c\x5b\x7c\x5c\x2d\x5c\x2d\x29\x2f\x2c\x62\x67\x3d\x7b\ +\x6f\x70\x74\x69\x6f\x6e\x3a\x5b\x31\x2c\x22\x3c\x73\x65\x6c\x65\ +\x63\x74\x20\x6d\x75\x6c\x74\x69\x70\x6c\x65\x3d\x27\x6d\x75\x6c\ +\x74\x69\x70\x6c\x65\x27\x3e\x22\x2c\x22\x3c\x2f\x73\x65\x6c\x65\ +\x63\x74\x3e\x22\x5d\x2c\x6c\x65\x67\x65\x6e\x64\x3a\x5b\x31\x2c\ +\x22\x3c\x66\x69\x65\x6c\x64\x73\x65\x74\x3e\x22\x2c\x22\x3c\x2f\ +\x66\x69\x65\x6c\x64\x73\x65\x74\x3e\x22\x5d\x2c\x74\x68\x65\x61\ +\x64\x3a\x5b\x31\x2c\x22\x3c\x74\x61\x62\x6c\x65\x3e\x22\x2c\x22\ +\x3c\x2f\x74\x61\x62\x6c\x65\x3e\x22\x5d\x2c\x74\x72\x3a\x5b\x32\ +\x2c\x22\x3c\x74\x61\x62\x6c\x65\x3e\x3c\x74\x62\x6f\x64\x79\x3e\ +\x22\x2c\x22\x3c\x2f\x74\x62\x6f\x64\x79\x3e\x3c\x2f\x74\x61\x62\ +\x6c\x65\x3e\x22\x5d\x2c\x74\x64\x3a\x5b\x33\x2c\x22\x3c\x74\x61\ +\x62\x6c\x65\x3e\x3c\x74\x62\x6f\x64\x79\x3e\x3c\x74\x72\x3e\x22\ +\x2c\x22\x3c\x2f\x74\x72\x3e\x3c\x2f\x74\x62\x6f\x64\x79\x3e\x3c\ +\x2f\x74\x61\x62\x6c\x65\x3e\x22\x5d\x2c\x63\x6f\x6c\x3a\x5b\x32\ +\x2c\x22\x3c\x74\x61\x62\x6c\x65\x3e\x3c\x74\x62\x6f\x64\x79\x3e\ +\x3c\x2f\x74\x62\x6f\x64\x79\x3e\x3c\x63\x6f\x6c\x67\x72\x6f\x75\ +\x70\x3e\x22\x2c\x22\x3c\x2f\x63\x6f\x6c\x67\x72\x6f\x75\x70\x3e\ +\x3c\x2f\x74\x61\x62\x6c\x65\x3e\x22\x5d\x2c\x61\x72\x65\x61\x3a\ +\x5b\x31\x2c\x22\x3c\x6d\x61\x70\x3e\x22\x2c\x22\x3c\x2f\x6d\x61\ +\x70\x3e\x22\x5d\x2c\x5f\x64\x65\x66\x61\x75\x6c\x74\x3a\x5b\x30\ +\x2c\x22\x22\x2c\x22\x22\x5d\x7d\x2c\x62\x68\x3d\x55\x28\x63\x29\ +\x3b\x62\x67\x2e\x6f\x70\x74\x67\x72\x6f\x75\x70\x3d\x62\x67\x2e\ +\x6f\x70\x74\x69\x6f\x6e\x2c\x62\x67\x2e\x74\x62\x6f\x64\x79\x3d\ +\x62\x67\x2e\x74\x66\x6f\x6f\x74\x3d\x62\x67\x2e\x63\x6f\x6c\x67\ +\x72\x6f\x75\x70\x3d\x62\x67\x2e\x63\x61\x70\x74\x69\x6f\x6e\x3d\ +\x62\x67\x2e\x74\x68\x65\x61\x64\x2c\x62\x67\x2e\x74\x68\x3d\x62\ +\x67\x2e\x74\x64\x2c\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\x68\ +\x74\x6d\x6c\x53\x65\x72\x69\x61\x6c\x69\x7a\x65\x7c\x7c\x28\x62\ +\x67\x2e\x5f\x64\x65\x66\x61\x75\x6c\x74\x3d\x5b\x31\x2c\x22\x64\ +\x69\x76\x3c\x64\x69\x76\x3e\x22\x2c\x22\x3c\x2f\x64\x69\x76\x3e\ +\x22\x5d\x29\x2c\x66\x2e\x66\x6e\x2e\x65\x78\x74\x65\x6e\x64\x28\ +\x7b\x74\x65\x78\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x29\x7b\x69\x66\x28\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x29\x29\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\ +\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\ +\x29\x7b\x76\x61\x72\x20\x63\x3d\x66\x28\x74\x68\x69\x73\x29\x3b\ +\x63\x2e\x74\x65\x78\x74\x28\x61\x2e\x63\x61\x6c\x6c\x28\x74\x68\ +\x69\x73\x2c\x62\x2c\x63\x2e\x74\x65\x78\x74\x28\x29\x29\x29\x7d\ +\x29\x3b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x61\x21\x3d\x22\ +\x6f\x62\x6a\x65\x63\x74\x22\x26\x26\x61\x21\x3d\x3d\x62\x29\x72\ +\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x6d\x70\x74\x79\ +\x28\x29\x2e\x61\x70\x70\x65\x6e\x64\x28\x28\x74\x68\x69\x73\x5b\ +\x30\x5d\x26\x26\x74\x68\x69\x73\x5b\x30\x5d\x2e\x6f\x77\x6e\x65\ +\x72\x44\x6f\x63\x75\x6d\x65\x6e\x74\x7c\x7c\x63\x29\x2e\x63\x72\ +\x65\x61\x74\x65\x54\x65\x78\x74\x4e\x6f\x64\x65\x28\x61\x29\x29\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x74\x65\x78\x74\x28\x74\ +\x68\x69\x73\x29\x7d\x2c\x77\x72\x61\x70\x41\x6c\x6c\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x69\x66\x28\x66\x2e\x69\ +\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x29\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x62\x29\x7b\x66\x28\x74\x68\x69\x73\ +\x29\x2e\x77\x72\x61\x70\x41\x6c\x6c\x28\x61\x2e\x63\x61\x6c\x6c\ +\x28\x74\x68\x69\x73\x2c\x62\x29\x29\x7d\x29\x3b\x69\x66\x28\x74\ +\x68\x69\x73\x5b\x30\x5d\x29\x7b\x76\x61\x72\x20\x62\x3d\x66\x28\ +\x61\x2c\x74\x68\x69\x73\x5b\x30\x5d\x2e\x6f\x77\x6e\x65\x72\x44\ +\x6f\x63\x75\x6d\x65\x6e\x74\x29\x2e\x65\x71\x28\x30\x29\x2e\x63\ +\x6c\x6f\x6e\x65\x28\x21\x30\x29\x3b\x74\x68\x69\x73\x5b\x30\x5d\ +\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x26\x26\x62\x2e\x69\ +\x6e\x73\x65\x72\x74\x42\x65\x66\x6f\x72\x65\x28\x74\x68\x69\x73\ +\x5b\x30\x5d\x29\x2c\x62\x2e\x6d\x61\x70\x28\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x29\x7b\x76\x61\x72\x20\x61\x3d\x74\x68\x69\x73\ +\x3b\x77\x68\x69\x6c\x65\x28\x61\x2e\x66\x69\x72\x73\x74\x43\x68\ +\x69\x6c\x64\x26\x26\x61\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\ +\x64\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x29\x61\ +\x3d\x61\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x3b\x72\x65\ +\x74\x75\x72\x6e\x20\x61\x7d\x29\x2e\x61\x70\x70\x65\x6e\x64\x28\ +\x74\x68\x69\x73\x29\x7d\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\ +\x73\x7d\x2c\x77\x72\x61\x70\x49\x6e\x6e\x65\x72\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x69\x66\x28\x66\x2e\x69\x73\ +\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x29\x72\x65\x74\x75\ +\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x62\x29\x7b\x66\x28\x74\x68\x69\x73\x29\ +\x2e\x77\x72\x61\x70\x49\x6e\x6e\x65\x72\x28\x61\x2e\x63\x61\x6c\ +\x6c\x28\x74\x68\x69\x73\x2c\x62\x29\x29\x7d\x29\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x76\x61\x72\x20\x62\x3d\x66\ +\x28\x74\x68\x69\x73\x29\x2c\x63\x3d\x62\x2e\x63\x6f\x6e\x74\x65\ +\x6e\x74\x73\x28\x29\x3b\x63\x2e\x6c\x65\x6e\x67\x74\x68\x3f\x63\ +\x2e\x77\x72\x61\x70\x41\x6c\x6c\x28\x61\x29\x3a\x62\x2e\x61\x70\ +\x70\x65\x6e\x64\x28\x61\x29\x7d\x29\x7d\x2c\x77\x72\x61\x70\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\ +\x62\x3d\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\ +\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x63\x29\x7b\x66\ +\x28\x74\x68\x69\x73\x29\x2e\x77\x72\x61\x70\x41\x6c\x6c\x28\x62\ +\x3f\x61\x2e\x63\x61\x6c\x6c\x28\x74\x68\x69\x73\x2c\x63\x29\x3a\ +\x61\x29\x7d\x29\x7d\x2c\x75\x6e\x77\x72\x61\x70\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x2e\x70\x61\x72\x65\x6e\x74\x28\x29\x2e\x65\x61\x63\ +\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x66\x2e\x6e\ +\x6f\x64\x65\x4e\x61\x6d\x65\x28\x74\x68\x69\x73\x2c\x22\x62\x6f\ +\x64\x79\x22\x29\x7c\x7c\x66\x28\x74\x68\x69\x73\x29\x2e\x72\x65\ +\x70\x6c\x61\x63\x65\x57\x69\x74\x68\x28\x74\x68\x69\x73\x2e\x63\ +\x68\x69\x6c\x64\x4e\x6f\x64\x65\x73\x29\x7d\x29\x2e\x65\x6e\x64\ +\x28\x29\x7d\x2c\x61\x70\x70\x65\x6e\x64\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\ +\x73\x2e\x64\x6f\x6d\x4d\x61\x6e\x69\x70\x28\x61\x72\x67\x75\x6d\ +\x65\x6e\x74\x73\x2c\x21\x30\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x7b\x74\x68\x69\x73\x2e\x6e\x6f\x64\x65\x54\x79\x70\ +\x65\x3d\x3d\x3d\x31\x26\x26\x74\x68\x69\x73\x2e\x61\x70\x70\x65\ +\x6e\x64\x43\x68\x69\x6c\x64\x28\x61\x29\x7d\x29\x7d\x2c\x70\x72\ +\x65\x70\x65\x6e\x64\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\ +\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x64\x6f\x6d\ +\x4d\x61\x6e\x69\x70\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2c\ +\x21\x30\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x74\ +\x68\x69\x73\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\ +\x26\x26\x74\x68\x69\x73\x2e\x69\x6e\x73\x65\x72\x74\x42\x65\x66\ +\x6f\x72\x65\x28\x61\x2c\x74\x68\x69\x73\x2e\x66\x69\x72\x73\x74\ +\x43\x68\x69\x6c\x64\x29\x7d\x29\x7d\x2c\x62\x65\x66\x6f\x72\x65\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x69\x66\x28\x74\ +\x68\x69\x73\x5b\x30\x5d\x26\x26\x74\x68\x69\x73\x5b\x30\x5d\x2e\ +\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x29\x72\x65\x74\x75\x72\ +\x6e\x20\x74\x68\x69\x73\x2e\x64\x6f\x6d\x4d\x61\x6e\x69\x70\x28\ +\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2c\x21\x31\x2c\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x74\x68\x69\x73\x2e\x70\x61\ +\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2e\x69\x6e\x73\x65\x72\x74\x42\ +\x65\x66\x6f\x72\x65\x28\x61\x2c\x74\x68\x69\x73\x29\x7d\x29\x3b\ +\x69\x66\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2e\x6c\x65\x6e\ +\x67\x74\x68\x29\x7b\x76\x61\x72\x20\x61\x3d\x66\x2e\x63\x6c\x65\ +\x61\x6e\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\x3b\x61\x2e\ +\x70\x75\x73\x68\x2e\x61\x70\x70\x6c\x79\x28\x61\x2c\x74\x68\x69\ +\x73\x2e\x74\x6f\x41\x72\x72\x61\x79\x28\x29\x29\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x70\x75\x73\x68\x53\x74\x61\ +\x63\x6b\x28\x61\x2c\x22\x62\x65\x66\x6f\x72\x65\x22\x2c\x61\x72\ +\x67\x75\x6d\x65\x6e\x74\x73\x29\x7d\x7d\x2c\x61\x66\x74\x65\x72\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x69\x66\x28\x74\ +\x68\x69\x73\x5b\x30\x5d\x26\x26\x74\x68\x69\x73\x5b\x30\x5d\x2e\ +\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x29\x72\x65\x74\x75\x72\ +\x6e\x20\x74\x68\x69\x73\x2e\x64\x6f\x6d\x4d\x61\x6e\x69\x70\x28\ +\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2c\x21\x31\x2c\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x74\x68\x69\x73\x2e\x70\x61\ +\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2e\x69\x6e\x73\x65\x72\x74\x42\ +\x65\x66\x6f\x72\x65\x28\x61\x2c\x74\x68\x69\x73\x2e\x6e\x65\x78\ +\x74\x53\x69\x62\x6c\x69\x6e\x67\x29\x7d\x29\x3b\x69\x66\x28\x61\ +\x72\x67\x75\x6d\x65\x6e\x74\x73\x2e\x6c\x65\x6e\x67\x74\x68\x29\ +\x7b\x76\x61\x72\x20\x61\x3d\x74\x68\x69\x73\x2e\x70\x75\x73\x68\ +\x53\x74\x61\x63\x6b\x28\x74\x68\x69\x73\x2c\x22\x61\x66\x74\x65\ +\x72\x22\x2c\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\x3b\x61\x2e\ +\x70\x75\x73\x68\x2e\x61\x70\x70\x6c\x79\x28\x61\x2c\x66\x2e\x63\ +\x6c\x65\x61\x6e\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\x29\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x61\x7d\x7d\x2c\x72\x65\x6d\x6f\ +\x76\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\ +\x7b\x66\x6f\x72\x28\x76\x61\x72\x20\x63\x3d\x30\x2c\x64\x3b\x28\ +\x64\x3d\x74\x68\x69\x73\x5b\x63\x5d\x29\x21\x3d\x6e\x75\x6c\x6c\ +\x3b\x63\x2b\x2b\x29\x69\x66\x28\x21\x61\x7c\x7c\x66\x2e\x66\x69\ +\x6c\x74\x65\x72\x28\x61\x2c\x5b\x64\x5d\x29\x2e\x6c\x65\x6e\x67\ +\x74\x68\x29\x21\x62\x26\x26\x64\x2e\x6e\x6f\x64\x65\x54\x79\x70\ +\x65\x3d\x3d\x3d\x31\x26\x26\x28\x66\x2e\x63\x6c\x65\x61\x6e\x44\ +\x61\x74\x61\x28\x64\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\ +\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\x2a\x22\x29\x29\ +\x2c\x66\x2e\x63\x6c\x65\x61\x6e\x44\x61\x74\x61\x28\x5b\x64\x5d\ +\x29\x29\x2c\x64\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x26\ +\x26\x64\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2e\x72\x65\ +\x6d\x6f\x76\x65\x43\x68\x69\x6c\x64\x28\x64\x29\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x65\x6d\x70\x74\x79\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x0a\x7b\x66\x6f\x72\x28\ +\x76\x61\x72\x20\x61\x3d\x30\x2c\x62\x3b\x28\x62\x3d\x74\x68\x69\ +\x73\x5b\x61\x5d\x29\x21\x3d\x6e\x75\x6c\x6c\x3b\x61\x2b\x2b\x29\ +\x7b\x62\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x26\ +\x26\x66\x2e\x63\x6c\x65\x61\x6e\x44\x61\x74\x61\x28\x62\x2e\x67\ +\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\ +\x61\x6d\x65\x28\x22\x2a\x22\x29\x29\x3b\x77\x68\x69\x6c\x65\x28\ +\x62\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x29\x62\x2e\x72\ +\x65\x6d\x6f\x76\x65\x43\x68\x69\x6c\x64\x28\x62\x2e\x66\x69\x72\ +\x73\x74\x43\x68\x69\x6c\x64\x29\x7d\x72\x65\x74\x75\x72\x6e\x20\ +\x74\x68\x69\x73\x7d\x2c\x63\x6c\x6f\x6e\x65\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x61\x3d\x61\x3d\x3d\x6e\ +\x75\x6c\x6c\x3f\x21\x31\x3a\x61\x2c\x62\x3d\x62\x3d\x3d\x6e\x75\ +\x6c\x6c\x3f\x61\x3a\x62\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\ +\x69\x73\x2e\x6d\x61\x70\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x63\x6c\x6f\x6e\x65\ +\x28\x74\x68\x69\x73\x2c\x61\x2c\x62\x29\x7d\x29\x7d\x2c\x68\x74\ +\x6d\x6c\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x69\ +\x66\x28\x61\x3d\x3d\x3d\x62\x29\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x5b\x30\x5d\x26\x26\x74\x68\x69\x73\x5b\x30\x5d\x2e\ +\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x3f\x74\x68\x69\ +\x73\x5b\x30\x5d\x2e\x69\x6e\x6e\x65\x72\x48\x54\x4d\x4c\x2e\x72\ +\x65\x70\x6c\x61\x63\x65\x28\x57\x2c\x22\x22\x29\x3a\x6e\x75\x6c\ +\x6c\x3b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x61\x3d\x3d\x22\ +\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x21\x62\x61\x2e\x74\x65\x73\ +\x74\x28\x61\x29\x26\x26\x28\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\ +\x2e\x6c\x65\x61\x64\x69\x6e\x67\x57\x68\x69\x74\x65\x73\x70\x61\ +\x63\x65\x7c\x7c\x21\x58\x2e\x74\x65\x73\x74\x28\x61\x29\x29\x26\ +\x26\x21\x62\x67\x5b\x28\x5a\x2e\x65\x78\x65\x63\x28\x61\x29\x7c\ +\x7c\x5b\x22\x22\x2c\x22\x22\x5d\x29\x5b\x31\x5d\x2e\x74\x6f\x4c\ +\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x5d\x29\x7b\x61\x3d\x61\ +\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x59\x2c\x22\x3c\x24\x31\x3e\ +\x3c\x2f\x24\x32\x3e\x22\x29\x3b\x74\x72\x79\x7b\x66\x6f\x72\x28\ +\x76\x61\x72\x20\x63\x3d\x30\x2c\x64\x3d\x74\x68\x69\x73\x2e\x6c\ +\x65\x6e\x67\x74\x68\x3b\x63\x3c\x64\x3b\x63\x2b\x2b\x29\x74\x68\ +\x69\x73\x5b\x63\x5d\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\ +\x3d\x31\x26\x26\x28\x66\x2e\x63\x6c\x65\x61\x6e\x44\x61\x74\x61\ +\x28\x74\x68\x69\x73\x5b\x63\x5d\x2e\x67\x65\x74\x45\x6c\x65\x6d\ +\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\x28\x22\x2a\ +\x22\x29\x29\x2c\x74\x68\x69\x73\x5b\x63\x5d\x2e\x69\x6e\x6e\x65\ +\x72\x48\x54\x4d\x4c\x3d\x61\x29\x7d\x63\x61\x74\x63\x68\x28\x65\ +\x29\x7b\x74\x68\x69\x73\x2e\x65\x6d\x70\x74\x79\x28\x29\x2e\x61\ +\x70\x70\x65\x6e\x64\x28\x61\x29\x7d\x7d\x65\x6c\x73\x65\x20\x66\ +\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x3f\x74\ +\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\x66\x28\x74\x68\x69\ +\x73\x29\x3b\x63\x2e\x68\x74\x6d\x6c\x28\x61\x2e\x63\x61\x6c\x6c\ +\x28\x74\x68\x69\x73\x2c\x62\x2c\x63\x2e\x68\x74\x6d\x6c\x28\x29\ +\x29\x29\x7d\x29\x3a\x74\x68\x69\x73\x2e\x65\x6d\x70\x74\x79\x28\ +\x29\x2e\x61\x70\x70\x65\x6e\x64\x28\x61\x29\x3b\x72\x65\x74\x75\ +\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x72\x65\x70\x6c\x61\x63\x65\ +\x57\x69\x74\x68\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\ +\x7b\x69\x66\x28\x74\x68\x69\x73\x5b\x30\x5d\x26\x26\x74\x68\x69\ +\x73\x5b\x30\x5d\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x29\ +\x7b\x69\x66\x28\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x29\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\ +\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\x29\ +\x7b\x76\x61\x72\x20\x63\x3d\x66\x28\x74\x68\x69\x73\x29\x2c\x64\ +\x3d\x63\x2e\x68\x74\x6d\x6c\x28\x29\x3b\x63\x2e\x72\x65\x70\x6c\ +\x61\x63\x65\x57\x69\x74\x68\x28\x61\x2e\x63\x61\x6c\x6c\x28\x74\ +\x68\x69\x73\x2c\x62\x2c\x64\x29\x29\x7d\x29\x3b\x74\x79\x70\x65\ +\x6f\x66\x20\x61\x21\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\ +\x28\x61\x3d\x66\x28\x61\x29\x2e\x64\x65\x74\x61\x63\x68\x28\x29\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\ +\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x76\x61\ +\x72\x20\x62\x3d\x74\x68\x69\x73\x2e\x6e\x65\x78\x74\x53\x69\x62\ +\x6c\x69\x6e\x67\x2c\x63\x3d\x74\x68\x69\x73\x2e\x70\x61\x72\x65\ +\x6e\x74\x4e\x6f\x64\x65\x3b\x66\x28\x74\x68\x69\x73\x29\x2e\x72\ +\x65\x6d\x6f\x76\x65\x28\x29\x2c\x62\x3f\x66\x28\x62\x29\x2e\x62\ +\x65\x66\x6f\x72\x65\x28\x61\x29\x3a\x66\x28\x63\x29\x2e\x61\x70\ +\x70\x65\x6e\x64\x28\x61\x29\x7d\x29\x7d\x72\x65\x74\x75\x72\x6e\ +\x20\x74\x68\x69\x73\x2e\x6c\x65\x6e\x67\x74\x68\x3f\x74\x68\x69\ +\x73\x2e\x70\x75\x73\x68\x53\x74\x61\x63\x6b\x28\x66\x28\x66\x2e\ +\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x3f\x61\x28\ +\x29\x3a\x61\x29\x2c\x22\x72\x65\x70\x6c\x61\x63\x65\x57\x69\x74\ +\x68\x22\x2c\x61\x29\x3a\x74\x68\x69\x73\x7d\x2c\x64\x65\x74\x61\ +\x63\x68\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\ +\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x72\x65\x6d\x6f\x76\ +\x65\x28\x61\x2c\x21\x30\x29\x7d\x2c\x64\x6f\x6d\x4d\x61\x6e\x69\ +\x70\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x2c\x64\ +\x29\x7b\x76\x61\x72\x20\x65\x2c\x67\x2c\x68\x2c\x69\x2c\x6a\x3d\ +\x61\x5b\x30\x5d\x2c\x6b\x3d\x5b\x5d\x3b\x69\x66\x28\x21\x66\x2e\ +\x73\x75\x70\x70\x6f\x72\x74\x2e\x63\x68\x65\x63\x6b\x43\x6c\x6f\ +\x6e\x65\x26\x26\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2e\x6c\x65\ +\x6e\x67\x74\x68\x3d\x3d\x3d\x33\x26\x26\x74\x79\x70\x65\x6f\x66\ +\x20\x6a\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x62\x64\ +\x2e\x74\x65\x73\x74\x28\x6a\x29\x29\x72\x65\x74\x75\x72\x6e\x20\ +\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x29\x7b\x66\x28\x74\x68\x69\x73\x29\x2e\x64\x6f\x6d\ +\x4d\x61\x6e\x69\x70\x28\x61\x2c\x63\x2c\x64\x2c\x21\x30\x29\x7d\ +\x29\x3b\x69\x66\x28\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x6a\x29\x29\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\ +\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x65\ +\x29\x7b\x76\x61\x72\x20\x67\x3d\x66\x28\x74\x68\x69\x73\x29\x3b\ +\x61\x5b\x30\x5d\x3d\x6a\x2e\x63\x61\x6c\x6c\x28\x74\x68\x69\x73\ +\x2c\x65\x2c\x63\x3f\x67\x2e\x68\x74\x6d\x6c\x28\x29\x3a\x62\x29\ +\x2c\x67\x2e\x64\x6f\x6d\x4d\x61\x6e\x69\x70\x28\x61\x2c\x63\x2c\ +\x64\x29\x7d\x29\x3b\x69\x66\x28\x74\x68\x69\x73\x5b\x30\x5d\x29\ +\x7b\x69\x3d\x6a\x26\x26\x6a\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\ +\x64\x65\x2c\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\x70\x61\x72\ +\x65\x6e\x74\x4e\x6f\x64\x65\x26\x26\x69\x26\x26\x69\x2e\x6e\x6f\ +\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\x31\x26\x26\x69\x2e\x63\ +\x68\x69\x6c\x64\x4e\x6f\x64\x65\x73\x2e\x6c\x65\x6e\x67\x74\x68\ +\x3d\x3d\x3d\x74\x68\x69\x73\x2e\x6c\x65\x6e\x67\x74\x68\x3f\x65\ +\x3d\x7b\x66\x72\x61\x67\x6d\x65\x6e\x74\x3a\x69\x7d\x3a\x65\x3d\ +\x66\x2e\x62\x75\x69\x6c\x64\x46\x72\x61\x67\x6d\x65\x6e\x74\x28\ +\x61\x2c\x74\x68\x69\x73\x2c\x6b\x29\x2c\x68\x3d\x65\x2e\x66\x72\ +\x61\x67\x6d\x65\x6e\x74\x2c\x68\x2e\x63\x68\x69\x6c\x64\x4e\x6f\ +\x64\x65\x73\x2e\x6c\x65\x6e\x67\x74\x68\x3d\x3d\x3d\x31\x3f\x67\ +\x3d\x68\x3d\x68\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x3a\ +\x67\x3d\x68\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x3b\x69\ +\x66\x28\x67\x29\x7b\x63\x3d\x63\x26\x26\x66\x2e\x6e\x6f\x64\x65\ +\x4e\x61\x6d\x65\x28\x67\x2c\x22\x74\x72\x22\x29\x3b\x66\x6f\x72\ +\x28\x76\x61\x72\x20\x6c\x3d\x30\x2c\x6d\x3d\x74\x68\x69\x73\x2e\ +\x6c\x65\x6e\x67\x74\x68\x2c\x6e\x3d\x6d\x2d\x31\x3b\x6c\x3c\x6d\ +\x3b\x6c\x2b\x2b\x29\x64\x2e\x63\x61\x6c\x6c\x28\x63\x3f\x62\x69\ +\x28\x74\x68\x69\x73\x5b\x6c\x5d\x2c\x67\x29\x3a\x74\x68\x69\x73\ +\x5b\x6c\x5d\x2c\x65\x2e\x63\x61\x63\x68\x65\x61\x62\x6c\x65\x7c\ +\x7c\x6d\x3e\x31\x26\x26\x6c\x3c\x6e\x3f\x66\x2e\x63\x6c\x6f\x6e\ +\x65\x28\x68\x2c\x21\x30\x2c\x21\x30\x29\x3a\x68\x29\x7d\x6b\x2e\ +\x6c\x65\x6e\x67\x74\x68\x26\x26\x66\x2e\x65\x61\x63\x68\x28\x6b\ +\x2c\x62\x70\x29\x7d\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\ +\x7d\x7d\x29\x2c\x66\x2e\x62\x75\x69\x6c\x64\x46\x72\x61\x67\x6d\ +\x65\x6e\x74\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\ +\x2c\x64\x29\x7b\x76\x61\x72\x20\x65\x2c\x67\x2c\x68\x2c\x69\x2c\ +\x6a\x3d\x61\x5b\x30\x5d\x3b\x62\x26\x26\x62\x5b\x30\x5d\x26\x26\ +\x28\x69\x3d\x62\x5b\x30\x5d\x2e\x6f\x77\x6e\x65\x72\x44\x6f\x63\ +\x75\x6d\x65\x6e\x74\x7c\x7c\x62\x5b\x30\x5d\x29\x2c\x69\x2e\x63\ +\x72\x65\x61\x74\x65\x44\x6f\x63\x75\x6d\x65\x6e\x74\x46\x72\x61\ +\x67\x6d\x65\x6e\x74\x7c\x7c\x28\x69\x3d\x63\x29\x2c\x61\x2e\x6c\ +\x65\x6e\x67\x74\x68\x3d\x3d\x3d\x31\x26\x26\x74\x79\x70\x65\x6f\ +\x66\x20\x6a\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x6a\ +\x2e\x6c\x65\x6e\x67\x74\x68\x3c\x35\x31\x32\x26\x26\x69\x3d\x3d\ +\x3d\x63\x26\x26\x6a\x2e\x63\x68\x61\x72\x41\x74\x28\x30\x29\x3d\ +\x3d\x3d\x22\x3c\x22\x26\x26\x21\x62\x62\x2e\x74\x65\x73\x74\x28\ +\x6a\x29\x26\x26\x28\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\x63\ +\x68\x65\x63\x6b\x43\x6c\x6f\x6e\x65\x7c\x7c\x21\x62\x64\x2e\x74\ +\x65\x73\x74\x28\x6a\x29\x29\x26\x26\x28\x66\x2e\x73\x75\x70\x70\ +\x6f\x72\x74\x2e\x68\x74\x6d\x6c\x35\x43\x6c\x6f\x6e\x65\x7c\x7c\ +\x21\x62\x63\x2e\x74\x65\x73\x74\x28\x6a\x29\x29\x26\x26\x28\x67\ +\x3d\x21\x30\x2c\x68\x3d\x66\x2e\x66\x72\x61\x67\x6d\x65\x6e\x74\ +\x73\x5b\x6a\x5d\x2c\x68\x26\x26\x68\x21\x3d\x3d\x31\x26\x26\x28\ +\x65\x3d\x68\x29\x29\x2c\x65\x7c\x7c\x28\x65\x3d\x69\x2e\x63\x72\ +\x65\x61\x74\x65\x44\x6f\x63\x75\x6d\x65\x6e\x74\x46\x72\x61\x67\ +\x6d\x65\x6e\x74\x28\x29\x2c\x66\x2e\x63\x6c\x65\x61\x6e\x28\x61\ +\x2c\x69\x2c\x65\x2c\x64\x29\x29\x2c\x67\x26\x26\x28\x66\x2e\x66\ +\x72\x61\x67\x6d\x65\x6e\x74\x73\x5b\x6a\x5d\x3d\x68\x3f\x65\x3a\ +\x31\x29\x3b\x72\x65\x74\x75\x72\x6e\x7b\x66\x72\x61\x67\x6d\x65\ +\x6e\x74\x3a\x65\x2c\x63\x61\x63\x68\x65\x61\x62\x6c\x65\x3a\x67\ +\x7d\x7d\x2c\x66\x2e\x66\x72\x61\x67\x6d\x65\x6e\x74\x73\x3d\x7b\ +\x7d\x2c\x66\x2e\x65\x61\x63\x68\x28\x7b\x61\x70\x70\x65\x6e\x64\ +\x54\x6f\x3a\x22\x61\x70\x70\x65\x6e\x64\x22\x2c\x70\x72\x65\x70\ +\x65\x6e\x64\x54\x6f\x3a\x22\x70\x72\x65\x70\x65\x6e\x64\x22\x2c\ +\x69\x6e\x73\x65\x72\x74\x42\x65\x66\x6f\x72\x65\x3a\x22\x62\x65\ +\x66\x6f\x72\x65\x22\x2c\x69\x6e\x73\x65\x72\x74\x41\x66\x74\x65\ +\x72\x3a\x22\x61\x66\x74\x65\x72\x22\x2c\x72\x65\x70\x6c\x61\x63\ +\x65\x41\x6c\x6c\x3a\x22\x72\x65\x70\x6c\x61\x63\x65\x57\x69\x74\ +\x68\x22\x7d\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\ +\x29\x7b\x66\x2e\x66\x6e\x5b\x61\x5d\x3d\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x63\x29\x7b\x76\x61\x72\x20\x64\x3d\x5b\x5d\x2c\x65\ +\x3d\x66\x28\x63\x29\x2c\x67\x3d\x74\x68\x69\x73\x2e\x6c\x65\x6e\ +\x67\x74\x68\x3d\x3d\x3d\x31\x26\x26\x74\x68\x69\x73\x5b\x30\x5d\ +\x2e\x70\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x3b\x69\x66\x28\x67\ +\x26\x26\x67\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\ +\x31\x26\x26\x67\x2e\x63\x68\x69\x6c\x64\x4e\x6f\x64\x65\x73\x2e\ +\x6c\x65\x6e\x67\x74\x68\x3d\x3d\x3d\x31\x26\x26\x65\x2e\x6c\x65\ +\x6e\x67\x74\x68\x3d\x3d\x3d\x31\x29\x7b\x65\x5b\x62\x5d\x28\x74\ +\x68\x69\x73\x5b\x30\x5d\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x7d\x66\x6f\x72\x28\x76\x61\x72\x20\x68\x3d\x30\x2c\ +\x69\x3d\x65\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x68\x3c\x69\x3b\x68\ +\x2b\x2b\x29\x7b\x76\x61\x72\x20\x6a\x3d\x28\x68\x3e\x30\x3f\x74\ +\x68\x69\x73\x2e\x63\x6c\x6f\x6e\x65\x28\x21\x30\x29\x3a\x74\x68\ +\x69\x73\x29\x2e\x67\x65\x74\x28\x29\x3b\x66\x28\x65\x5b\x68\x5d\ +\x29\x5b\x62\x5d\x28\x6a\x29\x2c\x64\x3d\x64\x2e\x63\x6f\x6e\x63\ +\x61\x74\x28\x6a\x29\x7d\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\ +\x73\x2e\x70\x75\x73\x68\x53\x74\x61\x63\x6b\x28\x64\x2c\x61\x2c\ +\x65\x2e\x73\x65\x6c\x65\x63\x74\x6f\x72\x29\x7d\x7d\x29\x2c\x66\ +\x2e\x65\x78\x74\x65\x6e\x64\x28\x7b\x63\x6c\x6f\x6e\x65\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x76\ +\x61\x72\x20\x64\x2c\x65\x2c\x67\x2c\x68\x3d\x66\x2e\x73\x75\x70\ +\x70\x6f\x72\x74\x2e\x68\x74\x6d\x6c\x35\x43\x6c\x6f\x6e\x65\x7c\ +\x7c\x21\x62\x63\x2e\x74\x65\x73\x74\x28\x22\x3c\x22\x2b\x61\x2e\ +\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x29\x3f\x61\x2e\x63\x6c\x6f\x6e\ +\x65\x4e\x6f\x64\x65\x28\x21\x30\x29\x3a\x62\x6f\x28\x61\x29\x3b\ +\x69\x66\x28\x28\x21\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\x6e\ +\x6f\x43\x6c\x6f\x6e\x65\x45\x76\x65\x6e\x74\x7c\x7c\x21\x66\x2e\ +\x73\x75\x70\x70\x6f\x72\x74\x2e\x6e\x6f\x43\x6c\x6f\x6e\x65\x43\ +\x68\x65\x63\x6b\x65\x64\x29\x26\x26\x28\x61\x2e\x6e\x6f\x64\x65\ +\x54\x79\x70\x65\x3d\x3d\x3d\x31\x7c\x7c\x61\x2e\x6e\x6f\x64\x65\ +\x54\x79\x70\x65\x3d\x3d\x3d\x31\x31\x29\x26\x26\x21\x66\x2e\x69\ +\x73\x58\x4d\x4c\x44\x6f\x63\x28\x61\x29\x29\x7b\x62\x6b\x28\x61\ +\x2c\x68\x29\x2c\x64\x3d\x62\x6c\x28\x61\x29\x2c\x65\x3d\x62\x6c\ +\x28\x68\x29\x3b\x66\x6f\x72\x28\x67\x3d\x30\x3b\x64\x5b\x67\x5d\ +\x3b\x2b\x2b\x67\x29\x65\x5b\x67\x5d\x26\x26\x62\x6b\x28\x64\x5b\ +\x67\x5d\x2c\x65\x5b\x67\x5d\x29\x7d\x69\x66\x28\x62\x29\x7b\x62\ +\x6a\x28\x61\x2c\x68\x29\x3b\x69\x66\x28\x63\x29\x7b\x64\x3d\x62\ +\x6c\x28\x61\x29\x2c\x65\x3d\x62\x6c\x28\x68\x29\x3b\x66\x6f\x72\ +\x28\x67\x3d\x30\x3b\x64\x5b\x67\x5d\x3b\x2b\x2b\x67\x29\x62\x6a\ +\x28\x64\x5b\x67\x5d\x2c\x65\x5b\x67\x5d\x29\x7d\x7d\x64\x3d\x65\ +\x3d\x6e\x75\x6c\x6c\x3b\x72\x65\x74\x75\x72\x6e\x20\x68\x7d\x2c\ +\x63\x6c\x65\x61\x6e\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x62\x2c\x64\x2c\x65\x29\x7b\x76\x61\x72\x20\x67\x3b\x62\x3d\ +\x62\x7c\x7c\x63\x2c\x74\x79\x70\x65\x6f\x66\x20\x62\x2e\x63\x72\ +\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x3d\x3d\x22\x75\x6e\ +\x64\x65\x66\x69\x6e\x65\x64\x22\x26\x26\x28\x62\x3d\x62\x2e\x6f\ +\x77\x6e\x65\x72\x44\x6f\x63\x75\x6d\x65\x6e\x74\x7c\x7c\x62\x5b\ +\x30\x5d\x26\x26\x62\x5b\x30\x5d\x2e\x6f\x77\x6e\x65\x72\x44\x6f\ +\x63\x75\x6d\x65\x6e\x74\x7c\x7c\x63\x29\x3b\x76\x61\x72\x20\x68\ +\x3d\x5b\x5d\x2c\x69\x3b\x66\x6f\x72\x28\x76\x61\x72\x20\x6a\x3d\ +\x30\x2c\x6b\x3b\x28\x6b\x3d\x61\x5b\x6a\x5d\x29\x21\x3d\x6e\x75\ +\x6c\x6c\x3b\x6a\x2b\x2b\x29\x7b\x74\x79\x70\x65\x6f\x66\x20\x6b\ +\x3d\x3d\x22\x6e\x75\x6d\x62\x65\x72\x22\x26\x26\x28\x6b\x2b\x3d\ +\x22\x22\x29\x3b\x69\x66\x28\x21\x6b\x29\x63\x6f\x6e\x74\x69\x6e\ +\x75\x65\x3b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\x6b\x3d\x3d\ +\x22\x73\x74\x72\x69\x6e\x67\x22\x29\x69\x66\x28\x21\x5f\x2e\x74\ +\x65\x73\x74\x28\x6b\x29\x29\x6b\x3d\x62\x2e\x63\x72\x65\x61\x74\ +\x65\x54\x65\x78\x74\x4e\x6f\x64\x65\x28\x6b\x29\x3b\x65\x6c\x73\ +\x65\x7b\x6b\x3d\x6b\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x59\x2c\ +\x22\x3c\x24\x31\x3e\x3c\x2f\x24\x32\x3e\x22\x29\x3b\x76\x61\x72\ +\x20\x6c\x3d\x28\x5a\x2e\x65\x78\x65\x63\x28\x6b\x29\x7c\x7c\x5b\ +\x22\x22\x2c\x22\x22\x5d\x29\x5b\x31\x5d\x2e\x74\x6f\x4c\x6f\x77\ +\x65\x72\x43\x61\x73\x65\x28\x29\x2c\x6d\x3d\x62\x67\x5b\x6c\x5d\ +\x7c\x7c\x62\x67\x2e\x5f\x64\x65\x66\x61\x75\x6c\x74\x2c\x6e\x3d\ +\x6d\x5b\x30\x5d\x2c\x6f\x3d\x62\x2e\x63\x72\x65\x61\x74\x65\x45\ +\x6c\x65\x6d\x65\x6e\x74\x28\x22\x64\x69\x76\x22\x29\x3b\x62\x3d\ +\x3d\x3d\x63\x3f\x62\x68\x2e\x61\x70\x70\x65\x6e\x64\x43\x68\x69\ +\x6c\x64\x28\x6f\x29\x3a\x55\x28\x62\x29\x2e\x61\x70\x70\x65\x6e\ +\x64\x43\x68\x69\x6c\x64\x28\x6f\x29\x2c\x6f\x2e\x69\x6e\x6e\x65\ +\x72\x48\x54\x4d\x4c\x3d\x6d\x5b\x31\x5d\x2b\x6b\x2b\x6d\x5b\x32\ +\x5d\x3b\x77\x68\x69\x6c\x65\x28\x6e\x2d\x2d\x29\x6f\x3d\x6f\x2e\ +\x6c\x61\x73\x74\x43\x68\x69\x6c\x64\x3b\x69\x66\x28\x21\x66\x2e\ +\x73\x75\x70\x70\x6f\x72\x74\x2e\x74\x62\x6f\x64\x79\x29\x7b\x76\ +\x61\x72\x20\x70\x3d\x24\x2e\x74\x65\x73\x74\x28\x6b\x29\x2c\x71\ +\x3d\x6c\x3d\x3d\x3d\x22\x74\x61\x62\x6c\x65\x22\x26\x26\x21\x70\ +\x3f\x6f\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x26\x26\x6f\ +\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x2e\x63\x68\x69\x6c\ +\x64\x4e\x6f\x64\x65\x73\x3a\x6d\x5b\x31\x5d\x3d\x3d\x3d\x22\x3c\ +\x74\x61\x62\x6c\x65\x3e\x22\x26\x26\x21\x70\x3f\x6f\x2e\x63\x68\ +\x69\x6c\x64\x4e\x6f\x64\x65\x73\x3a\x5b\x5d\x3b\x66\x6f\x72\x28\ +\x69\x3d\x71\x2e\x6c\x65\x6e\x67\x74\x68\x2d\x31\x3b\x69\x3e\x3d\ +\x30\x3b\x2d\x2d\x69\x29\x66\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\ +\x28\x71\x5b\x69\x5d\x2c\x22\x74\x62\x6f\x64\x79\x22\x29\x26\x26\ +\x21\x71\x5b\x69\x5d\x2e\x63\x68\x69\x6c\x64\x4e\x6f\x64\x65\x73\ +\x2e\x6c\x65\x6e\x67\x74\x68\x26\x26\x71\x5b\x69\x5d\x2e\x70\x61\ +\x72\x65\x6e\x74\x4e\x6f\x64\x65\x2e\x72\x65\x6d\x6f\x76\x65\x43\ +\x68\x69\x6c\x64\x28\x71\x5b\x69\x5d\x29\x7d\x21\x66\x2e\x73\x75\ +\x70\x70\x6f\x72\x74\x2e\x6c\x65\x61\x64\x69\x6e\x67\x57\x68\x69\ +\x74\x65\x73\x70\x61\x63\x65\x26\x26\x58\x2e\x74\x65\x73\x74\x28\ +\x6b\x29\x26\x26\x6f\x2e\x69\x6e\x73\x65\x72\x74\x42\x65\x66\x6f\ +\x72\x65\x28\x62\x2e\x63\x72\x65\x61\x74\x65\x54\x65\x78\x74\x4e\ +\x6f\x64\x65\x28\x58\x2e\x65\x78\x65\x63\x28\x6b\x29\x5b\x30\x5d\ +\x29\x2c\x6f\x2e\x66\x69\x72\x73\x74\x43\x68\x69\x6c\x64\x29\x2c\ +\x6b\x3d\x6f\x2e\x63\x68\x69\x6c\x64\x4e\x6f\x64\x65\x73\x7d\x76\ +\x61\x72\x20\x72\x3b\x69\x66\x28\x21\x66\x2e\x73\x75\x70\x70\x6f\ +\x72\x74\x2e\x61\x70\x70\x65\x6e\x64\x43\x68\x65\x63\x6b\x65\x64\ +\x29\x69\x66\x28\x6b\x5b\x30\x5d\x26\x26\x74\x79\x70\x65\x6f\x66\ +\x20\x28\x72\x3d\x6b\x2e\x6c\x65\x6e\x67\x74\x68\x29\x3d\x3d\x22\ +\x6e\x75\x6d\x62\x65\x72\x22\x29\x66\x6f\x72\x28\x69\x3d\x30\x3b\ +\x69\x3c\x72\x3b\x69\x2b\x2b\x29\x62\x6e\x28\x6b\x5b\x69\x5d\x29\ +\x3b\x65\x6c\x73\x65\x20\x62\x6e\x28\x6b\x29\x3b\x6b\x2e\x6e\x6f\ +\x64\x65\x54\x79\x70\x65\x3f\x68\x2e\x70\x75\x73\x68\x28\x6b\x29\ +\x3a\x68\x3d\x66\x2e\x6d\x65\x72\x67\x65\x28\x68\x2c\x6b\x29\x7d\ +\x69\x66\x28\x64\x29\x7b\x67\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x21\x61\x2e\x74\x79\x70\ +\x65\x7c\x7c\x62\x65\x2e\x74\x65\x73\x74\x28\x61\x2e\x74\x79\x70\ +\x65\x29\x7d\x3b\x66\x6f\x72\x28\x6a\x3d\x30\x3b\x68\x5b\x6a\x5d\ +\x3b\x6a\x2b\x2b\x29\x69\x66\x28\x65\x26\x26\x66\x2e\x6e\x6f\x64\ +\x65\x4e\x61\x6d\x65\x28\x68\x5b\x6a\x5d\x2c\x22\x73\x63\x72\x69\ +\x70\x74\x22\x29\x26\x26\x28\x21\x68\x5b\x6a\x5d\x2e\x74\x79\x70\ +\x65\x7c\x7c\x68\x5b\x6a\x5d\x2e\x74\x79\x70\x65\x2e\x74\x6f\x4c\ +\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x3d\x3d\x3d\x22\x74\x65\ +\x78\x74\x2f\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x22\x29\x29\ +\x65\x2e\x70\x75\x73\x68\x28\x68\x5b\x6a\x5d\x2e\x70\x61\x72\x65\ +\x6e\x74\x4e\x6f\x64\x65\x3f\x68\x5b\x6a\x5d\x2e\x70\x61\x72\x65\ +\x6e\x74\x4e\x6f\x64\x65\x2e\x72\x65\x6d\x6f\x76\x65\x43\x68\x69\ +\x6c\x64\x28\x68\x5b\x6a\x5d\x29\x3a\x68\x5b\x6a\x5d\x29\x3b\x65\ +\x6c\x73\x65\x7b\x69\x66\x28\x68\x5b\x6a\x5d\x2e\x6e\x6f\x64\x65\ +\x54\x79\x70\x65\x3d\x3d\x3d\x31\x29\x7b\x76\x61\x72\x20\x73\x3d\ +\x66\x2e\x67\x72\x65\x70\x28\x68\x5b\x6a\x5d\x2e\x67\x65\x74\x45\ +\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\x61\x6d\x65\ +\x28\x22\x73\x63\x72\x69\x70\x74\x22\x29\x2c\x67\x29\x3b\x68\x2e\ +\x73\x70\x6c\x69\x63\x65\x2e\x61\x70\x70\x6c\x79\x28\x68\x2c\x5b\ +\x6a\x2b\x31\x2c\x30\x5d\x2e\x63\x6f\x6e\x63\x61\x74\x28\x73\x29\ +\x29\x7d\x64\x2e\x61\x70\x70\x65\x6e\x64\x43\x68\x69\x6c\x64\x28\ +\x68\x5b\x6a\x5d\x29\x7d\x7d\x72\x65\x74\x75\x72\x6e\x20\x68\x7d\ +\x2c\x63\x6c\x65\x61\x6e\x44\x61\x74\x61\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\x2c\x63\x2c\x64\ +\x3d\x66\x2e\x63\x61\x63\x68\x65\x2c\x65\x3d\x66\x2e\x65\x76\x65\ +\x6e\x74\x2e\x73\x70\x65\x63\x69\x61\x6c\x2c\x67\x3d\x66\x2e\x73\ +\x75\x70\x70\x6f\x72\x74\x2e\x64\x65\x6c\x65\x74\x65\x45\x78\x70\ +\x61\x6e\x64\x6f\x3b\x66\x6f\x72\x28\x76\x61\x72\x20\x68\x3d\x30\ +\x2c\x69\x3b\x28\x69\x3d\x61\x5b\x68\x5d\x29\x21\x3d\x6e\x75\x6c\ +\x6c\x3b\x68\x2b\x2b\x29\x7b\x69\x66\x28\x69\x2e\x6e\x6f\x64\x65\ +\x4e\x61\x6d\x65\x26\x26\x66\x2e\x6e\x6f\x44\x61\x74\x61\x5b\x69\ +\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x2e\x74\x6f\x4c\x6f\x77\x65\ +\x72\x43\x61\x73\x65\x28\x29\x5d\x29\x63\x6f\x6e\x74\x69\x6e\x75\ +\x65\x3b\x63\x3d\x69\x5b\x66\x2e\x65\x78\x70\x61\x6e\x64\x6f\x5d\ +\x3b\x69\x66\x28\x63\x29\x7b\x62\x3d\x64\x5b\x63\x5d\x3b\x69\x66\ +\x28\x62\x26\x26\x62\x2e\x65\x76\x65\x6e\x74\x73\x29\x7b\x66\x6f\ +\x72\x28\x76\x61\x72\x20\x6a\x20\x69\x6e\x20\x62\x2e\x65\x76\x65\ +\x6e\x74\x73\x29\x65\x5b\x6a\x5d\x3f\x66\x2e\x65\x76\x65\x6e\x74\ +\x2e\x72\x65\x6d\x6f\x76\x65\x28\x69\x2c\x6a\x29\x3a\x66\x2e\x72\ +\x65\x6d\x6f\x76\x65\x45\x76\x65\x6e\x74\x28\x69\x2c\x6a\x2c\x62\ +\x2e\x68\x61\x6e\x64\x6c\x65\x29\x3b\x62\x2e\x68\x61\x6e\x64\x6c\ +\x65\x26\x26\x28\x62\x2e\x68\x61\x6e\x64\x6c\x65\x2e\x65\x6c\x65\ +\x6d\x3d\x6e\x75\x6c\x6c\x29\x7d\x67\x3f\x64\x65\x6c\x65\x74\x65\ +\x20\x69\x5b\x66\x2e\x65\x78\x70\x61\x6e\x64\x6f\x5d\x3a\x69\x2e\ +\x72\x65\x6d\x6f\x76\x65\x41\x74\x74\x72\x69\x62\x75\x74\x65\x26\ +\x26\x69\x2e\x72\x65\x6d\x6f\x76\x65\x41\x74\x74\x72\x69\x62\x75\ +\x74\x65\x28\x66\x2e\x65\x78\x70\x61\x6e\x64\x6f\x29\x2c\x64\x65\ +\x6c\x65\x74\x65\x20\x64\x5b\x63\x5d\x7d\x7d\x7d\x7d\x29\x3b\x76\ +\x61\x72\x20\x62\x71\x3d\x2f\x61\x6c\x70\x68\x61\x5c\x28\x5b\x5e\ +\x29\x5d\x2a\x5c\x29\x2f\x69\x2c\x62\x72\x3d\x2f\x6f\x70\x61\x63\ +\x69\x74\x79\x3d\x28\x5b\x5e\x29\x5d\x2a\x29\x2f\x2c\x62\x73\x3d\ +\x2f\x28\x5b\x41\x2d\x5a\x5d\x7c\x5e\x6d\x73\x29\x2f\x67\x2c\x62\ +\x74\x3d\x2f\x5e\x2d\x3f\x5c\x64\x2b\x28\x3f\x3a\x70\x78\x29\x3f\ +\x24\x2f\x69\x2c\x62\x75\x3d\x2f\x5e\x2d\x3f\x5c\x64\x2f\x2c\x62\ +\x76\x3d\x2f\x5e\x28\x5b\x5c\x2d\x2b\x5d\x29\x3d\x28\x5b\x5c\x2d\ +\x2b\x2e\x5c\x64\x65\x5d\x2b\x29\x2f\x2c\x62\x77\x3d\x7b\x70\x6f\ +\x73\x69\x74\x69\x6f\x6e\x3a\x22\x61\x62\x73\x6f\x6c\x75\x74\x65\ +\x22\x2c\x76\x69\x73\x69\x62\x69\x6c\x69\x74\x79\x3a\x22\x68\x69\ +\x64\x64\x65\x6e\x22\x2c\x64\x69\x73\x70\x6c\x61\x79\x3a\x22\x62\ +\x6c\x6f\x63\x6b\x22\x7d\x2c\x62\x78\x3d\x5b\x22\x4c\x65\x66\x74\ +\x22\x2c\x22\x52\x69\x67\x68\x74\x22\x5d\x2c\x62\x79\x3d\x5b\x22\ +\x54\x6f\x70\x22\x2c\x22\x42\x6f\x74\x74\x6f\x6d\x22\x5d\x2c\x62\ +\x7a\x2c\x62\x41\x2c\x62\x42\x3b\x66\x2e\x66\x6e\x2e\x63\x73\x73\ +\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x69\ +\x66\x28\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x2e\x6c\x65\x6e\x67\ +\x74\x68\x3d\x3d\x3d\x32\x26\x26\x63\x3d\x3d\x3d\x62\x29\x72\x65\ +\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x3b\x72\x65\x74\x75\x72\x6e\ +\x20\x66\x2e\x61\x63\x63\x65\x73\x73\x28\x74\x68\x69\x73\x2c\x61\ +\x2c\x63\x2c\x21\x30\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x63\x2c\x64\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x64\x21\x3d\ +\x3d\x62\x3f\x66\x2e\x73\x74\x79\x6c\x65\x28\x61\x2c\x63\x2c\x64\ +\x29\x3a\x66\x2e\x63\x73\x73\x28\x61\x2c\x63\x29\x7d\x29\x7d\x2c\ +\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x7b\x63\x73\x73\x48\x6f\x6f\ +\x6b\x73\x3a\x7b\x6f\x70\x61\x63\x69\x74\x79\x3a\x7b\x67\x65\x74\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x69\ +\x66\x28\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\x62\x7a\x28\x61\x2c\ +\x22\x6f\x70\x61\x63\x69\x74\x79\x22\x2c\x22\x6f\x70\x61\x63\x69\ +\x74\x79\x22\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x63\x3d\x3d\x3d\ +\x22\x22\x3f\x22\x31\x22\x3a\x63\x7d\x72\x65\x74\x75\x72\x6e\x20\ +\x61\x2e\x73\x74\x79\x6c\x65\x2e\x6f\x70\x61\x63\x69\x74\x79\x7d\ +\x7d\x7d\x2c\x63\x73\x73\x4e\x75\x6d\x62\x65\x72\x3a\x7b\x66\x69\ +\x6c\x6c\x4f\x70\x61\x63\x69\x74\x79\x3a\x21\x30\x2c\x66\x6f\x6e\ +\x74\x57\x65\x69\x67\x68\x74\x3a\x21\x30\x2c\x6c\x69\x6e\x65\x48\ +\x65\x69\x67\x68\x74\x3a\x21\x30\x2c\x6f\x70\x61\x63\x69\x74\x79\ +\x3a\x21\x30\x2c\x6f\x72\x70\x68\x61\x6e\x73\x3a\x21\x30\x2c\x77\ +\x69\x64\x6f\x77\x73\x3a\x21\x30\x2c\x7a\x49\x6e\x64\x65\x78\x3a\ +\x21\x30\x2c\x7a\x6f\x6f\x6d\x3a\x21\x30\x7d\x2c\x63\x73\x73\x50\ +\x72\x6f\x70\x73\x3a\x7b\x22\x66\x6c\x6f\x61\x74\x22\x3a\x66\x2e\ +\x73\x75\x70\x70\x6f\x72\x74\x2e\x63\x73\x73\x46\x6c\x6f\x61\x74\ +\x3f\x22\x63\x73\x73\x46\x6c\x6f\x61\x74\x22\x3a\x22\x73\x74\x79\ +\x6c\x65\x46\x6c\x6f\x61\x74\x22\x7d\x2c\x73\x74\x79\x6c\x65\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x2c\x64\x2c\x65\ +\x29\x7b\x69\x66\x28\x21\x21\x61\x26\x26\x61\x2e\x6e\x6f\x64\x65\ +\x54\x79\x70\x65\x21\x3d\x3d\x33\x26\x26\x61\x2e\x6e\x6f\x64\x65\ +\x54\x79\x70\x65\x21\x3d\x3d\x38\x26\x26\x21\x21\x61\x2e\x73\x74\ +\x79\x6c\x65\x29\x7b\x76\x61\x72\x20\x67\x2c\x68\x2c\x69\x3d\x66\ +\x2e\x63\x61\x6d\x65\x6c\x43\x61\x73\x65\x28\x63\x29\x2c\x6a\x3d\ +\x61\x2e\x73\x74\x79\x6c\x65\x2c\x6b\x3d\x66\x2e\x63\x73\x73\x48\ +\x6f\x6f\x6b\x73\x5b\x69\x5d\x3b\x63\x3d\x66\x2e\x63\x73\x73\x50\ +\x72\x6f\x70\x73\x5b\x69\x5d\x7c\x7c\x69\x3b\x69\x66\x28\x64\x3d\ +\x3d\x3d\x62\x29\x7b\x69\x66\x28\x6b\x26\x26\x22\x67\x65\x74\x22\ +\x69\x6e\x20\x6b\x26\x26\x28\x67\x3d\x6b\x2e\x67\x65\x74\x28\x61\ +\x2c\x21\x31\x2c\x65\x29\x29\x21\x3d\x3d\x62\x29\x72\x65\x74\x75\ +\x72\x6e\x20\x67\x3b\x72\x65\x74\x75\x72\x6e\x20\x6a\x5b\x63\x5d\ +\x7d\x68\x3d\x74\x79\x70\x65\x6f\x66\x20\x64\x2c\x68\x3d\x3d\x3d\ +\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x28\x67\x3d\x62\x76\x2e\ +\x65\x78\x65\x63\x28\x64\x29\x29\x26\x26\x28\x64\x3d\x2b\x28\x67\ +\x5b\x31\x5d\x2b\x31\x29\x2a\x2b\x67\x5b\x32\x5d\x2b\x70\x61\x72\ +\x73\x65\x46\x6c\x6f\x61\x74\x28\x66\x2e\x63\x73\x73\x28\x61\x2c\ +\x63\x29\x29\x2c\x68\x3d\x22\x6e\x75\x6d\x62\x65\x72\x22\x29\x3b\ +\x69\x66\x28\x64\x3d\x3d\x6e\x75\x6c\x6c\x7c\x7c\x68\x3d\x3d\x3d\ +\x22\x6e\x75\x6d\x62\x65\x72\x22\x26\x26\x69\x73\x4e\x61\x4e\x28\ +\x64\x29\x29\x72\x65\x74\x75\x72\x6e\x3b\x68\x3d\x3d\x3d\x22\x6e\ +\x75\x6d\x62\x65\x72\x22\x26\x26\x21\x66\x2e\x63\x73\x73\x4e\x75\ +\x6d\x62\x65\x72\x5b\x69\x5d\x26\x26\x28\x64\x2b\x3d\x22\x70\x78\ +\x22\x29\x3b\x69\x66\x28\x21\x6b\x7c\x7c\x21\x28\x22\x73\x65\x74\ +\x22\x69\x6e\x20\x6b\x29\x7c\x7c\x28\x64\x3d\x6b\x2e\x73\x65\x74\ +\x28\x61\x2c\x64\x29\x29\x21\x3d\x3d\x62\x29\x74\x72\x79\x7b\x6a\ +\x5b\x63\x5d\x3d\x64\x7d\x63\x61\x74\x63\x68\x28\x6c\x29\x7b\x7d\ +\x7d\x7d\x2c\x63\x73\x73\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x2c\x63\x2c\x64\x29\x7b\x76\x61\x72\x20\x65\x2c\x67\x3b\x63\ +\x3d\x66\x2e\x63\x61\x6d\x65\x6c\x43\x61\x73\x65\x28\x63\x29\x2c\ +\x67\x3d\x66\x2e\x63\x73\x73\x48\x6f\x6f\x6b\x73\x5b\x63\x5d\x2c\ +\x63\x3d\x66\x2e\x63\x73\x73\x50\x72\x6f\x70\x73\x5b\x63\x5d\x7c\ +\x7c\x63\x2c\x63\x3d\x3d\x3d\x22\x63\x73\x73\x46\x6c\x6f\x61\x74\ +\x22\x26\x26\x28\x63\x3d\x22\x66\x6c\x6f\x61\x74\x22\x29\x3b\x69\ +\x66\x28\x67\x26\x26\x22\x67\x65\x74\x22\x69\x6e\x20\x67\x26\x26\ +\x28\x65\x3d\x67\x2e\x67\x65\x74\x28\x61\x2c\x21\x30\x2c\x64\x29\ +\x29\x21\x3d\x3d\x62\x29\x72\x65\x74\x75\x72\x6e\x20\x65\x3b\x69\ +\x66\x28\x62\x7a\x29\x72\x65\x74\x75\x72\x6e\x20\x62\x7a\x28\x61\ +\x2c\x63\x29\x7d\x2c\x73\x77\x61\x70\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x3d\ +\x7b\x7d\x3b\x66\x6f\x72\x28\x76\x61\x72\x20\x65\x20\x69\x6e\x20\ +\x62\x29\x64\x5b\x65\x5d\x3d\x61\x2e\x73\x74\x79\x6c\x65\x5b\x65\ +\x5d\x2c\x61\x2e\x73\x74\x79\x6c\x65\x5b\x65\x5d\x3d\x62\x5b\x65\ +\x5d\x3b\x63\x2e\x63\x61\x6c\x6c\x28\x61\x29\x3b\x66\x6f\x72\x28\ +\x65\x20\x69\x6e\x20\x62\x29\x61\x2e\x73\x74\x79\x6c\x65\x5b\x65\ +\x5d\x3d\x64\x5b\x65\x5d\x7d\x7d\x29\x2c\x66\x2e\x63\x75\x72\x43\ +\x53\x53\x3d\x66\x2e\x63\x73\x73\x2c\x66\x2e\x65\x61\x63\x68\x28\ +\x5b\x22\x68\x65\x69\x67\x68\x74\x22\x2c\x22\x77\x69\x64\x74\x68\ +\x22\x5d\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\ +\x7b\x66\x2e\x63\x73\x73\x48\x6f\x6f\x6b\x73\x5b\x62\x5d\x3d\x7b\ +\x67\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\ +\x2c\x64\x29\x7b\x76\x61\x72\x20\x65\x3b\x69\x66\x28\x63\x29\x7b\ +\x69\x66\x28\x61\x2e\x6f\x66\x66\x73\x65\x74\x57\x69\x64\x74\x68\ +\x21\x3d\x3d\x30\x29\x72\x65\x74\x75\x72\x6e\x20\x62\x43\x28\x61\ +\x2c\x62\x2c\x64\x29\x3b\x66\x2e\x73\x77\x61\x70\x28\x61\x2c\x62\ +\x77\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x65\x3d\x62\ +\x43\x28\x61\x2c\x62\x2c\x64\x29\x7d\x29\x3b\x72\x65\x74\x75\x72\ +\x6e\x20\x65\x7d\x7d\x2c\x73\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x69\x66\x28\x21\x62\x74\x2e\x74\ +\x65\x73\x74\x28\x62\x29\x29\x72\x65\x74\x75\x72\x6e\x20\x62\x3b\ +\x62\x3d\x70\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\x28\x62\x29\x3b\ +\x69\x66\x28\x62\x3e\x3d\x30\x29\x72\x65\x74\x75\x72\x6e\x20\x62\ +\x2b\x22\x70\x78\x22\x7d\x7d\x7d\x29\x2c\x66\x2e\x73\x75\x70\x70\ +\x6f\x72\x74\x2e\x6f\x70\x61\x63\x69\x74\x79\x7c\x7c\x28\x66\x2e\ +\x63\x73\x73\x48\x6f\x6f\x6b\x73\x2e\x6f\x70\x61\x63\x69\x74\x79\ +\x3d\x7b\x67\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x62\x72\x2e\x74\x65\ +\x73\x74\x28\x28\x62\x26\x26\x61\x2e\x63\x75\x72\x72\x65\x6e\x74\ +\x53\x74\x79\x6c\x65\x3f\x61\x2e\x63\x75\x72\x72\x65\x6e\x74\x53\ +\x74\x79\x6c\x65\x2e\x66\x69\x6c\x74\x65\x72\x3a\x61\x2e\x73\x74\ +\x79\x6c\x65\x2e\x66\x69\x6c\x74\x65\x72\x29\x7c\x7c\x22\x22\x29\ +\x3f\x70\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\x28\x52\x65\x67\x45\ +\x78\x70\x2e\x24\x31\x29\x2f\x31\x30\x30\x2b\x22\x22\x3a\x62\x3f\ +\x22\x31\x22\x3a\x22\x22\x7d\x2c\x73\x65\x74\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\ +\x61\x2e\x73\x74\x79\x6c\x65\x2c\x64\x3d\x61\x2e\x63\x75\x72\x72\ +\x65\x6e\x74\x53\x74\x79\x6c\x65\x2c\x65\x3d\x66\x2e\x69\x73\x4e\ +\x75\x6d\x65\x72\x69\x63\x28\x62\x29\x3f\x22\x61\x6c\x70\x68\x61\ +\x28\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x2b\x62\x2a\x31\x30\x30\ +\x2b\x22\x29\x22\x3a\x22\x22\x2c\x67\x3d\x64\x26\x26\x64\x2e\x66\ +\x69\x6c\x74\x65\x72\x7c\x7c\x63\x2e\x66\x69\x6c\x74\x65\x72\x7c\ +\x7c\x22\x22\x3b\x63\x2e\x7a\x6f\x6f\x6d\x3d\x31\x3b\x69\x66\x28\ +\x62\x3e\x3d\x31\x26\x26\x66\x2e\x74\x72\x69\x6d\x28\x67\x2e\x72\ +\x65\x70\x6c\x61\x63\x65\x28\x62\x71\x2c\x22\x22\x29\x29\x3d\x3d\ +\x3d\x22\x22\x29\x7b\x63\x2e\x72\x65\x6d\x6f\x76\x65\x41\x74\x74\ +\x72\x69\x62\x75\x74\x65\x28\x22\x66\x69\x6c\x74\x65\x72\x22\x29\ +\x3b\x69\x66\x28\x64\x26\x26\x21\x64\x2e\x66\x69\x6c\x74\x65\x72\ +\x29\x72\x65\x74\x75\x72\x6e\x7d\x63\x2e\x66\x69\x6c\x74\x65\x72\ +\x3d\x62\x71\x2e\x74\x65\x73\x74\x28\x67\x29\x3f\x67\x2e\x72\x65\ +\x70\x6c\x61\x63\x65\x28\x62\x71\x2c\x65\x29\x3a\x67\x2b\x22\x20\ +\x22\x2b\x65\x7d\x7d\x29\x2c\x66\x28\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x29\x7b\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\x72\x65\ +\x6c\x69\x61\x62\x6c\x65\x4d\x61\x72\x67\x69\x6e\x52\x69\x67\x68\ +\x74\x7c\x7c\x28\x66\x2e\x63\x73\x73\x48\x6f\x6f\x6b\x73\x2e\x6d\ +\x61\x72\x67\x69\x6e\x52\x69\x67\x68\x74\x3d\x7b\x67\x65\x74\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\ +\x72\x20\x63\x3b\x66\x2e\x73\x77\x61\x70\x28\x61\x2c\x7b\x64\x69\ +\x73\x70\x6c\x61\x79\x3a\x22\x69\x6e\x6c\x69\x6e\x65\x2d\x62\x6c\ +\x6f\x63\x6b\x22\x7d\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\ +\x7b\x62\x3f\x63\x3d\x62\x7a\x28\x61\x2c\x22\x6d\x61\x72\x67\x69\ +\x6e\x2d\x72\x69\x67\x68\x74\x22\x2c\x22\x6d\x61\x72\x67\x69\x6e\ +\x52\x69\x67\x68\x74\x22\x29\x3a\x63\x3d\x61\x2e\x73\x74\x79\x6c\ +\x65\x2e\x6d\x61\x72\x67\x69\x6e\x52\x69\x67\x68\x74\x7d\x29\x3b\ +\x72\x65\x74\x75\x72\x6e\x20\x63\x7d\x7d\x29\x7d\x29\x2c\x63\x2e\ +\x64\x65\x66\x61\x75\x6c\x74\x56\x69\x65\x77\x26\x26\x63\x2e\x64\ +\x65\x66\x61\x75\x6c\x74\x56\x69\x65\x77\x2e\x67\x65\x74\x43\x6f\ +\x6d\x70\x75\x74\x65\x64\x53\x74\x79\x6c\x65\x26\x26\x28\x62\x41\ +\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\ +\x61\x72\x20\x63\x2c\x64\x2c\x65\x3b\x62\x3d\x62\x2e\x72\x65\x70\ +\x6c\x61\x63\x65\x28\x62\x73\x2c\x22\x2d\x24\x31\x22\x29\x2e\x74\ +\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x2c\x28\x64\x3d\ +\x61\x2e\x6f\x77\x6e\x65\x72\x44\x6f\x63\x75\x6d\x65\x6e\x74\x2e\ +\x64\x65\x66\x61\x75\x6c\x74\x56\x69\x65\x77\x29\x26\x26\x28\x65\ +\x3d\x64\x2e\x67\x65\x74\x43\x6f\x6d\x70\x75\x74\x65\x64\x53\x74\ +\x79\x6c\x65\x28\x61\x2c\x6e\x75\x6c\x6c\x29\x29\x26\x26\x28\x63\ +\x3d\x65\x2e\x67\x65\x74\x50\x72\x6f\x70\x65\x72\x74\x79\x56\x61\ +\x6c\x75\x65\x28\x62\x29\x2c\x63\x3d\x3d\x3d\x22\x22\x26\x26\x21\ +\x66\x2e\x63\x6f\x6e\x74\x61\x69\x6e\x73\x28\x61\x2e\x6f\x77\x6e\ +\x65\x72\x44\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x64\x6f\x63\x75\x6d\ +\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x2c\x61\x29\x26\x26\x28\ +\x63\x3d\x66\x2e\x73\x74\x79\x6c\x65\x28\x61\x2c\x62\x29\x29\x29\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x63\x7d\x29\x2c\x63\x2e\x64\x6f\ +\x63\x75\x6d\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x2e\x63\x75\ +\x72\x72\x65\x6e\x74\x53\x74\x79\x6c\x65\x26\x26\x28\x62\x42\x3d\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\ +\x72\x20\x63\x2c\x64\x2c\x65\x2c\x66\x3d\x61\x2e\x63\x75\x72\x72\ +\x65\x6e\x74\x53\x74\x79\x6c\x65\x26\x26\x61\x2e\x63\x75\x72\x72\ +\x65\x6e\x74\x53\x74\x79\x6c\x65\x5b\x62\x5d\x2c\x67\x3d\x61\x2e\ +\x73\x74\x79\x6c\x65\x3b\x66\x3d\x3d\x3d\x6e\x75\x6c\x6c\x26\x26\ +\x67\x26\x26\x28\x65\x3d\x67\x5b\x62\x5d\x29\x26\x26\x28\x66\x3d\ +\x65\x29\x2c\x21\x62\x74\x2e\x74\x65\x73\x74\x28\x66\x29\x26\x26\ +\x62\x75\x2e\x74\x65\x73\x74\x28\x66\x29\x26\x26\x28\x63\x3d\x67\ +\x2e\x6c\x65\x66\x74\x2c\x64\x3d\x61\x2e\x72\x75\x6e\x74\x69\x6d\ +\x65\x53\x74\x79\x6c\x65\x26\x26\x61\x2e\x72\x75\x6e\x74\x69\x6d\ +\x65\x53\x74\x79\x6c\x65\x2e\x6c\x65\x66\x74\x2c\x64\x26\x26\x28\ +\x61\x2e\x72\x75\x6e\x74\x69\x6d\x65\x53\x74\x79\x6c\x65\x2e\x6c\ +\x65\x66\x74\x3d\x61\x2e\x63\x75\x72\x72\x65\x6e\x74\x53\x74\x79\ +\x6c\x65\x2e\x6c\x65\x66\x74\x29\x2c\x67\x2e\x6c\x65\x66\x74\x3d\ +\x62\x3d\x3d\x3d\x22\x66\x6f\x6e\x74\x53\x69\x7a\x65\x22\x3f\x22\ +\x31\x65\x6d\x22\x3a\x66\x7c\x7c\x30\x2c\x66\x3d\x67\x2e\x70\x69\ +\x78\x65\x6c\x4c\x65\x66\x74\x2b\x22\x70\x78\x22\x2c\x67\x2e\x6c\ +\x65\x66\x74\x3d\x63\x2c\x64\x26\x26\x28\x61\x2e\x72\x75\x6e\x74\ +\x69\x6d\x65\x53\x74\x79\x6c\x65\x2e\x6c\x65\x66\x74\x3d\x64\x29\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x66\x3d\x3d\x3d\x22\x22\x3f\ +\x22\x61\x75\x74\x6f\x22\x3a\x66\x7d\x29\x2c\x62\x7a\x3d\x62\x41\ +\x7c\x7c\x62\x42\x2c\x66\x2e\x65\x78\x70\x72\x26\x26\x66\x2e\x65\ +\x78\x70\x72\x2e\x66\x69\x6c\x74\x65\x72\x73\x26\x26\x28\x66\x2e\ +\x65\x78\x70\x72\x2e\x66\x69\x6c\x74\x65\x72\x73\x2e\x68\x69\x64\ +\x64\x65\x6e\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\ +\x76\x61\x72\x20\x62\x3d\x61\x2e\x6f\x66\x66\x73\x65\x74\x57\x69\ +\x64\x74\x68\x2c\x63\x3d\x61\x2e\x6f\x66\x66\x73\x65\x74\x48\x65\ +\x69\x67\x68\x74\x3b\x72\x65\x74\x75\x72\x6e\x20\x62\x3d\x3d\x3d\ +\x30\x26\x26\x63\x3d\x3d\x3d\x30\x7c\x7c\x21\x66\x2e\x73\x75\x70\ +\x70\x6f\x72\x74\x2e\x72\x65\x6c\x69\x61\x62\x6c\x65\x48\x69\x64\ +\x64\x65\x6e\x4f\x66\x66\x73\x65\x74\x73\x26\x26\x28\x61\x2e\x73\ +\x74\x79\x6c\x65\x26\x26\x61\x2e\x73\x74\x79\x6c\x65\x2e\x64\x69\ +\x73\x70\x6c\x61\x79\x7c\x7c\x66\x2e\x63\x73\x73\x28\x61\x2c\x22\ +\x64\x69\x73\x70\x6c\x61\x79\x22\x29\x29\x3d\x3d\x3d\x22\x6e\x6f\ +\x6e\x65\x22\x7d\x2c\x66\x2e\x65\x78\x70\x72\x2e\x66\x69\x6c\x74\ +\x65\x72\x73\x2e\x76\x69\x73\x69\x62\x6c\x65\x3d\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x21\x66\ +\x2e\x65\x78\x70\x72\x2e\x66\x69\x6c\x74\x65\x72\x73\x2e\x68\x69\ +\x64\x64\x65\x6e\x28\x61\x29\x7d\x29\x3b\x76\x61\x72\x20\x62\x44\ +\x3d\x2f\x25\x32\x30\x2f\x67\x2c\x62\x45\x3d\x2f\x5c\x5b\x5c\x5d\ +\x24\x2f\x2c\x62\x46\x3d\x2f\x5c\x72\x3f\x5c\x6e\x2f\x67\x2c\x62\ +\x47\x3d\x2f\x23\x2e\x2a\x24\x2f\x2c\x62\x48\x3d\x2f\x5e\x28\x2e\ +\x2a\x3f\x29\x3a\x5b\x20\x5c\x74\x5d\x2a\x28\x5b\x5e\x5c\x72\x5c\ +\x6e\x5d\x2a\x29\x5c\x72\x3f\x24\x2f\x6d\x67\x2c\x62\x49\x3d\x2f\ +\x5e\x28\x3f\x3a\x63\x6f\x6c\x6f\x72\x7c\x64\x61\x74\x65\x7c\x64\ +\x61\x74\x65\x74\x69\x6d\x65\x7c\x64\x61\x74\x65\x74\x69\x6d\x65\ +\x2d\x6c\x6f\x63\x61\x6c\x7c\x65\x6d\x61\x69\x6c\x7c\x68\x69\x64\ +\x64\x65\x6e\x7c\x6d\x6f\x6e\x74\x68\x7c\x6e\x75\x6d\x62\x65\x72\ +\x7c\x70\x61\x73\x73\x77\x6f\x72\x64\x7c\x72\x61\x6e\x67\x65\x7c\ +\x73\x65\x61\x72\x63\x68\x7c\x74\x65\x6c\x7c\x74\x65\x78\x74\x7c\ +\x74\x69\x6d\x65\x7c\x75\x72\x6c\x7c\x77\x65\x65\x6b\x29\x24\x2f\ +\x69\x2c\x62\x4a\x3d\x2f\x5e\x28\x3f\x3a\x61\x62\x6f\x75\x74\x7c\ +\x61\x70\x70\x7c\x61\x70\x70\x5c\x2d\x73\x74\x6f\x72\x61\x67\x65\ +\x7c\x2e\x2b\x5c\x2d\x65\x78\x74\x65\x6e\x73\x69\x6f\x6e\x7c\x66\ +\x69\x6c\x65\x7c\x72\x65\x73\x7c\x77\x69\x64\x67\x65\x74\x29\x3a\ +\x24\x2f\x2c\x62\x4b\x3d\x2f\x5e\x28\x3f\x3a\x47\x45\x54\x7c\x48\ +\x45\x41\x44\x29\x24\x2f\x2c\x62\x4c\x3d\x2f\x5e\x5c\x2f\x5c\x2f\ +\x2f\x2c\x62\x4d\x3d\x2f\x5c\x3f\x2f\x2c\x62\x4e\x3d\x2f\x3c\x73\ +\x63\x72\x69\x70\x74\x5c\x62\x5b\x5e\x3c\x5d\x2a\x28\x3f\x3a\x28\ +\x3f\x21\x3c\x5c\x2f\x73\x63\x72\x69\x70\x74\x3e\x29\x3c\x5b\x5e\ +\x3c\x5d\x2a\x29\x2a\x3c\x5c\x2f\x73\x63\x72\x69\x70\x74\x3e\x2f\ +\x67\x69\x2c\x62\x4f\x3d\x2f\x5e\x28\x3f\x3a\x73\x65\x6c\x65\x63\ +\x74\x7c\x74\x65\x78\x74\x61\x72\x65\x61\x29\x2f\x69\x2c\x62\x50\ +\x3d\x2f\x5c\x73\x2b\x2f\x2c\x62\x51\x3d\x2f\x28\x5b\x3f\x26\x5d\ +\x29\x5f\x3d\x5b\x5e\x26\x5d\x2a\x2f\x2c\x62\x52\x3d\x2f\x5e\x28\ +\x5b\x5c\x77\x5c\x2b\x5c\x2e\x5c\x2d\x5d\x2b\x3a\x29\x28\x3f\x3a\ +\x5c\x2f\x5c\x2f\x28\x5b\x5e\x5c\x2f\x3f\x23\x3a\x5d\x2a\x29\x28\ +\x3f\x3a\x3a\x28\x5c\x64\x2b\x29\x29\x3f\x29\x3f\x2f\x2c\x62\x53\ +\x3d\x66\x2e\x66\x6e\x2e\x6c\x6f\x61\x64\x2c\x62\x54\x3d\x7b\x7d\ +\x2c\x62\x55\x3d\x7b\x7d\x2c\x62\x56\x2c\x62\x57\x2c\x62\x58\x3d\ +\x5b\x22\x2a\x2f\x22\x5d\x2b\x5b\x22\x2a\x22\x5d\x3b\x74\x72\x79\ +\x7b\x62\x56\x3d\x65\x2e\x68\x72\x65\x66\x7d\x63\x61\x74\x63\x68\ +\x28\x62\x59\x29\x7b\x62\x56\x3d\x63\x2e\x63\x72\x65\x61\x74\x65\ +\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x61\x22\x29\x2c\x62\x56\x2e\ +\x68\x72\x65\x66\x3d\x22\x22\x2c\x62\x56\x3d\x62\x56\x2e\x68\x72\ +\x65\x66\x7d\x62\x57\x3d\x62\x52\x2e\x65\x78\x65\x63\x28\x62\x56\ +\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x29\x7c\ +\x7c\x5b\x5d\x2c\x66\x2e\x66\x6e\x2e\x65\x78\x74\x65\x6e\x64\x28\ +\x7b\x6c\x6f\x61\x64\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x63\x2c\x64\x29\x7b\x69\x66\x28\x74\x79\x70\x65\x6f\x66\x20\ +\x61\x21\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x62\x53\x29\ +\x72\x65\x74\x75\x72\x6e\x20\x62\x53\x2e\x61\x70\x70\x6c\x79\x28\ +\x74\x68\x69\x73\x2c\x61\x72\x67\x75\x6d\x65\x6e\x74\x73\x29\x3b\ +\x69\x66\x28\x21\x74\x68\x69\x73\x2e\x6c\x65\x6e\x67\x74\x68\x29\ +\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x3b\x76\x61\x72\x20\ +\x65\x3d\x61\x2e\x69\x6e\x64\x65\x78\x4f\x66\x28\x22\x20\x22\x29\ +\x3b\x69\x66\x28\x65\x3e\x3d\x30\x29\x7b\x76\x61\x72\x20\x67\x3d\ +\x61\x2e\x73\x6c\x69\x63\x65\x28\x65\x2c\x61\x2e\x6c\x65\x6e\x67\ +\x74\x68\x29\x3b\x61\x3d\x61\x2e\x73\x6c\x69\x63\x65\x28\x30\x2c\ +\x65\x29\x7d\x76\x61\x72\x20\x68\x3d\x22\x47\x45\x54\x22\x3b\x63\ +\x26\x26\x28\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x63\x29\x3f\x28\x64\x3d\x63\x2c\x63\x3d\x62\x29\x3a\x74\x79\x70\ +\x65\x6f\x66\x20\x63\x3d\x3d\x22\x6f\x62\x6a\x65\x63\x74\x22\x26\ +\x26\x28\x63\x3d\x66\x2e\x70\x61\x72\x61\x6d\x28\x63\x2c\x66\x2e\ +\x61\x6a\x61\x78\x53\x65\x74\x74\x69\x6e\x67\x73\x2e\x74\x72\x61\ +\x64\x69\x74\x69\x6f\x6e\x61\x6c\x29\x2c\x68\x3d\x22\x50\x4f\x53\ +\x54\x22\x29\x29\x3b\x76\x61\x72\x20\x69\x3d\x74\x68\x69\x73\x3b\ +\x66\x2e\x61\x6a\x61\x78\x28\x7b\x75\x72\x6c\x3a\x61\x2c\x74\x79\ +\x70\x65\x3a\x68\x2c\x64\x61\x74\x61\x54\x79\x70\x65\x3a\x22\x68\ +\x74\x6d\x6c\x22\x2c\x64\x61\x74\x61\x3a\x63\x2c\x63\x6f\x6d\x70\ +\x6c\x65\x74\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\ +\x62\x2c\x63\x29\x7b\x63\x3d\x61\x2e\x72\x65\x73\x70\x6f\x6e\x73\ +\x65\x54\x65\x78\x74\x2c\x61\x2e\x69\x73\x52\x65\x73\x6f\x6c\x76\ +\x65\x64\x28\x29\x26\x26\x28\x61\x2e\x64\x6f\x6e\x65\x28\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x63\x3d\x61\x7d\x29\x2c\ +\x69\x2e\x68\x74\x6d\x6c\x28\x67\x3f\x66\x28\x22\x3c\x64\x69\x76\ +\x3e\x22\x29\x2e\x61\x70\x70\x65\x6e\x64\x28\x63\x2e\x72\x65\x70\ +\x6c\x61\x63\x65\x28\x62\x4e\x2c\x22\x22\x29\x29\x2e\x66\x69\x6e\ +\x64\x28\x67\x29\x3a\x63\x29\x29\x2c\x64\x26\x26\x69\x2e\x65\x61\ +\x63\x68\x28\x64\x2c\x5b\x63\x2c\x62\x2c\x61\x5d\x29\x7d\x7d\x29\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x73\x65\ +\x72\x69\x61\x6c\x69\x7a\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x70\x61\x72\x61\ +\x6d\x28\x74\x68\x69\x73\x2e\x73\x65\x72\x69\x61\x6c\x69\x7a\x65\ +\x41\x72\x72\x61\x79\x28\x29\x29\x7d\x2c\x73\x65\x72\x69\x61\x6c\ +\x69\x7a\x65\x41\x72\x72\x61\x79\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\ +\x6d\x61\x70\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x72\ +\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x6c\x65\x6d\x65\ +\x6e\x74\x73\x3f\x66\x2e\x6d\x61\x6b\x65\x41\x72\x72\x61\x79\x28\ +\x74\x68\x69\x73\x2e\x65\x6c\x65\x6d\x65\x6e\x74\x73\x29\x3a\x74\ +\x68\x69\x73\x7d\x29\x2e\x66\x69\x6c\x74\x65\x72\x28\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x2e\x6e\x61\x6d\x65\x26\x26\x21\x74\x68\x69\x73\x2e\ +\x64\x69\x73\x61\x62\x6c\x65\x64\x26\x26\x28\x74\x68\x69\x73\x2e\ +\x63\x68\x65\x63\x6b\x65\x64\x7c\x7c\x62\x4f\x2e\x74\x65\x73\x74\ +\x28\x74\x68\x69\x73\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x29\x7c\ +\x7c\x62\x49\x2e\x74\x65\x73\x74\x28\x74\x68\x69\x73\x2e\x74\x79\ +\x70\x65\x29\x29\x7d\x29\x2e\x6d\x61\x70\x28\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\x66\ +\x28\x74\x68\x69\x73\x29\x2e\x76\x61\x6c\x28\x29\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x63\x3d\x3d\x6e\x75\x6c\x6c\x3f\x6e\x75\x6c\x6c\ +\x3a\x66\x2e\x69\x73\x41\x72\x72\x61\x79\x28\x63\x29\x3f\x66\x2e\ +\x6d\x61\x70\x28\x63\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x63\x29\x7b\x72\x65\x74\x75\x72\x6e\x7b\x6e\x61\x6d\x65\x3a\ +\x62\x2e\x6e\x61\x6d\x65\x2c\x76\x61\x6c\x75\x65\x3a\x61\x2e\x72\ +\x65\x70\x6c\x61\x63\x65\x28\x62\x46\x2c\x22\x5c\x72\x5c\x6e\x22\ +\x29\x7d\x7d\x29\x3a\x7b\x6e\x61\x6d\x65\x3a\x62\x2e\x6e\x61\x6d\ +\x65\x2c\x76\x61\x6c\x75\x65\x3a\x63\x2e\x72\x65\x70\x6c\x61\x63\ +\x65\x28\x62\x46\x2c\x22\x5c\x72\x5c\x6e\x22\x29\x7d\x7d\x29\x2e\ +\x67\x65\x74\x28\x29\x7d\x7d\x29\x2c\x66\x2e\x65\x61\x63\x68\x28\ +\x22\x61\x6a\x61\x78\x53\x74\x61\x72\x74\x20\x61\x6a\x61\x78\x53\ +\x74\x6f\x70\x20\x61\x6a\x61\x78\x43\x6f\x6d\x70\x6c\x65\x74\x65\ +\x20\x61\x6a\x61\x78\x45\x72\x72\x6f\x72\x20\x61\x6a\x61\x78\x53\ +\x75\x63\x63\x65\x73\x73\x20\x61\x6a\x61\x78\x53\x65\x6e\x64\x22\ +\x2e\x73\x70\x6c\x69\x74\x28\x22\x20\x22\x29\x2c\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x66\x2e\x66\x6e\x5b\x62\ +\x5d\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\ +\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x6f\x6e\x28\x62\x2c\x61\ +\x29\x7d\x7d\x29\x2c\x66\x2e\x65\x61\x63\x68\x28\x5b\x22\x67\x65\ +\x74\x22\x2c\x22\x70\x6f\x73\x74\x22\x5d\x2c\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x66\x5b\x63\x5d\x3d\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x64\x2c\x65\x2c\x67\x29\x7b\ +\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x64\x29\x26\ +\x26\x28\x67\x3d\x67\x7c\x7c\x65\x2c\x65\x3d\x64\x2c\x64\x3d\x62\ +\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x61\x6a\x61\x78\x28\ +\x7b\x74\x79\x70\x65\x3a\x63\x2c\x75\x72\x6c\x3a\x61\x2c\x64\x61\ +\x74\x61\x3a\x64\x2c\x73\x75\x63\x63\x65\x73\x73\x3a\x65\x2c\x64\ +\x61\x74\x61\x54\x79\x70\x65\x3a\x67\x7d\x29\x7d\x7d\x29\x2c\x66\ +\x2e\x65\x78\x74\x65\x6e\x64\x28\x7b\x67\x65\x74\x53\x63\x72\x69\ +\x70\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x29\ +\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x67\x65\x74\x28\x61\x2c\ +\x62\x2c\x63\x2c\x22\x73\x63\x72\x69\x70\x74\x22\x29\x7d\x2c\x67\ +\x65\x74\x4a\x53\x4f\x4e\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x2c\x62\x2c\x63\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\ +\x67\x65\x74\x28\x61\x2c\x62\x2c\x63\x2c\x22\x6a\x73\x6f\x6e\x22\ +\x29\x7d\x2c\x61\x6a\x61\x78\x53\x65\x74\x75\x70\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x62\x3f\x62\x5f\x28\ +\x61\x2c\x66\x2e\x61\x6a\x61\x78\x53\x65\x74\x74\x69\x6e\x67\x73\ +\x29\x3a\x28\x62\x3d\x61\x2c\x61\x3d\x66\x2e\x61\x6a\x61\x78\x53\ +\x65\x74\x74\x69\x6e\x67\x73\x29\x2c\x62\x5f\x28\x61\x2c\x62\x29\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x61\x7d\x2c\x61\x6a\x61\x78\x53\ +\x65\x74\x74\x69\x6e\x67\x73\x3a\x7b\x75\x72\x6c\x3a\x62\x56\x2c\ +\x69\x73\x4c\x6f\x63\x61\x6c\x3a\x62\x4a\x2e\x74\x65\x73\x74\x28\ +\x62\x57\x5b\x31\x5d\x29\x2c\x67\x6c\x6f\x62\x61\x6c\x3a\x21\x30\ +\x2c\x74\x79\x70\x65\x3a\x22\x47\x45\x54\x22\x2c\x63\x6f\x6e\x74\ +\x65\x6e\x74\x54\x79\x70\x65\x3a\x22\x61\x70\x70\x6c\x69\x63\x61\ +\x74\x69\x6f\x6e\x2f\x78\x2d\x77\x77\x77\x2d\x66\x6f\x72\x6d\x2d\ +\x75\x72\x6c\x65\x6e\x63\x6f\x64\x65\x64\x22\x2c\x70\x72\x6f\x63\ +\x65\x73\x73\x44\x61\x74\x61\x3a\x21\x30\x2c\x61\x73\x79\x6e\x63\ +\x3a\x21\x30\x2c\x61\x63\x63\x65\x70\x74\x73\x3a\x7b\x78\x6d\x6c\ +\x3a\x22\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\x78\x6d\ +\x6c\x2c\x20\x74\x65\x78\x74\x2f\x78\x6d\x6c\x22\x2c\x68\x74\x6d\ +\x6c\x3a\x22\x74\x65\x78\x74\x2f\x68\x74\x6d\x6c\x22\x2c\x74\x65\ +\x78\x74\x3a\x22\x74\x65\x78\x74\x2f\x70\x6c\x61\x69\x6e\x22\x2c\ +\x6a\x73\x6f\x6e\x3a\x22\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\ +\x6e\x2f\x6a\x73\x6f\x6e\x2c\x20\x74\x65\x78\x74\x2f\x6a\x61\x76\ +\x61\x73\x63\x72\x69\x70\x74\x22\x2c\x22\x2a\x22\x3a\x62\x58\x7d\ +\x2c\x63\x6f\x6e\x74\x65\x6e\x74\x73\x3a\x7b\x78\x6d\x6c\x3a\x2f\ +\x78\x6d\x6c\x2f\x2c\x68\x74\x6d\x6c\x3a\x2f\x68\x74\x6d\x6c\x2f\ +\x2c\x6a\x73\x6f\x6e\x3a\x2f\x6a\x73\x6f\x6e\x2f\x7d\x2c\x72\x65\ +\x73\x70\x6f\x6e\x73\x65\x46\x69\x65\x6c\x64\x73\x3a\x7b\x78\x6d\ +\x6c\x3a\x22\x72\x65\x73\x70\x6f\x6e\x73\x65\x58\x4d\x4c\x22\x2c\ +\x74\x65\x78\x74\x3a\x22\x72\x65\x73\x70\x6f\x6e\x73\x65\x54\x65\ +\x78\x74\x22\x7d\x2c\x63\x6f\x6e\x76\x65\x72\x74\x65\x72\x73\x3a\ +\x7b\x22\x2a\x20\x74\x65\x78\x74\x22\x3a\x61\x2e\x53\x74\x72\x69\ +\x6e\x67\x2c\x22\x74\x65\x78\x74\x20\x68\x74\x6d\x6c\x22\x3a\x21\ +\x30\x2c\x22\x74\x65\x78\x74\x20\x6a\x73\x6f\x6e\x22\x3a\x66\x2e\ +\x70\x61\x72\x73\x65\x4a\x53\x4f\x4e\x2c\x22\x74\x65\x78\x74\x20\ +\x78\x6d\x6c\x22\x3a\x66\x2e\x70\x61\x72\x73\x65\x58\x4d\x4c\x7d\ +\x2c\x66\x6c\x61\x74\x4f\x70\x74\x69\x6f\x6e\x73\x3a\x7b\x63\x6f\ +\x6e\x74\x65\x78\x74\x3a\x21\x30\x2c\x75\x72\x6c\x3a\x21\x30\x7d\ +\x7d\x2c\x61\x6a\x61\x78\x50\x72\x65\x66\x69\x6c\x74\x65\x72\x3a\ +\x62\x5a\x28\x62\x54\x29\x2c\x61\x6a\x61\x78\x54\x72\x61\x6e\x73\ +\x70\x6f\x72\x74\x3a\x62\x5a\x28\x62\x55\x29\x2c\x61\x6a\x61\x78\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x20\x77\x28\x61\x2c\x63\x2c\x6c\x2c\ +\x6d\x29\x7b\x69\x66\x28\x73\x21\x3d\x3d\x32\x29\x7b\x73\x3d\x32\ +\x2c\x71\x26\x26\x63\x6c\x65\x61\x72\x54\x69\x6d\x65\x6f\x75\x74\ +\x28\x71\x29\x2c\x70\x3d\x62\x2c\x6e\x3d\x6d\x7c\x7c\x22\x22\x2c\ +\x76\x2e\x72\x65\x61\x64\x79\x53\x74\x61\x74\x65\x3d\x61\x3e\x30\ +\x3f\x34\x3a\x30\x3b\x76\x61\x72\x20\x6f\x2c\x72\x2c\x75\x2c\x77\ +\x3d\x63\x2c\x78\x3d\x6c\x3f\x63\x62\x28\x64\x2c\x76\x2c\x6c\x29\ +\x3a\x62\x2c\x79\x2c\x7a\x3b\x69\x66\x28\x61\x3e\x3d\x32\x30\x30\ +\x26\x26\x61\x3c\x33\x30\x30\x7c\x7c\x61\x3d\x3d\x3d\x33\x30\x34\ +\x29\x7b\x69\x66\x28\x64\x2e\x69\x66\x4d\x6f\x64\x69\x66\x69\x65\ +\x64\x29\x7b\x69\x66\x28\x79\x3d\x76\x2e\x67\x65\x74\x52\x65\x73\ +\x70\x6f\x6e\x73\x65\x48\x65\x61\x64\x65\x72\x28\x22\x4c\x61\x73\ +\x74\x2d\x4d\x6f\x64\x69\x66\x69\x65\x64\x22\x29\x29\x66\x2e\x6c\ +\x61\x73\x74\x4d\x6f\x64\x69\x66\x69\x65\x64\x5b\x6b\x5d\x3d\x79\ +\x3b\x69\x66\x28\x7a\x3d\x76\x2e\x67\x65\x74\x52\x65\x73\x70\x6f\ +\x6e\x73\x65\x48\x65\x61\x64\x65\x72\x28\x22\x45\x74\x61\x67\x22\ +\x29\x29\x66\x2e\x65\x74\x61\x67\x5b\x6b\x5d\x3d\x7a\x7d\x69\x66\ +\x28\x61\x3d\x3d\x3d\x33\x30\x34\x29\x77\x3d\x22\x6e\x6f\x74\x6d\ +\x6f\x64\x69\x66\x69\x65\x64\x22\x2c\x6f\x3d\x21\x30\x3b\x65\x6c\ +\x73\x65\x20\x74\x72\x79\x7b\x72\x3d\x63\x63\x28\x64\x2c\x78\x29\ +\x2c\x77\x3d\x22\x73\x75\x63\x63\x65\x73\x73\x22\x2c\x6f\x3d\x21\ +\x30\x7d\x63\x61\x74\x63\x68\x28\x41\x29\x7b\x77\x3d\x22\x70\x61\ +\x72\x73\x65\x72\x65\x72\x72\x6f\x72\x22\x2c\x75\x3d\x41\x7d\x7d\ +\x65\x6c\x73\x65\x7b\x75\x3d\x77\x3b\x69\x66\x28\x21\x77\x7c\x7c\ +\x61\x29\x77\x3d\x22\x65\x72\x72\x6f\x72\x22\x2c\x61\x3c\x30\x26\ +\x26\x28\x61\x3d\x30\x29\x7d\x76\x2e\x73\x74\x61\x74\x75\x73\x3d\ +\x61\x2c\x76\x2e\x73\x74\x61\x74\x75\x73\x54\x65\x78\x74\x3d\x22\ +\x22\x2b\x28\x63\x7c\x7c\x77\x29\x2c\x6f\x3f\x68\x2e\x72\x65\x73\ +\x6f\x6c\x76\x65\x57\x69\x74\x68\x28\x65\x2c\x5b\x72\x2c\x77\x2c\ +\x76\x5d\x29\x3a\x68\x2e\x72\x65\x6a\x65\x63\x74\x57\x69\x74\x68\ +\x28\x65\x2c\x5b\x76\x2c\x77\x2c\x75\x5d\x29\x2c\x76\x2e\x73\x74\ +\x61\x74\x75\x73\x43\x6f\x64\x65\x28\x6a\x29\x2c\x6a\x3d\x62\x2c\ +\x74\x26\x26\x67\x2e\x74\x72\x69\x67\x67\x65\x72\x28\x22\x61\x6a\ +\x61\x78\x22\x2b\x28\x6f\x3f\x22\x53\x75\x63\x63\x65\x73\x73\x22\ +\x3a\x22\x45\x72\x72\x6f\x72\x22\x29\x2c\x5b\x76\x2c\x64\x2c\x6f\ +\x3f\x72\x3a\x75\x5d\x29\x2c\x69\x2e\x66\x69\x72\x65\x57\x69\x74\ +\x68\x28\x65\x2c\x5b\x76\x2c\x77\x5d\x29\x2c\x74\x26\x26\x28\x67\ +\x2e\x74\x72\x69\x67\x67\x65\x72\x28\x22\x61\x6a\x61\x78\x43\x6f\ +\x6d\x70\x6c\x65\x74\x65\x22\x2c\x5b\x76\x2c\x64\x5d\x29\x2c\x2d\ +\x2d\x66\x2e\x61\x63\x74\x69\x76\x65\x7c\x7c\x66\x2e\x65\x76\x65\ +\x6e\x74\x2e\x74\x72\x69\x67\x67\x65\x72\x28\x22\x61\x6a\x61\x78\ +\x53\x74\x6f\x70\x22\x29\x29\x7d\x7d\x74\x79\x70\x65\x6f\x66\x20\ +\x61\x3d\x3d\x22\x6f\x62\x6a\x65\x63\x74\x22\x26\x26\x28\x63\x3d\ +\x61\x2c\x61\x3d\x62\x29\x2c\x63\x3d\x63\x7c\x7c\x7b\x7d\x3b\x76\ +\x61\x72\x20\x64\x3d\x66\x2e\x61\x6a\x61\x78\x53\x65\x74\x75\x70\ +\x28\x7b\x7d\x2c\x63\x29\x2c\x65\x3d\x64\x2e\x63\x6f\x6e\x74\x65\ +\x78\x74\x7c\x7c\x64\x2c\x67\x3d\x65\x21\x3d\x3d\x64\x26\x26\x28\ +\x65\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x7c\x7c\x65\x20\x69\x6e\ +\x73\x74\x61\x6e\x63\x65\x6f\x66\x20\x66\x29\x3f\x66\x28\x65\x29\ +\x3a\x66\x2e\x65\x76\x65\x6e\x74\x2c\x68\x3d\x66\x2e\x44\x65\x66\ +\x65\x72\x72\x65\x64\x28\x29\x2c\x69\x3d\x66\x2e\x43\x61\x6c\x6c\ +\x62\x61\x63\x6b\x73\x28\x22\x6f\x6e\x63\x65\x20\x6d\x65\x6d\x6f\ +\x72\x79\x22\x29\x2c\x6a\x3d\x64\x2e\x73\x74\x61\x74\x75\x73\x43\ +\x6f\x64\x65\x7c\x7c\x7b\x7d\x2c\x6b\x2c\x6c\x3d\x7b\x7d\x2c\x6d\ +\x3d\x7b\x7d\x2c\x6e\x2c\x6f\x2c\x70\x2c\x71\x2c\x72\x2c\x73\x3d\ +\x30\x2c\x74\x2c\x75\x2c\x76\x3d\x7b\x72\x65\x61\x64\x79\x53\x74\ +\x61\x74\x65\x3a\x30\x2c\x73\x65\x74\x52\x65\x71\x75\x65\x73\x74\ +\x48\x65\x61\x64\x65\x72\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x2c\x62\x29\x7b\x69\x66\x28\x21\x73\x29\x7b\x76\x61\x72\x20\ +\x63\x3d\x61\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\ +\x29\x3b\x61\x3d\x6d\x5b\x63\x5d\x3d\x6d\x5b\x63\x5d\x7c\x7c\x61\ +\x2c\x6c\x5b\x61\x5d\x3d\x62\x7d\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x7d\x2c\x67\x65\x74\x41\x6c\x6c\x52\x65\x73\x70\x6f\ +\x6e\x73\x65\x48\x65\x61\x64\x65\x72\x73\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x73\x3d\x3d\ +\x3d\x32\x3f\x6e\x3a\x6e\x75\x6c\x6c\x7d\x2c\x67\x65\x74\x52\x65\ +\x73\x70\x6f\x6e\x73\x65\x48\x65\x61\x64\x65\x72\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x63\x3b\x69\ +\x66\x28\x73\x3d\x3d\x3d\x32\x29\x7b\x69\x66\x28\x21\x6f\x29\x7b\ +\x6f\x3d\x7b\x7d\x3b\x77\x68\x69\x6c\x65\x28\x63\x3d\x62\x48\x2e\ +\x65\x78\x65\x63\x28\x6e\x29\x29\x6f\x5b\x63\x5b\x31\x5d\x2e\x74\ +\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x5d\x3d\x63\x5b\ +\x32\x5d\x7d\x63\x3d\x6f\x5b\x61\x2e\x74\x6f\x4c\x6f\x77\x65\x72\ +\x43\x61\x73\x65\x28\x29\x5d\x7d\x72\x65\x74\x75\x72\x6e\x20\x63\ +\x3d\x3d\x3d\x62\x3f\x6e\x75\x6c\x6c\x3a\x63\x7d\x2c\x6f\x76\x65\ +\x72\x72\x69\x64\x65\x4d\x69\x6d\x65\x54\x79\x70\x65\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x73\x7c\x7c\x28\x64\x2e\ +\x6d\x69\x6d\x65\x54\x79\x70\x65\x3d\x61\x29\x3b\x72\x65\x74\x75\ +\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x61\x62\x6f\x72\x74\x3a\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x61\x3d\x61\x7c\x7c\ +\x22\x61\x62\x6f\x72\x74\x22\x2c\x70\x26\x26\x70\x2e\x61\x62\x6f\ +\x72\x74\x28\x61\x29\x2c\x77\x28\x30\x2c\x61\x29\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\x7d\x3b\x68\x2e\x70\x72\x6f\ +\x6d\x69\x73\x65\x28\x76\x29\x2c\x76\x2e\x73\x75\x63\x63\x65\x73\ +\x73\x3d\x76\x2e\x64\x6f\x6e\x65\x2c\x76\x2e\x65\x72\x72\x6f\x72\ +\x3d\x76\x2e\x66\x61\x69\x6c\x2c\x76\x2e\x63\x6f\x6d\x70\x6c\x65\ +\x74\x65\x3d\x69\x2e\x61\x64\x64\x2c\x76\x2e\x73\x74\x61\x74\x75\ +\x73\x43\x6f\x64\x65\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x29\x7b\x69\x66\x28\x61\x29\x7b\x76\x61\x72\x20\x62\x3b\x69\x66\ +\x28\x73\x3c\x32\x29\x66\x6f\x72\x28\x62\x20\x69\x6e\x20\x61\x29\ +\x6a\x5b\x62\x5d\x3d\x5b\x6a\x5b\x62\x5d\x2c\x61\x5b\x62\x5d\x5d\ +\x3b\x65\x6c\x73\x65\x20\x62\x3d\x61\x5b\x76\x2e\x73\x74\x61\x74\ +\x75\x73\x5d\x2c\x76\x2e\x74\x68\x65\x6e\x28\x62\x2c\x62\x29\x7d\ +\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x64\x2e\x75\ +\x72\x6c\x3d\x28\x28\x61\x7c\x7c\x64\x2e\x75\x72\x6c\x29\x2b\x22\ +\x22\x29\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x62\x47\x2c\x22\x22\ +\x29\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\x62\x4c\x2c\x62\x57\x5b\ +\x31\x5d\x2b\x22\x2f\x2f\x22\x29\x2c\x64\x2e\x64\x61\x74\x61\x54\ +\x79\x70\x65\x73\x3d\x66\x2e\x74\x72\x69\x6d\x28\x64\x2e\x64\x61\ +\x74\x61\x54\x79\x70\x65\x7c\x7c\x22\x2a\x22\x29\x2e\x74\x6f\x4c\ +\x6f\x77\x65\x72\x43\x61\x73\x65\x28\x29\x2e\x73\x70\x6c\x69\x74\ +\x28\x62\x50\x29\x2c\x64\x2e\x63\x72\x6f\x73\x73\x44\x6f\x6d\x61\ +\x69\x6e\x3d\x3d\x6e\x75\x6c\x6c\x26\x26\x28\x72\x3d\x62\x52\x2e\ +\x65\x78\x65\x63\x28\x64\x2e\x75\x72\x6c\x2e\x74\x6f\x4c\x6f\x77\ +\x65\x72\x43\x61\x73\x65\x28\x29\x29\x2c\x64\x2e\x63\x72\x6f\x73\ +\x73\x44\x6f\x6d\x61\x69\x6e\x3d\x21\x28\x21\x72\x7c\x7c\x72\x5b\ +\x31\x5d\x3d\x3d\x62\x57\x5b\x31\x5d\x26\x26\x72\x5b\x32\x5d\x3d\ +\x3d\x62\x57\x5b\x32\x5d\x26\x26\x28\x72\x5b\x33\x5d\x7c\x7c\x28\ +\x72\x5b\x31\x5d\x3d\x3d\x3d\x22\x68\x74\x74\x70\x3a\x22\x3f\x38\ +\x30\x3a\x34\x34\x33\x29\x29\x3d\x3d\x28\x62\x57\x5b\x33\x5d\x7c\ +\x7c\x28\x62\x57\x5b\x31\x5d\x3d\x3d\x3d\x22\x68\x74\x74\x70\x3a\ +\x22\x3f\x38\x30\x3a\x34\x34\x33\x29\x29\x29\x29\x2c\x64\x2e\x64\ +\x61\x74\x61\x26\x26\x64\x2e\x70\x72\x6f\x63\x65\x73\x73\x44\x61\ +\x74\x61\x26\x26\x74\x79\x70\x65\x6f\x66\x20\x64\x2e\x64\x61\x74\ +\x61\x21\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x28\x64\x2e\ +\x64\x61\x74\x61\x3d\x66\x2e\x70\x61\x72\x61\x6d\x28\x64\x2e\x64\ +\x61\x74\x61\x2c\x64\x2e\x74\x72\x61\x64\x69\x74\x69\x6f\x6e\x61\ +\x6c\x29\x29\x2c\x62\x24\x28\x62\x54\x2c\x64\x2c\x63\x2c\x76\x29\ +\x3b\x69\x66\x28\x73\x3d\x3d\x3d\x32\x29\x72\x65\x74\x75\x72\x6e\ +\x21\x31\x3b\x74\x3d\x64\x2e\x67\x6c\x6f\x62\x61\x6c\x2c\x64\x2e\ +\x74\x79\x70\x65\x3d\x64\x2e\x74\x79\x70\x65\x2e\x74\x6f\x55\x70\ +\x70\x65\x72\x43\x61\x73\x65\x28\x29\x2c\x64\x2e\x68\x61\x73\x43\ +\x6f\x6e\x74\x65\x6e\x74\x3d\x21\x62\x4b\x2e\x74\x65\x73\x74\x28\ +\x64\x2e\x74\x79\x70\x65\x29\x2c\x74\x26\x26\x66\x2e\x61\x63\x74\ +\x69\x76\x65\x2b\x2b\x3d\x3d\x3d\x30\x26\x26\x66\x2e\x65\x76\x65\ +\x6e\x74\x2e\x74\x72\x69\x67\x67\x65\x72\x28\x22\x61\x6a\x61\x78\ +\x53\x74\x61\x72\x74\x22\x29\x3b\x69\x66\x28\x21\x64\x2e\x68\x61\ +\x73\x43\x6f\x6e\x74\x65\x6e\x74\x29\x7b\x64\x2e\x64\x61\x74\x61\ +\x26\x26\x28\x64\x2e\x75\x72\x6c\x2b\x3d\x28\x62\x4d\x2e\x74\x65\ +\x73\x74\x28\x64\x2e\x75\x72\x6c\x29\x3f\x22\x26\x22\x3a\x22\x3f\ +\x22\x29\x2b\x64\x2e\x64\x61\x74\x61\x2c\x64\x65\x6c\x65\x74\x65\ +\x20\x64\x2e\x64\x61\x74\x61\x29\x2c\x6b\x3d\x64\x2e\x75\x72\x6c\ +\x3b\x69\x66\x28\x64\x2e\x63\x61\x63\x68\x65\x3d\x3d\x3d\x21\x31\ +\x29\x7b\x76\x61\x72\x20\x78\x3d\x66\x2e\x6e\x6f\x77\x28\x29\x2c\ +\x79\x3d\x64\x2e\x75\x72\x6c\x2e\x72\x65\x70\x6c\x61\x63\x65\x28\ +\x62\x51\x2c\x22\x24\x31\x5f\x3d\x22\x2b\x78\x29\x3b\x64\x2e\x75\ +\x72\x6c\x3d\x79\x2b\x28\x79\x3d\x3d\x3d\x64\x2e\x75\x72\x6c\x3f\ +\x28\x62\x4d\x2e\x74\x65\x73\x74\x28\x64\x2e\x75\x72\x6c\x29\x3f\ +\x22\x26\x22\x3a\x22\x3f\x22\x29\x2b\x22\x5f\x3d\x22\x2b\x78\x3a\ +\x22\x22\x29\x7d\x7d\x28\x64\x2e\x64\x61\x74\x61\x26\x26\x64\x2e\ +\x68\x61\x73\x43\x6f\x6e\x74\x65\x6e\x74\x26\x26\x64\x2e\x63\x6f\ +\x6e\x74\x65\x6e\x74\x54\x79\x70\x65\x21\x3d\x3d\x21\x31\x7c\x7c\ +\x63\x2e\x63\x6f\x6e\x74\x65\x6e\x74\x54\x79\x70\x65\x29\x26\x26\ +\x76\x2e\x73\x65\x74\x52\x65\x71\x75\x65\x73\x74\x48\x65\x61\x64\ +\x65\x72\x28\x22\x43\x6f\x6e\x74\x65\x6e\x74\x2d\x54\x79\x70\x65\ +\x22\x2c\x64\x2e\x63\x6f\x6e\x74\x65\x6e\x74\x54\x79\x70\x65\x29\ +\x2c\x64\x2e\x69\x66\x4d\x6f\x64\x69\x66\x69\x65\x64\x26\x26\x28\ +\x6b\x3d\x6b\x7c\x7c\x64\x2e\x75\x72\x6c\x2c\x66\x2e\x6c\x61\x73\ +\x74\x4d\x6f\x64\x69\x66\x69\x65\x64\x5b\x6b\x5d\x26\x26\x76\x2e\ +\x73\x65\x74\x52\x65\x71\x75\x65\x73\x74\x48\x65\x61\x64\x65\x72\ +\x28\x22\x49\x66\x2d\x4d\x6f\x64\x69\x66\x69\x65\x64\x2d\x53\x69\ +\x6e\x63\x65\x22\x2c\x66\x2e\x6c\x61\x73\x74\x4d\x6f\x64\x69\x66\ +\x69\x65\x64\x5b\x6b\x5d\x29\x2c\x66\x2e\x65\x74\x61\x67\x5b\x6b\ +\x5d\x26\x26\x76\x2e\x73\x65\x74\x52\x65\x71\x75\x65\x73\x74\x48\ +\x65\x61\x64\x65\x72\x28\x22\x49\x66\x2d\x4e\x6f\x6e\x65\x2d\x4d\ +\x61\x74\x63\x68\x22\x2c\x66\x2e\x65\x74\x61\x67\x5b\x6b\x5d\x29\ +\x29\x2c\x76\x2e\x73\x65\x74\x52\x65\x71\x75\x65\x73\x74\x48\x65\ +\x61\x64\x65\x72\x28\x22\x41\x63\x63\x65\x70\x74\x22\x2c\x64\x2e\ +\x64\x61\x74\x61\x54\x79\x70\x65\x73\x5b\x30\x5d\x26\x26\x64\x2e\ +\x61\x63\x63\x65\x70\x74\x73\x5b\x64\x2e\x64\x61\x74\x61\x54\x79\ +\x70\x65\x73\x5b\x30\x5d\x5d\x3f\x64\x2e\x61\x63\x63\x65\x70\x74\ +\x73\x5b\x64\x2e\x64\x61\x74\x61\x54\x79\x70\x65\x73\x5b\x30\x5d\ +\x5d\x2b\x28\x64\x2e\x64\x61\x74\x61\x54\x79\x70\x65\x73\x5b\x30\ +\x5d\x21\x3d\x3d\x22\x2a\x22\x3f\x22\x2c\x20\x22\x2b\x62\x58\x2b\ +\x22\x3b\x20\x71\x3d\x30\x2e\x30\x31\x22\x3a\x22\x22\x29\x3a\x64\ +\x2e\x61\x63\x63\x65\x70\x74\x73\x5b\x22\x2a\x22\x5d\x29\x3b\x66\ +\x6f\x72\x28\x75\x20\x69\x6e\x20\x64\x2e\x68\x65\x61\x64\x65\x72\ +\x73\x29\x76\x2e\x73\x65\x74\x52\x65\x71\x75\x65\x73\x74\x48\x65\ +\x61\x64\x65\x72\x28\x75\x2c\x64\x2e\x68\x65\x61\x64\x65\x72\x73\ +\x5b\x75\x5d\x29\x3b\x69\x66\x28\x64\x2e\x62\x65\x66\x6f\x72\x65\ +\x53\x65\x6e\x64\x26\x26\x28\x64\x2e\x62\x65\x66\x6f\x72\x65\x53\ +\x65\x6e\x64\x2e\x63\x61\x6c\x6c\x28\x65\x2c\x76\x2c\x64\x29\x3d\ +\x3d\x3d\x21\x31\x7c\x7c\x73\x3d\x3d\x3d\x32\x29\x29\x7b\x76\x2e\ +\x61\x62\x6f\x72\x74\x28\x29\x3b\x72\x65\x74\x75\x72\x6e\x21\x31\ +\x7d\x66\x6f\x72\x28\x75\x20\x69\x6e\x7b\x73\x75\x63\x63\x65\x73\ +\x73\x3a\x31\x2c\x65\x72\x72\x6f\x72\x3a\x31\x2c\x63\x6f\x6d\x70\ +\x6c\x65\x74\x65\x3a\x31\x7d\x29\x76\x5b\x75\x5d\x28\x64\x5b\x75\ +\x5d\x29\x3b\x70\x3d\x62\x24\x28\x62\x55\x2c\x64\x2c\x63\x2c\x76\ +\x29\x3b\x69\x66\x28\x21\x70\x29\x77\x28\x2d\x31\x2c\x22\x4e\x6f\ +\x20\x54\x72\x61\x6e\x73\x70\x6f\x72\x74\x22\x29\x3b\x65\x6c\x73\ +\x65\x7b\x76\x2e\x72\x65\x61\x64\x79\x53\x74\x61\x74\x65\x3d\x31\ +\x2c\x74\x26\x26\x67\x2e\x74\x72\x69\x67\x67\x65\x72\x28\x22\x61\ +\x6a\x61\x78\x53\x65\x6e\x64\x22\x2c\x5b\x76\x2c\x64\x5d\x29\x2c\ +\x64\x2e\x61\x73\x79\x6e\x63\x26\x26\x64\x2e\x74\x69\x6d\x65\x6f\ +\x75\x74\x3e\x30\x26\x26\x28\x71\x3d\x73\x65\x74\x54\x69\x6d\x65\ +\x6f\x75\x74\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x76\ +\x2e\x61\x62\x6f\x72\x74\x28\x22\x74\x69\x6d\x65\x6f\x75\x74\x22\ +\x29\x7d\x2c\x64\x2e\x74\x69\x6d\x65\x6f\x75\x74\x29\x29\x3b\x74\ +\x72\x79\x7b\x73\x3d\x31\x2c\x70\x2e\x73\x65\x6e\x64\x28\x6c\x2c\ +\x77\x29\x7d\x63\x61\x74\x63\x68\x28\x7a\x29\x7b\x69\x66\x28\x73\ +\x3c\x32\x29\x77\x28\x2d\x31\x2c\x7a\x29\x3b\x65\x6c\x73\x65\x20\ +\x74\x68\x72\x6f\x77\x20\x7a\x7d\x7d\x72\x65\x74\x75\x72\x6e\x20\ +\x76\x7d\x2c\x70\x61\x72\x61\x6d\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x3d\x5b\x5d\x2c\ +\x65\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\ +\x62\x3d\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\ +\x29\x3f\x62\x28\x29\x3a\x62\x2c\x64\x5b\x64\x2e\x6c\x65\x6e\x67\ +\x74\x68\x5d\x3d\x65\x6e\x63\x6f\x64\x65\x55\x52\x49\x43\x6f\x6d\ +\x70\x6f\x6e\x65\x6e\x74\x28\x61\x29\x2b\x22\x3d\x22\x2b\x65\x6e\ +\x63\x6f\x64\x65\x55\x52\x49\x43\x6f\x6d\x70\x6f\x6e\x65\x6e\x74\ +\x28\x62\x29\x7d\x3b\x63\x3d\x3d\x3d\x62\x26\x26\x28\x63\x3d\x66\ +\x2e\x61\x6a\x61\x78\x53\x65\x74\x74\x69\x6e\x67\x73\x2e\x74\x72\ +\x61\x64\x69\x74\x69\x6f\x6e\x61\x6c\x29\x3b\x69\x66\x28\x66\x2e\ +\x69\x73\x41\x72\x72\x61\x79\x28\x61\x29\x7c\x7c\x61\x2e\x6a\x71\ +\x75\x65\x72\x79\x26\x26\x21\x66\x2e\x69\x73\x50\x6c\x61\x69\x6e\ +\x4f\x62\x6a\x65\x63\x74\x28\x61\x29\x29\x66\x2e\x65\x61\x63\x68\ +\x28\x61\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x65\x28\ +\x74\x68\x69\x73\x2e\x6e\x61\x6d\x65\x2c\x74\x68\x69\x73\x2e\x76\ +\x61\x6c\x75\x65\x29\x7d\x29\x3b\x65\x6c\x73\x65\x20\x66\x6f\x72\ +\x28\x76\x61\x72\x20\x67\x20\x69\x6e\x20\x61\x29\x63\x61\x28\x67\ +\x2c\x61\x5b\x67\x5d\x2c\x63\x2c\x65\x29\x3b\x72\x65\x74\x75\x72\ +\x6e\x20\x64\x2e\x6a\x6f\x69\x6e\x28\x22\x26\x22\x29\x2e\x72\x65\ +\x70\x6c\x61\x63\x65\x28\x62\x44\x2c\x22\x2b\x22\x29\x7d\x7d\x29\ +\x2c\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x7b\x61\x63\x74\x69\x76\ +\x65\x3a\x30\x2c\x6c\x61\x73\x74\x4d\x6f\x64\x69\x66\x69\x65\x64\ +\x3a\x7b\x7d\x2c\x65\x74\x61\x67\x3a\x7b\x7d\x7d\x29\x3b\x76\x61\ +\x72\x20\x63\x64\x3d\x66\x2e\x6e\x6f\x77\x28\x29\x2c\x63\x65\x3d\ +\x2f\x28\x5c\x3d\x29\x5c\x3f\x28\x26\x7c\x24\x29\x7c\x5c\x3f\x5c\ +\x3f\x2f\x69\x3b\x66\x2e\x61\x6a\x61\x78\x53\x65\x74\x75\x70\x28\ +\x7b\x6a\x73\x6f\x6e\x70\x3a\x22\x63\x61\x6c\x6c\x62\x61\x63\x6b\ +\x22\x2c\x6a\x73\x6f\x6e\x70\x43\x61\x6c\x6c\x62\x61\x63\x6b\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\ +\x6e\x20\x66\x2e\x65\x78\x70\x61\x6e\x64\x6f\x2b\x22\x5f\x22\x2b\ +\x63\x64\x2b\x2b\x7d\x7d\x29\x2c\x66\x2e\x61\x6a\x61\x78\x50\x72\ +\x65\x66\x69\x6c\x74\x65\x72\x28\x22\x6a\x73\x6f\x6e\x20\x6a\x73\ +\x6f\x6e\x70\x22\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\x2c\ +\x63\x2c\x64\x29\x7b\x76\x61\x72\x20\x65\x3d\x62\x2e\x63\x6f\x6e\ +\x74\x65\x6e\x74\x54\x79\x70\x65\x3d\x3d\x3d\x22\x61\x70\x70\x6c\ +\x69\x63\x61\x74\x69\x6f\x6e\x2f\x78\x2d\x77\x77\x77\x2d\x66\x6f\ +\x72\x6d\x2d\x75\x72\x6c\x65\x6e\x63\x6f\x64\x65\x64\x22\x26\x26\ +\x74\x79\x70\x65\x6f\x66\x20\x62\x2e\x64\x61\x74\x61\x3d\x3d\x22\ +\x73\x74\x72\x69\x6e\x67\x22\x3b\x69\x66\x28\x62\x2e\x64\x61\x74\ +\x61\x54\x79\x70\x65\x73\x5b\x30\x5d\x3d\x3d\x3d\x22\x6a\x73\x6f\ +\x6e\x70\x22\x7c\x7c\x62\x2e\x6a\x73\x6f\x6e\x70\x21\x3d\x3d\x21\ +\x31\x26\x26\x28\x63\x65\x2e\x74\x65\x73\x74\x28\x62\x2e\x75\x72\ +\x6c\x29\x7c\x7c\x65\x26\x26\x63\x65\x2e\x74\x65\x73\x74\x28\x62\ +\x2e\x64\x61\x74\x61\x29\x29\x29\x7b\x76\x61\x72\x20\x67\x2c\x68\ +\x3d\x62\x2e\x6a\x73\x6f\x6e\x70\x43\x61\x6c\x6c\x62\x61\x63\x6b\ +\x3d\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\x2e\ +\x6a\x73\x6f\x6e\x70\x43\x61\x6c\x6c\x62\x61\x63\x6b\x29\x3f\x62\ +\x2e\x6a\x73\x6f\x6e\x70\x43\x61\x6c\x6c\x62\x61\x63\x6b\x28\x29\ +\x3a\x62\x2e\x6a\x73\x6f\x6e\x70\x43\x61\x6c\x6c\x62\x61\x63\x6b\ +\x2c\x69\x3d\x61\x5b\x68\x5d\x2c\x6a\x3d\x62\x2e\x75\x72\x6c\x2c\ +\x6b\x3d\x62\x2e\x64\x61\x74\x61\x2c\x6c\x3d\x22\x24\x31\x22\x2b\ +\x68\x2b\x22\x24\x32\x22\x3b\x62\x2e\x6a\x73\x6f\x6e\x70\x21\x3d\ +\x3d\x21\x31\x26\x26\x28\x6a\x3d\x6a\x2e\x72\x65\x70\x6c\x61\x63\ +\x65\x28\x63\x65\x2c\x6c\x29\x2c\x62\x2e\x75\x72\x6c\x3d\x3d\x3d\ +\x6a\x26\x26\x28\x65\x26\x26\x28\x6b\x3d\x6b\x2e\x72\x65\x70\x6c\ +\x61\x63\x65\x28\x63\x65\x2c\x6c\x29\x29\x2c\x62\x2e\x64\x61\x74\ +\x61\x3d\x3d\x3d\x6b\x26\x26\x28\x6a\x2b\x3d\x28\x2f\x5c\x3f\x2f\ +\x2e\x74\x65\x73\x74\x28\x6a\x29\x3f\x22\x26\x22\x3a\x22\x3f\x22\ +\x29\x2b\x62\x2e\x6a\x73\x6f\x6e\x70\x2b\x22\x3d\x22\x2b\x68\x29\ +\x29\x29\x2c\x62\x2e\x75\x72\x6c\x3d\x6a\x2c\x62\x2e\x64\x61\x74\ +\x61\x3d\x6b\x2c\x61\x5b\x68\x5d\x3d\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x29\x7b\x67\x3d\x5b\x61\x5d\x7d\x2c\x64\x2e\x61\x6c\ +\x77\x61\x79\x73\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\ +\x61\x5b\x68\x5d\x3d\x69\x2c\x67\x26\x26\x66\x2e\x69\x73\x46\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x69\x29\x26\x26\x61\x5b\x68\x5d\x28\ +\x67\x5b\x30\x5d\x29\x7d\x29\x2c\x62\x2e\x63\x6f\x6e\x76\x65\x72\ +\x74\x65\x72\x73\x5b\x22\x73\x63\x72\x69\x70\x74\x20\x6a\x73\x6f\ +\x6e\x22\x5d\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x67\ +\x7c\x7c\x66\x2e\x65\x72\x72\x6f\x72\x28\x68\x2b\x22\x20\x77\x61\ +\x73\x20\x6e\x6f\x74\x20\x63\x61\x6c\x6c\x65\x64\x22\x29\x3b\x72\ +\x65\x74\x75\x72\x6e\x20\x67\x5b\x30\x5d\x7d\x2c\x62\x2e\x64\x61\ +\x74\x61\x54\x79\x70\x65\x73\x5b\x30\x5d\x3d\x22\x6a\x73\x6f\x6e\ +\x22\x3b\x72\x65\x74\x75\x72\x6e\x22\x73\x63\x72\x69\x70\x74\x22\ +\x7d\x7d\x29\x2c\x66\x2e\x61\x6a\x61\x78\x53\x65\x74\x75\x70\x28\ +\x7b\x61\x63\x63\x65\x70\x74\x73\x3a\x7b\x73\x63\x72\x69\x70\x74\ +\x3a\x22\x74\x65\x78\x74\x2f\x6a\x61\x76\x61\x73\x63\x72\x69\x70\ +\x74\x2c\x20\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\x6a\ +\x61\x76\x61\x73\x63\x72\x69\x70\x74\x2c\x20\x61\x70\x70\x6c\x69\ +\x63\x61\x74\x69\x6f\x6e\x2f\x65\x63\x6d\x61\x73\x63\x72\x69\x70\ +\x74\x2c\x20\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\x78\ +\x2d\x65\x63\x6d\x61\x73\x63\x72\x69\x70\x74\x22\x7d\x2c\x63\x6f\ +\x6e\x74\x65\x6e\x74\x73\x3a\x7b\x73\x63\x72\x69\x70\x74\x3a\x2f\ +\x6a\x61\x76\x61\x73\x63\x72\x69\x70\x74\x7c\x65\x63\x6d\x61\x73\ +\x63\x72\x69\x70\x74\x2f\x7d\x2c\x63\x6f\x6e\x76\x65\x72\x74\x65\ +\x72\x73\x3a\x7b\x22\x74\x65\x78\x74\x20\x73\x63\x72\x69\x70\x74\ +\x22\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x66\x2e\ +\x67\x6c\x6f\x62\x61\x6c\x45\x76\x61\x6c\x28\x61\x29\x3b\x72\x65\ +\x74\x75\x72\x6e\x20\x61\x7d\x7d\x7d\x29\x2c\x66\x2e\x61\x6a\x61\ +\x78\x50\x72\x65\x66\x69\x6c\x74\x65\x72\x28\x22\x73\x63\x72\x69\ +\x70\x74\x22\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\ +\x61\x2e\x63\x61\x63\x68\x65\x3d\x3d\x3d\x62\x26\x26\x28\x61\x2e\ +\x63\x61\x63\x68\x65\x3d\x21\x31\x29\x2c\x61\x2e\x63\x72\x6f\x73\ +\x73\x44\x6f\x6d\x61\x69\x6e\x26\x26\x28\x61\x2e\x74\x79\x70\x65\ +\x3d\x22\x47\x45\x54\x22\x2c\x61\x2e\x67\x6c\x6f\x62\x61\x6c\x3d\ +\x21\x31\x29\x7d\x29\x2c\x66\x2e\x61\x6a\x61\x78\x54\x72\x61\x6e\ +\x73\x70\x6f\x72\x74\x28\x22\x73\x63\x72\x69\x70\x74\x22\x2c\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x69\x66\x28\x61\x2e\ +\x63\x72\x6f\x73\x73\x44\x6f\x6d\x61\x69\x6e\x29\x7b\x76\x61\x72\ +\x20\x64\x2c\x65\x3d\x63\x2e\x68\x65\x61\x64\x7c\x7c\x63\x2e\x67\ +\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x73\x42\x79\x54\x61\x67\x4e\ +\x61\x6d\x65\x28\x22\x68\x65\x61\x64\x22\x29\x5b\x30\x5d\x7c\x7c\ +\x63\x2e\x64\x6f\x63\x75\x6d\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\ +\x74\x3b\x72\x65\x74\x75\x72\x6e\x7b\x73\x65\x6e\x64\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x66\x2c\x67\x29\x7b\x64\x3d\x63\x2e\ +\x63\x72\x65\x61\x74\x65\x45\x6c\x65\x6d\x65\x6e\x74\x28\x22\x73\ +\x63\x72\x69\x70\x74\x22\x29\x2c\x64\x2e\x61\x73\x79\x6e\x63\x3d\ +\x22\x61\x73\x79\x6e\x63\x22\x2c\x61\x2e\x73\x63\x72\x69\x70\x74\ +\x43\x68\x61\x72\x73\x65\x74\x26\x26\x28\x64\x2e\x63\x68\x61\x72\ +\x73\x65\x74\x3d\x61\x2e\x73\x63\x72\x69\x70\x74\x43\x68\x61\x72\ +\x73\x65\x74\x29\x2c\x64\x2e\x73\x72\x63\x3d\x61\x2e\x75\x72\x6c\ +\x2c\x64\x2e\x6f\x6e\x6c\x6f\x61\x64\x3d\x64\x2e\x6f\x6e\x72\x65\ +\x61\x64\x79\x73\x74\x61\x74\x65\x63\x68\x61\x6e\x67\x65\x3d\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x69\x66\x28\ +\x63\x7c\x7c\x21\x64\x2e\x72\x65\x61\x64\x79\x53\x74\x61\x74\x65\ +\x7c\x7c\x2f\x6c\x6f\x61\x64\x65\x64\x7c\x63\x6f\x6d\x70\x6c\x65\ +\x74\x65\x2f\x2e\x74\x65\x73\x74\x28\x64\x2e\x72\x65\x61\x64\x79\ +\x53\x74\x61\x74\x65\x29\x29\x64\x2e\x6f\x6e\x6c\x6f\x61\x64\x3d\ +\x64\x2e\x6f\x6e\x72\x65\x61\x64\x79\x73\x74\x61\x74\x65\x63\x68\ +\x61\x6e\x67\x65\x3d\x6e\x75\x6c\x6c\x2c\x65\x26\x26\x64\x2e\x70\ +\x61\x72\x65\x6e\x74\x4e\x6f\x64\x65\x26\x26\x65\x2e\x72\x65\x6d\ +\x6f\x76\x65\x43\x68\x69\x6c\x64\x28\x64\x29\x2c\x64\x3d\x62\x2c\ +\x63\x7c\x7c\x67\x28\x32\x30\x30\x2c\x22\x73\x75\x63\x63\x65\x73\ +\x73\x22\x29\x7d\x2c\x65\x2e\x69\x6e\x73\x65\x72\x74\x42\x65\x66\ +\x6f\x72\x65\x28\x64\x2c\x65\x2e\x66\x69\x72\x73\x74\x43\x68\x69\ +\x6c\x64\x29\x7d\x2c\x61\x62\x6f\x72\x74\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x29\x7b\x64\x26\x26\x64\x2e\x6f\x6e\x6c\x6f\x61\ +\x64\x28\x30\x2c\x31\x29\x7d\x7d\x7d\x7d\x29\x3b\x76\x61\x72\x20\ +\x63\x66\x3d\x61\x2e\x41\x63\x74\x69\x76\x65\x58\x4f\x62\x6a\x65\ +\x63\x74\x3f\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x66\x6f\ +\x72\x28\x76\x61\x72\x20\x61\x20\x69\x6e\x20\x63\x68\x29\x63\x68\ +\x5b\x61\x5d\x28\x30\x2c\x31\x29\x7d\x3a\x21\x31\x2c\x63\x67\x3d\ +\x30\x2c\x63\x68\x3b\x66\x2e\x61\x6a\x61\x78\x53\x65\x74\x74\x69\ +\x6e\x67\x73\x2e\x78\x68\x72\x3d\x61\x2e\x41\x63\x74\x69\x76\x65\ +\x58\x4f\x62\x6a\x65\x63\x74\x3f\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x21\x74\x68\x69\x73\x2e\x69\ +\x73\x4c\x6f\x63\x61\x6c\x26\x26\x63\x69\x28\x29\x7c\x7c\x63\x6a\ +\x28\x29\x7d\x3a\x63\x69\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x29\x7b\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x66\x2e\x73\x75\ +\x70\x70\x6f\x72\x74\x2c\x7b\x61\x6a\x61\x78\x3a\x21\x21\x61\x2c\ +\x63\x6f\x72\x73\x3a\x21\x21\x61\x26\x26\x22\x77\x69\x74\x68\x43\ +\x72\x65\x64\x65\x6e\x74\x69\x61\x6c\x73\x22\x69\x6e\x20\x61\x7d\ +\x29\x7d\x28\x66\x2e\x61\x6a\x61\x78\x53\x65\x74\x74\x69\x6e\x67\ +\x73\x2e\x78\x68\x72\x28\x29\x29\x2c\x66\x2e\x73\x75\x70\x70\x6f\ +\x72\x74\x2e\x61\x6a\x61\x78\x26\x26\x66\x2e\x61\x6a\x61\x78\x54\ +\x72\x61\x6e\x73\x70\x6f\x72\x74\x28\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x63\x29\x7b\x69\x66\x28\x21\x63\x2e\x63\x72\x6f\x73\x73\ +\x44\x6f\x6d\x61\x69\x6e\x7c\x7c\x66\x2e\x73\x75\x70\x70\x6f\x72\ +\x74\x2e\x63\x6f\x72\x73\x29\x7b\x76\x61\x72\x20\x64\x3b\x72\x65\ +\x74\x75\x72\x6e\x7b\x73\x65\x6e\x64\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x65\x2c\x67\x29\x7b\x76\x61\x72\x20\x68\x3d\x63\x2e\ +\x78\x68\x72\x28\x29\x2c\x69\x2c\x6a\x3b\x63\x2e\x75\x73\x65\x72\ +\x6e\x61\x6d\x65\x3f\x68\x2e\x6f\x70\x65\x6e\x28\x63\x2e\x74\x79\ +\x70\x65\x2c\x63\x2e\x75\x72\x6c\x2c\x63\x2e\x61\x73\x79\x6e\x63\ +\x2c\x63\x2e\x75\x73\x65\x72\x6e\x61\x6d\x65\x2c\x63\x2e\x70\x61\ +\x73\x73\x77\x6f\x72\x64\x29\x3a\x68\x2e\x6f\x70\x65\x6e\x28\x63\ +\x2e\x74\x79\x70\x65\x2c\x63\x2e\x75\x72\x6c\x2c\x63\x2e\x61\x73\ +\x79\x6e\x63\x29\x3b\x69\x66\x28\x63\x2e\x78\x68\x72\x46\x69\x65\ +\x6c\x64\x73\x29\x66\x6f\x72\x28\x6a\x20\x69\x6e\x20\x63\x2e\x78\ +\x68\x72\x46\x69\x65\x6c\x64\x73\x29\x68\x5b\x6a\x5d\x3d\x63\x2e\ +\x78\x68\x72\x46\x69\x65\x6c\x64\x73\x5b\x6a\x5d\x3b\x63\x2e\x6d\ +\x69\x6d\x65\x54\x79\x70\x65\x26\x26\x68\x2e\x6f\x76\x65\x72\x72\ +\x69\x64\x65\x4d\x69\x6d\x65\x54\x79\x70\x65\x26\x26\x68\x2e\x6f\ +\x76\x65\x72\x72\x69\x64\x65\x4d\x69\x6d\x65\x54\x79\x70\x65\x28\ +\x63\x2e\x6d\x69\x6d\x65\x54\x79\x70\x65\x29\x2c\x21\x63\x2e\x63\ +\x72\x6f\x73\x73\x44\x6f\x6d\x61\x69\x6e\x26\x26\x21\x65\x5b\x22\ +\x58\x2d\x52\x65\x71\x75\x65\x73\x74\x65\x64\x2d\x57\x69\x74\x68\ +\x22\x5d\x26\x26\x28\x65\x5b\x22\x58\x2d\x52\x65\x71\x75\x65\x73\ +\x74\x65\x64\x2d\x57\x69\x74\x68\x22\x5d\x3d\x22\x58\x4d\x4c\x48\ +\x74\x74\x70\x52\x65\x71\x75\x65\x73\x74\x22\x29\x3b\x74\x72\x79\ +\x7b\x66\x6f\x72\x28\x6a\x20\x69\x6e\x20\x65\x29\x68\x2e\x73\x65\ +\x74\x52\x65\x71\x75\x65\x73\x74\x48\x65\x61\x64\x65\x72\x28\x6a\ +\x2c\x65\x5b\x6a\x5d\x29\x7d\x63\x61\x74\x63\x68\x28\x6b\x29\x7b\ +\x7d\x68\x2e\x73\x65\x6e\x64\x28\x63\x2e\x68\x61\x73\x43\x6f\x6e\ +\x74\x65\x6e\x74\x26\x26\x63\x2e\x64\x61\x74\x61\x7c\x7c\x6e\x75\ +\x6c\x6c\x29\x2c\x64\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x65\x29\x7b\x76\x61\x72\x20\x6a\x2c\x6b\x2c\x6c\x2c\x6d\x2c\ +\x6e\x3b\x74\x72\x79\x7b\x69\x66\x28\x64\x26\x26\x28\x65\x7c\x7c\ +\x68\x2e\x72\x65\x61\x64\x79\x53\x74\x61\x74\x65\x3d\x3d\x3d\x34\ +\x29\x29\x7b\x64\x3d\x62\x2c\x69\x26\x26\x28\x68\x2e\x6f\x6e\x72\ +\x65\x61\x64\x79\x73\x74\x61\x74\x65\x63\x68\x61\x6e\x67\x65\x3d\ +\x66\x2e\x6e\x6f\x6f\x70\x2c\x63\x66\x26\x26\x64\x65\x6c\x65\x74\ +\x65\x20\x63\x68\x5b\x69\x5d\x29\x3b\x69\x66\x28\x65\x29\x68\x2e\ +\x72\x65\x61\x64\x79\x53\x74\x61\x74\x65\x21\x3d\x3d\x34\x26\x26\ +\x68\x2e\x61\x62\x6f\x72\x74\x28\x29\x3b\x65\x6c\x73\x65\x7b\x6a\ +\x3d\x68\x2e\x73\x74\x61\x74\x75\x73\x2c\x6c\x3d\x68\x2e\x67\x65\ +\x74\x41\x6c\x6c\x52\x65\x73\x70\x6f\x6e\x73\x65\x48\x65\x61\x64\ +\x65\x72\x73\x28\x29\x2c\x6d\x3d\x7b\x7d\x2c\x6e\x3d\x68\x2e\x72\ +\x65\x73\x70\x6f\x6e\x73\x65\x58\x4d\x4c\x2c\x6e\x26\x26\x6e\x2e\ +\x64\x6f\x63\x75\x6d\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x26\ +\x26\x28\x6d\x2e\x78\x6d\x6c\x3d\x6e\x29\x2c\x6d\x2e\x74\x65\x78\ +\x74\x3d\x68\x2e\x72\x65\x73\x70\x6f\x6e\x73\x65\x54\x65\x78\x74\ +\x3b\x74\x72\x79\x7b\x6b\x3d\x68\x2e\x73\x74\x61\x74\x75\x73\x54\ +\x65\x78\x74\x7d\x63\x61\x74\x63\x68\x28\x6f\x29\x7b\x6b\x3d\x22\ +\x22\x7d\x21\x6a\x26\x26\x63\x2e\x69\x73\x4c\x6f\x63\x61\x6c\x26\ +\x26\x21\x63\x2e\x63\x72\x6f\x73\x73\x44\x6f\x6d\x61\x69\x6e\x3f\ +\x6a\x3d\x6d\x2e\x74\x65\x78\x74\x3f\x32\x30\x30\x3a\x34\x30\x34\ +\x3a\x6a\x3d\x3d\x3d\x31\x32\x32\x33\x26\x26\x28\x6a\x3d\x32\x30\ +\x34\x29\x7d\x7d\x7d\x63\x61\x74\x63\x68\x28\x70\x29\x7b\x65\x7c\ +\x7c\x67\x28\x2d\x31\x2c\x70\x29\x7d\x6d\x26\x26\x67\x28\x6a\x2c\ +\x6b\x2c\x6d\x2c\x6c\x29\x7d\x2c\x21\x63\x2e\x61\x73\x79\x6e\x63\ +\x7c\x7c\x68\x2e\x72\x65\x61\x64\x79\x53\x74\x61\x74\x65\x3d\x3d\ +\x3d\x34\x3f\x64\x28\x29\x3a\x28\x69\x3d\x2b\x2b\x63\x67\x2c\x63\ +\x66\x26\x26\x28\x63\x68\x7c\x7c\x28\x63\x68\x3d\x7b\x7d\x2c\x66\ +\x28\x61\x29\x2e\x75\x6e\x6c\x6f\x61\x64\x28\x63\x66\x29\x29\x2c\ +\x63\x68\x5b\x69\x5d\x3d\x64\x29\x2c\x68\x2e\x6f\x6e\x72\x65\x61\ +\x64\x79\x73\x74\x61\x74\x65\x63\x68\x61\x6e\x67\x65\x3d\x64\x29\ +\x7d\x2c\x61\x62\x6f\x72\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x29\x7b\x64\x26\x26\x64\x28\x30\x2c\x31\x29\x7d\x7d\x7d\x7d\ +\x29\x3b\x76\x61\x72\x20\x63\x6b\x3d\x7b\x7d\x2c\x63\x6c\x2c\x63\ +\x6d\x2c\x63\x6e\x3d\x2f\x5e\x28\x3f\x3a\x74\x6f\x67\x67\x6c\x65\ +\x7c\x73\x68\x6f\x77\x7c\x68\x69\x64\x65\x29\x24\x2f\x2c\x63\x6f\ +\x3d\x2f\x5e\x28\x5b\x2b\x5c\x2d\x5d\x3d\x29\x3f\x28\x5b\x5c\x64\ +\x2b\x2e\x5c\x2d\x5d\x2b\x29\x28\x5b\x61\x2d\x7a\x25\x5d\x2a\x29\ +\x24\x2f\x69\x2c\x63\x70\x2c\x63\x71\x3d\x5b\x5b\x22\x68\x65\x69\ +\x67\x68\x74\x22\x2c\x22\x6d\x61\x72\x67\x69\x6e\x54\x6f\x70\x22\ +\x2c\x22\x6d\x61\x72\x67\x69\x6e\x42\x6f\x74\x74\x6f\x6d\x22\x2c\ +\x22\x70\x61\x64\x64\x69\x6e\x67\x54\x6f\x70\x22\x2c\x22\x70\x61\ +\x64\x64\x69\x6e\x67\x42\x6f\x74\x74\x6f\x6d\x22\x5d\x2c\x5b\x22\ +\x77\x69\x64\x74\x68\x22\x2c\x22\x6d\x61\x72\x67\x69\x6e\x4c\x65\ +\x66\x74\x22\x2c\x22\x6d\x61\x72\x67\x69\x6e\x52\x69\x67\x68\x74\ +\x22\x2c\x22\x70\x61\x64\x64\x69\x6e\x67\x4c\x65\x66\x74\x22\x2c\ +\x22\x70\x61\x64\x64\x69\x6e\x67\x52\x69\x67\x68\x74\x22\x5d\x2c\ +\x5b\x22\x6f\x70\x61\x63\x69\x74\x79\x22\x5d\x5d\x2c\x63\x72\x3b\ +\x66\x2e\x66\x6e\x2e\x65\x78\x74\x65\x6e\x64\x28\x7b\x73\x68\x6f\ +\x77\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\ +\x29\x7b\x76\x61\x72\x20\x64\x2c\x65\x3b\x69\x66\x28\x61\x7c\x7c\ +\x61\x3d\x3d\x3d\x30\x29\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\ +\x73\x2e\x61\x6e\x69\x6d\x61\x74\x65\x28\x63\x75\x28\x22\x73\x68\ +\x6f\x77\x22\x2c\x33\x29\x2c\x61\x2c\x62\x2c\x63\x29\x3b\x66\x6f\ +\x72\x28\x76\x61\x72\x20\x67\x3d\x30\x2c\x68\x3d\x74\x68\x69\x73\ +\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x67\x3c\x68\x3b\x67\x2b\x2b\x29\ +\x64\x3d\x74\x68\x69\x73\x5b\x67\x5d\x2c\x64\x2e\x73\x74\x79\x6c\ +\x65\x26\x26\x28\x65\x3d\x64\x2e\x73\x74\x79\x6c\x65\x2e\x64\x69\ +\x73\x70\x6c\x61\x79\x2c\x21\x66\x2e\x5f\x64\x61\x74\x61\x28\x64\ +\x2c\x22\x6f\x6c\x64\x64\x69\x73\x70\x6c\x61\x79\x22\x29\x26\x26\ +\x65\x3d\x3d\x3d\x22\x6e\x6f\x6e\x65\x22\x26\x26\x28\x65\x3d\x64\ +\x2e\x73\x74\x79\x6c\x65\x2e\x64\x69\x73\x70\x6c\x61\x79\x3d\x22\ +\x22\x29\x2c\x65\x3d\x3d\x3d\x22\x22\x26\x26\x66\x2e\x63\x73\x73\ +\x28\x64\x2c\x22\x64\x69\x73\x70\x6c\x61\x79\x22\x29\x3d\x3d\x3d\ +\x22\x6e\x6f\x6e\x65\x22\x26\x26\x66\x2e\x5f\x64\x61\x74\x61\x28\ +\x64\x2c\x22\x6f\x6c\x64\x64\x69\x73\x70\x6c\x61\x79\x22\x2c\x63\ +\x76\x28\x64\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x29\x29\x29\x3b\ +\x66\x6f\x72\x28\x67\x3d\x30\x3b\x67\x3c\x68\x3b\x67\x2b\x2b\x29\ +\x7b\x64\x3d\x74\x68\x69\x73\x5b\x67\x5d\x3b\x69\x66\x28\x64\x2e\ +\x73\x74\x79\x6c\x65\x29\x7b\x65\x3d\x64\x2e\x73\x74\x79\x6c\x65\ +\x2e\x64\x69\x73\x70\x6c\x61\x79\x3b\x69\x66\x28\x65\x3d\x3d\x3d\ +\x22\x22\x7c\x7c\x65\x3d\x3d\x3d\x22\x6e\x6f\x6e\x65\x22\x29\x64\ +\x2e\x73\x74\x79\x6c\x65\x2e\x64\x69\x73\x70\x6c\x61\x79\x3d\x66\ +\x2e\x5f\x64\x61\x74\x61\x28\x64\x2c\x22\x6f\x6c\x64\x64\x69\x73\ +\x70\x6c\x61\x79\x22\x29\x7c\x7c\x22\x22\x7d\x7d\x72\x65\x74\x75\ +\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x68\x69\x64\x65\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x69\x66\ +\x28\x61\x7c\x7c\x61\x3d\x3d\x3d\x30\x29\x72\x65\x74\x75\x72\x6e\ +\x20\x74\x68\x69\x73\x2e\x61\x6e\x69\x6d\x61\x74\x65\x28\x63\x75\ +\x28\x22\x68\x69\x64\x65\x22\x2c\x33\x29\x2c\x61\x2c\x62\x2c\x63\ +\x29\x3b\x76\x61\x72\x20\x64\x2c\x65\x2c\x67\x3d\x30\x2c\x68\x3d\ +\x74\x68\x69\x73\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x66\x6f\x72\x28\ +\x3b\x67\x3c\x68\x3b\x67\x2b\x2b\x29\x64\x3d\x74\x68\x69\x73\x5b\ +\x67\x5d\x2c\x64\x2e\x73\x74\x79\x6c\x65\x26\x26\x28\x65\x3d\x66\ +\x2e\x63\x73\x73\x28\x64\x2c\x22\x64\x69\x73\x70\x6c\x61\x79\x22\ +\x29\x2c\x65\x21\x3d\x3d\x22\x6e\x6f\x6e\x65\x22\x26\x26\x21\x66\ +\x2e\x5f\x64\x61\x74\x61\x28\x64\x2c\x22\x6f\x6c\x64\x64\x69\x73\ +\x70\x6c\x61\x79\x22\x29\x26\x26\x66\x2e\x5f\x64\x61\x74\x61\x28\ +\x64\x2c\x22\x6f\x6c\x64\x64\x69\x73\x70\x6c\x61\x79\x22\x2c\x65\ +\x29\x29\x3b\x66\x6f\x72\x28\x67\x3d\x30\x3b\x67\x3c\x68\x3b\x67\ +\x2b\x2b\x29\x74\x68\x69\x73\x5b\x67\x5d\x2e\x73\x74\x79\x6c\x65\ +\x26\x26\x28\x74\x68\x69\x73\x5b\x67\x5d\x2e\x73\x74\x79\x6c\x65\ +\x2e\x64\x69\x73\x70\x6c\x61\x79\x3d\x22\x6e\x6f\x6e\x65\x22\x29\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x7d\x2c\x5f\x74\ +\x6f\x67\x67\x6c\x65\x3a\x66\x2e\x66\x6e\x2e\x74\x6f\x67\x67\x6c\ +\x65\x2c\x74\x6f\x67\x67\x6c\x65\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x61\x2c\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x3d\x74\ +\x79\x70\x65\x6f\x66\x20\x61\x3d\x3d\x22\x62\x6f\x6f\x6c\x65\x61\ +\x6e\x22\x3b\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x29\x26\x26\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x62\x29\x3f\x74\x68\x69\x73\x2e\x5f\x74\x6f\x67\x67\x6c\x65\ +\x2e\x61\x70\x70\x6c\x79\x28\x74\x68\x69\x73\x2c\x61\x72\x67\x75\ +\x6d\x65\x6e\x74\x73\x29\x3a\x61\x3d\x3d\x6e\x75\x6c\x6c\x7c\x7c\ +\x64\x3f\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x29\x7b\x76\x61\x72\x20\x62\x3d\x64\x3f\x61\ +\x3a\x66\x28\x74\x68\x69\x73\x29\x2e\x69\x73\x28\x22\x3a\x68\x69\ +\x64\x64\x65\x6e\x22\x29\x3b\x66\x28\x74\x68\x69\x73\x29\x5b\x62\ +\x3f\x22\x73\x68\x6f\x77\x22\x3a\x22\x68\x69\x64\x65\x22\x5d\x28\ +\x29\x7d\x29\x3a\x74\x68\x69\x73\x2e\x61\x6e\x69\x6d\x61\x74\x65\ +\x28\x63\x75\x28\x22\x74\x6f\x67\x67\x6c\x65\x22\x2c\x33\x29\x2c\ +\x61\x2c\x62\x2c\x63\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\ +\x69\x73\x7d\x2c\x66\x61\x64\x65\x54\x6f\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\x64\x29\x7b\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x66\x69\x6c\x74\x65\x72\x28\ +\x22\x3a\x68\x69\x64\x64\x65\x6e\x22\x29\x2e\x63\x73\x73\x28\x22\ +\x6f\x70\x61\x63\x69\x74\x79\x22\x2c\x30\x29\x2e\x73\x68\x6f\x77\ +\x28\x29\x2e\x65\x6e\x64\x28\x29\x2e\x61\x6e\x69\x6d\x61\x74\x65\ +\x28\x7b\x6f\x70\x61\x63\x69\x74\x79\x3a\x62\x7d\x2c\x61\x2c\x63\ +\x2c\x64\x29\x7d\x2c\x61\x6e\x69\x6d\x61\x74\x65\x3a\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\x64\x29\x7b\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x20\x67\x28\x29\x7b\x65\x2e\x71\x75\ +\x65\x75\x65\x3d\x3d\x3d\x21\x31\x26\x26\x66\x2e\x5f\x6d\x61\x72\ +\x6b\x28\x74\x68\x69\x73\x29\x3b\x76\x61\x72\x20\x62\x3d\x66\x2e\ +\x65\x78\x74\x65\x6e\x64\x28\x7b\x7d\x2c\x65\x29\x2c\x63\x3d\x74\ +\x68\x69\x73\x2e\x6e\x6f\x64\x65\x54\x79\x70\x65\x3d\x3d\x3d\x31\ +\x2c\x64\x3d\x63\x26\x26\x66\x28\x74\x68\x69\x73\x29\x2e\x69\x73\ +\x28\x22\x3a\x68\x69\x64\x64\x65\x6e\x22\x29\x2c\x67\x2c\x68\x2c\ +\x69\x2c\x6a\x2c\x6b\x2c\x6c\x2c\x6d\x2c\x6e\x2c\x6f\x3b\x62\x2e\ +\x61\x6e\x69\x6d\x61\x74\x65\x64\x50\x72\x6f\x70\x65\x72\x74\x69\ +\x65\x73\x3d\x7b\x7d\x3b\x66\x6f\x72\x28\x69\x20\x69\x6e\x20\x61\ +\x29\x7b\x67\x3d\x66\x2e\x63\x61\x6d\x65\x6c\x43\x61\x73\x65\x28\ +\x69\x29\x2c\x69\x21\x3d\x3d\x67\x26\x26\x28\x61\x5b\x67\x5d\x3d\ +\x61\x5b\x69\x5d\x2c\x64\x65\x6c\x65\x74\x65\x20\x61\x5b\x69\x5d\ +\x29\x2c\x68\x3d\x61\x5b\x67\x5d\x2c\x66\x2e\x69\x73\x41\x72\x72\ +\x61\x79\x28\x68\x29\x3f\x28\x62\x2e\x61\x6e\x69\x6d\x61\x74\x65\ +\x64\x50\x72\x6f\x70\x65\x72\x74\x69\x65\x73\x5b\x67\x5d\x3d\x68\ +\x5b\x31\x5d\x2c\x68\x3d\x61\x5b\x67\x5d\x3d\x68\x5b\x30\x5d\x29\ +\x3a\x62\x2e\x61\x6e\x69\x6d\x61\x74\x65\x64\x50\x72\x6f\x70\x65\ +\x72\x74\x69\x65\x73\x5b\x67\x5d\x3d\x62\x2e\x73\x70\x65\x63\x69\ +\x61\x6c\x45\x61\x73\x69\x6e\x67\x26\x26\x62\x2e\x73\x70\x65\x63\ +\x69\x61\x6c\x45\x61\x73\x69\x6e\x67\x5b\x67\x5d\x7c\x7c\x62\x2e\ +\x65\x61\x73\x69\x6e\x67\x7c\x7c\x22\x73\x77\x69\x6e\x67\x22\x3b\ +\x69\x66\x28\x68\x3d\x3d\x3d\x22\x68\x69\x64\x65\x22\x26\x26\x64\ +\x7c\x7c\x68\x3d\x3d\x3d\x22\x73\x68\x6f\x77\x22\x26\x26\x21\x64\ +\x29\x72\x65\x74\x75\x72\x6e\x20\x62\x2e\x63\x6f\x6d\x70\x6c\x65\ +\x74\x65\x2e\x63\x61\x6c\x6c\x28\x74\x68\x69\x73\x29\x3b\x63\x26\ +\x26\x28\x67\x3d\x3d\x3d\x22\x68\x65\x69\x67\x68\x74\x22\x7c\x7c\ +\x67\x3d\x3d\x3d\x22\x77\x69\x64\x74\x68\x22\x29\x26\x26\x28\x62\ +\x2e\x6f\x76\x65\x72\x66\x6c\x6f\x77\x3d\x5b\x74\x68\x69\x73\x2e\ +\x73\x74\x79\x6c\x65\x2e\x6f\x76\x65\x72\x66\x6c\x6f\x77\x2c\x74\ +\x68\x69\x73\x2e\x73\x74\x79\x6c\x65\x2e\x6f\x76\x65\x72\x66\x6c\ +\x6f\x77\x58\x2c\x74\x68\x69\x73\x2e\x73\x74\x79\x6c\x65\x2e\x6f\ +\x76\x65\x72\x66\x6c\x6f\x77\x59\x5d\x2c\x66\x2e\x63\x73\x73\x28\ +\x74\x68\x69\x73\x2c\x22\x64\x69\x73\x70\x6c\x61\x79\x22\x29\x3d\ +\x3d\x3d\x22\x69\x6e\x6c\x69\x6e\x65\x22\x26\x26\x66\x2e\x63\x73\ +\x73\x28\x74\x68\x69\x73\x2c\x22\x66\x6c\x6f\x61\x74\x22\x29\x3d\ +\x3d\x3d\x22\x6e\x6f\x6e\x65\x22\x26\x26\x28\x21\x66\x2e\x73\x75\ +\x70\x70\x6f\x72\x74\x2e\x69\x6e\x6c\x69\x6e\x65\x42\x6c\x6f\x63\ +\x6b\x4e\x65\x65\x64\x73\x4c\x61\x79\x6f\x75\x74\x7c\x7c\x63\x76\ +\x28\x74\x68\x69\x73\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x29\x3d\ +\x3d\x3d\x22\x69\x6e\x6c\x69\x6e\x65\x22\x3f\x74\x68\x69\x73\x2e\ +\x73\x74\x79\x6c\x65\x2e\x64\x69\x73\x70\x6c\x61\x79\x3d\x22\x69\ +\x6e\x6c\x69\x6e\x65\x2d\x62\x6c\x6f\x63\x6b\x22\x3a\x74\x68\x69\ +\x73\x2e\x73\x74\x79\x6c\x65\x2e\x7a\x6f\x6f\x6d\x3d\x31\x29\x29\ +\x7d\x62\x2e\x6f\x76\x65\x72\x66\x6c\x6f\x77\x21\x3d\x6e\x75\x6c\ +\x6c\x26\x26\x28\x74\x68\x69\x73\x2e\x73\x74\x79\x6c\x65\x2e\x6f\ +\x76\x65\x72\x66\x6c\x6f\x77\x3d\x22\x68\x69\x64\x64\x65\x6e\x22\ +\x29\x3b\x66\x6f\x72\x28\x69\x20\x69\x6e\x20\x61\x29\x6a\x3d\x6e\ +\x65\x77\x20\x66\x2e\x66\x78\x28\x74\x68\x69\x73\x2c\x62\x2c\x69\ +\x29\x2c\x68\x3d\x61\x5b\x69\x5d\x2c\x63\x6e\x2e\x74\x65\x73\x74\ +\x28\x68\x29\x3f\x28\x6f\x3d\x66\x2e\x5f\x64\x61\x74\x61\x28\x74\ +\x68\x69\x73\x2c\x22\x74\x6f\x67\x67\x6c\x65\x22\x2b\x69\x29\x7c\ +\x7c\x28\x68\x3d\x3d\x3d\x22\x74\x6f\x67\x67\x6c\x65\x22\x3f\x64\ +\x3f\x22\x73\x68\x6f\x77\x22\x3a\x22\x68\x69\x64\x65\x22\x3a\x30\ +\x29\x2c\x6f\x3f\x28\x66\x2e\x5f\x64\x61\x74\x61\x28\x74\x68\x69\ +\x73\x2c\x22\x74\x6f\x67\x67\x6c\x65\x22\x2b\x69\x2c\x6f\x3d\x3d\ +\x3d\x22\x73\x68\x6f\x77\x22\x3f\x22\x68\x69\x64\x65\x22\x3a\x22\ +\x73\x68\x6f\x77\x22\x29\x2c\x6a\x5b\x6f\x5d\x28\x29\x29\x3a\x6a\ +\x5b\x68\x5d\x28\x29\x29\x3a\x28\x6b\x3d\x63\x6f\x2e\x65\x78\x65\ +\x63\x28\x68\x29\x2c\x6c\x3d\x6a\x2e\x63\x75\x72\x28\x29\x2c\x6b\ +\x3f\x28\x6d\x3d\x70\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\x28\x6b\ +\x5b\x32\x5d\x29\x2c\x6e\x3d\x6b\x5b\x33\x5d\x7c\x7c\x28\x66\x2e\ +\x63\x73\x73\x4e\x75\x6d\x62\x65\x72\x5b\x69\x5d\x3f\x22\x22\x3a\ +\x22\x70\x78\x22\x29\x2c\x6e\x21\x3d\x3d\x22\x70\x78\x22\x26\x26\ +\x28\x66\x2e\x73\x74\x79\x6c\x65\x28\x74\x68\x69\x73\x2c\x69\x2c\ +\x28\x6d\x7c\x7c\x31\x29\x2b\x6e\x29\x2c\x6c\x3d\x28\x6d\x7c\x7c\ +\x31\x29\x2f\x6a\x2e\x63\x75\x72\x28\x29\x2a\x6c\x2c\x66\x2e\x73\ +\x74\x79\x6c\x65\x28\x74\x68\x69\x73\x2c\x69\x2c\x6c\x2b\x6e\x29\ +\x29\x2c\x6b\x5b\x31\x5d\x26\x26\x28\x6d\x3d\x28\x6b\x5b\x31\x5d\ +\x3d\x3d\x3d\x22\x2d\x3d\x22\x3f\x2d\x31\x3a\x31\x29\x2a\x6d\x2b\ +\x6c\x29\x2c\x6a\x2e\x63\x75\x73\x74\x6f\x6d\x28\x6c\x2c\x6d\x2c\ +\x6e\x29\x29\x3a\x6a\x2e\x63\x75\x73\x74\x6f\x6d\x28\x6c\x2c\x68\ +\x2c\x22\x22\x29\x29\x3b\x72\x65\x74\x75\x72\x6e\x21\x30\x7d\x76\ +\x61\x72\x20\x65\x3d\x66\x2e\x73\x70\x65\x65\x64\x28\x62\x2c\x63\ +\x2c\x64\x29\x3b\x69\x66\x28\x66\x2e\x69\x73\x45\x6d\x70\x74\x79\ +\x4f\x62\x6a\x65\x63\x74\x28\x61\x29\x29\x72\x65\x74\x75\x72\x6e\ +\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x65\x2e\x63\x6f\x6d\ +\x70\x6c\x65\x74\x65\x2c\x5b\x21\x31\x5d\x29\x3b\x61\x3d\x66\x2e\ +\x65\x78\x74\x65\x6e\x64\x28\x7b\x7d\x2c\x61\x29\x3b\x72\x65\x74\ +\x75\x72\x6e\x20\x65\x2e\x71\x75\x65\x75\x65\x3d\x3d\x3d\x21\x31\ +\x3f\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x67\x29\x3a\x74\x68\ +\x69\x73\x2e\x71\x75\x65\x75\x65\x28\x65\x2e\x71\x75\x65\x75\x65\ +\x2c\x67\x29\x7d\x2c\x73\x74\x6f\x70\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x2c\x63\x2c\x64\x29\x7b\x74\x79\x70\x65\x6f\x66\ +\x20\x61\x21\x3d\x22\x73\x74\x72\x69\x6e\x67\x22\x26\x26\x28\x64\ +\x3d\x63\x2c\x63\x3d\x61\x2c\x61\x3d\x62\x29\x2c\x63\x26\x26\x61\ +\x21\x3d\x3d\x21\x31\x26\x26\x74\x68\x69\x73\x2e\x71\x75\x65\x75\ +\x65\x28\x61\x7c\x7c\x22\x66\x78\x22\x2c\x5b\x5d\x29\x3b\x72\x65\ +\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\x28\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x20\x68\x28\x61\x2c\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\ +\x65\x3d\x62\x5b\x63\x5d\x3b\x66\x2e\x72\x65\x6d\x6f\x76\x65\x44\ +\x61\x74\x61\x28\x61\x2c\x63\x2c\x21\x30\x29\x2c\x65\x2e\x73\x74\ +\x6f\x70\x28\x64\x29\x7d\x76\x61\x72\x20\x62\x2c\x63\x3d\x21\x31\ +\x2c\x65\x3d\x66\x2e\x74\x69\x6d\x65\x72\x73\x2c\x67\x3d\x66\x2e\ +\x5f\x64\x61\x74\x61\x28\x74\x68\x69\x73\x29\x3b\x64\x7c\x7c\x66\ +\x2e\x5f\x75\x6e\x6d\x61\x72\x6b\x28\x21\x30\x2c\x74\x68\x69\x73\ +\x29\x3b\x69\x66\x28\x61\x3d\x3d\x6e\x75\x6c\x6c\x29\x66\x6f\x72\ +\x28\x62\x20\x69\x6e\x20\x67\x29\x67\x5b\x62\x5d\x26\x26\x67\x5b\ +\x62\x5d\x2e\x73\x74\x6f\x70\x26\x26\x62\x2e\x69\x6e\x64\x65\x78\ +\x4f\x66\x28\x22\x2e\x72\x75\x6e\x22\x29\x3d\x3d\x3d\x62\x2e\x6c\ +\x65\x6e\x67\x74\x68\x2d\x34\x26\x26\x68\x28\x74\x68\x69\x73\x2c\ +\x67\x2c\x62\x29\x3b\x65\x6c\x73\x65\x20\x67\x5b\x62\x3d\x61\x2b\ +\x22\x2e\x72\x75\x6e\x22\x5d\x26\x26\x67\x5b\x62\x5d\x2e\x73\x74\ +\x6f\x70\x26\x26\x68\x28\x74\x68\x69\x73\x2c\x67\x2c\x62\x29\x3b\ +\x66\x6f\x72\x28\x62\x3d\x65\x2e\x6c\x65\x6e\x67\x74\x68\x3b\x62\ +\x2d\x2d\x3b\x29\x65\x5b\x62\x5d\x2e\x65\x6c\x65\x6d\x3d\x3d\x3d\ +\x74\x68\x69\x73\x26\x26\x28\x61\x3d\x3d\x6e\x75\x6c\x6c\x7c\x7c\ +\x65\x5b\x62\x5d\x2e\x71\x75\x65\x75\x65\x3d\x3d\x3d\x61\x29\x26\ +\x26\x28\x64\x3f\x65\x5b\x62\x5d\x28\x21\x30\x29\x3a\x65\x5b\x62\ +\x5d\x2e\x73\x61\x76\x65\x53\x74\x61\x74\x65\x28\x29\x2c\x63\x3d\ +\x21\x30\x2c\x65\x2e\x73\x70\x6c\x69\x63\x65\x28\x62\x2c\x31\x29\ +\x29\x3b\x28\x21\x64\x7c\x7c\x21\x63\x29\x26\x26\x66\x2e\x64\x65\ +\x71\x75\x65\x75\x65\x28\x74\x68\x69\x73\x2c\x61\x29\x7d\x29\x7d\ +\x7d\x29\x2c\x66\x2e\x65\x61\x63\x68\x28\x7b\x73\x6c\x69\x64\x65\ +\x44\x6f\x77\x6e\x3a\x63\x75\x28\x22\x73\x68\x6f\x77\x22\x2c\x31\ +\x29\x2c\x73\x6c\x69\x64\x65\x55\x70\x3a\x63\x75\x28\x22\x68\x69\ +\x64\x65\x22\x2c\x31\x29\x2c\x73\x6c\x69\x64\x65\x54\x6f\x67\x67\ +\x6c\x65\x3a\x63\x75\x28\x22\x74\x6f\x67\x67\x6c\x65\x22\x2c\x31\ +\x29\x2c\x66\x61\x64\x65\x49\x6e\x3a\x7b\x6f\x70\x61\x63\x69\x74\ +\x79\x3a\x22\x73\x68\x6f\x77\x22\x7d\x2c\x66\x61\x64\x65\x4f\x75\ +\x74\x3a\x7b\x6f\x70\x61\x63\x69\x74\x79\x3a\x22\x68\x69\x64\x65\ +\x22\x7d\x2c\x66\x61\x64\x65\x54\x6f\x67\x67\x6c\x65\x3a\x7b\x6f\ +\x70\x61\x63\x69\x74\x79\x3a\x22\x74\x6f\x67\x67\x6c\x65\x22\x7d\ +\x7d\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\ +\x66\x2e\x66\x6e\x5b\x61\x5d\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x2c\x63\x2c\x64\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\ +\x68\x69\x73\x2e\x61\x6e\x69\x6d\x61\x74\x65\x28\x62\x2c\x61\x2c\ +\x63\x2c\x64\x29\x7d\x7d\x29\x2c\x66\x2e\x65\x78\x74\x65\x6e\x64\ +\x28\x7b\x73\x70\x65\x65\x64\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x61\x2c\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x3d\x61\x26\ +\x26\x74\x79\x70\x65\x6f\x66\x20\x61\x3d\x3d\x22\x6f\x62\x6a\x65\ +\x63\x74\x22\x3f\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x7b\x7d\x2c\ +\x61\x29\x3a\x7b\x63\x6f\x6d\x70\x6c\x65\x74\x65\x3a\x63\x7c\x7c\ +\x21\x63\x26\x26\x62\x7c\x7c\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x26\x26\x61\x2c\x64\x75\x72\x61\x74\x69\ +\x6f\x6e\x3a\x61\x2c\x65\x61\x73\x69\x6e\x67\x3a\x63\x26\x26\x62\ +\x7c\x7c\x62\x26\x26\x21\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x62\x29\x26\x26\x62\x7d\x3b\x64\x2e\x64\x75\x72\x61\ +\x74\x69\x6f\x6e\x3d\x66\x2e\x66\x78\x2e\x6f\x66\x66\x3f\x30\x3a\ +\x74\x79\x70\x65\x6f\x66\x20\x64\x2e\x64\x75\x72\x61\x74\x69\x6f\ +\x6e\x3d\x3d\x22\x6e\x75\x6d\x62\x65\x72\x22\x3f\x64\x2e\x64\x75\ +\x72\x61\x74\x69\x6f\x6e\x3a\x64\x2e\x64\x75\x72\x61\x74\x69\x6f\ +\x6e\x20\x69\x6e\x20\x66\x2e\x66\x78\x2e\x73\x70\x65\x65\x64\x73\ +\x3f\x66\x2e\x66\x78\x2e\x73\x70\x65\x65\x64\x73\x5b\x64\x2e\x64\ +\x75\x72\x61\x74\x69\x6f\x6e\x5d\x3a\x66\x2e\x66\x78\x2e\x73\x70\ +\x65\x65\x64\x73\x2e\x5f\x64\x65\x66\x61\x75\x6c\x74\x3b\x69\x66\ +\x28\x64\x2e\x71\x75\x65\x75\x65\x3d\x3d\x6e\x75\x6c\x6c\x7c\x7c\ +\x64\x2e\x71\x75\x65\x75\x65\x3d\x3d\x3d\x21\x30\x29\x64\x2e\x71\ +\x75\x65\x75\x65\x3d\x22\x66\x78\x22\x3b\x64\x2e\x6f\x6c\x64\x3d\ +\x64\x2e\x63\x6f\x6d\x70\x6c\x65\x74\x65\x2c\x64\x2e\x63\x6f\x6d\ +\x70\x6c\x65\x74\x65\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x29\x7b\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x64\ +\x2e\x6f\x6c\x64\x29\x26\x26\x64\x2e\x6f\x6c\x64\x2e\x63\x61\x6c\ +\x6c\x28\x74\x68\x69\x73\x29\x2c\x64\x2e\x71\x75\x65\x75\x65\x3f\ +\x66\x2e\x64\x65\x71\x75\x65\x75\x65\x28\x74\x68\x69\x73\x2c\x64\ +\x2e\x71\x75\x65\x75\x65\x29\x3a\x61\x21\x3d\x3d\x21\x31\x26\x26\ +\x66\x2e\x5f\x75\x6e\x6d\x61\x72\x6b\x28\x74\x68\x69\x73\x29\x7d\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x64\x7d\x2c\x65\x61\x73\x69\x6e\ +\x67\x3a\x7b\x6c\x69\x6e\x65\x61\x72\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\x64\x29\x7b\x72\x65\x74\x75\ +\x72\x6e\x20\x63\x2b\x64\x2a\x61\x7d\x2c\x73\x77\x69\x6e\x67\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x62\x2c\x63\x2c\x64\ +\x29\x7b\x72\x65\x74\x75\x72\x6e\x28\x2d\x4d\x61\x74\x68\x2e\x63\ +\x6f\x73\x28\x61\x2a\x4d\x61\x74\x68\x2e\x50\x49\x29\x2f\x32\x2b\ +\x2e\x35\x29\x2a\x64\x2b\x63\x7d\x7d\x2c\x74\x69\x6d\x65\x72\x73\ +\x3a\x5b\x5d\x2c\x66\x78\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x2c\x62\x2c\x63\x29\x7b\x74\x68\x69\x73\x2e\x6f\x70\x74\x69\ +\x6f\x6e\x73\x3d\x62\x2c\x74\x68\x69\x73\x2e\x65\x6c\x65\x6d\x3d\ +\x61\x2c\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x3d\x63\x2c\x62\x2e\ +\x6f\x72\x69\x67\x3d\x62\x2e\x6f\x72\x69\x67\x7c\x7c\x7b\x7d\x7d\ +\x7d\x29\x2c\x66\x2e\x66\x78\x2e\x70\x72\x6f\x74\x6f\x74\x79\x70\ +\x65\x3d\x7b\x75\x70\x64\x61\x74\x65\x3a\x66\x75\x6e\x63\x74\x69\ +\x6f\x6e\x28\x29\x7b\x74\x68\x69\x73\x2e\x6f\x70\x74\x69\x6f\x6e\ +\x73\x2e\x73\x74\x65\x70\x26\x26\x74\x68\x69\x73\x2e\x6f\x70\x74\ +\x69\x6f\x6e\x73\x2e\x73\x74\x65\x70\x2e\x63\x61\x6c\x6c\x28\x74\ +\x68\x69\x73\x2e\x65\x6c\x65\x6d\x2c\x74\x68\x69\x73\x2e\x6e\x6f\ +\x77\x2c\x74\x68\x69\x73\x29\x2c\x28\x66\x2e\x66\x78\x2e\x73\x74\ +\x65\x70\x5b\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x5d\x7c\x7c\x66\ +\x2e\x66\x78\x2e\x73\x74\x65\x70\x2e\x5f\x64\x65\x66\x61\x75\x6c\ +\x74\x29\x28\x74\x68\x69\x73\x29\x7d\x2c\x63\x75\x72\x3a\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x69\x66\x28\x74\x68\x69\x73\ +\x2e\x65\x6c\x65\x6d\x5b\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x5d\ +\x21\x3d\x6e\x75\x6c\x6c\x26\x26\x28\x21\x74\x68\x69\x73\x2e\x65\ +\x6c\x65\x6d\x2e\x73\x74\x79\x6c\x65\x7c\x7c\x74\x68\x69\x73\x2e\ +\x65\x6c\x65\x6d\x2e\x73\x74\x79\x6c\x65\x5b\x74\x68\x69\x73\x2e\ +\x70\x72\x6f\x70\x5d\x3d\x3d\x6e\x75\x6c\x6c\x29\x29\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x6c\x65\x6d\x5b\x74\x68\ +\x69\x73\x2e\x70\x72\x6f\x70\x5d\x3b\x76\x61\x72\x20\x61\x2c\x62\ +\x3d\x66\x2e\x63\x73\x73\x28\x74\x68\x69\x73\x2e\x65\x6c\x65\x6d\ +\x2c\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x29\x3b\x72\x65\x74\x75\ +\x72\x6e\x20\x69\x73\x4e\x61\x4e\x28\x61\x3d\x70\x61\x72\x73\x65\ +\x46\x6c\x6f\x61\x74\x28\x62\x29\x29\x3f\x21\x62\x7c\x7c\x62\x3d\ +\x3d\x3d\x22\x61\x75\x74\x6f\x22\x3f\x30\x3a\x62\x3a\x61\x7d\x2c\ +\x63\x75\x73\x74\x6f\x6d\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x61\x2c\x63\x2c\x64\x29\x7b\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\ +\x68\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x65\x2e\x73\x74\ +\x65\x70\x28\x61\x29\x7d\x76\x61\x72\x20\x65\x3d\x74\x68\x69\x73\ +\x2c\x67\x3d\x66\x2e\x66\x78\x3b\x74\x68\x69\x73\x2e\x73\x74\x61\ +\x72\x74\x54\x69\x6d\x65\x3d\x63\x72\x7c\x7c\x63\x73\x28\x29\x2c\ +\x74\x68\x69\x73\x2e\x65\x6e\x64\x3d\x63\x2c\x74\x68\x69\x73\x2e\ +\x6e\x6f\x77\x3d\x74\x68\x69\x73\x2e\x73\x74\x61\x72\x74\x3d\x61\ +\x2c\x74\x68\x69\x73\x2e\x70\x6f\x73\x3d\x74\x68\x69\x73\x2e\x73\ +\x74\x61\x74\x65\x3d\x30\x2c\x74\x68\x69\x73\x2e\x75\x6e\x69\x74\ +\x3d\x64\x7c\x7c\x74\x68\x69\x73\x2e\x75\x6e\x69\x74\x7c\x7c\x28\ +\x66\x2e\x63\x73\x73\x4e\x75\x6d\x62\x65\x72\x5b\x74\x68\x69\x73\ +\x2e\x70\x72\x6f\x70\x5d\x3f\x22\x22\x3a\x22\x70\x78\x22\x29\x2c\ +\x68\x2e\x71\x75\x65\x75\x65\x3d\x74\x68\x69\x73\x2e\x6f\x70\x74\ +\x69\x6f\x6e\x73\x2e\x71\x75\x65\x75\x65\x2c\x68\x2e\x65\x6c\x65\ +\x6d\x3d\x74\x68\x69\x73\x2e\x65\x6c\x65\x6d\x2c\x68\x2e\x73\x61\ +\x76\x65\x53\x74\x61\x74\x65\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x29\x7b\x65\x2e\x6f\x70\x74\x69\x6f\x6e\x73\x2e\x68\x69\x64\ +\x65\x26\x26\x66\x2e\x5f\x64\x61\x74\x61\x28\x65\x2e\x65\x6c\x65\ +\x6d\x2c\x22\x66\x78\x73\x68\x6f\x77\x22\x2b\x65\x2e\x70\x72\x6f\ +\x70\x29\x3d\x3d\x3d\x62\x26\x26\x66\x2e\x5f\x64\x61\x74\x61\x28\ +\x65\x2e\x65\x6c\x65\x6d\x2c\x22\x66\x78\x73\x68\x6f\x77\x22\x2b\ +\x65\x2e\x70\x72\x6f\x70\x2c\x65\x2e\x73\x74\x61\x72\x74\x29\x7d\ +\x2c\x68\x28\x29\x26\x26\x66\x2e\x74\x69\x6d\x65\x72\x73\x2e\x70\ +\x75\x73\x68\x28\x68\x29\x26\x26\x21\x63\x70\x26\x26\x28\x63\x70\ +\x3d\x73\x65\x74\x49\x6e\x74\x65\x72\x76\x61\x6c\x28\x67\x2e\x74\ +\x69\x63\x6b\x2c\x67\x2e\x69\x6e\x74\x65\x72\x76\x61\x6c\x29\x29\ +\x7d\x2c\x73\x68\x6f\x77\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x29\x7b\x76\x61\x72\x20\x61\x3d\x66\x2e\x5f\x64\x61\x74\x61\x28\ +\x74\x68\x69\x73\x2e\x65\x6c\x65\x6d\x2c\x22\x66\x78\x73\x68\x6f\ +\x77\x22\x2b\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x29\x3b\x74\x68\ +\x69\x73\x2e\x6f\x70\x74\x69\x6f\x6e\x73\x2e\x6f\x72\x69\x67\x5b\ +\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x5d\x3d\x61\x7c\x7c\x66\x2e\ +\x73\x74\x79\x6c\x65\x28\x74\x68\x69\x73\x2e\x65\x6c\x65\x6d\x2c\ +\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x29\x2c\x74\x68\x69\x73\x2e\ +\x6f\x70\x74\x69\x6f\x6e\x73\x2e\x73\x68\x6f\x77\x3d\x21\x30\x2c\ +\x61\x21\x3d\x3d\x62\x3f\x74\x68\x69\x73\x2e\x63\x75\x73\x74\x6f\ +\x6d\x28\x74\x68\x69\x73\x2e\x63\x75\x72\x28\x29\x2c\x61\x29\x3a\ +\x74\x68\x69\x73\x2e\x63\x75\x73\x74\x6f\x6d\x28\x74\x68\x69\x73\ +\x2e\x70\x72\x6f\x70\x3d\x3d\x3d\x22\x77\x69\x64\x74\x68\x22\x7c\ +\x7c\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x3d\x3d\x3d\x22\x68\x65\ +\x69\x67\x68\x74\x22\x3f\x31\x3a\x30\x2c\x74\x68\x69\x73\x2e\x63\ +\x75\x72\x28\x29\x29\x2c\x66\x28\x74\x68\x69\x73\x2e\x65\x6c\x65\ +\x6d\x29\x2e\x73\x68\x6f\x77\x28\x29\x7d\x2c\x68\x69\x64\x65\x3a\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x74\x68\x69\x73\x2e\ +\x6f\x70\x74\x69\x6f\x6e\x73\x2e\x6f\x72\x69\x67\x5b\x74\x68\x69\ +\x73\x2e\x70\x72\x6f\x70\x5d\x3d\x66\x2e\x5f\x64\x61\x74\x61\x28\ +\x74\x68\x69\x73\x2e\x65\x6c\x65\x6d\x2c\x22\x66\x78\x73\x68\x6f\ +\x77\x22\x2b\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x29\x7c\x7c\x66\ +\x2e\x73\x74\x79\x6c\x65\x28\x74\x68\x69\x73\x2e\x65\x6c\x65\x6d\ +\x2c\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x29\x2c\x74\x68\x69\x73\ +\x2e\x6f\x70\x74\x69\x6f\x6e\x73\x2e\x68\x69\x64\x65\x3d\x21\x30\ +\x2c\x74\x68\x69\x73\x2e\x63\x75\x73\x74\x6f\x6d\x28\x74\x68\x69\ +\x73\x2e\x63\x75\x72\x28\x29\x2c\x30\x29\x7d\x2c\x73\x74\x65\x70\ +\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\ +\x20\x62\x2c\x63\x2c\x64\x2c\x65\x3d\x63\x72\x7c\x7c\x63\x73\x28\ +\x29\x2c\x67\x3d\x21\x30\x2c\x68\x3d\x74\x68\x69\x73\x2e\x65\x6c\ +\x65\x6d\x2c\x69\x3d\x74\x68\x69\x73\x2e\x6f\x70\x74\x69\x6f\x6e\ +\x73\x3b\x69\x66\x28\x61\x7c\x7c\x65\x3e\x3d\x69\x2e\x64\x75\x72\ +\x61\x74\x69\x6f\x6e\x2b\x74\x68\x69\x73\x2e\x73\x74\x61\x72\x74\ +\x54\x69\x6d\x65\x29\x7b\x74\x68\x69\x73\x2e\x6e\x6f\x77\x3d\x74\ +\x68\x69\x73\x2e\x65\x6e\x64\x2c\x74\x68\x69\x73\x2e\x70\x6f\x73\ +\x3d\x74\x68\x69\x73\x2e\x73\x74\x61\x74\x65\x3d\x31\x2c\x74\x68\ +\x69\x73\x2e\x75\x70\x64\x61\x74\x65\x28\x29\x2c\x69\x2e\x61\x6e\ +\x69\x6d\x61\x74\x65\x64\x50\x72\x6f\x70\x65\x72\x74\x69\x65\x73\ +\x5b\x74\x68\x69\x73\x2e\x70\x72\x6f\x70\x5d\x3d\x21\x30\x3b\x66\ +\x6f\x72\x28\x62\x20\x69\x6e\x20\x69\x2e\x61\x6e\x69\x6d\x61\x74\ +\x65\x64\x50\x72\x6f\x70\x65\x72\x74\x69\x65\x73\x29\x69\x2e\x61\ +\x6e\x69\x6d\x61\x74\x65\x64\x50\x72\x6f\x70\x65\x72\x74\x69\x65\ +\x73\x5b\x62\x5d\x21\x3d\x3d\x21\x30\x26\x26\x28\x67\x3d\x21\x31\ +\x29\x3b\x69\x66\x28\x67\x29\x7b\x69\x2e\x6f\x76\x65\x72\x66\x6c\ +\x6f\x77\x21\x3d\x6e\x75\x6c\x6c\x26\x26\x21\x66\x2e\x73\x75\x70\ +\x70\x6f\x72\x74\x2e\x73\x68\x72\x69\x6e\x6b\x57\x72\x61\x70\x42\ +\x6c\x6f\x63\x6b\x73\x26\x26\x66\x2e\x65\x61\x63\x68\x28\x5b\x22\ +\x22\x2c\x22\x58\x22\x2c\x22\x59\x22\x5d\x2c\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x2c\x62\x29\x7b\x68\x2e\x73\x74\x79\x6c\x65\ +\x5b\x22\x6f\x76\x65\x72\x66\x6c\x6f\x77\x22\x2b\x62\x5d\x3d\x69\ +\x2e\x6f\x76\x65\x72\x66\x6c\x6f\x77\x5b\x61\x5d\x7d\x29\x2c\x69\ +\x2e\x68\x69\x64\x65\x26\x26\x66\x28\x68\x29\x2e\x68\x69\x64\x65\ +\x28\x29\x3b\x69\x66\x28\x69\x2e\x68\x69\x64\x65\x7c\x7c\x69\x2e\ +\x73\x68\x6f\x77\x29\x66\x6f\x72\x28\x62\x20\x69\x6e\x20\x69\x2e\ +\x61\x6e\x69\x6d\x61\x74\x65\x64\x50\x72\x6f\x70\x65\x72\x74\x69\ +\x65\x73\x29\x66\x2e\x73\x74\x79\x6c\x65\x28\x68\x2c\x62\x2c\x69\ +\x2e\x6f\x72\x69\x67\x5b\x62\x5d\x29\x2c\x66\x2e\x72\x65\x6d\x6f\ +\x76\x65\x44\x61\x74\x61\x28\x68\x2c\x22\x66\x78\x73\x68\x6f\x77\ +\x22\x2b\x62\x2c\x21\x30\x29\x2c\x66\x2e\x72\x65\x6d\x6f\x76\x65\ +\x44\x61\x74\x61\x28\x68\x2c\x22\x74\x6f\x67\x67\x6c\x65\x22\x2b\ +\x62\x2c\x21\x30\x29\x3b\x64\x3d\x69\x2e\x63\x6f\x6d\x70\x6c\x65\ +\x74\x65\x2c\x64\x26\x26\x28\x69\x2e\x63\x6f\x6d\x70\x6c\x65\x74\ +\x65\x3d\x21\x31\x2c\x64\x2e\x63\x61\x6c\x6c\x28\x68\x29\x29\x7d\ +\x72\x65\x74\x75\x72\x6e\x21\x31\x7d\x69\x2e\x64\x75\x72\x61\x74\ +\x69\x6f\x6e\x3d\x3d\x49\x6e\x66\x69\x6e\x69\x74\x79\x3f\x74\x68\ +\x69\x73\x2e\x6e\x6f\x77\x3d\x65\x3a\x28\x63\x3d\x65\x2d\x74\x68\ +\x69\x73\x2e\x73\x74\x61\x72\x74\x54\x69\x6d\x65\x2c\x74\x68\x69\ +\x73\x2e\x73\x74\x61\x74\x65\x3d\x63\x2f\x69\x2e\x64\x75\x72\x61\ +\x74\x69\x6f\x6e\x2c\x74\x68\x69\x73\x2e\x70\x6f\x73\x3d\x66\x2e\ +\x65\x61\x73\x69\x6e\x67\x5b\x69\x2e\x61\x6e\x69\x6d\x61\x74\x65\ +\x64\x50\x72\x6f\x70\x65\x72\x74\x69\x65\x73\x5b\x74\x68\x69\x73\ +\x2e\x70\x72\x6f\x70\x5d\x5d\x28\x74\x68\x69\x73\x2e\x73\x74\x61\ +\x74\x65\x2c\x63\x2c\x30\x2c\x31\x2c\x69\x2e\x64\x75\x72\x61\x74\ +\x69\x6f\x6e\x29\x2c\x74\x68\x69\x73\x2e\x6e\x6f\x77\x3d\x74\x68\ +\x69\x73\x2e\x73\x74\x61\x72\x74\x2b\x28\x74\x68\x69\x73\x2e\x65\ +\x6e\x64\x2d\x74\x68\x69\x73\x2e\x73\x74\x61\x72\x74\x29\x2a\x74\ +\x68\x69\x73\x2e\x70\x6f\x73\x29\x2c\x74\x68\x69\x73\x2e\x75\x70\ +\x64\x61\x74\x65\x28\x29\x3b\x72\x65\x74\x75\x72\x6e\x21\x30\x7d\ +\x7d\x2c\x66\x2e\x65\x78\x74\x65\x6e\x64\x28\x66\x2e\x66\x78\x2c\ +\x7b\x74\x69\x63\x6b\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\ +\x7b\x76\x61\x72\x20\x61\x2c\x62\x3d\x66\x2e\x74\x69\x6d\x65\x72\ +\x73\x2c\x63\x3d\x30\x3b\x66\x6f\x72\x28\x3b\x63\x3c\x62\x2e\x6c\ +\x65\x6e\x67\x74\x68\x3b\x63\x2b\x2b\x29\x61\x3d\x62\x5b\x63\x5d\ +\x2c\x21\x61\x28\x29\x26\x26\x62\x5b\x63\x5d\x3d\x3d\x3d\x61\x26\ +\x26\x62\x2e\x73\x70\x6c\x69\x63\x65\x28\x63\x2d\x2d\x2c\x31\x29\ +\x3b\x62\x2e\x6c\x65\x6e\x67\x74\x68\x7c\x7c\x66\x2e\x66\x78\x2e\ +\x73\x74\x6f\x70\x28\x29\x7d\x2c\x69\x6e\x74\x65\x72\x76\x61\x6c\ +\x3a\x31\x33\x2c\x73\x74\x6f\x70\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x29\x7b\x63\x6c\x65\x61\x72\x49\x6e\x74\x65\x72\x76\x61\ +\x6c\x28\x63\x70\x29\x2c\x63\x70\x3d\x6e\x75\x6c\x6c\x7d\x2c\x73\ +\x70\x65\x65\x64\x73\x3a\x7b\x73\x6c\x6f\x77\x3a\x36\x30\x30\x2c\ +\x66\x61\x73\x74\x3a\x32\x30\x30\x2c\x5f\x64\x65\x66\x61\x75\x6c\ +\x74\x3a\x34\x30\x30\x7d\x2c\x73\x74\x65\x70\x3a\x7b\x6f\x70\x61\ +\x63\x69\x74\x79\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\ +\x7b\x66\x2e\x73\x74\x79\x6c\x65\x28\x61\x2e\x65\x6c\x65\x6d\x2c\ +\x22\x6f\x70\x61\x63\x69\x74\x79\x22\x2c\x61\x2e\x6e\x6f\x77\x29\ +\x7d\x2c\x5f\x64\x65\x66\x61\x75\x6c\x74\x3a\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x61\x2e\x65\x6c\x65\x6d\x2e\x73\x74\ +\x79\x6c\x65\x26\x26\x61\x2e\x65\x6c\x65\x6d\x2e\x73\x74\x79\x6c\ +\x65\x5b\x61\x2e\x70\x72\x6f\x70\x5d\x21\x3d\x6e\x75\x6c\x6c\x3f\ +\x61\x2e\x65\x6c\x65\x6d\x2e\x73\x74\x79\x6c\x65\x5b\x61\x2e\x70\ +\x72\x6f\x70\x5d\x3d\x61\x2e\x6e\x6f\x77\x2b\x61\x2e\x75\x6e\x69\ +\x74\x3a\x61\x2e\x65\x6c\x65\x6d\x5b\x61\x2e\x70\x72\x6f\x70\x5d\ +\x3d\x61\x2e\x6e\x6f\x77\x7d\x7d\x7d\x29\x2c\x66\x2e\x65\x61\x63\ +\x68\x28\x5b\x22\x77\x69\x64\x74\x68\x22\x2c\x22\x68\x65\x69\x67\ +\x68\x74\x22\x5d\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\ +\x62\x29\x7b\x66\x2e\x66\x78\x2e\x73\x74\x65\x70\x5b\x62\x5d\x3d\ +\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x66\x2e\x73\x74\ +\x79\x6c\x65\x28\x61\x2e\x65\x6c\x65\x6d\x2c\x62\x2c\x4d\x61\x74\ +\x68\x2e\x6d\x61\x78\x28\x30\x2c\x61\x2e\x6e\x6f\x77\x29\x2b\x61\ +\x2e\x75\x6e\x69\x74\x29\x7d\x7d\x29\x2c\x66\x2e\x65\x78\x70\x72\ +\x26\x26\x66\x2e\x65\x78\x70\x72\x2e\x66\x69\x6c\x74\x65\x72\x73\ +\x26\x26\x28\x66\x2e\x65\x78\x70\x72\x2e\x66\x69\x6c\x74\x65\x72\ +\x73\x2e\x61\x6e\x69\x6d\x61\x74\x65\x64\x3d\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\ +\x67\x72\x65\x70\x28\x66\x2e\x74\x69\x6d\x65\x72\x73\x2c\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x62\x29\x7b\x72\x65\x74\x75\x72\x6e\ +\x20\x61\x3d\x3d\x3d\x62\x2e\x65\x6c\x65\x6d\x7d\x29\x2e\x6c\x65\ +\x6e\x67\x74\x68\x7d\x29\x3b\x76\x61\x72\x20\x63\x77\x3d\x2f\x5e\ +\x74\x28\x3f\x3a\x61\x62\x6c\x65\x7c\x64\x7c\x68\x29\x24\x2f\x69\ +\x2c\x63\x78\x3d\x2f\x5e\x28\x3f\x3a\x62\x6f\x64\x79\x7c\x68\x74\ +\x6d\x6c\x29\x24\x2f\x69\x3b\x22\x67\x65\x74\x42\x6f\x75\x6e\x64\ +\x69\x6e\x67\x43\x6c\x69\x65\x6e\x74\x52\x65\x63\x74\x22\x69\x6e\ +\x20\x63\x2e\x64\x6f\x63\x75\x6d\x65\x6e\x74\x45\x6c\x65\x6d\x65\ +\x6e\x74\x3f\x66\x2e\x66\x6e\x2e\x6f\x66\x66\x73\x65\x74\x3d\x66\ +\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\ +\x3d\x74\x68\x69\x73\x5b\x30\x5d\x2c\x63\x3b\x69\x66\x28\x61\x29\ +\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\x68\ +\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\x29\x7b\x66\x2e\x6f\ +\x66\x66\x73\x65\x74\x2e\x73\x65\x74\x4f\x66\x66\x73\x65\x74\x28\ +\x74\x68\x69\x73\x2c\x61\x2c\x62\x29\x7d\x29\x3b\x69\x66\x28\x21\ +\x62\x7c\x7c\x21\x62\x2e\x6f\x77\x6e\x65\x72\x44\x6f\x63\x75\x6d\ +\x65\x6e\x74\x29\x72\x65\x74\x75\x72\x6e\x20\x6e\x75\x6c\x6c\x3b\ +\x69\x66\x28\x62\x3d\x3d\x3d\x62\x2e\x6f\x77\x6e\x65\x72\x44\x6f\ +\x63\x75\x6d\x65\x6e\x74\x2e\x62\x6f\x64\x79\x29\x72\x65\x74\x75\ +\x72\x6e\x20\x66\x2e\x6f\x66\x66\x73\x65\x74\x2e\x62\x6f\x64\x79\ +\x4f\x66\x66\x73\x65\x74\x28\x62\x29\x3b\x74\x72\x79\x7b\x63\x3d\ +\x62\x2e\x67\x65\x74\x42\x6f\x75\x6e\x64\x69\x6e\x67\x43\x6c\x69\ +\x65\x6e\x74\x52\x65\x63\x74\x28\x29\x7d\x63\x61\x74\x63\x68\x28\ +\x64\x29\x7b\x7d\x76\x61\x72\x20\x65\x3d\x62\x2e\x6f\x77\x6e\x65\ +\x72\x44\x6f\x63\x75\x6d\x65\x6e\x74\x2c\x67\x3d\x65\x2e\x64\x6f\ +\x63\x75\x6d\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x3b\x69\x66\ +\x28\x21\x63\x7c\x7c\x21\x66\x2e\x63\x6f\x6e\x74\x61\x69\x6e\x73\ +\x28\x67\x2c\x62\x29\x29\x72\x65\x74\x75\x72\x6e\x20\x63\x3f\x7b\ +\x74\x6f\x70\x3a\x63\x2e\x74\x6f\x70\x2c\x6c\x65\x66\x74\x3a\x63\ +\x2e\x6c\x65\x66\x74\x7d\x3a\x7b\x74\x6f\x70\x3a\x30\x2c\x6c\x65\ +\x66\x74\x3a\x30\x7d\x3b\x76\x61\x72\x20\x68\x3d\x65\x2e\x62\x6f\ +\x64\x79\x2c\x69\x3d\x63\x79\x28\x65\x29\x2c\x6a\x3d\x67\x2e\x63\ +\x6c\x69\x65\x6e\x74\x54\x6f\x70\x7c\x7c\x68\x2e\x63\x6c\x69\x65\ +\x6e\x74\x54\x6f\x70\x7c\x7c\x30\x2c\x6b\x3d\x67\x2e\x63\x6c\x69\ +\x65\x6e\x74\x4c\x65\x66\x74\x7c\x7c\x68\x2e\x63\x6c\x69\x65\x6e\ +\x74\x4c\x65\x66\x74\x7c\x7c\x30\x2c\x6c\x3d\x69\x2e\x70\x61\x67\ +\x65\x59\x4f\x66\x66\x73\x65\x74\x7c\x7c\x66\x2e\x73\x75\x70\x70\ +\x6f\x72\x74\x2e\x62\x6f\x78\x4d\x6f\x64\x65\x6c\x26\x26\x67\x2e\ +\x73\x63\x72\x6f\x6c\x6c\x54\x6f\x70\x7c\x7c\x68\x2e\x73\x63\x72\ +\x6f\x6c\x6c\x54\x6f\x70\x2c\x6d\x3d\x69\x2e\x70\x61\x67\x65\x58\ +\x4f\x66\x66\x73\x65\x74\x7c\x7c\x66\x2e\x73\x75\x70\x70\x6f\x72\ +\x74\x2e\x62\x6f\x78\x4d\x6f\x64\x65\x6c\x26\x26\x67\x2e\x73\x63\ +\x72\x6f\x6c\x6c\x4c\x65\x66\x74\x7c\x7c\x68\x2e\x73\x63\x72\x6f\ +\x6c\x6c\x4c\x65\x66\x74\x2c\x6e\x3d\x63\x2e\x74\x6f\x70\x2b\x6c\ +\x2d\x6a\x2c\x6f\x3d\x63\x2e\x6c\x65\x66\x74\x2b\x6d\x2d\x6b\x3b\ +\x72\x65\x74\x75\x72\x6e\x7b\x74\x6f\x70\x3a\x6e\x2c\x6c\x65\x66\ +\x74\x3a\x6f\x7d\x7d\x3a\x66\x2e\x66\x6e\x2e\x6f\x66\x66\x73\x65\ +\x74\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\ +\x72\x20\x62\x3d\x74\x68\x69\x73\x5b\x30\x5d\x3b\x69\x66\x28\x61\ +\x29\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x65\x61\x63\ +\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\x29\x7b\x66\x2e\ +\x6f\x66\x66\x73\x65\x74\x2e\x73\x65\x74\x4f\x66\x66\x73\x65\x74\ +\x28\x74\x68\x69\x73\x2c\x61\x2c\x62\x29\x7d\x29\x3b\x69\x66\x28\ +\x21\x62\x7c\x7c\x21\x62\x2e\x6f\x77\x6e\x65\x72\x44\x6f\x63\x75\ +\x6d\x65\x6e\x74\x29\x72\x65\x74\x75\x72\x6e\x20\x6e\x75\x6c\x6c\ +\x3b\x69\x66\x28\x62\x3d\x3d\x3d\x62\x2e\x6f\x77\x6e\x65\x72\x44\ +\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x62\x6f\x64\x79\x29\x72\x65\x74\ +\x75\x72\x6e\x20\x66\x2e\x6f\x66\x66\x73\x65\x74\x2e\x62\x6f\x64\ +\x79\x4f\x66\x66\x73\x65\x74\x28\x62\x29\x3b\x76\x61\x72\x20\x63\ +\x2c\x64\x3d\x62\x2e\x6f\x66\x66\x73\x65\x74\x50\x61\x72\x65\x6e\ +\x74\x2c\x65\x3d\x62\x2c\x67\x3d\x62\x2e\x6f\x77\x6e\x65\x72\x44\ +\x6f\x63\x75\x6d\x65\x6e\x74\x2c\x68\x3d\x67\x2e\x64\x6f\x63\x75\ +\x6d\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x2c\x69\x3d\x67\x2e\ +\x62\x6f\x64\x79\x2c\x6a\x3d\x67\x2e\x64\x65\x66\x61\x75\x6c\x74\ +\x56\x69\x65\x77\x2c\x6b\x3d\x6a\x3f\x6a\x2e\x67\x65\x74\x43\x6f\ +\x6d\x70\x75\x74\x65\x64\x53\x74\x79\x6c\x65\x28\x62\x2c\x6e\x75\ +\x6c\x6c\x29\x3a\x62\x2e\x63\x75\x72\x72\x65\x6e\x74\x53\x74\x79\ +\x6c\x65\x2c\x6c\x3d\x62\x2e\x6f\x66\x66\x73\x65\x74\x54\x6f\x70\ +\x2c\x6d\x3d\x62\x2e\x6f\x66\x66\x73\x65\x74\x4c\x65\x66\x74\x3b\ +\x77\x68\x69\x6c\x65\x28\x28\x62\x3d\x62\x2e\x70\x61\x72\x65\x6e\ +\x74\x4e\x6f\x64\x65\x29\x26\x26\x62\x21\x3d\x3d\x69\x26\x26\x62\ +\x21\x3d\x3d\x68\x29\x7b\x69\x66\x28\x66\x2e\x73\x75\x70\x70\x6f\ +\x72\x74\x2e\x66\x69\x78\x65\x64\x50\x6f\x73\x69\x74\x69\x6f\x6e\ +\x26\x26\x6b\x2e\x70\x6f\x73\x69\x74\x69\x6f\x6e\x3d\x3d\x3d\x22\ +\x66\x69\x78\x65\x64\x22\x29\x62\x72\x65\x61\x6b\x3b\x63\x3d\x6a\ +\x3f\x6a\x2e\x67\x65\x74\x43\x6f\x6d\x70\x75\x74\x65\x64\x53\x74\ +\x79\x6c\x65\x28\x62\x2c\x6e\x75\x6c\x6c\x29\x3a\x62\x2e\x63\x75\ +\x72\x72\x65\x6e\x74\x53\x74\x79\x6c\x65\x2c\x6c\x2d\x3d\x62\x2e\ +\x73\x63\x72\x6f\x6c\x6c\x54\x6f\x70\x2c\x6d\x2d\x3d\x62\x2e\x73\ +\x63\x72\x6f\x6c\x6c\x4c\x65\x66\x74\x2c\x62\x3d\x3d\x3d\x64\x26\ +\x26\x28\x6c\x2b\x3d\x62\x2e\x6f\x66\x66\x73\x65\x74\x54\x6f\x70\ +\x2c\x6d\x2b\x3d\x62\x2e\x6f\x66\x66\x73\x65\x74\x4c\x65\x66\x74\ +\x2c\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\x64\x6f\x65\x73\x4e\ +\x6f\x74\x41\x64\x64\x42\x6f\x72\x64\x65\x72\x26\x26\x28\x21\x66\ +\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\x64\x6f\x65\x73\x41\x64\x64\ +\x42\x6f\x72\x64\x65\x72\x46\x6f\x72\x54\x61\x62\x6c\x65\x41\x6e\ +\x64\x43\x65\x6c\x6c\x73\x7c\x7c\x21\x63\x77\x2e\x74\x65\x73\x74\ +\x28\x62\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x29\x29\x26\x26\x28\ +\x6c\x2b\x3d\x70\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\x28\x63\x2e\ +\x62\x6f\x72\x64\x65\x72\x54\x6f\x70\x57\x69\x64\x74\x68\x29\x7c\ +\x7c\x30\x2c\x6d\x2b\x3d\x70\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\ +\x28\x63\x2e\x62\x6f\x72\x64\x65\x72\x4c\x65\x66\x74\x57\x69\x64\ +\x74\x68\x29\x7c\x7c\x30\x29\x2c\x65\x3d\x64\x2c\x64\x3d\x62\x2e\ +\x6f\x66\x66\x73\x65\x74\x50\x61\x72\x65\x6e\x74\x29\x2c\x66\x2e\ +\x73\x75\x70\x70\x6f\x72\x74\x2e\x73\x75\x62\x74\x72\x61\x63\x74\ +\x73\x42\x6f\x72\x64\x65\x72\x46\x6f\x72\x4f\x76\x65\x72\x66\x6c\ +\x6f\x77\x4e\x6f\x74\x56\x69\x73\x69\x62\x6c\x65\x26\x26\x63\x2e\ +\x6f\x76\x65\x72\x66\x6c\x6f\x77\x21\x3d\x3d\x22\x76\x69\x73\x69\ +\x62\x6c\x65\x22\x26\x26\x28\x6c\x2b\x3d\x70\x61\x72\x73\x65\x46\ +\x6c\x6f\x61\x74\x28\x63\x2e\x62\x6f\x72\x64\x65\x72\x54\x6f\x70\ +\x57\x69\x64\x74\x68\x29\x7c\x7c\x30\x2c\x6d\x2b\x3d\x70\x61\x72\ +\x73\x65\x46\x6c\x6f\x61\x74\x28\x63\x2e\x62\x6f\x72\x64\x65\x72\ +\x4c\x65\x66\x74\x57\x69\x64\x74\x68\x29\x7c\x7c\x30\x29\x2c\x6b\ +\x3d\x63\x7d\x69\x66\x28\x6b\x2e\x70\x6f\x73\x69\x74\x69\x6f\x6e\ +\x3d\x3d\x3d\x22\x72\x65\x6c\x61\x74\x69\x76\x65\x22\x7c\x7c\x6b\ +\x2e\x70\x6f\x73\x69\x74\x69\x6f\x6e\x3d\x3d\x3d\x22\x73\x74\x61\ +\x74\x69\x63\x22\x29\x6c\x2b\x3d\x69\x2e\x6f\x66\x66\x73\x65\x74\ +\x54\x6f\x70\x2c\x6d\x2b\x3d\x69\x2e\x6f\x66\x66\x73\x65\x74\x4c\ +\x65\x66\x74\x3b\x66\x2e\x73\x75\x70\x70\x6f\x72\x74\x2e\x66\x69\ +\x78\x65\x64\x50\x6f\x73\x69\x74\x69\x6f\x6e\x26\x26\x6b\x2e\x70\ +\x6f\x73\x69\x74\x69\x6f\x6e\x3d\x3d\x3d\x22\x66\x69\x78\x65\x64\ +\x22\x26\x26\x28\x6c\x2b\x3d\x4d\x61\x74\x68\x2e\x6d\x61\x78\x28\ +\x68\x2e\x73\x63\x72\x6f\x6c\x6c\x54\x6f\x70\x2c\x69\x2e\x73\x63\ +\x72\x6f\x6c\x6c\x54\x6f\x70\x29\x2c\x6d\x2b\x3d\x4d\x61\x74\x68\ +\x2e\x6d\x61\x78\x28\x68\x2e\x73\x63\x72\x6f\x6c\x6c\x4c\x65\x66\ +\x74\x2c\x69\x2e\x73\x63\x72\x6f\x6c\x6c\x4c\x65\x66\x74\x29\x29\ +\x3b\x72\x65\x74\x75\x72\x6e\x7b\x74\x6f\x70\x3a\x6c\x2c\x6c\x65\ +\x66\x74\x3a\x6d\x7d\x7d\x2c\x66\x2e\x6f\x66\x66\x73\x65\x74\x3d\ +\x7b\x62\x6f\x64\x79\x4f\x66\x66\x73\x65\x74\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\x3d\x61\x2e\ +\x6f\x66\x66\x73\x65\x74\x54\x6f\x70\x2c\x63\x3d\x61\x2e\x6f\x66\ +\x66\x73\x65\x74\x4c\x65\x66\x74\x3b\x66\x2e\x73\x75\x70\x70\x6f\ +\x72\x74\x2e\x64\x6f\x65\x73\x4e\x6f\x74\x49\x6e\x63\x6c\x75\x64\ +\x65\x4d\x61\x72\x67\x69\x6e\x49\x6e\x42\x6f\x64\x79\x4f\x66\x66\ +\x73\x65\x74\x26\x26\x28\x62\x2b\x3d\x70\x61\x72\x73\x65\x46\x6c\ +\x6f\x61\x74\x28\x66\x2e\x63\x73\x73\x28\x61\x2c\x22\x6d\x61\x72\ +\x67\x69\x6e\x54\x6f\x70\x22\x29\x29\x7c\x7c\x30\x2c\x63\x2b\x3d\ +\x70\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\x28\x66\x2e\x63\x73\x73\ +\x28\x61\x2c\x22\x6d\x61\x72\x67\x69\x6e\x4c\x65\x66\x74\x22\x29\ +\x29\x7c\x7c\x30\x29\x3b\x72\x65\x74\x75\x72\x6e\x7b\x74\x6f\x70\ +\x3a\x62\x2c\x6c\x65\x66\x74\x3a\x63\x7d\x7d\x2c\x73\x65\x74\x4f\ +\x66\x66\x73\x65\x74\x3a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\ +\x2c\x62\x2c\x63\x29\x7b\x76\x61\x72\x20\x64\x3d\x66\x2e\x63\x73\ +\x73\x28\x61\x2c\x22\x70\x6f\x73\x69\x74\x69\x6f\x6e\x22\x29\x3b\ +\x64\x3d\x3d\x3d\x22\x73\x74\x61\x74\x69\x63\x22\x26\x26\x28\x61\ +\x2e\x73\x74\x79\x6c\x65\x2e\x70\x6f\x73\x69\x74\x69\x6f\x6e\x3d\ +\x22\x72\x65\x6c\x61\x74\x69\x76\x65\x22\x29\x3b\x76\x61\x72\x20\ +\x65\x3d\x66\x28\x61\x29\x2c\x67\x3d\x65\x2e\x6f\x66\x66\x73\x65\ +\x74\x28\x29\x2c\x68\x3d\x66\x2e\x63\x73\x73\x28\x61\x2c\x22\x74\ +\x6f\x70\x22\x29\x2c\x69\x3d\x66\x2e\x63\x73\x73\x28\x61\x2c\x22\ +\x6c\x65\x66\x74\x22\x29\x2c\x6a\x3d\x28\x64\x3d\x3d\x3d\x22\x61\ +\x62\x73\x6f\x6c\x75\x74\x65\x22\x7c\x7c\x64\x3d\x3d\x3d\x22\x66\ +\x69\x78\x65\x64\x22\x29\x26\x26\x66\x2e\x69\x6e\x41\x72\x72\x61\ +\x79\x28\x22\x61\x75\x74\x6f\x22\x2c\x5b\x68\x2c\x69\x5d\x29\x3e\ +\x2d\x31\x2c\x6b\x3d\x7b\x7d\x2c\x6c\x3d\x7b\x7d\x2c\x6d\x2c\x6e\ +\x3b\x6a\x3f\x28\x6c\x3d\x65\x2e\x70\x6f\x73\x69\x74\x69\x6f\x6e\ +\x28\x29\x2c\x6d\x3d\x6c\x2e\x74\x6f\x70\x2c\x6e\x3d\x6c\x2e\x6c\ +\x65\x66\x74\x29\x3a\x28\x6d\x3d\x70\x61\x72\x73\x65\x46\x6c\x6f\ +\x61\x74\x28\x68\x29\x7c\x7c\x30\x2c\x6e\x3d\x70\x61\x72\x73\x65\ +\x46\x6c\x6f\x61\x74\x28\x69\x29\x7c\x7c\x30\x29\x2c\x66\x2e\x69\ +\x73\x46\x75\x6e\x63\x74\x69\x6f\x6e\x28\x62\x29\x26\x26\x28\x62\ +\x3d\x62\x2e\x63\x61\x6c\x6c\x28\x61\x2c\x63\x2c\x67\x29\x29\x2c\ +\x62\x2e\x74\x6f\x70\x21\x3d\x6e\x75\x6c\x6c\x26\x26\x28\x6b\x2e\ +\x74\x6f\x70\x3d\x62\x2e\x74\x6f\x70\x2d\x67\x2e\x74\x6f\x70\x2b\ +\x6d\x29\x2c\x62\x2e\x6c\x65\x66\x74\x21\x3d\x6e\x75\x6c\x6c\x26\ +\x26\x28\x6b\x2e\x6c\x65\x66\x74\x3d\x62\x2e\x6c\x65\x66\x74\x2d\ +\x67\x2e\x6c\x65\x66\x74\x2b\x6e\x29\x2c\x22\x75\x73\x69\x6e\x67\ +\x22\x69\x6e\x20\x62\x3f\x62\x2e\x75\x73\x69\x6e\x67\x2e\x63\x61\ +\x6c\x6c\x28\x61\x2c\x6b\x29\x3a\x65\x2e\x63\x73\x73\x28\x6b\x29\ +\x7d\x7d\x2c\x66\x2e\x66\x6e\x2e\x65\x78\x74\x65\x6e\x64\x28\x7b\ +\x70\x6f\x73\x69\x74\x69\x6f\x6e\x3a\x66\x75\x6e\x63\x74\x69\x6f\ +\x6e\x28\x29\x7b\x69\x66\x28\x21\x74\x68\x69\x73\x5b\x30\x5d\x29\ +\x72\x65\x74\x75\x72\x6e\x20\x6e\x75\x6c\x6c\x3b\x76\x61\x72\x20\ +\x61\x3d\x74\x68\x69\x73\x5b\x30\x5d\x2c\x62\x3d\x74\x68\x69\x73\ +\x2e\x6f\x66\x66\x73\x65\x74\x50\x61\x72\x65\x6e\x74\x28\x29\x2c\ +\x63\x3d\x74\x68\x69\x73\x2e\x6f\x66\x66\x73\x65\x74\x28\x29\x2c\ +\x64\x3d\x63\x78\x2e\x74\x65\x73\x74\x28\x62\x5b\x30\x5d\x2e\x6e\ +\x6f\x64\x65\x4e\x61\x6d\x65\x29\x3f\x7b\x74\x6f\x70\x3a\x30\x2c\ +\x6c\x65\x66\x74\x3a\x30\x7d\x3a\x62\x2e\x6f\x66\x66\x73\x65\x74\ +\x28\x29\x3b\x63\x2e\x74\x6f\x70\x2d\x3d\x70\x61\x72\x73\x65\x46\ +\x6c\x6f\x61\x74\x28\x66\x2e\x63\x73\x73\x28\x61\x2c\x22\x6d\x61\ +\x72\x67\x69\x6e\x54\x6f\x70\x22\x29\x29\x7c\x7c\x30\x2c\x63\x2e\ +\x6c\x65\x66\x74\x2d\x3d\x70\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\ +\x28\x66\x2e\x63\x73\x73\x28\x61\x2c\x22\x6d\x61\x72\x67\x69\x6e\ +\x4c\x65\x66\x74\x22\x29\x29\x7c\x7c\x30\x2c\x64\x2e\x74\x6f\x70\ +\x2b\x3d\x70\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\x28\x66\x2e\x63\ +\x73\x73\x28\x62\x5b\x30\x5d\x2c\x22\x62\x6f\x72\x64\x65\x72\x54\ +\x6f\x70\x57\x69\x64\x74\x68\x22\x29\x29\x7c\x7c\x30\x2c\x64\x2e\ +\x6c\x65\x66\x74\x2b\x3d\x70\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\ +\x28\x66\x2e\x63\x73\x73\x28\x62\x5b\x30\x5d\x2c\x22\x62\x6f\x72\ +\x64\x65\x72\x4c\x65\x66\x74\x57\x69\x64\x74\x68\x22\x29\x29\x7c\ +\x7c\x30\x3b\x72\x65\x74\x75\x72\x6e\x7b\x74\x6f\x70\x3a\x63\x2e\ +\x74\x6f\x70\x2d\x64\x2e\x74\x6f\x70\x2c\x6c\x65\x66\x74\x3a\x63\ +\x2e\x6c\x65\x66\x74\x2d\x64\x2e\x6c\x65\x66\x74\x7d\x7d\x2c\x6f\ +\x66\x66\x73\x65\x74\x50\x61\x72\x65\x6e\x74\x3a\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x74\x68\ +\x69\x73\x2e\x6d\x61\x70\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\ +\x29\x7b\x76\x61\x72\x20\x61\x3d\x74\x68\x69\x73\x2e\x6f\x66\x66\ +\x73\x65\x74\x50\x61\x72\x65\x6e\x74\x7c\x7c\x63\x2e\x62\x6f\x64\ +\x79\x3b\x77\x68\x69\x6c\x65\x28\x61\x26\x26\x21\x63\x78\x2e\x74\ +\x65\x73\x74\x28\x61\x2e\x6e\x6f\x64\x65\x4e\x61\x6d\x65\x29\x26\ +\x26\x66\x2e\x63\x73\x73\x28\x61\x2c\x22\x70\x6f\x73\x69\x74\x69\ +\x6f\x6e\x22\x29\x3d\x3d\x3d\x22\x73\x74\x61\x74\x69\x63\x22\x29\ +\x61\x3d\x61\x2e\x6f\x66\x66\x73\x65\x74\x50\x61\x72\x65\x6e\x74\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x61\x7d\x29\x7d\x7d\x29\x2c\x66\ +\x2e\x65\x61\x63\x68\x28\x5b\x22\x4c\x65\x66\x74\x22\x2c\x22\x54\ +\x6f\x70\x22\x5d\x2c\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\ +\x63\x29\x7b\x76\x61\x72\x20\x64\x3d\x22\x73\x63\x72\x6f\x6c\x6c\ +\x22\x2b\x63\x3b\x66\x2e\x66\x6e\x5b\x64\x5d\x3d\x66\x75\x6e\x63\ +\x74\x69\x6f\x6e\x28\x63\x29\x7b\x76\x61\x72\x20\x65\x2c\x67\x3b\ +\x69\x66\x28\x63\x3d\x3d\x3d\x62\x29\x7b\x65\x3d\x74\x68\x69\x73\ +\x5b\x30\x5d\x3b\x69\x66\x28\x21\x65\x29\x72\x65\x74\x75\x72\x6e\ +\x20\x6e\x75\x6c\x6c\x3b\x67\x3d\x63\x79\x28\x65\x29\x3b\x72\x65\ +\x74\x75\x72\x6e\x20\x67\x3f\x22\x70\x61\x67\x65\x58\x4f\x66\x66\ +\x73\x65\x74\x22\x69\x6e\x20\x67\x3f\x67\x5b\x61\x3f\x22\x70\x61\ +\x67\x65\x59\x4f\x66\x66\x73\x65\x74\x22\x3a\x22\x70\x61\x67\x65\ +\x58\x4f\x66\x66\x73\x65\x74\x22\x5d\x3a\x66\x2e\x73\x75\x70\x70\ +\x6f\x72\x74\x2e\x62\x6f\x78\x4d\x6f\x64\x65\x6c\x26\x26\x67\x2e\ +\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x64\x6f\x63\x75\x6d\x65\x6e\ +\x74\x45\x6c\x65\x6d\x65\x6e\x74\x5b\x64\x5d\x7c\x7c\x67\x2e\x64\ +\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x62\x6f\x64\x79\x5b\x64\x5d\x3a\ +\x65\x5b\x64\x5d\x7d\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73\ +\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\ +\x7b\x67\x3d\x63\x79\x28\x74\x68\x69\x73\x29\x2c\x67\x3f\x67\x2e\ +\x73\x63\x72\x6f\x6c\x6c\x54\x6f\x28\x61\x3f\x66\x28\x67\x29\x2e\ +\x73\x63\x72\x6f\x6c\x6c\x4c\x65\x66\x74\x28\x29\x3a\x63\x2c\x61\ +\x3f\x63\x3a\x66\x28\x67\x29\x2e\x73\x63\x72\x6f\x6c\x6c\x54\x6f\ +\x70\x28\x29\x29\x3a\x74\x68\x69\x73\x5b\x64\x5d\x3d\x63\x7d\x29\ +\x7d\x7d\x29\x2c\x66\x2e\x65\x61\x63\x68\x28\x5b\x22\x48\x65\x69\ +\x67\x68\x74\x22\x2c\x22\x57\x69\x64\x74\x68\x22\x5d\x2c\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x28\x61\x2c\x63\x29\x7b\x76\x61\x72\x20\ +\x64\x3d\x63\x2e\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x28\ +\x29\x3b\x66\x2e\x66\x6e\x5b\x22\x69\x6e\x6e\x65\x72\x22\x2b\x63\ +\x5d\x3d\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x29\x7b\x76\x61\x72\ +\x20\x61\x3d\x74\x68\x69\x73\x5b\x30\x5d\x3b\x72\x65\x74\x75\x72\ +\x6e\x20\x61\x3f\x61\x2e\x73\x74\x79\x6c\x65\x3f\x70\x61\x72\x73\ +\x65\x46\x6c\x6f\x61\x74\x28\x66\x2e\x63\x73\x73\x28\x61\x2c\x64\ +\x2c\x22\x70\x61\x64\x64\x69\x6e\x67\x22\x29\x29\x3a\x74\x68\x69\ +\x73\x5b\x64\x5d\x28\x29\x3a\x6e\x75\x6c\x6c\x7d\x2c\x66\x2e\x66\ +\x6e\x5b\x22\x6f\x75\x74\x65\x72\x22\x2b\x63\x5d\x3d\x66\x75\x6e\ +\x63\x74\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x62\x3d\x74\ +\x68\x69\x73\x5b\x30\x5d\x3b\x72\x65\x74\x75\x72\x6e\x20\x62\x3f\ +\x62\x2e\x73\x74\x79\x6c\x65\x3f\x70\x61\x72\x73\x65\x46\x6c\x6f\ +\x61\x74\x28\x66\x2e\x63\x73\x73\x28\x62\x2c\x64\x2c\x61\x3f\x22\ +\x6d\x61\x72\x67\x69\x6e\x22\x3a\x22\x62\x6f\x72\x64\x65\x72\x22\ +\x29\x29\x3a\x74\x68\x69\x73\x5b\x64\x5d\x28\x29\x3a\x6e\x75\x6c\ +\x6c\x7d\x2c\x66\x2e\x66\x6e\x5b\x64\x5d\x3d\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x7b\x76\x61\x72\x20\x65\x3d\x74\x68\x69\ +\x73\x5b\x30\x5d\x3b\x69\x66\x28\x21\x65\x29\x72\x65\x74\x75\x72\ +\x6e\x20\x61\x3d\x3d\x6e\x75\x6c\x6c\x3f\x6e\x75\x6c\x6c\x3a\x74\ +\x68\x69\x73\x3b\x69\x66\x28\x66\x2e\x69\x73\x46\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x61\x29\x29\x72\x65\x74\x75\x72\x6e\x20\x74\x68\ +\x69\x73\x2e\x65\x61\x63\x68\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\ +\x28\x62\x29\x7b\x76\x61\x72\x20\x63\x3d\x66\x28\x74\x68\x69\x73\ +\x29\x3b\x63\x5b\x64\x5d\x28\x61\x2e\x63\x61\x6c\x6c\x28\x74\x68\ +\x69\x73\x2c\x62\x2c\x63\x5b\x64\x5d\x28\x29\x29\x29\x7d\x29\x3b\ +\x69\x66\x28\x66\x2e\x69\x73\x57\x69\x6e\x64\x6f\x77\x28\x65\x29\ +\x29\x7b\x76\x61\x72\x20\x67\x3d\x65\x2e\x64\x6f\x63\x75\x6d\x65\ +\x6e\x74\x2e\x64\x6f\x63\x75\x6d\x65\x6e\x74\x45\x6c\x65\x6d\x65\ +\x6e\x74\x5b\x22\x63\x6c\x69\x65\x6e\x74\x22\x2b\x63\x5d\x2c\x68\ +\x3d\x65\x2e\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x62\x6f\x64\x79\ +\x3b\x72\x65\x74\x75\x72\x6e\x20\x65\x2e\x64\x6f\x63\x75\x6d\x65\ +\x6e\x74\x2e\x63\x6f\x6d\x70\x61\x74\x4d\x6f\x64\x65\x3d\x3d\x3d\ +\x22\x43\x53\x53\x31\x43\x6f\x6d\x70\x61\x74\x22\x26\x26\x67\x7c\ +\x7c\x68\x26\x26\x68\x5b\x22\x63\x6c\x69\x65\x6e\x74\x22\x2b\x63\ +\x5d\x7c\x7c\x67\x7d\x69\x66\x28\x65\x2e\x6e\x6f\x64\x65\x54\x79\ +\x70\x65\x3d\x3d\x3d\x39\x29\x72\x65\x74\x75\x72\x6e\x20\x4d\x61\ +\x74\x68\x2e\x6d\x61\x78\x28\x65\x2e\x64\x6f\x63\x75\x6d\x65\x6e\ +\x74\x45\x6c\x65\x6d\x65\x6e\x74\x5b\x22\x63\x6c\x69\x65\x6e\x74\ +\x22\x2b\x63\x5d\x2c\x65\x2e\x62\x6f\x64\x79\x5b\x22\x73\x63\x72\ +\x6f\x6c\x6c\x22\x2b\x63\x5d\x2c\x65\x2e\x64\x6f\x63\x75\x6d\x65\ +\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x5b\x22\x73\x63\x72\x6f\x6c\ +\x6c\x22\x2b\x63\x5d\x2c\x65\x2e\x62\x6f\x64\x79\x5b\x22\x6f\x66\ +\x66\x73\x65\x74\x22\x2b\x63\x5d\x2c\x65\x2e\x64\x6f\x63\x75\x6d\ +\x65\x6e\x74\x45\x6c\x65\x6d\x65\x6e\x74\x5b\x22\x6f\x66\x66\x73\ +\x65\x74\x22\x2b\x63\x5d\x29\x3b\x69\x66\x28\x61\x3d\x3d\x3d\x62\ +\x29\x7b\x76\x61\x72\x20\x69\x3d\x66\x2e\x63\x73\x73\x28\x65\x2c\ +\x64\x29\x2c\x6a\x3d\x70\x61\x72\x73\x65\x46\x6c\x6f\x61\x74\x28\ +\x69\x29\x3b\x72\x65\x74\x75\x72\x6e\x20\x66\x2e\x69\x73\x4e\x75\ +\x6d\x65\x72\x69\x63\x28\x6a\x29\x3f\x6a\x3a\x69\x7d\x72\x65\x74\ +\x75\x72\x6e\x20\x74\x68\x69\x73\x2e\x63\x73\x73\x28\x64\x2c\x74\ +\x79\x70\x65\x6f\x66\x20\x61\x3d\x3d\x22\x73\x74\x72\x69\x6e\x67\ +\x22\x3f\x61\x3a\x61\x2b\x22\x70\x78\x22\x29\x7d\x7d\x29\x2c\x61\ +\x2e\x6a\x51\x75\x65\x72\x79\x3d\x61\x2e\x24\x3d\x66\x2c\x74\x79\ +\x70\x65\x6f\x66\x20\x64\x65\x66\x69\x6e\x65\x3d\x3d\x22\x66\x75\ +\x6e\x63\x74\x69\x6f\x6e\x22\x26\x26\x64\x65\x66\x69\x6e\x65\x2e\ +\x61\x6d\x64\x26\x26\x64\x65\x66\x69\x6e\x65\x2e\x61\x6d\x64\x2e\ +\x6a\x51\x75\x65\x72\x79\x26\x26\x64\x65\x66\x69\x6e\x65\x28\x22\ +\x6a\x71\x75\x65\x72\x79\x22\x2c\x5b\x5d\x2c\x66\x75\x6e\x63\x74\ +\x69\x6f\x6e\x28\x29\x7b\x72\x65\x74\x75\x72\x6e\x20\x66\x7d\x29\ +\x7d\x29\x28\x77\x69\x6e\x64\x6f\x77\x29\x3b\ +" + +qt_resource_name = b"\ +\x00\x0a\ +\x08\x94\x81\xf4\ +\x00\x6a\ +\x00\x61\x00\x76\x00\x61\x00\x73\x00\x63\x00\x72\x00\x69\x00\x70\x00\x74\ +\x00\x0e\ +\x0e\x3a\xd6\x33\ +\x00\x71\ +\x00\x77\x00\x65\x00\x62\x00\x63\x00\x68\x00\x61\x00\x6e\x00\x6e\x00\x65\x00\x6c\x00\x2e\x00\x6a\x00\x73\ +\x00\x0c\ +\x06\x7a\xfc\x33\ +\x00\x6a\ +\x00\x71\x00\x75\x00\x65\x00\x72\x00\x79\x00\x2d\x00\x75\x00\x69\x00\x2e\x00\x6a\x00\x73\ +\x00\x09\ +\x0b\xc9\x66\x13\ +\x00\x6a\ +\x00\x71\x00\x75\x00\x65\x00\x72\x00\x79\x00\x2e\x00\x6a\x00\x73\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x02\ +\x00\x00\x00\x3c\x00\x01\x00\x00\x00\x01\x00\x00\x0e\xfe\ +\x00\x00\x00\x5a\x00\x00\x00\x00\x00\x01\x00\x00\x33\x36\ +\x00\x00\x00\x1a\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/qml.qrc Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,6 @@ +<!DOCTYPE RCC> +<RCC version="1.0"> +<qresource> + <file>qml/thumbnailer.qml</file> +</qresource> +</RCC>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/qml/thumbnailer.qml Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,15 @@ +import QtQuick 2.2 +import QtWebEngine 1.0 + +WebEngineView { + width: 1920 + height: 1080 + + onLoadingChanged: { + if (loadRequest.status == WebEngineView.LoadStartedStatus) + return; + + var ok = loadRequest.status == WebEngineView.LoadSucceededStatus; + thumbnailer.createThumbnail(ok); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/data/qml_rc.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.4.1) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x01\x48\ +\x69\ +\x6d\x70\x6f\x72\x74\x20\x51\x74\x51\x75\x69\x63\x6b\x20\x32\x2e\ +\x32\x0a\x69\x6d\x70\x6f\x72\x74\x20\x51\x74\x57\x65\x62\x45\x6e\ +\x67\x69\x6e\x65\x20\x31\x2e\x30\x0a\x0a\x57\x65\x62\x45\x6e\x67\ +\x69\x6e\x65\x56\x69\x65\x77\x20\x7b\x0a\x20\x20\x20\x20\x77\x69\ +\x64\x74\x68\x3a\x20\x31\x39\x32\x30\x0a\x20\x20\x20\x20\x68\x65\ +\x69\x67\x68\x74\x3a\x20\x31\x30\x38\x30\x0a\x0a\x20\x20\x20\x20\ +\x6f\x6e\x4c\x6f\x61\x64\x69\x6e\x67\x43\x68\x61\x6e\x67\x65\x64\ +\x3a\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x69\x66\x20\x28\ +\x6c\x6f\x61\x64\x52\x65\x71\x75\x65\x73\x74\x2e\x73\x74\x61\x74\ +\x75\x73\x20\x3d\x3d\x20\x57\x65\x62\x45\x6e\x67\x69\x6e\x65\x56\ +\x69\x65\x77\x2e\x4c\x6f\x61\x64\x53\x74\x61\x72\x74\x65\x64\x53\ +\x74\x61\x74\x75\x73\x29\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x72\x65\x74\x75\x72\x6e\x3b\x0a\x0a\x20\x20\x20\x20\ +\x20\x20\x20\x20\x76\x61\x72\x20\x6f\x6b\x20\x3d\x20\x6c\x6f\x61\ +\x64\x52\x65\x71\x75\x65\x73\x74\x2e\x73\x74\x61\x74\x75\x73\x20\ +\x3d\x3d\x20\x57\x65\x62\x45\x6e\x67\x69\x6e\x65\x56\x69\x65\x77\ +\x2e\x4c\x6f\x61\x64\x53\x75\x63\x63\x65\x65\x64\x65\x64\x53\x74\ +\x61\x74\x75\x73\x3b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x74\x68\ +\x75\x6d\x62\x6e\x61\x69\x6c\x65\x72\x2e\x63\x72\x65\x61\x74\x65\ +\x54\x68\x75\x6d\x62\x6e\x61\x69\x6c\x28\x6f\x6b\x29\x3b\x0a\x20\ +\x20\x20\x20\x7d\x0a\x7d\x0a\ +" + +qt_resource_name = b"\ +\x00\x03\ +\x00\x00\x78\x3c\ +\x00\x71\ +\x00\x6d\x00\x6c\ +\x00\x0f\ +\x0a\xb6\x34\x5c\ +\x00\x74\ +\x00\x68\x00\x75\x00\x6d\x00\x62\x00\x6e\x00\x61\x00\x69\x00\x6c\x00\x65\x00\x72\x00\x2e\x00\x71\x00\x6d\x00\x6c\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources()
--- a/eric6.e4p Sun Apr 03 12:29:37 2016 +0200 +++ b/eric6.e4p Sun Apr 03 16:33:37 2016 +0200 @@ -822,6 +822,8 @@ <Source>Preferences/ConfigurationPages/TrayStarterPage.py</Source> <Source>Preferences/ConfigurationPages/VcsPage.py</Source> <Source>Preferences/ConfigurationPages/ViewmanagerPage.py</Source> + <Source>Preferences/ConfigurationPages/WebBrowserAppearancePage.py</Source> + <Source>Preferences/ConfigurationPages/WebBrowserPage.py</Source> <Source>Preferences/ConfigurationPages/__init__.py</Source> <Source>Preferences/MouseClickDialog.py</Source> <Source>Preferences/PreferencesLexer.py</Source> @@ -1264,12 +1266,209 @@ <Source>ViewManager/BookmarkedFilesDialog.py</Source> <Source>ViewManager/ViewManager.py</Source> <Source>ViewManager/__init__.py</Source> + <Source>WebBrowser/AdBlock/AdBlockDialog.py</Source> + <Source>WebBrowser/AdBlock/AdBlockExceptionsDialog.py</Source> + <Source>WebBrowser/AdBlock/AdBlockIcon.py</Source> + <Source>WebBrowser/AdBlock/AdBlockManager.py</Source> + <Source>WebBrowser/AdBlock/AdBlockPage.py</Source> + <Source>WebBrowser/AdBlock/AdBlockRule.py</Source> + <Source>WebBrowser/AdBlock/AdBlockSubscription.py</Source> + <Source>WebBrowser/AdBlock/AdBlockTreeWidget.py</Source> + <Source>WebBrowser/AdBlock/AdBlockUrlInterceptor.py</Source> + <Source>WebBrowser/AdBlock/__init__.py</Source> + <Source>WebBrowser/Bookmarks/AddBookmarkDialog.py</Source> + <Source>WebBrowser/Bookmarks/BookmarkNode.py</Source> + <Source>WebBrowser/Bookmarks/BookmarkPropertiesDialog.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksDialog.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksImportDialog.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksImporters/BookmarksImporter.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksImporters/ChromeImporter.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksImporters/FirefoxImporter.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksImporters/HtmlImporter.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksImporters/IExplorerImporter.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksImporters/OperaImporter.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksImporters/SafariImporter.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksImporters/XbelImporter.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksImporters/__init__.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksManager.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksMenu.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksModel.py</Source> + <Source>WebBrowser/Bookmarks/BookmarksToolBar.py</Source> + <Source>WebBrowser/Bookmarks/DefaultBookmarks_rc.py</Source> + <Source>WebBrowser/Bookmarks/NsHtmlReader.py</Source> + <Source>WebBrowser/Bookmarks/NsHtmlWriter.py</Source> + <Source>WebBrowser/Bookmarks/XbelReader.py</Source> + <Source>WebBrowser/Bookmarks/XbelWriter.py</Source> + <Source>WebBrowser/Bookmarks/__init__.py</Source> + <Source>WebBrowser/ClosedTabsManager.py</Source> + <Source>WebBrowser/CookieJar/CookieDetailsDialog.py</Source> + <Source>WebBrowser/CookieJar/CookieExceptionsModel.py</Source> + <Source>WebBrowser/CookieJar/CookieJar.py</Source> + <Source>WebBrowser/CookieJar/CookieModel.py</Source> + <Source>WebBrowser/CookieJar/CookiesConfigurationDialog.py</Source> + <Source>WebBrowser/CookieJar/CookiesDialog.py</Source> + <Source>WebBrowser/CookieJar/CookiesExceptionsDialog.py</Source> + <Source>WebBrowser/CookieJar/__init__.py</Source> + <Source>WebBrowser/Download/DownloadAskActionDialog.py</Source> + <Source>WebBrowser/Download/DownloadItem.py</Source> + <Source>WebBrowser/Download/DownloadManager.py</Source> + <Source>WebBrowser/Download/DownloadModel.py</Source> + <Source>WebBrowser/Download/DownloadUtilities.py</Source> + <Source>WebBrowser/Download/__init__.py</Source> + <Source>WebBrowser/FeaturePermissions/FeaturePermissionBar.py</Source> + <Source>WebBrowser/FeaturePermissions/FeaturePermissionManager.py</Source> + <Source>WebBrowser/FeaturePermissions/FeaturePermissionsDialog.py</Source> + <Source>WebBrowser/FeaturePermissions/__init__.py</Source> + <Source>WebBrowser/Feeds/FeedEditDialog.py</Source> + <Source>WebBrowser/Feeds/FeedsDialog.py</Source> + <Source>WebBrowser/Feeds/FeedsManager.py</Source> + <Source>WebBrowser/Feeds/__init__.py</Source> + <Source>WebBrowser/FlashCookieManager/FlashCookie.py</Source> + <Source>WebBrowser/FlashCookieManager/FlashCookieManager.py</Source> + <Source>WebBrowser/FlashCookieManager/FlashCookieManagerDialog.py</Source> + <Source>WebBrowser/FlashCookieManager/FlashCookieNotification.py</Source> + <Source>WebBrowser/FlashCookieManager/FlashCookieReader.py</Source> + <Source>WebBrowser/FlashCookieManager/FlashCookieUtilities.py</Source> + <Source>WebBrowser/FlashCookieManager/__init__.py</Source> + <Source>WebBrowser/GreaseMonkey/GreaseMonkeyAddScriptDialog.py</Source> + <Source>WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationDialog.py</Source> + <Source>WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationListDelegate.py</Source> + <Source>WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationListWidget.py</Source> + <Source>WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationScriptInfoDialog.py</Source> + <Source>WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/__init__.py</Source> + <Source>WebBrowser/GreaseMonkey/GreaseMonkeyDownloader.py</Source> + <Source>WebBrowser/GreaseMonkey/GreaseMonkeyJavaScript.py</Source> + <Source>WebBrowser/GreaseMonkey/GreaseMonkeyManager.py</Source> + <Source>WebBrowser/GreaseMonkey/GreaseMonkeyScript.py</Source> + <Source>WebBrowser/GreaseMonkey/GreaseMonkeyUrlInterceptor.py</Source> + <Source>WebBrowser/GreaseMonkey/__init__.py</Source> + <Source>WebBrowser/History/HistoryCompleter.py</Source> + <Source>WebBrowser/History/HistoryDialog.py</Source> + <Source>WebBrowser/History/HistoryFilterModel.py</Source> + <Source>WebBrowser/History/HistoryManager.py</Source> + <Source>WebBrowser/History/HistoryMenu.py</Source> + <Source>WebBrowser/History/HistoryModel.py</Source> + <Source>WebBrowser/History/HistoryTreeModel.py</Source> + <Source>WebBrowser/History/__init__.py</Source> + <Source>WebBrowser/JavaScript/ExternalJsObject.py</Source> + <Source>WebBrowser/JavaScript/PasswordManagerJsObject.py</Source> + <Source>WebBrowser/JavaScript/StartPageJsObject.py</Source> + <Source>WebBrowser/JavaScript/__init__.py</Source> + <Source>WebBrowser/Network/EricSchemeHandler.py</Source> + <Source>WebBrowser/Network/NetworkManager.py</Source> + <Source>WebBrowser/Network/NetworkUrlInterceptor.py</Source> + <Source>WebBrowser/Network/QtHelpSchemeHandler.py</Source> + <Source>WebBrowser/Network/SendRefererWhitelistDialog.py</Source> + <Source>WebBrowser/Network/SslErrorExceptionsDialog.py</Source> + <Source>WebBrowser/Network/UrlInterceptor.py</Source> + <Source>WebBrowser/Network/__init__.py</Source> + <Source>WebBrowser/OpenSearch/DefaultSearchEngines/DefaultSearchEngines_rc.py</Source> + <Source>WebBrowser/OpenSearch/DefaultSearchEngines/__init__.py</Source> + <Source>WebBrowser/OpenSearch/OpenSearchDialog.py</Source> + <Source>WebBrowser/OpenSearch/OpenSearchEditDialog.py</Source> + <Source>WebBrowser/OpenSearch/OpenSearchEngine.py</Source> + <Source>WebBrowser/OpenSearch/OpenSearchEngineAction.py</Source> + <Source>WebBrowser/OpenSearch/OpenSearchEngineModel.py</Source> + <Source>WebBrowser/OpenSearch/OpenSearchManager.py</Source> + <Source>WebBrowser/OpenSearch/OpenSearchReader.py</Source> + <Source>WebBrowser/OpenSearch/OpenSearchWriter.py</Source> + <Source>WebBrowser/OpenSearch/__init__.py</Source> + <Source>WebBrowser/PageScreenDialog.py</Source> + <Source>WebBrowser/Passwords/LoginForm.py</Source> + <Source>WebBrowser/Passwords/PasswordManager.py</Source> + <Source>WebBrowser/Passwords/PasswordModel.py</Source> + <Source>WebBrowser/Passwords/PasswordReader.py</Source> + <Source>WebBrowser/Passwords/PasswordWriter.py</Source> + <Source>WebBrowser/Passwords/PasswordsDialog.py</Source> + <Source>WebBrowser/Passwords/__init__.py</Source> + <Source>WebBrowser/PersonalInformationManager/PersonalDataDialog.py</Source> + <Source>WebBrowser/PersonalInformationManager/PersonalInformationManager.py</Source> + <Source>WebBrowser/PersonalInformationManager/__init__.py</Source> + <Source>WebBrowser/QtHelp/HelpDocsInstaller.py</Source> + <Source>WebBrowser/QtHelp/HelpIndexWidget.py</Source> + <Source>WebBrowser/QtHelp/HelpSearchWidget.py</Source> + <Source>WebBrowser/QtHelp/HelpTocWidget.py</Source> + <Source>WebBrowser/QtHelp/HelpTopicDialog.py</Source> + <Source>WebBrowser/QtHelp/QtHelpDocumentationDialog.py</Source> + <Source>WebBrowser/QtHelp/QtHelpFiltersDialog.py</Source> + <Source>WebBrowser/QtHelp/__init__.py</Source> + <Source>WebBrowser/SearchWidget.py</Source> + <Source>WebBrowser/SiteInfo/SiteInfoDialog.py</Source> + <Source>WebBrowser/SiteInfo/__init__.py</Source> + <Source>WebBrowser/SpeedDial/Page.py</Source> + <Source>WebBrowser/SpeedDial/PageThumbnailer.py</Source> + <Source>WebBrowser/SpeedDial/SpeedDial.py</Source> + <Source>WebBrowser/SpeedDial/SpeedDialReader.py</Source> + <Source>WebBrowser/SpeedDial/SpeedDialWriter.py</Source> + <Source>WebBrowser/SpeedDial/__init__.py</Source> + <Source>WebBrowser/Sync/DirectorySyncHandler.py</Source> + <Source>WebBrowser/Sync/FtpSyncHandler.py</Source> + <Source>WebBrowser/Sync/SyncAssistantDialog.py</Source> + <Source>WebBrowser/Sync/SyncCheckPage.py</Source> + <Source>WebBrowser/Sync/SyncDataPage.py</Source> + <Source>WebBrowser/Sync/SyncDirectorySettingsPage.py</Source> + <Source>WebBrowser/Sync/SyncEncryptionPage.py</Source> + <Source>WebBrowser/Sync/SyncFtpSettingsPage.py</Source> + <Source>WebBrowser/Sync/SyncGlobals.py</Source> + <Source>WebBrowser/Sync/SyncHandler.py</Source> + <Source>WebBrowser/Sync/SyncHostTypePage.py</Source> + <Source>WebBrowser/Sync/SyncManager.py</Source> + <Source>WebBrowser/Sync/__init__.py</Source> + <Source>WebBrowser/Tools/DelayedFileWatcher.py</Source> + <Source>WebBrowser/Tools/Scripts.py</Source> + <Source>WebBrowser/Tools/WebBrowserTools.py</Source> + <Source>WebBrowser/Tools/WebHitTestResult.py</Source> + <Source>WebBrowser/Tools/WebIconDialog.py</Source> + <Source>WebBrowser/Tools/WebIconLoader.py</Source> + <Source>WebBrowser/Tools/WebIconProvider.py</Source> + <Source>WebBrowser/Tools/__init__.py</Source> + <Source>WebBrowser/UrlBar/BookmarkActionSelectionDialog.py</Source> + <Source>WebBrowser/UrlBar/BookmarkInfoDialog.py</Source> + <Source>WebBrowser/UrlBar/FavIconLabel.py</Source> + <Source>WebBrowser/UrlBar/StackedUrlBar.py</Source> + <Source>WebBrowser/UrlBar/UrlBar.py</Source> + <Source>WebBrowser/UrlBar/__init__.py</Source> + <Source>WebBrowser/UserAgent/UserAgentDefaults_rc.py</Source> + <Source>WebBrowser/UserAgent/UserAgentManager.py</Source> + <Source>WebBrowser/UserAgent/UserAgentMenu.py</Source> + <Source>WebBrowser/UserAgent/UserAgentModel.py</Source> + <Source>WebBrowser/UserAgent/UserAgentReader.py</Source> + <Source>WebBrowser/UserAgent/UserAgentWriter.py</Source> + <Source>WebBrowser/UserAgent/UserAgentsDialog.py</Source> + <Source>WebBrowser/UserAgent/__init__.py</Source> + <Source>WebBrowser/VirusTotal/VirusTotalApi.py</Source> + <Source>WebBrowser/VirusTotal/VirusTotalDomainReportDialog.py</Source> + <Source>WebBrowser/VirusTotal/VirusTotalIpReportDialog.py</Source> + <Source>WebBrowser/VirusTotal/VirusTotalWhoisDialog.py</Source> + <Source>WebBrowser/VirusTotal/__init__.py</Source> + <Source>WebBrowser/WebBrowserClearPrivateDataDialog.py</Source> + <Source>WebBrowser/WebBrowserJavaScriptConsole.py</Source> + <Source>WebBrowser/WebBrowserLanguagesDialog.py</Source> + <Source>WebBrowser/WebBrowserPage.py</Source> + <Source>WebBrowser/WebBrowserSnap.py</Source> + <Source>WebBrowser/WebBrowserTabBar.py</Source> + <Source>WebBrowser/WebBrowserTabWidget.py</Source> + <Source>WebBrowser/WebBrowserView.py</Source> + <Source>WebBrowser/WebBrowserWebSearchWidget.py</Source> + <Source>WebBrowser/WebBrowserWindow.py</Source> + <Source>WebBrowser/WebInspector.py</Source> + <Source>WebBrowser/ZoomManager/ZoomManager.py</Source> + <Source>WebBrowser/ZoomManager/ZoomValuesDialog.py</Source> + <Source>WebBrowser/ZoomManager/ZoomValuesModel.py</Source> + <Source>WebBrowser/ZoomManager/__init__.py</Source> + <Source>WebBrowser/__init__.py</Source> + <Source>WebBrowser/data/html_rc.py</Source> + <Source>WebBrowser/data/icons_rc.py</Source> + <Source>WebBrowser/data/javascript_rc.py</Source> + <Source>WebBrowser/data/qml_rc.py</Source> <Source>__init__.py</Source> <Source>cleanupSource.py</Source> <Source>compileUiFiles.py</Source> <Source>eric6.py</Source> <Source>eric6.pyw</Source> <Source>eric6_api.py</Source> + <Source>eric6_browser.py</Source> + <Source>eric6_browser.pyw</Source> <Source>eric6_compare.py</Source> <Source>eric6_compare.pyw</Source> <Source>eric6_configure.py</Source> @@ -1603,6 +1802,8 @@ <Form>Preferences/ConfigurationPages/TrayStarterPage.ui</Form> <Form>Preferences/ConfigurationPages/VcsPage.ui</Form> <Form>Preferences/ConfigurationPages/ViewmanagerPage.ui</Form> + <Form>Preferences/ConfigurationPages/WebBrowserAppearancePage.ui</Form> + <Form>Preferences/ConfigurationPages/WebBrowserPage.ui</Form> <Form>Preferences/MouseClickDialog.ui</Form> <Form>Preferences/ProgramsDialog.ui</Form> <Form>Preferences/ShortcutDialog.ui</Form> @@ -1659,6 +1860,56 @@ <Form>VCS/CommandOptionsDialog.ui</Form> <Form>VCS/RepositoryInfoDialog.ui</Form> <Form>ViewManager/BookmarkedFilesDialog.ui</Form> + <Form>WebBrowser/AdBlock/AdBlockDialog.ui</Form> + <Form>WebBrowser/AdBlock/AdBlockExceptionsDialog.ui</Form> + <Form>WebBrowser/Bookmarks/AddBookmarkDialog.ui</Form> + <Form>WebBrowser/Bookmarks/BookmarkPropertiesDialog.ui</Form> + <Form>WebBrowser/Bookmarks/BookmarksDialog.ui</Form> + <Form>WebBrowser/Bookmarks/BookmarksImportDialog.ui</Form> + <Form>WebBrowser/CookieJar/CookieDetailsDialog.ui</Form> + <Form>WebBrowser/CookieJar/CookiesConfigurationDialog.ui</Form> + <Form>WebBrowser/CookieJar/CookiesDialog.ui</Form> + <Form>WebBrowser/CookieJar/CookiesExceptionsDialog.ui</Form> + <Form>WebBrowser/Download/DownloadAskActionDialog.ui</Form> + <Form>WebBrowser/Download/DownloadItem.ui</Form> + <Form>WebBrowser/Download/DownloadManager.ui</Form> + <Form>WebBrowser/FeaturePermissions/FeaturePermissionsDialog.ui</Form> + <Form>WebBrowser/Feeds/FeedEditDialog.ui</Form> + <Form>WebBrowser/Feeds/FeedsDialog.ui</Form> + <Form>WebBrowser/Feeds/FeedsManager.ui</Form> + <Form>WebBrowser/FlashCookieManager/FlashCookieManagerDialog.ui</Form> + <Form>WebBrowser/GreaseMonkey/GreaseMonkeyAddScriptDialog.ui</Form> + <Form>WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationDialog.ui</Form> + <Form>WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationScriptInfoDialog.ui</Form> + <Form>WebBrowser/History/HistoryDialog.ui</Form> + <Form>WebBrowser/Network/SendRefererWhitelistDialog.ui</Form> + <Form>WebBrowser/Network/SslErrorExceptionsDialog.ui</Form> + <Form>WebBrowser/OpenSearch/OpenSearchDialog.ui</Form> + <Form>WebBrowser/OpenSearch/OpenSearchEditDialog.ui</Form> + <Form>WebBrowser/PageScreenDialog.ui</Form> + <Form>WebBrowser/Passwords/PasswordsDialog.ui</Form> + <Form>WebBrowser/PersonalInformationManager/PersonalDataDialog.ui</Form> + <Form>WebBrowser/QtHelp/HelpTopicDialog.ui</Form> + <Form>WebBrowser/QtHelp/QtHelpDocumentationDialog.ui</Form> + <Form>WebBrowser/QtHelp/QtHelpFiltersDialog.ui</Form> + <Form>WebBrowser/SearchWidget.ui</Form> + <Form>WebBrowser/SiteInfo/SiteInfoDialog.ui</Form> + <Form>WebBrowser/Sync/SyncCheckPage.ui</Form> + <Form>WebBrowser/Sync/SyncDataPage.ui</Form> + <Form>WebBrowser/Sync/SyncDirectorySettingsPage.ui</Form> + <Form>WebBrowser/Sync/SyncEncryptionPage.ui</Form> + <Form>WebBrowser/Sync/SyncFtpSettingsPage.ui</Form> + <Form>WebBrowser/Sync/SyncHostTypePage.ui</Form> + <Form>WebBrowser/Tools/WebIconDialog.ui</Form> + <Form>WebBrowser/UrlBar/BookmarkActionSelectionDialog.ui</Form> + <Form>WebBrowser/UrlBar/BookmarkInfoDialog.ui</Form> + <Form>WebBrowser/UserAgent/UserAgentsDialog.ui</Form> + <Form>WebBrowser/VirusTotal/VirusTotalDomainReportDialog.ui</Form> + <Form>WebBrowser/VirusTotal/VirusTotalIpReportDialog.ui</Form> + <Form>WebBrowser/VirusTotal/VirusTotalWhoisDialog.ui</Form> + <Form>WebBrowser/WebBrowserClearPrivateDataDialog.ui</Form> + <Form>WebBrowser/WebBrowserLanguagesDialog.ui</Form> + <Form>WebBrowser/ZoomManager/ZoomValuesDialog.ui</Form> </Forms> <Translations> <Translation>i18n/eric6_cs.qm</Translation> @@ -1690,6 +1941,13 @@ <Resource>Helpviewer/data/icons.qrc</Resource> <Resource>Helpviewer/data/javascript.qrc</Resource> <Resource>IconEditor/cursors/cursors.qrc</Resource> + <Resource>WebBrowser/Bookmarks/DefaultBookmarks.qrc</Resource> + <Resource>WebBrowser/OpenSearch/DefaultSearchEngines/DefaultSearchEngines.qrc</Resource> + <Resource>WebBrowser/UserAgent/UserAgentDefaults.qrc</Resource> + <Resource>WebBrowser/data/html.qrc</Resource> + <Resource>WebBrowser/data/icons.qrc</Resource> + <Resource>WebBrowser/data/javascript.qrc</Resource> + <Resource>WebBrowser/data/qml.qrc</Resource> </Resources> <Interfaces/> <Others> @@ -1794,12 +2052,51 @@ <Other>ThirdParty/Pygments/pygments/PKG-INFO</Other> <Other>ThirdParty/Send2Trash/LICENSE</Other> <Other>ThirdParty/enum/LICENSE</Other> + <Other>WebBrowser/Bookmarks/DefaultBookmarks.xbel</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/Amazoncom.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/Bing.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/DeEn_Beolingus.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/DuckDuckGo.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/Facebook.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/Google.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/Google_Im_Feeling_Lucky.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/LEO_DeuEng.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/LinuxMagazin.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/Reddit.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/Wikia.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/Wikia_en.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/Wikipedia.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/Wiktionary.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/Yahoo.xml</Other> + <Other>WebBrowser/OpenSearch/DefaultSearchEngines/YouTube.xml</Other> + <Other>WebBrowser/UserAgent/UserAgentDefaults.xml</Other> + <Other>WebBrowser/data/html/adblockPage.html</Other> + <Other>WebBrowser/data/html/speeddialPage.html</Other> + <Other>WebBrowser/data/html/startPage.html</Other> + <Other>WebBrowser/data/html/tabCrashPage.html</Other> + <Other>WebBrowser/data/icons/adBlockPlus16.png</Other> + <Other>WebBrowser/data/icons/adBlockPlus64.png</Other> + <Other>WebBrowser/data/icons/box-border-small.png</Other> + <Other>WebBrowser/data/icons/brokenPage.png</Other> + <Other>WebBrowser/data/icons/close.png</Other> + <Other>WebBrowser/data/icons/edit.png</Other> + <Other>WebBrowser/data/icons/ericWeb16.png</Other> + <Other>WebBrowser/data/icons/ericWeb32.png</Other> + <Other>WebBrowser/data/icons/loading.gif</Other> + <Other>WebBrowser/data/icons/plus.png</Other> + <Other>WebBrowser/data/icons/reload.png</Other> + <Other>WebBrowser/data/icons/setting.png</Other> + <Other>WebBrowser/data/javascript/jquery-ui.js</Other> + <Other>WebBrowser/data/javascript/jquery.js</Other> + <Other>WebBrowser/data/javascript/qwebchannel.js</Other> + <Other>WebBrowser/data/qml/thumbnailer.qml</Other> <Other>changelog</Other> <Other>default.e4k</Other> <Other>default_Mac.e4k</Other> <Other>eric6.appdata.xml</Other> <Other>eric6.desktop</Other> <Other>eric6.e4p</Other> + <Other>eric6_browser.desktop</Other> <Other>eric6_webbrowser.desktop</Other> <Other>eric6config.linux</Other> <Other>icons</Other> @@ -2203,7 +2500,7 @@ <string>ExcludeMessages</string> </key> <value> - <string>C101, E265, M811, N802, N803, N807, N808, N821, W293, E266, E402</string> + <string>C101, E265, E266, M811, N802, N803, N807, N808, N821, W293, E402</string> </value> <key> <string>FixCodes</string>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6_browser.desktop Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,15 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Exec=eric6_browser@MARKER@ +MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https; +Icon=ericWeb@MARKER@ +Terminal=false +Name=eric6 Web Browser@PY_MARKER@ +Name[de]=eric6 Web Browser@PY_MARKER@ +Comment=Web Browser for PyQt5 +Comment[de]=Web Browser für PyQt5 +GenericName=Web Browser +GenericName[de]=Web Browser +Categories=Qt;Python;Network;WebBrowser;QtWebEngine +StartupNotify=true
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6_browser.py Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2002 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Eric6 Web Browser. + +This is the main Python script that performs the necessary initialization +of the web browser and starts the Qt event loop. This is a standalone version +of the integrated web browser. It is based on QtWebEngine. +""" + +from __future__ import unicode_literals + +import Toolbox.PyQt4ImportHook # __IGNORE_WARNING__ + +try: # Only for Py2 + import Globals.compatibility_fixes # __IGNORE_WARNING__ +except (ImportError): + pass + +try: + import sip + sip.setdestroyonexit(False) +except AttributeError: + pass + +import sys +import os + +MIN_QT_VERSION = "5.6.0" + +from PyQt5.QtCore import qVersion +if qVersion() < MIN_QT_VERSION: + from PyQt5.QtCore import QTimer + from PyQt5.QtWidgets import QApplication + from E5Gui import E5MessageBox + app = QApplication([]) + QTimer.singleShot(0, lambda: E5MessageBox.critical( + None, + "eric6 Web Browser", + "You need at least Qt Version {0} to execute the web browser." + .format(MIN_QT_VERSION))) + app.exec_() + sys.exit(100) + +SettingsDir = None + +for arg in sys.argv[:]: + if arg.startswith("--config="): + import Globals + configDir = arg.replace("--config=", "") + Globals.setConfigDir(configDir) + sys.argv.remove(arg) + elif arg.startswith("--settings="): + from PyQt5.QtCore import QSettings + SettingsDir = os.path.expanduser(arg.replace("--settings=", "")) + if not os.path.isdir(SettingsDir): + os.makedirs(SettingsDir) + QSettings.setPath(QSettings.IniFormat, QSettings.UserScope, + SettingsDir) + sys.argv.remove(arg) + +# make ThirdParty package available as a packages repository +sys.path.insert(2, os.path.join(os.path.dirname(__file__), + "ThirdParty", "Pygments")) + +try: + from PyQt5 import QtWebEngineWidgets # __IGNORE_WARNING__ +except ImportError: + from PyQt5.QtCore import QTimer + from PyQt5.QtWidgets import QApplication + from E5Gui import E5MessageBox # __IGNORE_WARNING__ + app = QApplication([]) + QTimer.singleShot(0, lambda: E5MessageBox.critical( + None, + "eric6 Web Browser", + "QtWebEngineWidgets is not installed but needed to execute the" + " web browser.")) + app.exec_() + sys.exit(100) + +import Globals +from Globals import AppInfo + +from Toolbox import Startup + + +def createMainWidget(argv): + """ + Function to create the main widget. + + @param argv list of commandline parameters (list of strings) + @return reference to the main widget (QWidget) + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + + searchWord = None + private = False + qthelp = False + + for arg in reversed(argv): + if arg.startswith("--search="): + searchWord = argv[1].split("=", 1)[1] + argv.remove(arg) + elif arg == "--private": + private = True + argv.remove(arg) + elif arg == "--qthelp": + qthelp = True + argv.remove(arg) + elif arg.startswith("--"): + argv.remove(arg) + + try: + home = argv[1] + except IndexError: + home = "" + + browser = WebBrowserWindow(home, '.', None, 'web_browser', + searchWord=searchWord, private=private, + settingsDir=SettingsDir, qthelp=qthelp) + return browser + + +def main(): + """ + Main entry point into the application. + """ + options = [ + ("--config=configDir", + "use the given directory as the one containing the config files"), + ("--private", "start the browser in private browsing mode"), + ("--qthelp", "start the browser with support for QtHelp"), + ("--search=word", "search for the given word"), + ("--settings=settingsDir", + "use the given directory to store the settings files"), + ] + appinfo = AppInfo.makeAppInfo(sys.argv, + "eric6 Web Browser", + "file", + "web browser", + options) + + if not Globals.checkBlacklistedVersions(): + sys.exit(100) + + res = Startup.simpleAppStartup(sys.argv, + appinfo, + createMainWidget, + installErrorHandler=True) + sys.exit(res) + +if __name__ == '__main__': + main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6_browser.pyw Sun Apr 03 16:33:37 2016 +0200 @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Windows entry point. +""" + +from __future__ import unicode_literals + +from eric6_browser import main + +main()
--- a/eric6_configure.py Sun Apr 03 12:29:37 2016 +0200 +++ b/eric6_configure.py Sun Apr 03 16:33:37 2016 +0200 @@ -37,6 +37,16 @@ settingsDir) sys.argv.remove(arg) +from PyQt5.QtCore import qVersion +if qVersion() < "5.6.0": + WEBENGINE_AVAILABLE = False +else: + try: + from PyQt5 import QtWebEngineWidgets # __IGNORE_WARNING__ + WEBENGINE_AVAILABLE = True + except ImportError: + WEBENGINE_AVAILABLE = False + # make ThirdParty package available as a packages repository sys.path.insert(2, os.path.join(os.path.dirname(__file__), "ThirdParty", "Pygments")) @@ -54,7 +64,7 @@ @return reference to the main widget (QWidget) """ from Preferences.ConfigurationDialog import ConfigurationWindow - w = ConfigurationWindow() + w = ConfigurationWindow(webEngine=WEBENGINE_AVAILABLE) w.show() w.showConfigurationPageByName("empty") return w
--- a/install.py Sun Apr 03 12:29:37 2016 +0200 +++ b/install.py Sun Apr 03 16:33:37 2016 +0200 @@ -511,7 +511,10 @@ for name in ["/usr/share/applications/eric6" + marker + ".desktop", "/usr/share/appdata/eric6" + marker + ".appdata.xml", "/usr/share/applications/eric6_webbrowser" + marker + - ".desktop"]: + ".desktop", + "/usr/share/applications/eric6_browser" + marker + + ".desktop", + ]: if os.path.exists(name): os.remove(name) @@ -527,7 +530,7 @@ "eric6_plugininstall", "eric6_pluginuninstall", "eric6_pluginrepository", "eric6_sqlbrowser", "eric6_webbrowser", "eric6_iconeditor", - "eric6_snap", "eric6_hexeditor", + "eric6_snap", "eric6_hexeditor", "eric6_browser", ] if includePythonVariant: marker = PythonMarkers[sys.version_info.major] @@ -637,7 +640,7 @@ "eric6_qregularexpression", "eric6_re", "eric6_snap", "eric6_sqlbrowser", "eric6_tray", "eric6_trpreviewer", "eric6_uipreviewer", "eric6_unittest", "eric6_webbrowser", - "eric6"]: + "eric6_browser", "eric6"]: wnames.append(createPyWrapper(cfg['ericDir'], name)) # set install prefix, if not None @@ -814,6 +817,10 @@ os.path.join(sourceDir, "eric6_webbrowser.desktop"), os.path.join(dst, "eric6_webbrowser" + marker + ".desktop"), marker) + copyDesktopFile( + os.path.join(sourceDir, "eric6_browser.desktop"), + os.path.join(dst, "eric6_browser" + marker + ".desktop"), + marker) dst = os.path.normpath( os.path.join(distDir, "usr/share/appdata")) if not os.path.exists(dst): @@ -843,6 +850,11 @@ "/usr/share/applications/eric6_webbrowser" + marker + ".desktop", marker) + copyDesktopFile( + os.path.join(sourceDir, "eric6_browser.desktop"), + "/usr/share/applications/eric6_browser" + marker + + ".desktop", + marker) # Create a Mac application bundle if sys.platform == "darwin":
--- a/uninstall.py Sun Apr 03 12:29:37 2016 +0200 +++ b/uninstall.py Sun Apr 03 16:33:37 2016 +0200 @@ -115,6 +115,8 @@ "/usr/share/appdata/eric6" + marker + ".appdata.xml", "/usr/share/applications/eric6_webbrowser" + marker + ".desktop", + "/usr/share/applications/eric6_browser" + marker + + ".desktop", "/usr/share/pixmaps/eric" + marker + ".png", "/usr/share/pixmaps/ericWeb" + marker + ".png"]: if os.path.exists(name): @@ -132,7 +134,7 @@ "eric6_plugininstall", "eric6_pluginuninstall", "eric6_pluginrepository", "eric6_sqlbrowser", "eric6_webbrowser", "eric6_iconeditor", - "eric6_snap", "eric6_hexeditor", + "eric6_snap", "eric6_hexeditor", "eric6_browser", ] if includePythonVariant: marker = PythonMarkers[sys.version_info.major]