Continued porting the web browser. QtWebEngine

Sun, 06 Mar 2016 16:59:26 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 06 Mar 2016 16:59:26 +0100
branch
QtWebEngine
changeset 4817
0a4e2fb0e93c
parent 4816
4f5ca06fa93a
child 4818
9afce2f09ac0

Continued porting the web browser.

- added capability to save the SSL certificate errors exceptions to the network manager

Preferences/__init__.py file | annotate | diff | comparison | revisions
WebBrowser/Network/NetworkManager.py file | annotate | diff | comparison | revisions
WebBrowser/Network/SslErrorExceptionsDialog.py file | annotate | diff | comparison | revisions
WebBrowser/Network/SslErrorExceptionsDialog.ui file | annotate | diff | comparison | revisions
WebBrowser/Passwords/PasswordManager.py file | annotate | diff | comparison | revisions
WebBrowser/WebBrowserWindow.py file | annotate | diff | comparison | revisions
WebBrowser/ZoomManager/ZoomManager.py file | annotate | diff | comparison | revisions
eric6.e4p file | annotate | diff | comparison | revisions
diff -r 4f5ca06fa93a -r 0a4e2fb0e93c Preferences/__init__.py
--- a/Preferences/__init__.py	Sun Mar 06 14:12:58 2016 +0100
+++ b/Preferences/__init__.py	Sun Mar 06 16:59:26 2016 +0100
@@ -1032,6 +1032,7 @@
         "WebInspectorEnabled": False,
         "DiskCacheEnabled": True,
         "DiskCacheSize": 50,        # 50 MB
+        "SslExceptionsDB": "{}",    # empty JSON dictionary
         # Grease Monkey
         "GreaseMonkeyDisabledScripts": [],
         # Downloads
diff -r 4f5ca06fa93a -r 0a4e2fb0e93c WebBrowser/Network/NetworkManager.py
--- a/WebBrowser/Network/NetworkManager.py	Sun Mar 06 14:12:58 2016 +0100
+++ b/WebBrowser/Network/NetworkManager.py	Sun Mar 06 16:59:26 2016 +0100
@@ -9,6 +9,9 @@
 
 from __future__ import unicode_literals
 
+import json
+
+from PyQt5.QtCore import pyqtSignal
 from PyQt5.QtWidgets import QDialog
 from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkProxy
 
@@ -23,13 +26,18 @@
 
 from WebBrowser.WebBrowserWindow import WebBrowserWindow
 
+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, parent=None):
         """
         Constructor
@@ -52,13 +60,60 @@
             self.__sslErrorHandler = E5SslErrorHandler(self)
             self.sslErrors.connect(self.__sslErrorHandler.sslErrorsReplySlot)
         
-        self.__ignoredSslErrors = {}
-        # dictionary of temporarily ignored SSL errors
+        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))
     
+    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 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 certificateError(self, error, view):
         """
         Public method to handle SSL certificate errors.
@@ -70,16 +125,21 @@
         @return flag indicating to ignore this error
         @rtype bool
         """
-        # TODO: permanent SSL certificate error exceptions
+        self.__load()
+        
         host = error.url().host()
         
-        if host in self.__ignoredSslErrors and \
-                self.__ignoredSslErrors[host] == error.error():
+        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")
-        accept = E5MessageBox.yesNo(
-            view,
+        msgBox = E5MessageBox.E5MessageBox(
+            E5MessageBox.Warning,
             title,
             self.tr("""<b>{0}</b>"""
                     """<p>The page you are trying to access has errors"""
@@ -87,12 +147,26 @@
                     """<ul><li>{1}</li></ul>"""
                     """<p>Would you like to make an exception?</p>""")
             .format(title, error.errorDescription()),
-            icon=E5MessageBox.Warning)
-        if accept:
-            self.__ignoredSslErrors[error.url().host()] = error.error()
+            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
-        
-        return False
+        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):
         """
diff -r 4f5ca06fa93a -r 0a4e2fb0e93c WebBrowser/Network/SslErrorExceptionsDialog.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/Network/SslErrorExceptionsDialog.py	Sun Mar 06 16:59:26 2016 +0100
@@ -0,0 +1,167 @@
+# -*- 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, 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.__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
+    
+    @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
diff -r 4f5ca06fa93a -r 0a4e2fb0e93c WebBrowser/Network/SslErrorExceptionsDialog.ui
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/Network/SslErrorExceptionsDialog.ui	Sun Mar 06 16:59:26 2016 +0100
@@ -0,0 +1,169 @@
+<?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>&amp;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 &amp;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>
+    <layout class="QHBoxLayout">
+     <property name="spacing">
+      <number>6</number>
+     </property>
+     <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>
+      <spacer>
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>131</width>
+         <height>31</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="okButton">
+       <property name="text">
+        <string>&amp;OK</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="cancelButton">
+       <property name="text">
+        <string>&amp;Cancel</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>okButton</sender>
+   <signal>clicked()</signal>
+   <receiver>SslErrorExceptionsDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>278</x>
+     <y>253</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>96</x>
+     <y>254</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>cancelButton</sender>
+   <signal>clicked()</signal>
+   <receiver>SslErrorExceptionsDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>369</x>
+     <y>253</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>179</x>
+     <y>282</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff -r 4f5ca06fa93a -r 0a4e2fb0e93c WebBrowser/Passwords/PasswordManager.py
--- a/WebBrowser/Passwords/PasswordManager.py	Sun Mar 06 14:12:58 2016 +0100
+++ b/WebBrowser/Passwords/PasswordManager.py	Sun Mar 06 16:59:26 2016 +0100
@@ -150,19 +150,22 @@
         if not self.__loaded:
             return
         
-        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()
+        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):
         """
diff -r 4f5ca06fa93a -r 0a4e2fb0e93c WebBrowser/WebBrowserWindow.py
--- a/WebBrowser/WebBrowserWindow.py	Sun Mar 06 14:12:58 2016 +0100
+++ b/WebBrowser/WebBrowserWindow.py	Sun Mar 06 16:59:26 2016 +0100
@@ -1603,6 +1603,24 @@
 ##                    self.__showCertificatesDialog)
 ##            self.__actions.append(self.certificatesAct)
         
+        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)
+        
         # TODO: Network Monitor (?)
 ##        self.toolsMonitorAct = E5Action(
 ##            self.tr('Network Monitor'),
@@ -1850,7 +1868,8 @@
 ##        from .Network.NetworkManager import SSL_AVAILABLE
 ##        if SSL_AVAILABLE:
 ##            menu.addAction(self.certificatesAct)
-##        menu.addSeparator()
+        menu.addAction(self.certificateErrorsAct)
+        menu.addSeparator()
         menu.addAction(self.zoomValuesAct)
         menu.addSeparator()
 ##        menu.addAction(self.adblockAct)
@@ -3326,6 +3345,12 @@
 ##        dlg = E5SslCertificatesDialog(self)
 ##        dlg.exec_()
 ##        
+    def __showCertificateErrorsDialog(self):
+        """
+        Private slot to show the certificate errors management dialog.
+        """
+        self.networkManager().showSslErrorExceptionsDialog()
+    
     # TODO: AdBlock
 ##    def __showAdBlockDialog(self):
 ##        """
diff -r 4f5ca06fa93a -r 0a4e2fb0e93c WebBrowser/ZoomManager/ZoomManager.py
--- a/WebBrowser/ZoomManager/ZoomManager.py	Sun Mar 06 14:12:58 2016 +0100
+++ b/WebBrowser/ZoomManager/ZoomManager.py	Sun Mar 06 16:59:26 2016 +0100
@@ -70,8 +70,10 @@
         if not self.__loaded:
             return
         
-        dbString = json.dumps(self.__zoomDB)
-        Preferences.setWebBrowser("ZoomValuesDB", dbString)
+        from WebBrowser.WebBrowserWindow import WebBrowserWindow
+        if not WebBrowserWindow.isPrivate():
+            dbString = json.dumps(self.__zoomDB)
+            Preferences.setWebBrowser("ZoomValuesDB", dbString)
     
     def __keyFromUrl(self, url):
         """
diff -r 4f5ca06fa93a -r 0a4e2fb0e93c eric6.e4p
--- a/eric6.e4p	Sun Mar 06 14:12:58 2016 +0100
+++ b/eric6.e4p	Sun Mar 06 16:59:26 2016 +0100
@@ -1337,6 +1337,7 @@
     <Source>WebBrowser/Network/FollowRedirectReply.py</Source>
     <Source>WebBrowser/Network/LoadRequest.py</Source>
     <Source>WebBrowser/Network/NetworkManager.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>
@@ -1826,6 +1827,7 @@
     <Form>WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationDialog.ui</Form>
     <Form>WebBrowser/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationScriptInfoDialog.ui</Form>
     <Form>WebBrowser/History/HistoryDialog.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>

eric ide

mercurial