Sun, 02 Jul 2017 19:40:39 +0200
Continued implementing session support for the new web browser.
--- a/Preferences/ConfigurationPages/WebBrowserPage.py Sat Jul 01 19:14:01 2017 +0200 +++ b/Preferences/ConfigurationPages/WebBrowserPage.py Sun Jul 02 19:40:39 2017 +0200 @@ -340,7 +340,7 @@ @param index index of the selected entry (integer) """ - enable = index == 0 + enable = index == 1 self.homePageLabel.setEnabled(enable) self.homePageEdit.setEnabled(enable) self.defaultHomeButton.setEnabled(enable)
--- a/Preferences/ConfigurationPages/WebBrowserPage.ui Sat Jul 01 19:14:01 2017 +0200 +++ b/Preferences/ConfigurationPages/WebBrowserPage.ui Sun Jul 02 19:40:39 2017 +0200 @@ -150,6 +150,11 @@ </property> <item> <property name="text"> + <string>Show Empty Page</string> + </property> + </item> + <item> + <property name="text"> <string>Show Home Page</string> </property> </item> @@ -160,7 +165,12 @@ </item> <item> <property name="text"> - <string>Show Empty Page</string> + <string>Restore Session</string> + </property> + </item> + <item> + <property name="text"> + <string>Select Session</string> </property> </item> </widget>
--- a/Preferences/__init__.py Sat Jul 01 19:14:01 2017 +0200 +++ b/Preferences/__init__.py Sun Jul 02 19:40:39 2017 +0200 @@ -1032,7 +1032,12 @@ "StatusBarVisible": True, "SaveGeometry": True, "WebBrowserState": QByteArray(), - "StartupBehavior": 1, # show speed dial + "StartupBehavior": 2, # show speed dial + # 0 open empty page + # 1 open home page + # 2 open speed dial + # 3 restore last session + # 4 ask user for session "HomePage": "eric:home", "WarnOnMultipleClose": True, "DefaultScheme": "https://",
--- a/WebBrowser/Session/SessionManager.py Sat Jul 01 19:14:01 2017 +0200 +++ b/WebBrowser/Session/SessionManager.py Sun Jul 02 19:40:39 2017 +0200 @@ -13,8 +13,9 @@ import json from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QTimer, QDir, \ - QFile, QFileInfo, QFileSystemWatcher, QByteArray -from PyQt5.QtWidgets import QMenu, QAction, QActionGroup, QApplication + QFile, QFileInfo, QFileSystemWatcher, QByteArray, QDateTime +from PyQt5.QtWidgets import QMenu, QAction, QActionGroup, QApplication, \ + QInputDialog, QLineEdit from E5Gui import E5MessageBox @@ -261,7 +262,13 @@ sessionFilesInfoList = QDir(self.getSessionsDirectory()).entryInfoList( ["*.json"], QDir.Files, QDir.Time) + for sessionFileInfo in sessionFilesInfoList: + sessionData = self.__readSessionFromFile( + sessionFileInfo.absoluteFilePath()) + if not sessionData or not sessionData["Windows"]: + continue + data = SessionMetaData() data.name = sessionFileInfo.baseName() data.filePath = sessionFileInfo.canonicalFilePath() @@ -273,7 +280,11 @@ if self.__isActive(sessionFileInfo): data.isActive = True - self.__sessionMetaData.append(data) + if data.isDefault: + # default session is always first + self.__sessionMetaData.insert(0, data) + else: + self.__sessionMetaData.append(data) def __isActive(self, filePath): """ @@ -324,9 +335,14 @@ path = act.data() self.switchToSession(path) - def __openSession(self, sessionFilePath, flags=None): + def __openSession(self, sessionFilePath, flags=0): """ Private method to open a session from a given session file. + + @param sessionFilePath name of the session file to get session from + @type str + @param flags flags determining the open mode + @type int """ if self.__isActive(sessionFilePath): return @@ -337,7 +353,9 @@ from WebBrowser.WebBrowserWindow import WebBrowserWindow window = WebBrowserWindow.mainWindow() - if flags & SessionManager.SwitchSession: + + if ((flags & SessionManager.SwitchSession) == + SessionManager.SwitchSession): # save the current session self.writeCurrentSession(self.__lastActiveSession) @@ -349,7 +367,8 @@ if win is not window: win.forceClose() - if not (flags & SessionManager.ReplaceSession): + if not ((flags & SessionManager.ReplaceSession) == + SessionManager.ReplaceSession): self.__lastActiveSession = \ QFileInfo(sessionFilePath).canonicalFilePath() self.__sessionMetaData = [] @@ -376,13 +395,97 @@ QApplication.processEvents() QApplication.restoreOverrideCursor() - def renameSession(self, sessionFilePath="", flags=None): - # TODO: implement this - pass + def renameSession(self, sessionFilePath, flags=0): + """ + Public method to rename or clone a session + + @param sessionFilePath name of the session file + @type str + @param flags flags determining a rename or clone operation + @type int + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + + suggestedName = QFileInfo(sessionFilePath).baseName() + if flags & SessionManager.CloneSession: + suggestedName += "_cloned" + title = self.tr("Clone Session") + else: + suggestedName += "_renamed" + title = self.tr("Rename Session") + newName, ok = QInputDialog.getText( + WebBrowserWindow.getWindow(), + title, + self.tr("Please enter a new name:"), + QLineEdit.Normal, + suggestedName) + + if not ok: + return + + if not newName.endswith(".json"): + newName += ".json" + + newSessionPath = os.path.join(self.getSessionsDirectory(), newName) + if os.path.exists(newSessionPath): + E5MessageBox.information( + WebBrowserWindow.getWindow(), + title, + self.tr("""The session file "{0}" exists already. Please""" + """ enter another name.""").format(newName)) + self.renameSession(sessionFilePath, flags) + return + + if flags & SessionManager.CloneSession: + if not QFile.copy(sessionFilePath, newSessionPath): + E5MessageBox.critical( + WebBrowserWindow.getWindow(), + title, + self.tr("""An error occurred while cloning the session""" + """ file.""")) + return + else: + if not QFile.rename(sessionFilePath, newSessionPath): + E5MessageBox.critical( + WebBrowserWindow.getWindow(), + title, + self.tr("""An error occurred while renaming the session""" + """ file.""")) + return + if self.__isActive(sessionFilePath): + self.__lastActiveSession = newSessionPath + self.__sessionMetaData = [] def saveSession(self): - # TODO: implement this - pass + """ + Public method to save the current session. + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + newName, ok = QInputDialog.getText( + WebBrowserWindow.getWindow(), + self.tr("Save Session"), + self.tr("Please enter a name for the session:"), + QLineEdit.Normal, + self.tr("Saved Session ({0})").format( + QDateTime.currentDateTime().toString("yyyy-MM-dd HH-mm-ss"))) + + if not ok: + return + + if not newName.endswith(".json"): + newName += ".json" + + newSessionPath = os.path.join(self.getSessionsDirectory(), newName) + if os.path.exists(newSessionPath): + E5MessageBox.information( + WebBrowserWindow.getWindow(), + self.tr("Save Session"), + self.tr("""The session file "{0}" exists already. Please""" + """ enter another name.""").format(newName)) + self.saveSession() + return + + self.writeCurrentSession(newSessionPath) def replaceSession(self, sessionFilePath): """ @@ -393,9 +496,10 @@ """ from WebBrowser.WebBrowserWindow import WebBrowserWindow res = E5MessageBox.yesNo( - WebBrowserWindow.mainWindow(), + WebBrowserWindow.getWindow(), self.tr("Restore Backup"), - self.tr("""Are you sure you want to replace current session?""")) + self.tr("""Are you sure you want to replace the current""" + """ session?""")) if res: self.__openSession(sessionFilePath, SessionManager.ReplaceSession) @@ -409,18 +513,76 @@ self.__openSession(sessionFilePath, SessionManager.SwitchSession) def cloneSession(self, sessionFilePath): - # TODO: implement this - pass + """ + Public method to clone a session. + + @param sessionFilePath file name of the session file to be cloned + @type str + """ + self.renameSession(sessionFilePath, SessionManager.CloneSession) def deleteSession(self, sessionFilePath): - # TODO: implement this - pass + """ + Public method to delete a session. + + @param sessionFilePath file name of the session file to be deleted + @type str + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + res = E5MessageBox.yesNo( + WebBrowserWindow.getWindow(), + self.tr("Delete Session"), + self.tr("""Are you sure you want to delete session "{0}"?""") + .format(QFileInfo(sessionFilePath).baseName())) + if res: + QFile.remove(sessionFilePath) def newSession(self): - # TODO: implement this - pass + """ + Public method to start a new session. + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + newName, ok = QInputDialog.getText( + WebBrowserWindow.getWindow(), + self.tr("New Session"), + self.tr("Please enter a name for the new session:"), + QLineEdit.Normal, + self.tr("New Session ({0})").format( + QDateTime.currentDateTime().toString("yyyy-MM-dd HH-mm-ss"))) + + if not ok: + return + + if not newName.endswith(".json"): + newName += ".json" + + newSessionPath = os.path.join(self.getSessionsDirectory(), newName) + if os.path.exists(newSessionPath): + E5MessageBox.information( + WebBrowserWindow.getWindow(), + self.tr("New Session"), + self.tr("""The session file "{0}" exists already. Please""" + """ enter another name.""").format(newName)) + self.newSession() + return + + self.writeCurrentSession(self.__lastActiveSession) + + # create new window for the new session and close all existing windows + window = WebBrowserWindow.mainWindow().newWindow() + for win in WebBrowserWindow.mainWindows(): + if win is not window: + win.forceClose() + + self.__lastActiveSession = newSessionPath + self.__autoSaveSession() def showSessionManagerDialog(self): - # TODO: implement this - pass - print("showSessionManagerDialog()") + """ + Public method to show the session manager dialog. + """ + from WebBrowser.WebBrowserWindow import WebBrowserWindow + from .SessionManagerDialog import SessionManagerDialog + + dlg = SessionManagerDialog(WebBrowserWindow.getWindow()) + dlg.open()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Session/SessionManagerDialog.py Sun Jul 02 19:40:39 2017 +0200 @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to manage sessions. +""" + +from PyQt5.QtCore import pyqtSlot, Qt, QFileInfo +from PyQt5.QtGui import QPalette +from PyQt5.QtWidgets import QDialog, QTreeWidgetItem + +from .Ui_SessionManagerDialog import Ui_SessionManagerDialog + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + + +class SessionManagerDialog(QDialog, Ui_SessionManagerDialog): + """ + Class documentation goes here. + """ + SessionFileRole = Qt.UserRole + BackupSessionRole = Qt.UserRole + 1 + ActiveSessionRole = Qt.UserRole + 2 + DefaultSessionRole = Qt.UserRole + 3 + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget + @type QWidget + """ + super(SessionManagerDialog, self).__init__(parent) + self.setupUi(self) + + self.newButton.clicked.connect(self.__newSession) + self.renameButton.clicked.connect(self.__renameSession) + self.cloneButton.clicked.connect(self.__cloneSession) + self.deleteButton.clicked.connect(self.__deleteSession) + self.switchButton.clicked.connect(self.__switchToSession) + self.sessionsList.currentItemChanged.connect(self.__updateButtons) + + self.__refresh() + WebBrowserWindow.sessionManager().sessionsMetaDataChanged.connect( + self.__refresh) + + @pyqtSlot() + def __refresh(self): + """ + Private slot to refresh the list of sessions. + """ + self.sessionsList.clear() + + sessions = WebBrowserWindow.sessionManager().sessionMetaData( + includeBackups=True) + for session in sessions: + itm = QTreeWidgetItem() + itm.setText(0, session.name) + itm.setText(1, QFileInfo(session.filePath).lastModified() + .toString("yyyy-MM-dd hh:mm")) + itm.setData(0, SessionManagerDialog.SessionFileRole, + session.filePath) + itm.setData(0, SessionManagerDialog.BackupSessionRole, + session.isBackup) + itm.setData(0, SessionManagerDialog.ActiveSessionRole, + session.isActive) + itm.setData(0, SessionManagerDialog.DefaultSessionRole, + session.isDefault) + self.__updateSessionItem(itm) + self.sessionsList.addTopLevelItem(itm) + + self.__updateButtons() + + def __updateButtons(self): + """ + Private method to update the button state. + """ + itm = self.sessionsList.currentItem() + if itm: + isBackup = itm.data(0, SessionManagerDialog.BackupSessionRole) + isActive = itm.data(0, SessionManagerDialog.ActiveSessionRole) + isDefault = itm.data(0, SessionManagerDialog.DefaultSessionRole) + + self.renameButton.setEnabled(not isDefault and not isBackup) + self.cloneButton.setEnabled(not isBackup) + self.deleteButton.setEnabled(not isBackup and not isDefault and + not isActive) + self.switchButton.setEnabled(not isActive) + if isBackup: + self.switchButton.setText(self.tr("Restore")) + else: + self.switchButton.setText(self.tr("Switch To")) + else: + self.renameButton.setEnabled(False) + self.cloneButton.setEnabled(False) + self.deleteButton.setEnabled(False) + self.switchButton.setEnabled(False) + self.switchButton.setText(self.tr("Switch To")) + + def __updateSessionItem(self, itm): + """ + Private method to set various item properties. + + @param itm reference to the item to be updated + @type QTreeWidgetItem + """ + isBackup = itm.data(0, SessionManagerDialog.BackupSessionRole) + isActive = itm.data(0, SessionManagerDialog.ActiveSessionRole) + isDefault = itm.data(0, SessionManagerDialog.DefaultSessionRole) + + font = itm.font(0) + + if isBackup: + color = self.palette().color(QPalette.Disabled, + QPalette.WindowText) + itm.setForeground(0, color) + itm.setForeground(1, color) + + if isActive: + font.setBold(True) + itm.setFont(0, font) + itm.setFont(1, font) + + if isDefault: + font.setItalic(True) + itm.setFont(0, font) + itm.setFont(1, font) + + def showEvent(self, evt): + """ + Protected method handling the dialog being shown. + + @param evt reference to the event object + @type QShowEvent + """ + super(SessionManagerDialog, self).showEvent(evt) + self.__resizeViewHeader() + + def resizeEvent(self, evt): + """ + Protected method handling the dialog being resized. + + @param evt reference to the event object + @type QResizeEvent + """ + super(SessionManagerDialog, self).resizeEvent(evt) + self.__resizeViewHeader() + + def __resizeViewHeader(self): + """ + Private method to resize the session column of the list. + """ + headerWidth = self.sessionsList.header().width() + self.sessionsList.header().resizeSection( + 0, headerWidth - headerWidth / 2.5) + + @pyqtSlot() + def __newSession(self): + """ + Private slot to create a new session. + """ + WebBrowserWindow.sessionManager().newSession() + + @pyqtSlot() + def __renameSession(self): + """ + Private slot to rename the selected session. + """ + itm = self.sessionsList.currentItem() + if itm is None: + return + + filePath = itm.data(0, SessionManagerDialog.SessionFileRole) + if filePath: + WebBrowserWindow.sessionManager().renameSession(filePath) + + @pyqtSlot() + def __cloneSession(self): + """ + Private slot to clone the selected session. + """ + itm = self.sessionsList.currentItem() + if itm is None: + return + + filePath = itm.data(0, SessionManagerDialog.SessionFileRole) + if filePath: + WebBrowserWindow.sessionManager().cloneSession(filePath) + + @pyqtSlot() + def __deleteSession(self): + """ + Private slot to delete the selected session. + """ + itm = self.sessionsList.currentItem() + if itm is None: + return + + filePath = itm.data(0, SessionManagerDialog.SessionFileRole) + if filePath: + WebBrowserWindow.sessionManager().deleteSession(filePath) + + @pyqtSlot() + def __switchToSession(self): + """ + Private slot to switch to the selected session. + """ + itm = self.sessionsList.currentItem() + if itm is None: + return + + filePath = itm.data(0, SessionManagerDialog.SessionFileRole) + if filePath: + if itm.data(0, SessionManagerDialog.BackupSessionRole): + WebBrowserWindow.sessionManager().replaceSession(filePath) + else: + WebBrowserWindow.sessionManager().switchToSession(filePath)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/Session/SessionManagerDialog.ui Sun Jul 02 19:40:39 2017 +0200 @@ -0,0 +1,157 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SessionManagerDialog</class> + <widget class="QDialog" name="SessionManagerDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>400</height> + </rect> + </property> + <property name="windowTitle"> + <string>Session Manager</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QTreeWidget" name="sessionsList"> + <property name="toolTip"> + <string>Shows a list of available sessions</string> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <column> + <property name="text"> + <string>Session</string> + </property> + </column> + <column> + <property name="text"> + <string>Last Modified</string> + </property> + </column> + </widget> + </item> + <item row="0" column="1"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QPushButton" name="newButton"> + <property name="toolTip"> + <string>Press to create a new session</string> + </property> + <property name="text"> + <string>New</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="renameButton"> + <property name="toolTip"> + <string>Press to rename the selected session</string> + </property> + <property name="text"> + <string>Rename</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="cloneButton"> + <property name="toolTip"> + <string>Press to clone the selected session</string> + </property> + <property name="text"> + <string>Clone</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="deleteButton"> + <property name="toolTip"> + <string>Press to delete the selected session</string> + </property> + <property name="text"> + <string>Delete</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="switchButton"> + <property name="toolTip"> + <string>Press to switch to the selected session</string> + </property> + <property name="text"> + <string>Switch To</string> + </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>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <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::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SessionManagerDialog</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>SessionManagerDialog</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>
--- a/WebBrowser/WebBrowserTabWidget.py Sat Jul 01 19:14:01 2017 +0200 +++ b/WebBrowser/WebBrowserTabWidget.py Sun Jul 02 19:40:39 2017 +0200 @@ -456,9 +456,9 @@ self.__navigationButton.setEnabled(True) if not linkName and not restoreSession: - if Preferences.getWebBrowser("StartupBehavior") == 0: + if Preferences.getWebBrowser("StartupBehavior") == 1: linkName = Preferences.getWebBrowser("HomePage") - elif Preferences.getWebBrowser("StartupBehavior") == 1: + elif Preferences.getWebBrowser("StartupBehavior") == 2: linkName = "eric:speeddial" if linkName:
--- a/WebBrowser/WebBrowserWindow.py Sat Jul 01 19:14:01 2017 +0200 +++ b/WebBrowser/WebBrowserWindow.py Sun Jul 02 19:40:39 2017 +0200 @@ -110,6 +110,7 @@ _sessionManager = None _performingShutdown = False + _lastActiveWindow = None def __init__(self, home, path, parent, name, fromEric=False, initShortcutsOnly=False, searchWord=None, @@ -160,7 +161,6 @@ self.__mHistory = [] self.__lastConfigurationPageName = "" - self.__lastActiveWindow = None WebBrowserWindow._isPrivate = private @@ -4002,28 +4002,28 @@ """ return cls.BrowserWindows - def __appFocusChanged(self, old, now): + @pyqtSlot() + def __appFocusChanged(self): """ 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 + """ + focusWindow = e5App().activeWindow() + if isinstance(focusWindow, WebBrowserWindow): + WebBrowserWindow._lastActiveWindow = focusWindow - def getWindow(self): - """ - Public method to get a reference to the most recent active + @classmethod + def getWindow(cls): + """ + Class 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() + if cls._lastActiveWindow: + return cls._lastActiveWindow + + return cls.mainWindow() def openSearchManager(self): """
--- a/eric6.e4p Sat Jul 01 19:14:01 2017 +0200 +++ b/eric6.e4p Sun Jul 02 19:40:39 2017 +0200 @@ -1406,6 +1406,7 @@ <Source>WebBrowser/QtHelp/__init__.py</Source> <Source>WebBrowser/SearchWidget.py</Source> <Source>WebBrowser/Session/SessionManager.py</Source> + <Source>WebBrowser/Session/SessionManagerDialog.py</Source> <Source>WebBrowser/Session/__init__.py</Source> <Source>WebBrowser/SiteInfo/SiteInfoDialog.py</Source> <Source>WebBrowser/SiteInfo/__init__.py</Source> @@ -1933,6 +1934,7 @@ <Form>WebBrowser/QtHelp/QtHelpDocumentationSelectionDialog.ui</Form> <Form>WebBrowser/QtHelp/QtHelpFiltersDialog.ui</Form> <Form>WebBrowser/SearchWidget.ui</Form> + <Form>WebBrowser/Session/SessionManagerDialog.ui</Form> <Form>WebBrowser/SiteInfo/SiteInfoDialog.ui</Form> <Form>WebBrowser/StatusBar/JavaScriptSettingsDialog.ui</Form> <Form>WebBrowser/Sync/SyncCheckPage.ui</Form> @@ -1995,14 +1997,14 @@ <Interfaces/> <Others> <Other>.hgignore</Other> + <Other>APIs/Python/zope-2.10.7.api</Other> + <Other>APIs/Python/zope-2.11.2.api</Other> + <Other>APIs/Python/zope-3.3.1.api</Other> <Other>APIs/Python3/PyQt4.bas</Other> <Other>APIs/Python3/PyQt5.bas</Other> <Other>APIs/Python3/QScintilla2.bas</Other> <Other>APIs/Python3/eric6.api</Other> <Other>APIs/Python3/eric6.bas</Other> - <Other>APIs/Python/zope-2.10.7.api</Other> - <Other>APIs/Python/zope-2.11.2.api</Other> - <Other>APIs/Python/zope-3.3.1.api</Other> <Other>APIs/QSS/qss.api</Other> <Other>APIs/Ruby/Ruby-1.8.7.api</Other> <Other>APIs/Ruby/Ruby-1.8.7.bas</Other>