diff --git a/syncplay/client.py b/syncplay/client.py index ff54eb4..3e329c9 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -11,7 +11,6 @@ from syncplay.messages import getMissingStrings, getMessage from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \ PRIVACY_HIDDENFILENAME import collections - class SyncClientFactory(ClientFactory): def __init__(self, client, retry=constants.RECONNECT_RETRIES): self._client = client @@ -106,6 +105,7 @@ class SyncplayClient(object): self._speedChanged = False self.behindFirstDetected = None self.autoPlay = False + self.autoPlayThreshold = None self._warnings = self._WarningManager(self._player, self.userlist, self.ui, self) if constants.LIST_RELATIVE_CONFIGS and self._config.has_key('loadedRelativePaths') and self._config['loadedRelativePaths']: @@ -319,7 +319,6 @@ class SyncplayClient(object): self.ui.showMessage(getMessage("current-offset-notification").format(self._userOffset)) def onDisconnect(self): - self.resetAutoPlayState() if self._config['pauseOnLeave']: self.setPaused(True) self.lastPausedOnLeaveTime = time.time() @@ -404,9 +403,10 @@ class SyncplayClient(object): def getUsername(self): return self.userlist.currentUser.username - def setRoom(self, roomName): + def setRoom(self, roomName, resetAutoplay=False): self.userlist.currentUser.room = roomName - self.resetAutoPlayState() + if resetAutoplay: + self.resetAutoPlayState() def sendRoom(self): room = self.userlist.currentUser.room @@ -493,12 +493,16 @@ class SyncplayClient(object): return wrapper return requireMinVersionDecorator - def changeAutoPlayState(self, newState): + def changeAutoplayState(self, newState): self.autoPlay = newState - self.autoPlayCheck() + self.autoplayCheck() + + def changeAutoPlayThrehsold(self, newThreshold): + self.autoPlayThreshold = newThreshold + self.autoplayCheck() - def autoPlayCheck(self): - if self.autoPlay and self.userlist.currentUser.canControl() and self.userlist.isReadinessSupported() and self.userlist.areAllUsersInRoomReady(): + def autoplayCheck(self): + if self.autoPlay and self.userlist.currentUser.canControl() and self.userlist.isReadinessSupported() and self.userlist.areAllUsersInRoomReady() and self.autoPlayThreshold and self.userlist.usersInRoomCount() >= self.autoPlayThreshold: self.setPaused(False) def resetAutoPlayState(self): @@ -532,7 +536,7 @@ class SyncplayClient(object): def controlledRoomCreated(self, roomName, controlPassword): self.ui.showMessage(getMessage("created-controlled-room-notification").format(roomName, controlPassword)) - self.setRoom(roomName) + self.setRoom(roomName, resetAutoplay=True) self.sendRoom() self._protocol.requestControlledRoom(roomName, controlPassword) self.ui.updateRoomName(roomName) @@ -911,6 +915,13 @@ class SyncplayUserlist(object): if user.room == self.currentUser.room and user.isReady(): readyCount += 1 return readyCount + + def usersInRoomCount(self): + userCount = 1 + for user in self._users.itervalues(): + if user.room == self.currentUser.room and user.isReady(): + userCount += 1 + return userCount def usersInRoomNotReady(self): notReady = [] @@ -970,7 +981,7 @@ class SyncplayUserlist(object): self.currentUser.setReady(isReady) elif self._users.has_key(username): self._users[username].setReady(isReady) - self._client.autoPlayCheck() + self._client.autoplayCheck() def userListChange(self, room = None): if room is not None and self.isRoomSame(room): @@ -994,7 +1005,7 @@ class SyncplayUserlist(object): rooms[self.currentUser.room].append(self.currentUser) rooms = self.sortList(rooms) self.ui.showUserList(self.currentUser, rooms) - self._client.autoPlayCheck() + self._client.autoplayCheck() def clearList(self): self._users = {} diff --git a/syncplay/messages.py b/syncplay/messages.py index 222ad0d..35c1839 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -219,7 +219,8 @@ en = { "pause-menu-label" : "Pause", "playbackbuttons-menu-label" : u"Show playback buttons", "autoplay-menu-label" : u"Show auto-play button", - "autoplay-guipushbuttonlabel" : u"Auto-play when everyone is ready", + "autoplay-guipushbuttonlabel" : u"Play when all ready", + "autoplay-minimum-label" : u"Min users:", "ready-guipushbuttonlabel" : u"I'm ready to watch!", @@ -304,6 +305,8 @@ en = { "joinroom-tooltip" : "Leave current room and joins specified room.", "seektime-msgbox-label" : "Jump to specified time (in seconds / min:sec). Use +/- for relative seek.", + "ready-tooltip" : "Indicates whether you are ready to watch.", + "autoplay-tooltip" : "Auto-play when all users who have readiness indicator are ready and minimum user threshold met.", # In-userlist notes (GUI) "differentsize-note" : "Different size!", @@ -562,6 +565,7 @@ ru = { "playbackbuttons-menu-label" : u"Show playback buttons", # TODO: Translate into Russian "autoplay-menu-label" : u"Show auto-play button", # TODO: Translate into Russian "autoplay-guipushbuttonlabel" : u"Auto-play when everyone is ready", # TODO: Translate into Russian + "autoplay-minimum-label" : u"Min users:", # TODO: Translate into Russian "ready-guipushbuttonlabel" : u"I'm ready to watch!", # TODO: Translate into Russian @@ -647,6 +651,8 @@ ru = { "joinroom-tooltip" : u"Покинуть комнату и зайти в другую, указанную комнату.", "seektime-msgbox-label" : u"Перемотать к определенному моменту времени (указывать в секундах или мин:сек). Используйте +/-, чтобы перемотать вперед/назад относительно настоящего момента.", + "ready-tooltip" : "Indicates whether you are ready to watch.", # TODO: Translate into Russian + "autoplay-tooltip" : "Auto-play when all users who have readiness indicator are ready and minimum user threshold met.", # TODO: Translate into Russian # In-userlist notes (GUI) "differentsize-note" : u"Размер файла не совпадает!", @@ -903,6 +909,7 @@ de = { "playbackbuttons-menu-label" : u"Show playback buttons", # TODO: Translate into German "autoplay-menu-label" : u"Show auto-play button", # TODO: Translate into German "autoplay-guipushbuttonlabel" : u"Auto-play when everyone is ready", # TODO: Translate into German + "autoplay-minimum-label" : u"Min users:", # TODO: Translate into German "ready-guipushbuttonlabel" : u"I'm ready to watch!", # TODO: Translate into German @@ -989,6 +996,8 @@ de = { "joinroom-tooltip" : u"Den aktuellen Raum verlassen und stattdessen den angegebenen betreten.", "seektime-msgbox-label" : u"Springe zur angegebenen Zeit (in Sekunden oder min:sek). Verwende +/- zum relativen Springen.", + "ready-tooltip" : "Indicates whether you are ready to watch.", # TODO: Translate into German + "autoplay-tooltip" : "Auto-play when all users who have readiness indicator are ready and minimum user threshold met.", # TODO: Translate into German # In-userlist notes (GUI) "differentsize-note" : u"Verschiedene Größe!", diff --git a/syncplay/ui/ConfigurationGetter.py b/syncplay/ui/ConfigurationGetter.py index df3e1ab..29fc290 100755 --- a/syncplay/ui/ConfigurationGetter.py +++ b/syncplay/ui/ConfigurationGetter.py @@ -45,6 +45,8 @@ class ConfigurationGetter(object): "filesizePrivacyMode": constants.PRIVACY_SENDRAW_MODE, "pauseOnLeave": False, "readyAtStart": False, + "autoplayInitialState" : None, + "autoplayInitialThreshold" : -1, "clearGUIData": False, "language" : "", "checkForUpdatesAutomatically" : None, @@ -97,18 +99,20 @@ class ConfigurationGetter(object): "showDurationNotification" ] self._tristate = [ - "checkForUpdatesAutomatically" + "checkForUpdatesAutomatically", + "autoplayInitialState", ] self._numeric = [ "slowdownThreshold", "rewindThreshold", "fastforwardThreshold", + "autoplayInitialThreshold", ] self._iniStructure = { "server_data": ["host", "port", "password"], - "client_settings": ["name", "room", "playerPath", "slowdownThreshold", "rewindThreshold", "fastforwardThreshold", "slowOnDesync", "rewindOnDesync", "fastforwardOnDesync", "dontSlowDownWithMe", "forceGuiPrompt", "filenamePrivacyMode", "filesizePrivacyMode", "pauseOnLeave", "readyAtStart"], + "client_settings": ["name", "room", "playerPath", "slowdownThreshold", "rewindThreshold", "fastforwardThreshold", "slowOnDesync", "rewindOnDesync", "fastforwardOnDesync", "dontSlowDownWithMe", "forceGuiPrompt", "filenamePrivacyMode", "filesizePrivacyMode", "pauseOnLeave", "readyAtStart", "autoplayInitialThreshold", "autoplayInitialState"], "gui": ["showOSD", "showOSDWarnings", "showSlowdownOSD", "showDifferentRoomOSD", "showSameRoomOSD", "showNonControllerOSD", "showDurationNotification"], "general": ["language", "checkForUpdatesAutomatically", "lastCheckedForUpdates"] } diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index 8f42bfd..fcaf7cf 100644 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -144,7 +144,7 @@ class ConsoleUI(threading.Thread): else: room = self._syncplayClient.defaultRoom - self._syncplayClient.setRoom(room) + self._syncplayClient.setRoom(room, resetAutoplay=True) self._syncplayClient.sendRoom() elif command.group('command') in constants.COMMANDS_CREATE: roombasename = command.group('parameter') diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 1201d70..f0acfef 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -8,6 +8,7 @@ from datetime import datetime import re import os from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider, formatSize +from functools import wraps lastCheckedForUpdates = None class UserlistItemDelegate(QtGui.QStyledItemDelegate): @@ -71,14 +72,35 @@ class MainWindow(QtGui.QMainWindow): QtGui.QSplitterHandle.mouseMoveEvent(self, event) self.parent().parent().parent().updateListGeometry() + def needsClient(f): # @NoSelf + @wraps(f) + def wrapper(self, *args, **kwds): + if not self._syncplayClient: + self.showDebugMessage("Tried to use client before it was ready!") + return + return f(self, *args, **kwds) + return wrapper + def addClient(self, client): self._syncplayClient = client self.roomInput.setText(self._syncplayClient.getRoom()) self.config = self._syncplayClient.getConfig() try: - self.updateReadyState(self.config['readyAtStart']) + self.updateReadyState(self.config['readyAtStart']) + autoplayInitialState = self.config['autoplayInitialState'] + if autoplayInitialState is not None: + self.autoplayPushButton.blockSignals(True) + self.autoplayPushButton.setChecked(autoplayInitialState) + self.autoplayPushButton.blockSignals(False) + if self.config['autoplayInitialThreshold'] > 1: + self.autoplayThresholdSpinbox.blockSignals(True) + self.autoplayThresholdSpinbox.setValue(self.config['autoplayInitialThreshold']) + self.autoplayThresholdSpinbox.blockSignals(False) + self.changeAutoplayState() + self.changeAutoplayThreshold() + self.updateAutoPlayIcon() except: - pass + self.showErrorMessage("Failed to load some settings.") self.automaticUpdateCheck() def promptFor(self, prompt=">", message=""): @@ -212,6 +234,7 @@ class MainWindow(QtGui.QMainWindow): item = item.parent() self.joinRoom(item.sibling(item.row(), 0).data()) + @needsClient def userListChange(self): self._syncplayClient.showUserList() @@ -230,6 +253,7 @@ class MainWindow(QtGui.QMainWindow): message = "".format(constants.STYLE_ERRORNOTIFICATION) + message + "" self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "
") + @needsClient def joinRoom(self, room=None): if room == None: room = self.roomInput.text() @@ -240,7 +264,7 @@ class MainWindow(QtGui.QMainWindow): room = self._syncplayClient.defaultRoom self.roomInput.setText(room) if room != self._syncplayClient.getRoom(): - self._syncplayClient.setRoom(room) + self._syncplayClient.setRoom(room, resetAutoplay=True) self._syncplayClient.sendRoom() def seekPositionDialog(self): @@ -253,6 +277,7 @@ class MainWindow(QtGui.QMainWindow): def seekFromButton(self): self.seekPosition(self.seekInput.text()) + @needsClient def seekPosition(self, seekTime): s = re.match(constants.UI_SEEK_REGEX, seekTime) if s: @@ -266,20 +291,25 @@ class MainWindow(QtGui.QMainWindow): else: self.showErrorMessage(getMessage("invalid-seek-value")) + @needsClient def undoSeek(self): tmp_pos = self._syncplayClient.getPlayerPosition() self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek) self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos + @needsClient def togglePause(self): self._syncplayClient.setPaused(not self._syncplayClient.getPlayerPaused()) + @needsClient def play(self): self._syncplayClient.setPaused(False) + @needsClient def pause(self): self._syncplayClient.setPaused(True) + @needsClient def exitSyncplay(self): self._syncplayClient.stop() @@ -299,6 +329,7 @@ class MainWindow(QtGui.QMainWindow): settings.setValue("mediadir", self.mediadirectory) settings.endGroup() + @needsClient def browseMediapath(self): if self._syncplayClient._player.customOpenDialog == True: self._syncplayClient._player.openCustomOpenDialog() @@ -324,6 +355,7 @@ class MainWindow(QtGui.QMainWindow): self.saveMediaBrowseSettings() self._syncplayClient._player.openFile(fileName) + @needsClient def promptForStreamURL(self): streamURL, ok = QtGui.QInputDialog.getText(self, getMessage("promptforstreamurl-msgbox-label"), getMessage("promptforstreamurlinfo-msgbox-label"), QtGui.QLineEdit.Normal, @@ -331,6 +363,7 @@ class MainWindow(QtGui.QMainWindow): if ok and streamURL != '': self._syncplayClient._player.openFile(streamURL) + @needsClient def createControlledRoom(self): controlroom, ok = QtGui.QInputDialog.getText(self, getMessage("createcontrolledroom-msgbox-label"), getMessage("controlledroominfo-msgbox-label"), QtGui.QLineEdit.Normal, @@ -338,6 +371,7 @@ class MainWindow(QtGui.QMainWindow): if ok and controlroom != '': self._syncplayClient.createControlledRoom(controlroom) + @needsClient def identifyAsController(self): msgboxtitle = getMessage("identifyascontroller-msgbox-label") msgboxtext = getMessage("identifyinfo-msgbox-label") @@ -354,6 +388,7 @@ class MainWindow(QtGui.QMainWindow): else: return None + @needsClient def setOffset(self): newoffset, ok = QtGui.QInputDialog.getText(self, getMessage("setoffset-msgbox-label"), getMessage("offsetinfo-msgbox-label"), QtGui.QLineEdit.Normal, @@ -465,19 +500,41 @@ class MainWindow(QtGui.QMainWindow): window.readyPushButton.toggled.connect(self.changeReadyState) window.readyPushButton.setFont(readyFont) window.readyPushButton.setStyleSheet(constants.STYLE_READY_PUSHBUTTON) + window.readyPushButton.setToolTip(getMessage("ready-tooltip")) window.listLayout.addWidget(window.readyPushButton, Qt.AlignRight) - window.autoPlayPushButton = QtGui.QPushButton() - window.autoPlayPushButton.setVisible(False) + + window.autoplayLayout = QtGui.QHBoxLayout() + window.autoplayFrame = QtGui.QFrame() + window.autoplayFrame.setVisible(False) + window.autoplayLayout.setContentsMargins(0,0,0,0) + window.autoplayFrame.setLayout(window.autoplayLayout) + window.autoplayPushButton = QtGui.QPushButton() autoPlayFont = QtGui.QFont() autoPlayFont.setWeight(QtGui.QFont.Bold) - window.autoPlayPushButton.setText(getMessage("autoplay-guipushbuttonlabel")) - window.autoPlayPushButton.setCheckable(True) - window.autoPlayPushButton.setAutoExclusive(False) - window.autoPlayPushButton.toggled.connect(self.changeAutoPlayState) - window.autoPlayPushButton.setFont(autoPlayFont) - window.autoPlayPushButton.setStyleSheet(constants.STYLE_AUTO_PLAY_PUSHBUTTON) - window.listLayout.addWidget(window.autoPlayPushButton, Qt.AlignRight) - self.updateAutoPlayIcon() + window.autoplayPushButton.setText(getMessage("autoplay-guipushbuttonlabel")) + window.autoplayPushButton.setCheckable(True) + window.autoplayPushButton.setAutoExclusive(False) + window.autoplayPushButton.toggled.connect(self.changeAutoplayState) + window.autoplayPushButton.setFont(autoPlayFont) + window.autoplayPushButton.setStyleSheet(constants.STYLE_AUTO_PLAY_PUSHBUTTON) + window.autoplayPushButton.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + window.autoplayPushButton.setToolTip(getMessage("autoplay-tooltip")) + window.autoplayLabel = QtGui.QLabel(getMessage("autoplay-minimum-label")) + window.autoplayLabel.setSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum) + window.autoplayLabel.setMaximumWidth(window.autoplayLabel.minimumSizeHint().width()) + window.autoplayLabel.setToolTip(getMessage("autoplay-tooltip")) + window.autoplayThresholdSpinbox = QtGui.QSpinBox() + window.autoplayThresholdSpinbox.setMaximumWidth(window.autoplayThresholdSpinbox.minimumSizeHint().width()) + window.autoplayThresholdSpinbox.setMinimum(2) + window.autoplayThresholdSpinbox.setMaximum(99) + window.autoplayThresholdSpinbox.setToolTip(getMessage("autoplay-tooltip")) + window.autoplayThresholdSpinbox.valueChanged.connect(self.changeAutoplayThreshold) + window.autoplayLayout.addWidget(window.autoplayPushButton, Qt.AlignRight) + window.autoplayLayout.addWidget(window.autoplayLabel, Qt.AlignRight) + window.autoplayLayout.addWidget(window.autoplayThresholdSpinbox, Qt.AlignRight) + + window.listLayout.addWidget(window.autoplayFrame, Qt.AlignLeft) + window.autoplayFrame.setMaximumHeight(window.autoplayFrame.sizeHint().height()) window.mainLayout.addWidget(window.bottomFrame, Qt.AlignLeft) window.bottomFrame.setMaximumHeight(window.bottomFrame.minimumSizeHint().height()) @@ -617,21 +674,34 @@ class MainWindow(QtGui.QMainWindow): self.playbackFrame.setVisible(self.playbackAction.isChecked()) def updateAutoplayVisibility(self): - self.autoPlayPushButton.setVisible(self.autoplayAction.isChecked()) + self.autoplayFrame.setVisible(self.autoplayAction.isChecked()) def changeReadyState(self): self.updateReadyIcon() - self._syncplayClient.changeReadyState(self.readyPushButton.isChecked()) + if self._syncplayClient: + self._syncplayClient.changeReadyState(self.readyPushButton.isChecked()) + else: + self.showDebugMessage("Tried to change ready state too soon.") + + @needsClient + def changeAutoplayThreshold(self, source=None): + self._syncplayClient.changeAutoPlayThrehsold(self.autoplayThresholdSpinbox.value()) def updateAutoPlayState(self, newState): - oldState = self.autoPlayPushButton.isChecked() + oldState = self.autoplayPushButton.isChecked() if newState != oldState and newState != None: - self.autoPlayPushButton.setChecked(newState) + self.autoplayPushButton.blockSignals(True) + self.autoplayPushButton.setChecked(newState) + self.autoplayPushButton.blockSignals(False) self.updateAutoPlayIcon() - def changeAutoPlayState(self): + @needsClient + def changeAutoplayState(self, source=None): self.updateAutoPlayIcon() - self._syncplayClient.changeAutoPlayState(self.autoPlayPushButton.isChecked()) + if self._syncplayClient: + self._syncplayClient.changeAutoplayState(self.autoplayPushButton.isChecked()) + else: + self.showDebugMessage("Tried to set AutoplayState too soon") def updateReadyIcon(self): ready = self.readyPushButton.isChecked() @@ -641,11 +711,11 @@ class MainWindow(QtGui.QMainWindow): self.readyPushButton.setIcon(QtGui.QIcon(self.resourcespath + 'empty_checkbox.png')) def updateAutoPlayIcon(self): - ready = self.autoPlayPushButton.isChecked() + ready = self.autoplayPushButton.isChecked() if ready: - self.autoPlayPushButton.setIcon(QtGui.QIcon(self.resourcespath + 'tick_checkbox.png')) + self.autoplayPushButton.setIcon(QtGui.QIcon(self.resourcespath + 'tick_checkbox.png')) else: - self.autoPlayPushButton.setIcon(QtGui.QIcon(self.resourcespath + 'empty_checkbox.png')) + self.autoplayPushButton.setIcon(QtGui.QIcon(self.resourcespath + 'empty_checkbox.png')) def automaticUpdateCheck(self): currentDateTime = datetime.utcnow() @@ -665,6 +735,7 @@ class MainWindow(QtGui.QMainWindow): def userCheckForUpdates(self): self.checkForUpdates(userInitiated=True) + @needsClient def checkForUpdates(self, userInitiated=False): self.lastCheckedForUpdates = datetime.utcnow() updateStatus, updateMessage, updateURL = self._syncplayClient.checkForUpdate(userInitiated) @@ -717,6 +788,8 @@ class MainWindow(QtGui.QMainWindow): settings.setValue("pos", self.pos()) settings.setValue("showPlaybackButtons", self.playbackAction.isChecked()) settings.setValue("showAutoPlayButton", self.autoplayAction.isChecked()) + settings.setValue("autoplayChecked", self.autoplayPushButton.isChecked()) + settings.setValue("autoplayMinUsers", self.autoplayThresholdSpinbox.value()) settings.endGroup() settings = QSettings("Syncplay", "Interface") settings.beginGroup("Update") @@ -734,6 +807,12 @@ class MainWindow(QtGui.QMainWindow): if settings.value("showAutoPlayButton", "false") == "true": self.autoplayAction.setChecked(True) self.updateAutoplayVisibility() + if settings.value("autoplayChecked", "false") == "true": + self.updateAutoPlayState(True) + self.autoplayPushButton.setChecked(True) + self.autoplayThresholdSpinbox.blockSignals(True) + self.autoplayThresholdSpinbox.setValue(settings.value("autoplayMinUsers", 2)) + self.autoplayThresholdSpinbox.blockSignals(False) settings.endGroup() settings = QSettings("Syncplay", "Interface") settings.beginGroup("Update") @@ -741,6 +820,7 @@ class MainWindow(QtGui.QMainWindow): def __init__(self): super(MainWindow, self).__init__() + self._syncplayClient = None self.QtGui = QtGui if sys.platform.startswith('win'): self.resourcespath = utils.findWorkingDir() + "\\resources\\"