diff --git a/resources/arrow_undo.png b/resources/arrow_undo.png new file mode 100644 index 0000000..6972c5e Binary files /dev/null and b/resources/arrow_undo.png differ diff --git a/resources/clock_go.png b/resources/clock_go.png new file mode 100644 index 0000000..a1a24d3 Binary files /dev/null and b/resources/clock_go.png differ diff --git a/resources/control_pause_blue.png b/resources/control_pause_blue.png new file mode 100644 index 0000000..ec61099 Binary files /dev/null and b/resources/control_pause_blue.png differ diff --git a/resources/cross.png b/resources/cross.png new file mode 100644 index 0000000..1514d51 Binary files /dev/null and b/resources/cross.png differ diff --git a/resources/door_in.png b/resources/door_in.png new file mode 100644 index 0000000..41676a0 Binary files /dev/null and b/resources/door_in.png differ diff --git a/resources/table_refresh.png b/resources/table_refresh.png new file mode 100644 index 0000000..ab92010 Binary files /dev/null and b/resources/table_refresh.png differ diff --git a/resources/timeline_marker.png b/resources/timeline_marker.png new file mode 100644 index 0000000..a3fbddf Binary files /dev/null and b/resources/timeline_marker.png differ diff --git a/syncplay/client.py b/syncplay/client.py index 9a35b48..abd1b2a 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -317,6 +317,9 @@ class SyncplayClient(object): if(self._protocol and self._protocol.logged): self._protocol.sendList() + def showUserList(self): + self.userlist.showUserList() + def getPassword(self): return self._serverPassword @@ -504,7 +507,6 @@ class SyncplayUserlist(object): self.ui.showMessage(message) def addUser(self, username, room, file_, position = 0, noMessage = False): - self._roomUsersChanged = True if(username == self.currentUser.username): self.currentUser.lastPosition = position return @@ -512,14 +514,15 @@ class SyncplayUserlist(object): self._users[username] = user if(not noMessage): self.__showUserChangeMessage(username, room, file_) + self.userListChange() def removeUser(self, username): - self._roomUsersChanged = True if(self._users.has_key(username)): self._users.pop(username) message = getMessage("en", "left-notification").format(username) self.ui.showMessage(message) - + self.userListChange() + def __displayModUserMessage(self, username, room, file_, user): if (file_ and not user.isFileSame(file_)): self.__showUserChangeMessage(username, room, file_) @@ -527,7 +530,6 @@ class SyncplayUserlist(object): self.__showUserChangeMessage(username, room, None) def modUser(self, username, room, file_): - self._roomUsersChanged = True if(self._users.has_key(username)): user = self._users[username] self.__displayModUserMessage(username, room, file_, user) @@ -537,7 +539,8 @@ class SyncplayUserlist(object): self.__showUserChangeMessage(username, room, file_) else: self.addUser(username, room, file_) - + self.userListChange() + def __addUserWithFileToList(self, rooms, user): currentPosition = utils.formatTime(user.lastPosition) file_key = '\'{}\' ({}/{})'.format(user.file['name'], currentPosition, utils.formatTime(user.file['duration'])) @@ -575,27 +578,28 @@ class SyncplayUserlist(object): return message def __displayFileWatchersInRoomList(self, key, users): - self.ui.showMessage(getMessage("en", "file-played-by-notification").format(key), True, True) + self.ui.showListMessage(getMessage("en", "file-played-by-notification").format(key)) for user in sorted(users.itervalues()): message = "<"+user.username+">" if(self.currentUser.username == user.username): message = "*" + message + "*" message = self.__addDifferentFileMessageIfNecessary(user, message) - self.ui.showMessage("\t" + message, True, True) + self.ui.showListMessage("\t" + message) def __displayPeopleInRoomWithNoFile(self, noFileList): if (noFileList): - self.ui.showMessage(getMessage("en", "notplaying-notification"), True, True) + self.ui.showListMessage(getMessage("en", "notplaying-notification")) for user in sorted(noFileList.itervalues()): - self.ui.showMessage("\t<" + user.username + ">", True, True) + self.ui.showListMessage("\t<" + user.username + ">") def __displayListOfPeople(self, rooms): for roomName in sorted(rooms.iterkeys()): - self.ui.showMessage(getMessage("en", "userlist-room-notification").format(roomName), True, False) + self.ui.showListMessage(getMessage("en", "userlist-room-notification").format(roomName)) noFileList = rooms[roomName].pop("__noFile__") if (rooms[roomName].has_key("__noFile__")) else None for key in sorted(rooms[roomName].iterkeys()): self.__displayFileWatchersInRoomList(key, rooms[roomName][key]) self.__displayPeopleInRoomWithNoFile(noFileList) + self.ui.markEndOfUserlist() def areAllFilesInRoomSame(self): for user in self._users.itervalues(): @@ -609,6 +613,10 @@ class SyncplayUserlist(object): return False return True + def userListChange(self): + self._roomUsersChanged = True + self.ui.userListChange() + def roomStateConfirmed(self): self._roomUsersChanged = False @@ -632,6 +640,9 @@ class UiManager(object): if(not noPlayer): self.showOSDMessage(message) self.__ui.showMessage(message, noTimestamp) + def showListMessage(self, message): + self.__ui.showListMessage(message) + def showOSDMessage(self, message, duration = constants.OSD_DURATION): if(self._client._player): self._client._player.displayMessage(message, duration * 1000) @@ -641,5 +652,11 @@ class UiManager(object): def promptFor(self, prompt): return self.__ui.promptFor(prompt) + + def userListChange(self): + self.__ui.userListChange() + def markEndOfUserlist(self): + self.__ui.markEndOfUserlist() + diff --git a/syncplay/clientManager.py b/syncplay/clientManager.py index 043a5f6..4bb1c1b 100644 --- a/syncplay/clientManager.py +++ b/syncplay/clientManager.py @@ -1,4 +1,3 @@ -from syncplay.client import SyncplayClient from syncplay.ui.ConfigurationGetter import ConfigurationGetter from syncplay import ui from syncplay.messages import getMessage @@ -6,6 +5,7 @@ from syncplay.messages import getMessage class SyncplayClientManager(object): def run(self): config = ConfigurationGetter().getConfiguration() + from syncplay.client import SyncplayClient #Imported later, so the proper reactor is installed interface = ui.getUi(graphical=not config["noGui"]) syncplayClient = SyncplayClient(config["playerClass"], interface, config) if(syncplayClient): diff --git a/syncplay/players/mpc.py b/syncplay/players/mpc.py index 36cb091..9d9310b 100644 --- a/syncplay/players/mpc.py +++ b/syncplay/players/mpc.py @@ -307,10 +307,12 @@ class MPCHCAPIPlayer(BasePlayer): speedSupported = False def __init__(self, client): + from twisted.internet import reactor + self.reactor = reactor self.__client = client self._mpcApi = MpcHcApi() self._mpcApi.callbacks.onUpdateFilename = lambda _: self.__makePing() - self._mpcApi.callbacks.onMpcClosed = lambda _: self.__client.stop(False) + self._mpcApi.callbacks.onMpcClosed = lambda _: self.reactor.callFromThread(self.__client.stop, (False),) self._mpcApi.callbacks.onFileStateChange = lambda _: self.__lockAsking() self._mpcApi.callbacks.onUpdatePlaystate = lambda _: self.__unlockAsking() self._mpcApi.callbacks.onGetCurrentPosition = lambda _: self.__onGetPosition() @@ -355,7 +357,7 @@ class MPCHCAPIPlayer(BasePlayer): self._mpcApi.askForVersion() if(not self.__versionUpdate.wait(0.1) or not self._mpcApi.version): self.__mpcError(getMessage("en", "mpc-version-insufficient-error").format(constants.MPC_MIN_VER)) - self.__client.stop(True) + self.reactor.callFromThread(self.__client.stop, (True),) def __testMpcReady(self): if(not self.__preventAsking.wait(10)): @@ -365,12 +367,12 @@ class MPCHCAPIPlayer(BasePlayer): try: self.__testMpcReady() self._mpcApi.callbacks.onUpdateFilename = lambda _: self.__handleUpdatedFilename() - self.__client.initPlayer(self) + self.reactor.callFromThread(self.__client.initPlayer, (self)) self.__handleUpdatedFilename() self.askForStatus() except Exception, err: self.__client.ui.showErrorMessage(err.message) - self.__client.stop() + self.reactor.callFromThread(self.__client.stop) def initPlayer(self, filePath): self.__dropIfNotSufficientVersion() @@ -448,11 +450,12 @@ class MPCHCAPIPlayer(BasePlayer): def __handleUpdatedFilename(self): with self.__fileUpdate: self.__setUpStateForNewlyOpenedFile() - self.__client.updateFile(self._mpcApi.filePlaying, self._mpcApi.fileDuration, self._mpcApi.filePath) + args = (self._mpcApi.filePlaying, self._mpcApi.fileDuration, self._mpcApi.filePath) + self.reactor.callFromThread(self.__client.updateFile, *args) def __mpcError(self, err=""): self.__client.ui.showErrorMessage(err) - self.__client.stop() + self.reactor.callFromThread(self.__client.stop) def sendCustomCommand(self, cmd, val): self._mpcApi.sendRawCommand(cmd, val) diff --git a/syncplay/ui/ConfigurationGetter.py b/syncplay/ui/ConfigurationGetter.py index 7ee31b7..249f472 100644 --- a/syncplay/ui/ConfigurationGetter.py +++ b/syncplay/ui/ConfigurationGetter.py @@ -8,6 +8,8 @@ from syncplay.players.playerFactory import PlayerFactory import codecs try: from syncplay.ui.GuiConfiguration import GuiConfiguration + from PySide import QtGui #@UnresolvedImport + from PySide.QtCore import Qt, QCoreApplication except ImportError: GuiConfiguration = None @@ -205,8 +207,9 @@ class ConfigurationGetter(object): except InvalidConfigValue: pass try: - for key, value in self._promptForMissingArguments().items(): - self._config[key] = value + if(self._config['noGui'] == False): + for key, value in self._promptForMissingArguments().items(): + self._config[key] = value except: sys.exit() @@ -240,5 +243,10 @@ class ConfigurationGetter(object): self._saveConfig(iniPath) if(self._config['file']): self._loadRelativeConfiguration() + if(not self._config['noGui']): + from syncplay.vendor import qt4reactor + if QCoreApplication.instance() is None: + self.app = QtGui.QApplication(sys.argv) + qt4reactor.install() return self._config diff --git a/syncplay/ui/GuiConfiguration.py b/syncplay/ui/GuiConfiguration.py index 84dfcd4..fdfd4e8 100644 --- a/syncplay/ui/GuiConfiguration.py +++ b/syncplay/ui/GuiConfiguration.py @@ -1,5 +1,5 @@ from PySide import QtCore, QtGui -from PySide.QtCore import QSettings, Qt +from PySide.QtCore import QSettings, Qt, QCoreApplication from PySide.QtGui import QApplication, QLineEdit, QCursor, QLabel, QCheckBox, QDesktopServices, QIcon import os @@ -14,10 +14,8 @@ class GuiConfiguration: def run(self): - try: + if QCoreApplication.instance() is None: self.app = QtGui.QApplication(sys.argv) - except: - pass dialog = ConfigDialog(self.config, self._availablePlayerPaths, self.error) dialog.exec_() diff --git a/syncplay/ui/__init__.py b/syncplay/ui/__init__.py index bca0a02..0848c4f 100644 --- a/syncplay/ui/__init__.py +++ b/syncplay/ui/__init__.py @@ -1,11 +1,11 @@ -from syncplay.ui.gui import GraphicalUI +from syncplay.ui.gui import MainDialog as GraphicalUI from syncplay.ui.consoleUI import ConsoleUI def getUi(graphical=True): - if(False): #graphical): #TODO: Add graphical ui + if(graphical): #TODO: Add graphical ui ui = GraphicalUI() else: ui = ConsoleUI() - ui.setDaemon(True) - ui.start() + ui.setDaemon(True) + ui.start() return ui diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index 33bdf7d..82761de 100644 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -40,6 +40,15 @@ class ConsoleUI(threading.Thread): self.promptMode.wait() return self.PromptResult + def showListMessage(self, message): + self.showMessage(message, True) + + def markEndOfUserlist(self): + pass + + def userListChange(self): + pass + def showMessage(self, message, noTimestamp=False): message = message.encode(sys.stdout.encoding, 'replace') if(noTimestamp): diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 0ae7f4d..cedec7a 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -1,9 +1,264 @@ -''' -Created on 05-07-2012 +from PySide import QtGui #@UnresolvedImport +from PySide.QtCore import Qt #@UnresolvedImport +from syncplay import utils, constants +import sys +import time +import re -@author: Uriziel -''' +class MainDialog(QtGui.QDialog): + def addClient(self, client): + self._syncplayClient = client + + def promptFor(self, prompt=">", message=""): + #TODO: Prompt user + return None -class GraphicalUI(object): + def showMessage(self, message, noTimestamp=False): + message = message.encode(sys.stdout.encoding, 'replace') + message = message.replace("&", "&").replace('"', """).replace("<", "<").replace(">", ">") + message = message.replace("\n", "
") + if(noTimestamp): + self.newMessage(message + "
") + else: + self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "
") + + def showListMessage(self, message): + message = message.encode(sys.stdout.encoding, 'replace') + message = message.replace("&", "&").replace('"', """).replace("<", "<").replace(">", ">") + message = message.replace("\t", " "*4) + self._listBuffer += message + "
" + + def markEndOfUserlist(self): + self.resetList() + self.newListItem(self._listBuffer) + self._listBuffer = ""; + + def userListChange(self): + self._syncplayClient.showUserList() + + def showDebugMessage(self, message): + print(message) + + def showErrorMessage(self, message): + print("ERROR:\t" + message) + + def joinRoom(self): + room = self.roomInput.text() + if room == "": + if self._syncplayClient.userlist.currentUser.file: + room = self._syncplayClient.userlist.currentUser.file["name"] + else: + room = self._syncplayClient.defaultRoom + self._syncplayClient.setRoom(room) + self._syncplayClient.sendRoom() + + def seekPosition(self): + s = re.match(constants.UI_SEEK_REGEX, self.seekInput.text()) + if(s): + sign = self._extractSign(s.group('sign')) + t = utils.parseTime(s.group('time')) + if(t is None): + return + if(sign): + t = self._syncplayClient.getGlobalPosition() + sign * t + self._syncplayClient.setPosition(t) + self.seekInput.setText("") + else: + self.showMessage("Invalid seek value", True) + + def showList(self): + self._syncplayClient.getUserList() #TODO: remove? + + def undoSeek(self): + tmp_pos = self._syncplayClient.getPlayerPosition() + self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek) + self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos + + def togglePause(self): + self._syncplayClient.setPaused(not self._syncplayClient.getPlayerPaused()) + + def exitSyncplay(self): + self._syncplayClient.stop() + + def closeEvent(self, event): + self.exitSyncplay() + event.ignore() + + def _extractSign(self, m): + if(m): + if(m == "-"): + return -1 + else: + return 1 + else: + return None + + def setOffset(self): + newoffset, ok = QtGui.QInputDialog.getText(self,"Set Offset", + "Offset (+/-):", QtGui.QLineEdit.Normal, + "") + if ok and newoffset != '': + o = re.match(constants.UI_OFFSET_REGEX, newoffset) + if(o): + sign = self._extractSign(o.group('sign')) + t = utils.parseTime(o.group('time')) + if(t is None): + return + if (o.group('sign') == "/"): + t = self._syncplayClient.getPlayerPosition() - t + elif(sign): + t = self._syncplayClient.getUserOffset() + sign * t + self._syncplayClient.setUserOffset(t) + else: + self.showMessage("Invalid offset value", True) + + def openUserGuide(self): + self.QtGui.QDesktopServices.openUrl("http://syncplay.pl/guide/") + + def addTopLayout(self, dialog): + dialog.topSplit = QtGui.QSplitter(Qt.Horizontal) + + dialog.outputLayout = QtGui.QVBoxLayout() + dialog.outputbox = QtGui.QTextEdit() + dialog.outputbox.setReadOnly(True) + dialog.outputlabel = QtGui.QLabel("Notifications") + dialog.outputFrame = QtGui.QFrame() + dialog.outputFrame.setLineWidth(0) + dialog.outputFrame.setMidLineWidth(0) + dialog.outputLayout.setContentsMargins(0,0,0,0) + dialog.outputLayout.addWidget(dialog.outputlabel) + dialog.outputLayout.addWidget(dialog.outputbox) + dialog.outputFrame.setLayout(dialog.outputLayout) + + dialog.listLayout = QtGui.QVBoxLayout() + dialog.listbox = QtGui.QTextEdit() + dialog.listbox.setReadOnly(True) + dialog.listlabel = QtGui.QLabel("List of who is playing what") + dialog.listFrame = QtGui.QFrame() + dialog.listFrame.setLineWidth(0) + dialog.listFrame.setMidLineWidth(0) + dialog.listLayout.setContentsMargins(0,0,0,0) + dialog.listLayout.addWidget(dialog.listlabel) + dialog.listLayout.addWidget(dialog.listbox) + dialog.listFrame.setLayout(dialog.listLayout) + + dialog.topSplit.addWidget(dialog.outputFrame) + dialog.topSplit.addWidget(dialog.listFrame) + dialog.topSplit.setStretchFactor(0,3) + dialog.topSplit.setStretchFactor(1,2) + dialog.mainLayout.addWidget(dialog.topSplit) + dialog.topSplit.setSizePolicy(QtGui.QSizePolicy.Preferred,QtGui.QSizePolicy.Expanding) + + def addBottomLayout(self, dialog): + dialog.bottomLayout = QtGui.QHBoxLayout() + + dialog.addRoomBox(MainDialog) + dialog.addSeekBox(MainDialog) + dialog.addMiscBox(MainDialog) + + dialog.bottomLayout.addWidget(dialog.roomGroup, Qt.AlignLeft) + dialog.bottomLayout.addWidget(dialog.seekGroup, Qt.AlignLeft) + dialog.bottomLayout.addWidget(dialog.miscGroup, Qt.AlignLeft) + + dialog.mainLayout.addLayout(dialog.bottomLayout, Qt.AlignLeft) + + def addRoomBox(self, dialog): + dialog.roomGroup = QtGui.QGroupBox("Room") + + dialog.roomInput = QtGui.QLineEdit() + dialog.roomButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'door_in.png'), "Join room") + dialog.roomButton.pressed.connect(self.joinRoom) + dialog.roomLayout = QtGui.QHBoxLayout() + dialog.roomInput.setMaximumWidth(150) + + dialog.roomLayout.addWidget(dialog.roomInput) + dialog.roomLayout.addWidget(dialog.roomButton) + + dialog.roomGroup.setLayout(dialog.roomLayout) + dialog.roomGroup.setFixedSize(dialog.roomGroup.sizeHint()) + + def addSeekBox(self, dialog): + dialog.seekGroup = QtGui.QGroupBox("Seek") + + dialog.seekInput = QtGui.QLineEdit() + dialog.seekButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'clock_go.png'),"Seek to position") + dialog.seekButton.pressed.connect(self.seekPosition) + + dialog.seekLayout = QtGui.QHBoxLayout() + dialog.seekInput.setMaximumWidth(100) + + dialog.seekLayout.addWidget(dialog.seekInput) + dialog.seekLayout.addWidget(dialog.seekButton) + + dialog.seekGroup.setLayout(dialog.seekLayout) + dialog.seekGroup.setFixedSize(dialog.seekGroup.sizeHint()) + + def addMiscBox(self, dialog): + dialog.miscGroup = QtGui.QGroupBox("Other Commands") + + dialog.unseekButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'arrow_undo.png'),"Undo last seek") + dialog.unseekButton.pressed.connect(self.undoSeek) + dialog.pauseButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_pause_blue.png'),"Toggle pause") + dialog.pauseButton.pressed.connect(self.togglePause) + dialog.showListButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'table_refresh.png'),"Update list") + dialog.showListButton.pressed.connect(self.showList) + + dialog.miscLayout = QtGui.QHBoxLayout() + dialog.miscLayout.addWidget(dialog.unseekButton) + dialog.miscLayout.addWidget(dialog.pauseButton) + dialog.miscLayout.addWidget(dialog.showListButton) + + dialog.miscGroup.setLayout(dialog.miscLayout) + dialog.miscGroup.setFixedSize(dialog.miscGroup.sizeHint()) + + + def addMenubar(self, dialog): + dialog.menuBar = QtGui.QMenuBar() + + dialog.fileMenu = QtGui.QMenu("&File", self) + dialog.exitAction = dialog.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'cross.png'), "E&xit") + dialog.exitAction.triggered.connect(self.exitSyncplay) + dialog.menuBar.addMenu(dialog.fileMenu) + + dialog.advancedMenu = QtGui.QMenu("&Advanced", self) + dialog.setoffsetAction = dialog.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'timeline_marker.png'),"Set &Offset") + dialog.setoffsetAction.triggered.connect(self.setOffset) + dialog.menuBar.addMenu(dialog.advancedMenu) + + dialog.helpMenu = QtGui.QMenu("&Help", self) + dialog.userguideAction = dialog.helpMenu.addAction(QtGui.QIcon(self.resourcespath + 'help.png'), "Open User &Guide") + dialog.userguideAction.triggered.connect(self.openUserGuide) + + dialog.menuBar.addMenu(dialog.helpMenu) + dialog.mainLayout.setMenuBar(dialog.menuBar) + + def newMessage(self, message): + self.outputbox.moveCursor(QtGui.QTextCursor.End) + self.outputbox.insertHtml(message) + self.outputbox.moveCursor(QtGui.QTextCursor.End) + + def resetList(self): + self.listbox.setText("") + + def newListItem(self, item): + self.listbox.moveCursor(QtGui.QTextCursor.End) + self.listbox.insertHtml(item) + self.listbox.moveCursor(QtGui.QTextCursor.End) + def __init__(self): - pass + super(MainDialog, self).__init__() + self.QtGui = QtGui + self._listBuffer = "" + if sys.platform.startswith('linux'): + self.resourcespath = utils.findWorkingDir() + "/resources/" + else: + self.resourcespath = utils.findWorkingDir() + "\\resources\\" + self.setWindowTitle("Syncplay - Main Window") + self.mainLayout = QtGui.QVBoxLayout() + self.addTopLayout(self) + self.addBottomLayout(self) + self.addMenubar(self) + self.setLayout(self.mainLayout) + self.resize(700,500) + self.setWindowIcon(QtGui.QIcon(self.resourcespath + "syncplay.png")) + self.show() diff --git a/syncplay/vendor/__init__.py b/syncplay/vendor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/syncplay/vendor/qt4reactor.py b/syncplay/vendor/qt4reactor.py new file mode 100644 index 0000000..a337099 --- /dev/null +++ b/syncplay/vendor/qt4reactor.py @@ -0,0 +1,359 @@ +# Copyright (c) 2001-2011 Twisted Matrix Laboratories. +# See LICENSE for details. + + +""" +This module provides support for Twisted to be driven by the Qt mainloop. + +In order to use this support, simply do the following:: + | app = QApplication(sys.argv) # your code to init Qt + | import qt4reactor + | qt4reactor.install() + +alternatively: + + | from twisted.application import reactors + | reactors.installReactor('qt4') + +Then use twisted.internet APIs as usual. The other methods here are not +intended to be called directly. + +If you don't instantiate a QApplication or QCoreApplication prior to +installing the reactor, a QCoreApplication will be constructed +by the reactor. QCoreApplication does not require a GUI so trial testing +can occur normally. + +Twisted can be initialized after QApplication.exec_() with a call to +reactor.runReturn(). calling reactor.stop() will unhook twisted but +leave your Qt application running + +API Stability: stable + +Maintainer: U{Glenn H Tarbox, PhD} + +Previous maintainer: U{Itamar Shtull-Trauring} +Original port to QT4: U{Gabe Rudy} +Subsequent port by therve +""" + +import sys +import time +from zope.interface import implements +from twisted.internet.interfaces import IReactorFDSet +from twisted.python import log, runtime +from twisted.internet import posixbase +from twisted.python.runtime import platformType, platform + +try: + from PyQt4.QtCore import QSocketNotifier, QObject, SIGNAL, QTimer, QCoreApplication + from PyQt4.QtCore import QEventLoop +except ImportError: + from PySide.QtCore import QSocketNotifier, QObject, SIGNAL, QTimer, QCoreApplication + from PySide.QtCore import QEventLoop + + +class TwistedSocketNotifier(QObject): + """ + Connection between an fd event and reader/writer callbacks. + """ + + def __init__(self, parent, reactor, watcher, socketType): + QObject.__init__(self, parent) + self.reactor = reactor + self.watcher = watcher + fd = watcher.fileno() + self.notifier = QSocketNotifier(fd, socketType, parent) + self.notifier.setEnabled(True) + if socketType == QSocketNotifier.Read: + self.fn = self.read + else: + self.fn = self.write + QObject.connect(self.notifier, SIGNAL("activated(int)"), self.fn) + + + def shutdown(self): + self.notifier.setEnabled(False) + self.disconnect(self.notifier, SIGNAL("activated(int)"), self.fn) + self.fn = self.watcher = None + self.notifier.deleteLater() + self.deleteLater() + + + def read(self, fd): + if not self.watcher: + return + w = self.watcher + # doRead can cause self.shutdown to be called so keep a reference to self.watcher + def _read(): + #Don't call me again, until the data has been read + self.notifier.setEnabled(False) + why = None + try: + why = w.doRead() + inRead = True + except: + inRead = False + log.err() + why = sys.exc_info()[1] + if why: + self.reactor._disconnectSelectable(w, why, inRead) + elif self.watcher: + self.notifier.setEnabled(True) # Re enable notification following sucessfull read + self.reactor._iterate(fromqt=True) + log.callWithLogger(w, _read) + + def write(self, sock): + if not self.watcher: + return + w = self.watcher + def _write(): + why = None + self.notifier.setEnabled(False) + + try: + why = w.doWrite() + except: + log.err() + why = sys.exc_info()[1] + if why: + self.reactor._disconnectSelectable(w, why, False) + elif self.watcher: + self.notifier.setEnabled(True) + self.reactor._iterate(fromqt=True) + log.callWithLogger(w, _write) + + + +class QtReactor(posixbase.PosixReactorBase): + implements(IReactorFDSet) + + def __init__(self): + self._reads = {} + self._writes = {} + self._notifiers = {} + self._timer = QTimer() + self._timer.setSingleShot(True) + QObject.connect(self._timer, SIGNAL("timeout()"), self.iterate) + + if QCoreApplication.instance() is None: + # Application Object has not been started yet + self.qApp=QCoreApplication([]) + self._ownApp=True + else: + self.qApp = QCoreApplication.instance() + self._ownApp=False + self._blockApp = None + posixbase.PosixReactorBase.__init__(self) + + + def _add(self, xer, primary, type): + """ + Private method for adding a descriptor from the event loop. + + It takes care of adding it if new or modifying it if already added + for another state (read -> read/write for example). + """ + if xer not in primary: + primary[xer] = TwistedSocketNotifier(None, self, xer, type) + + + def addReader(self, reader): + """ + Add a FileDescriptor for notification of data available to read. + """ + self._add(reader, self._reads, QSocketNotifier.Read) + + + def addWriter(self, writer): + """ + Add a FileDescriptor for notification of data available to write. + """ + self._add(writer, self._writes, QSocketNotifier.Write) + + + def _remove(self, xer, primary): + """ + Private method for removing a descriptor from the event loop. + + It does the inverse job of _add, and also add a check in case of the fd + has gone away. + """ + if xer in primary: + notifier = primary.pop(xer) + notifier.shutdown() + + + def removeReader(self, reader): + """ + Remove a Selectable for notification of data available to read. + """ + self._remove(reader, self._reads) + + + def removeWriter(self, writer): + """ + Remove a Selectable for notification of data available to write. + """ + self._remove(writer, self._writes) + + + def removeAll(self): + """ + Remove all selectables, and return a list of them. + """ + rv = self._removeAll(self._reads, self._writes) + return rv + + + def getReaders(self): + return self._reads.keys() + + + def getWriters(self): + return self._writes.keys() + + + def callLater(self,howlong, *args, **kargs): + rval = super(QtReactor,self).callLater(howlong, *args, **kargs) + self.reactorInvocation() + return rval + + + def reactorInvocation(self): + self._timer.stop() + self._timer.setInterval(0) + self._timer.start() + + + def _iterate(self, delay=None, fromqt=False): + """See twisted.internet.interfaces.IReactorCore.iterate. + """ + self.runUntilCurrent() + self.doIteration(delay, fromqt) + + iterate = _iterate + + def doIteration(self, delay=None, fromqt=False): + 'This method is called by a Qt timer or by network activity on a file descriptor' + + if not self.running and self._blockApp: + self._blockApp.quit() + self._timer.stop() + delay = max(delay, 1) + if not fromqt: + self.qApp.processEvents(QEventLoop.AllEvents, delay * 1000) + if self.timeout() is None: + timeout = 0.1 + elif self.timeout() == 0: + timeout = 0 + else: + timeout = self.timeout() + self._timer.setInterval(timeout * 1000) + self._timer.start() + + + def runReturn(self, installSignalHandlers=True): + self.startRunning(installSignalHandlers=installSignalHandlers) + self.reactorInvocation() + + + def run(self, installSignalHandlers=True): + if self._ownApp: + self._blockApp = self.qApp + else: + self._blockApp = QEventLoop() + self.runReturn() + self._blockApp.exec_() + + +class QtEventReactor(QtReactor): + def __init__(self, *args, **kwargs): + self._events = {} + super(QtEventReactor, self).__init__() + + + def addEvent(self, event, fd, action): + """ + Add a new win32 event to the event loop. + """ + self._events[event] = (fd, action) + + + def removeEvent(self, event): + """ + Remove an event. + """ + if event in self._events: + del self._events[event] + + + def doEvents(self): + handles = self._events.keys() + if len(handles) > 0: + val = None + while val != WAIT_TIMEOUT: + val = MsgWaitForMultipleObjects(handles, 0, 0, QS_ALLINPUT | QS_ALLEVENTS) + if val >= WAIT_OBJECT_0 and val < WAIT_OBJECT_0 + len(handles): + event_id = handles[val - WAIT_OBJECT_0] + if event_id in self._events: + fd, action = self._events[event_id] + log.callWithLogger(fd, self._runAction, action, fd) + elif val == WAIT_TIMEOUT: + pass + else: + #print 'Got an unexpected return of %r' % val + return + + + def _runAction(self, action, fd): + try: + closed = getattr(fd, action)() + except: + closed = sys.exc_info()[1] + log.deferr() + + if closed: + self._disconnectSelectable(fd, closed, action == 'doRead') + + + def timeout(self): + t = super(QtEventReactor, self).timeout() + return min(t, 0.01) + + + def iterate(self, delay=None): + """See twisted.internet.interfaces.IReactorCore.iterate. + """ + self.runUntilCurrent() + self.doEvents() + self.doIteration(delay) + + +def posixinstall(): + """ + Install the Qt reactor. + """ + p = QtReactor() + from twisted.internet.main import installReactor + installReactor(p) + + +def win32install(): + """ + Install the Qt reactor. + """ + p = QtEventReactor() + from twisted.internet.main import installReactor + installReactor(p) + + +if runtime.platform.getType() == 'win32': + from win32event import CreateEvent, MsgWaitForMultipleObjects + from win32event import WAIT_OBJECT_0, WAIT_TIMEOUT, QS_ALLINPUT, QS_ALLEVENTS + install = win32install +else: + install = posixinstall + + +__all__ = ["install"] +