class sliderField(dialogField): """A field made up of an integer slider.""" def __init__(self, name, value=None, min=0, max=10): """Initializes the field. name -- the name of the field value -- the initial position of the slider min -- the minimum value allowed max -- the maximum value allowed """ super(sliderField, self).__init__(name, value) self.min = min self.max = max def _cleanValue(self, value): if value <= self.max and value >= self.min: return value raise validationError(translate('sliderField', 'You must enter a valid choice for the {0} field.').format(self.name)) def _createWidget(self, parent): widget = QSlider(parent) widget.setOrientation(Qt.Horizontal) widget.setMinimum(self.min) widget.setMaximum(self.max) widget.setValue(self.value) widget.setTickInterval(1) widget.setTickPosition(QSlider.TicksAbove) widget.setPageStep(1) widget.valueChanged.connect(self.emitEvil) return widget def emitEvil(self, trash): self.evil.emit() def _getWidgetValue(self, widget): return (widget.value()) evil = signal(doc= """Ponies!""" )
class dropDownField(dialogField): """A field made up of several independent choices.""" def __init__(self, name, choices, value=None): """Initializes the field. name -- the name of the field choices -- the list of choices allowed value -- the initial value to store """ super(dropDownField, self).__init__(name, value) self.choices = choices def _cleanValue(self, value): if len(self.choices) <= 0: return '' if value in self.choices: return value raise validationError(translate('dropDownField', 'You must enter a valid choice for the {0} field.').format(self.name)) def _createWidget(self, parent): index = -1 widget = QComboBox(parent) for choice in self.choices: if choice == self.value: index = widget.count() widget.addItem(choice) if index >= 0: widget.setCurrentIndex(index) widget.currentIndexChanged.connect(self.emitEvil) return widget def emitEvil(self, trash): self.evil.emit() def _getWidgetValue(self, widget): return UNICODE_STRING(widget.currentText()) evil = signal(doc= """Ponies!""" )
class chatWidget(QDockWidget): def __init__(self, mainWindow): super(QDockWidget, self).__init__(mainWindow) self.setToolTip( self.tr("A widget for out-of-character chat and system messages.")) self.setWindowTitle(self.tr("OOC Chat / System")) self.widgetEditor = QTextBrowser(mainWindow) self.widgetLineInput = chatLineEdit(mainWindow) self.widgetLineInput.setToolTip( self.tr( "Type text here and press Enter or Return to transmit it.")) self.widget = QWidget(mainWindow) self.widgetEditor.setReadOnly(True) self.widgetEditor.setOpenLinks(False) self.layout = QBoxLayout(2) self.layout.addWidget(self.widgetEditor) self.layout.addWidget(self.widgetLineInput) self.widget.setLayout(self.layout) self.setWidget(self.widget) self.setObjectName("Chat Widget") self.timestamp = False self.timestampformat = "[%H:%M:%S]" self.messageCache = [] try: js = jsonload(ospath.join(SAVE_DIR, "ui_settings.rgs")) except: pass try: self.toggleTimestamp( loadString('chatWidget.timestamp', js.get('timestamp'))) except: pass try: self.timestampformat = loadString('chatWidget.timestampformat', js.get('timestampformat')) except: pass mainWindow.addDockWidget(Qt.BottomDockWidgetArea, self) self.widgetEditor.anchorClicked.connect(self.anchorClicked) self.widgetLineInput.returnPressed.connect(self.processInput) def toggleDarkBackgroundSupport(self, dark): if dark: self.widgetEditor.document().setDefaultStyleSheet( "a {color: cyan; }") else: self.widgetEditor.document().setDefaultStyleSheet( "a {color: blue; }") self.refreshMessages() def refreshMessages(self): '''Clear the text display and re-add all messages with current style settings etc.''' self.widgetEditor.clear() for message in self.messageCache: self.widgetEditor.append(message) def anchorClicked(self, url): '''If the url appears to be one of the /tell links in a player name, load it to the input.''' if "/tell" in UNICODE_STRING(url): self.widgetLineInput.setText(url.toString()) else: QDesktopServices.openUrl(QUrl(url)) def toggleTimestamp(self, newsetting): if newsetting == "On": self.timestamp = True else: self.timestamp = False def insertMessage(self, mes): self.scroll = (self.widgetEditor.verticalScrollBar().value() == self.widgetEditor.verticalScrollBar().maximum()) if self.timestamp: message = " ".join((strftime(self.timestampformat, localtime()), mes)) self.messageCache.append(message) self.widgetEditor.append(message) else: self.messageCache.append(mes) self.widgetEditor.append(mes) if self.scroll: self.widgetEditor.verticalScrollBar().setValue( self.widgetEditor.verticalScrollBar().maximum()) try: try: self.logfile = open( ospath.join(LOG_DIR, strftime("%b_%d_%Y.log", localtime())), 'a') self.logfile.write(mes + "\n") finally: self.logfile.close() except: pass def processTags(self, message): message = message.replace("<", "<").replace(">", ">") for validTag in ("i", "b", "u", "s"): message = message.replace("".join(("[", validTag, "]")), "".join( ("<", validTag, ">"))) message = message.replace("".join(("[", "/", validTag, "]")), "".join(("<", "/", validTag, ">"))) message = sub(r"\[url\](.*?)\[/url\]", r"<a href=\1>\1</a>", message) message = message.replace( "/>", ">") #prevents anchor from closing with trailing slash in URL return message def processInput(self): self.newmes = UNICODE_STRING(self.widgetLineInput.text()) self.newmes = self.processTags(self.newmes) self.widgetLineInput.clear() self.widgetLineInput.addMessage(self.newmes) self.chatInput.emit(self.newmes) chatInput = signal(BASE_STRING, doc="""Called when chat input is received. text -- the message entered """)
class userListWidget(QDockWidget): """The list of connected users.""" def __init__(self, mainWindow): """Initializes the user list.""" super(QDockWidget, self).__init__(mainWindow) self.setToolTip(self.tr("People presently playing.")) self.setWindowTitle(self.tr("Connected Users")) self.widget = QWidget(mainWindow) self.listOfUsers = userListList(mainWindow, self) self.internalList = [] self.layout = QGridLayout() self.layout.addWidget(self.listOfUsers, 0, 0, 1, 2) self.widget.setLayout(self.layout) self.widget.setMaximumWidth( 200) #Arbitrary; keeps it from taking over 1/3 of the screen self.setWidget(self.widget) self.setObjectName("User List Widget") self.gmname = None self.localname = None mainWindow.addDockWidget(Qt.BottomDockWidgetArea, self) def addUser(self, name, host=False): self.internalList.append((name, host)) nametmp = name if host: if name == self.localname: self.kickbutton = QPushButton(self.tr("Kick")) self.kickbutton.setToolTip( self.tr("Disconnect the selected user.")) self.layout.addWidget(self.kickbutton, 1, 0) self.kickbutton.clicked.connect(self.requestKick) self.banbutton = QPushButton(self.tr("Manage Banlist")) self.banbutton.setToolTip( self.tr("View and edit a list of banned IPs.")) self.layout.addWidget(self.banbutton, 1, 1) self.banbutton.clicked.connect(self.openBanDialog) nametmp = "[Host] " + nametmp if self.gmname == name: nametmp = "[GM] " + nametmp self.listOfUsers.addItem(nametmp) def removeUser(self, name): for i, item in enumerate(self.internalList): if item[0] == name: self.internalList.pop(i) self.listOfUsers.takeItem(i) def getUsers(self): return self.internalList def clearUserList(self): self.internalList = [] self.listOfUsers.clear() def refreshDisplay(self): self.listOfUsers.clear() for item in self.internalList: nametmp = item[0] if item[1]: nametmp = "[Host] " + nametmp if self.gmname == item[0]: nametmp = "[GM] " + nametmp self.listOfUsers.addItem(nametmp) def setGM(self, new): self.gmname = new self.refreshDisplay() def provideOptions(self, ID): if self.gmname != self.localname: return name = self.internalList[ID][0] #self.setGM(name) self.selectGM.emit(name) def requestKick(self): name = self.internalList[self.listOfUsers.currentRow()][0] if name == self.localname: return self.kickPlayer.emit(name) def openBanDialog(self): banDialog().exec_() self.requestBanlistUpdate.emit() selectGM = signal( BASE_STRING, doc= """Called to request a menu be summoned containing actions targeting the selected player. Sorry for the misleading legacy name.""") kickPlayer = signal(BASE_STRING, doc="""Called to request player kicking.""") requestBanlistUpdate = signal( doc="""Called to request that the banlist be updated.""")
class statefulSocket(object): """A socket wrapper that manages its connection state.""" _debugcounter = 0 def __init__(self, name="S-UNK", socket=None, hostname=None, port=None): """Initializes the socket, connecting or wrapping a connection.""" self.clientside = bool(hostname) self.state = QAbstractSocket.UnconnectedState self.debugid = statefulSocket._debugcounter statefulSocket._debugcounter += 1 self.imbueName(name) self.active = False self.ready = False self.sentfile = None self.receivedfile = None if self.clientside: assert (hostname and port) assert (not socket) self.socket = QTcpSocket(mainWindow) else: assert (not hostname and not port) assert (socket) assert (socket.state() == QAbstractSocket.ConnectedState) self.socket = socket self.state = QAbstractSocket.ConnectedState # Attach signals self.socket.error.connect(self._error) self.socket.hostFound.connect(self._hostFound) self.socket.disconnected.connect(self._disconnected) self.socket.stateChanged.connect(self._stateChanged) self.socket.readyRead.connect(self._readyRead) self.socket.bytesWritten.connect(self._bytesWritten) # Connect client if self.clientside: self.socket.connectToHost(hostname, port) def imbueName(self, name): """Changes the name of this object.""" self.context = "{0}-{1}".format(name, self.debugid) def activate(self): """Activates this socket.""" assert (not self.ready) self.ready = True self.active = True print("[{0}] Activated!".format(self.context)) @property def busy(self): """Whether the socket is busy sending data already.""" return (not self.ready or self.sentfile is not None or self.receivedfile is not None) @property def closed(self): """Whether the socket is already closed.""" return (self.state == QAbstractSocket.UnconnectedState) def close(self): """Disconnect all network activity.""" if not self.closed: self.ready = False self.state = QAbstractSocket.UnconnectedState if self.sentfile: self.sentfile.file.close() self.sentfile = None if self.receivedfile: self.receivedfile.file.close() self.receivedfile = None if self.clientside: self.socket.disconnectFromHost() else: self.socket.close() print("[{0}] Closed connection.".format(self.context)) def _closeWithPrejudice(self): """Close the socket, reporting a disconnection message if not already closed.""" if self.closed: return oldState = self.state self.close() self.disconnected.emit( self, self.disconnectionError(oldState, self.socket.error())) def _respondToSocketError(self, header, result, length=0, text=None): """Responds to a read or write error. header -- "Read Error" or "Write Error" result -- the number of bytes read or negative to indicate error length -- the length of the text if text is not provided text -- if human-readable, the message text """ if text: length = len(text) ERROR_LENGTH = 12 if len(text) > ERROR_LENGTH: text = text[:ERROR_LENGTH] text = "'{0}' ".format(text) else: text = '' assert (length > 0) assert (length != result) if result < 0: message = "[{context}] {header}: could not process message {sample}length {length}" else: message = "[{context}] {header}: partial message {sample}length {partial}/{length}" #sample = statefulSocket._sampleText(serial) print( message.format(context=self.context, header=header, sample=text, length=length, partial=result)) self._closeWithPrejudice() def disconnectionError(self, previousState, err): """Find a human-readable error message for when the connection fails.""" s = QAbstractSocket # Messages for what happens when we weren't yet connected if self.clientside: if previousState != s.ConnectedState: # ConnectionRefusedError could mean either refused or timed out # SocketTimeoutError is probably not relevant here if err == s.ConnectionRefusedError or err == s.SocketTimeoutError: return fake.translate( 'socket', 'The connection was refused or timed out.') # Couldn't look up the IP or url that the user specified (didn't reach DNS) if err == s.HostNotFoundError: return fake.translate( 'socket', 'The system could not find the specified host address.' ) # Local firewall or privileges denied access to sockets if err == s.SocketAccessError: return fake.translate( 'socket', 'The program was denied access to network hardware.') # Lot of stuff that probably won't apply; SSL, Proxies, etc # Mostly they mean there's a cable unplugged or something return fake.translate( 'socket', 'The program could not find the specified host.') # Connected but not ready if not self.active: return fake.translate('socket', 'The server refused the connection.') # Client gracefully quit if err == s.RemoteHostClosedError: if self.clientside: return fake.translate('socket', 'The server closed the connection.') return fake.translate('socket', 'The client closed the connection.') # Client died without saying anything if err == s.SocketTimeoutError: return fake.translate('socket', 'The connection timed out.') # Something random happened if self.clientside: return fake.translate('socket', 'You have been disconnected.') return fake.translate('socket', 'The client was disconnected.') # SENDING # Memoization of previously sent value because # server data is often sent to multiple destinations _memoizeKey = None _memoizeData = None @staticmethod def _serialize(data): """Serialize an object to a message that can be sent.""" serial = jsondumps(data) return serial def _rawsend(self, serial): """Sends serialized data.""" try: result = bytes(serial, "utf-8") except TypeError: result = serial result = self.socket.write(result) if result == len(serial): # I guess flush forces synchronous sending. #self.socket.flush() return True self._respondToSocketError("Write Error", result, text=serial) return False def _rawreadline(self): """Reads a line from the socket.""" assert (self.socket.canReadLine()) data = self.socket.readLine() if version_info >= (3, ): data = str(data, "UTF-8") if len(data) > 0: assert (data[-1] == '\n') return data self._respondToSocketError("Line read Error", -1, length=0) return None def _rawread(self, length): """Reads a line from the socket.""" if length <= 0: return None assert (length > 0) data = self.socket.read(length) if len(data) == length: return data self._respondToSocketError("Read Error", -1, length=length) return None def sendObject(self, data): """Sends a JSON object over the wire. Precondition: self.ready """ assert (not self.busy or not self.ready) if statefulSocket._memoizeKey is data: serial = statefulSocket._memoizeData # TODO: This check negates the memoization # should uncomment for release. assert (self._serialize(data) == serial) else: serial = statefulSocket._serialize(data) statefulSocket._memoizeKey = data statefulSocket._memoizeData = serial self._rawsend(serial + "\n") def sendMessage(self, command, **kwargs): """Sends a message across the wire.""" assert (command is not None) obj = { PARM_COMMAND: command, PARM_INTERNAL: True, } for key, arg in list(kwargs.items()): obj[key] = arg self.sendObject(obj) def sendFile(self, filedata): """Begins sending file data directly over the wire. filedata -- an open filedata object """ assert (self.ready) assert (not self.busy) filedata.seekToBeginning() self.sentfile = filedata self.updateSend() def updateSend(self): """Continues sending the current send file.""" if not self.sentfile: return while self.socket.bytesToWrite() < CHUNK_SIZE: data = self.sentfile.read(self.context) if data is None: self._closeWithPrejudice() return if not self._rawsend(data): return self.filePartlySent.emit(self.sentfile.filename, UNICODE_STRING(self.sentfile.size), UNICODE_STRING(self.sentfile.processed)) if self.sentfile.file.atEnd(): sentfile = self.sentfile self.sentfile = None sentfile.file.close() self.fileSent.emit(self, sentfile.filename) return # RECEIVING def receiveFile(self, filedata): """Writes the next size bytes to the specified file.""" filedata.seekToBeginning() self.receivedfile = filedata self.updateReceive() def updateReceive(self): """Parses incoming data into messages or objects.""" while True: if self.receivedfile is not None: length = min( CHUNK_SIZE, self.receivedfile.size - self.receivedfile.processed) if self.socket.bytesAvailable() < length: return data = self._rawread(length) if data is None: return if not self.receivedfile.write(self.context, data): self._closeWithPrejudice() return self.filePartlyReceived.emit( self.receivedfile.filename, UNICODE_STRING(self.receivedfile.size), UNICODE_STRING(self.receivedfile.processed)) if self.receivedfile.size == self.receivedfile.processed: receivedfile = self.receivedfile self.receivedfile = None receivedfile.file.close() self.fileReceived.emit(self, receivedfile.filename) # May be more available continue else: if not self.socket.canReadLine(): return serial = self._rawreadline() if serial is None: return # Allow empty lines if EMPTY_REGEX.match(UNICODE_STRING(serial)): continue try: obj = jsonloads(UNICODE_STRING(serial)) except: self._respondToSocketError("JSON Error", -1, text=serial) return self.receiveObject(obj) def receiveObject(self, obj): """Look for internal commands or pass directly to object handler.""" if obj.get(PARM_INTERNAL) == True: command = obj.get(PARM_COMMAND) if command is not None: del obj[PARM_COMMAND] del obj[PARM_INTERNAL] self.commandReceived.emit(self, command, obj) else: self.objectReceived.emit(self, obj) # SIGNALS connected = signal( object, doc="""Called when the socket becomes ready to start sending; when ready becomes True. Happens when a remote socket identifies itself as a user or a local socket receives a connection notification from the server. socket -- this socket """) disconnected = signal( object, BASE_STRING, doc="""Called when the socket disconnects or fails to connect. Not called when disconnected manually (through close()). socket -- this socket errorMessage -- the untranslated reason the connection failed """) objectReceived = signal(object, dict, doc="""Called when data is received over the wire. socket -- this socket data -- the data received """) commandReceived = signal(object, BASE_STRING, dict, doc="""Called when data is received over the wire. socket -- this socket command -- the command to carry out arguments -- the keyword arguments """) fileSent = signal(object, BASE_STRING, doc="""Called when a file is done sending. socket -- this socket name -- the name of the file """) fileReceived = signal(object, BASE_STRING, doc="""Called when a file is done receiving. socket -- this socket name -- the name of the file """) filePartlySent = signal(BASE_STRING, BASE_STRING, BASE_STRING, doc="""Called when a chunk of a file is sent. filename -- the filename of the file sent size -- the total file size processed -- the amount written so far """) filePartlyReceived = signal( BASE_STRING, BASE_STRING, BASE_STRING, doc="""Called when a chunk of a file is received and written. filename -- the filename of the file received size -- the total file size processed -- the amount written so far """) def _receive(self): """Call to make the client receive data.""" self.updateReceive() def _readyRead(self): """Called when data is ready.""" self.updateReceive() def _bytesWritten(self, bytes): """Occurs when some data is eaten out of the buffer.""" self.updateSend() def _disconnected(self): """Called when disconnected from the server.""" print("[{0}] Disconnected.".format(self.context)) # TODO: Should we delete the socket here? self.socket.deleteLater() def _stateChanged(self, newState): """Detects connection changes.""" oldState = self.state self.state = newState #print "State: {0} {1}".format(oldState, newState) if oldState == newState: return s = QAbstractSocket if self.clientside: if newState == s.HostLookupState: print("[{0}] Looking up host...".format(self.context)) return if newState == s.ConnectingState: print("[{0}] Connecting...".format(self.context)) return if newState == s.ConnectedState: print("[{0}] Connected. Awaiting activation...".format( self.context)) self.connected.emit(self) return if oldState not in (s.HostLookupState, s.ConnectingState, s.ConnectedState): return if oldState == s.UnconnectedState: return print("[{context}] Closing error #{id} {message}".format( context=self.context, id=self.socket.error(), message=self.socket.errorString())) self.state = s.ConnectedState self.close() self.disconnected.emit( self, self.disconnectionError(oldState, self.socket.error())) def _hostFound(self): """Responds to host name being resolved. (DNS)""" # Apparently the resolved host is still not available #print "[{0}] Host found: {1} resolved to {2}:{3}".format( # self.context, self.socket.peerName(), self.socket.peerAddress().toString(), self.socket.peerPort()) def _error(self, err): """Writes errors to the console.""" print("[{context}] ERROR #{id} {message}".format( context=self.context, id=self.socket.error(), message=self.socket.errorString()))
class BaseClient(object): """Base class for local and remote clients.""" def __init__(self): """Initializes the client.""" # Sockets/server self.obj = None self.xfer = None self.server = None self.hostname = None self.port = None self.password = None # File transfer lists self.sendList = set() self.getList = set() self.sentfile = None self.receivedfile = None # Doesn't need translation self.username = UNICODE_STRING(localUser()) or 'localhost' assert (self.username) self.timer = QTimer() self.timer.timeout.connect(self._updatetransfer) self.timer.start(1000) #self.timer = QTimer() #self.timer.timeout.connect(self.transferHack) #self.timer.start(3600000) @property def ready(self): return bool(self.obj) and self.obj.ready def close(self): """Disconnect all network activity.""" self.sendList = set() self.getList = set() self.hostname = None self.port = None if self.obj: x = self.obj self.obj = None x.connected.disconnect() x.disconnected.disconnect() x.objectReceived.disconnect() x.commandReceived.disconnect() x.close() self._closeXfer() def _openXfer(self, socket): """Open the transfer socket.""" assert (self.ready) self._closeXfer() self.xfer = socket socket.connected.connect(self._socketConnected) socket.disconnected.connect(self._socketDisconnected) socket.objectReceived.connect(self._socketObject) socket.commandReceived.connect(self._socketCommand) socket.fileSent.connect(self._fileSent) socket.fileReceived.connect(self._fileReceived) socket.filePartlySent.connect(self._filePartlySent) socket.filePartlyReceived.connect(self._filePartlyReceived) def _closeXfer(self): """Disconnect the transfer socket.""" if self.xfer: if self.sentfile: x = self.sentfile self.sentfile = None x.file.close() self.receivedfile = None x = self.xfer self.xfer = None x.connected.disconnect() x.disconnected.disconnect() x.objectReceived.disconnect() x.commandReceived.disconnect() x.fileSent.disconnect() x.fileReceived.disconnect() x.close() def _filePartlySent(self, filename, size, processed): self.partialTransferEvent.emit(self, filename, size, processed) def _filePartlyReceived(self, filename, size, processed): self.partialTransferEvent.emit(self, filename, size, processed) def send(self, data): """Call to send an object over the wire.""" self.server.receive(self.username, data) def receive(self, obj): """Make the client receive some data.""" pass def requestFile(self, filename): if filename in self.getList: # NOTE: could update checksum here (for refreshing the file when updated locally) return False try: file = QFile(makeLocalFilename(filename)) if not file.open(QFile.ReadWrite): try: makedirs(path.dirname(filename)) except: pass if not file.open(QFile.ReadWrite): return False try: size = file.size() digest = generateChecksum(file) filedata = fileData(file, filename, size, digest) finally: file.close() except IOError: return False if stat(filename).st_size == 0: remove(filename) self.getList.add(filename) self.obj.sendMessage(MESSAGE_GET, filename=filedata.filename, size=filedata.size, checksum=filedata.digest) message = "[{0}] Requested transfer of {filename} [{size} {checksum}]" print( message.format(self.obj.context, filename=filedata.filename, size=filedata.size, checksum=filedata.digest)) self.fileEvent.emit(self, filename, "Requested") self._updatetransfer() return True def _updateSendReceive(self): """Determines the order of updates to prioritize server GETs.""" raise NotImplementedError("Must override.") def _updateSend(self): """Updates the transfers to send.""" while self.sendList and not self.sentfile: self.fileEvent.emit( self, "", "SENDING: " + str(len(self.sendList)) + " QUEUED") filedata = next(iter(self.sendList)) self.sendList.remove(filedata) if self._shouldSendFile(filedata): self.sentfile = filedata self.xfer.sendMessage(MESSAGE_PUT, filename=filedata.filename, size=filedata.size, digest=filedata.digest) socket = self.xfer message = "[{0}] Offering transfer of {filename} [{size} {checksum}]" else: self.obj.sendMessage(MESSAGE_IGNORE, filename=filedata.filename) socket = self.obj message = "[{0}] Ignored transfer of {filename} [{size} {checksum}]" print( message.format(socket.context, filename=filedata.filename, size=filedata.size, checksum=filedata.digest)) if self.sentfile: self.fileEvent.emit( self, "", "EXITED SEND LOOP DUE TO SENTFILE (" + str(len(self.sendList)) + " FILES REMAIN QUEUED)") def _updateReceive(self): """Updates the transfers to receive.""" # Did the server want to send us something? if self.receivedfile: filename = self.receivedfile.filename if self._shouldReceiveFile(self.receivedfile): message = "[{0}] Accepted transfer of {filename} [{size} {checksum}]" print( message.format(self.xfer.context, filename=filename, size=self.receivedfile.size, checksum=self.receivedfile.digest)) self.xfer.sendMessage(MESSAGE_ACCEPT, filename=filename) self.xfer.receiveFile(self.receivedfile) else: message = "[{0}] Rejected transfer of {filename} [{size} {checksum}]" print( message.format(self.xfer.context, filename=filename, size=self.receivedfile.size, checksum=self.receivedfile.digest)) self._fileFailed(filename) self.xfer.sendMessage(MESSAGE_REJECT, filename=filename) self.getList.discard(filename) self.receivedfile = None def _updatetransfer(self): """Opens or updates the transfer socket.""" if not self.ready: self.fileEvent.emit(self, "", "NOT READY") return if not self.getList and not self.sendList: self.fileEvent.emit(self, "", "NO SEND LIST") return if not self.xfer: self.fileEvent.emit(self, "", "NO XFER SOCKET") self._openXfer() return self._updateSendReceive() def preemptivelyOpenTransferSocket(self): if not self.ready: return if not self.xfer: self._openXfer() return def transferHack(self): '''Kiiiind of crazy...''' if self.ready and not self.getList and not self.sendList and not self.xfer.busy: self._openXfer() def allowSend(client, filename, size, checksum): """Replacable hook for determining which files should be sent.""" return True def allowReceipt(client, filename, size, checksum): """Replacable hook for determining which files should be accepted.""" return True def _shouldSendFile(self, fileData): """Check if we should send a file. Does extra mutating work.""" filename = fileData.filename try: # Can we open the file? if not fileData.file.open(QFile.ReadOnly): self.fileEvent.emit(self, filename, "SENDFILE Could not open") print("SENDFILE Could not open") return False try: file = fileData.file # Do we already have an identical copy? size = fileData.file.size() digest = generateChecksum(file) if fileData.size is not None and fileData.digest is not None: if size == fileData.size and digest == fileData.digest: self.fileEvent.emit(self, filename, "SENDFILE Size and digest match") print("SENDFILE Size and digest match") return False fileData.size = file.size() fileData.digest = digest # User hook if not self.allowSend(fileData.filename, fileData.size, fileData.digest): self.fileEvent.emit(self, filename, "SENDFILE User hook") print("SENDFILE User hook") return False file = None self.fileEvent.emit(self, filename, "SENDFILE Success") print("SENDFILE Success") return True finally: if file: file.close() except IOError: pass return False def _shouldReceiveFile(self, fileData): """Check if we should receive a file. Does extra mutating work.""" file, filename = fileData.file, fileData.filename # Did we ask for the file? if not filename in self.getList: self.fileEvent.emit(self, filename, "RECVFILE Duplicate") print("RECVFILE Duplicate") return False try: # Can we open the file? if not file.open(QFile.ReadWrite): self.fileEvent.emit(self, filename, "RECVFILE Could not open") print("RECVFILE Could not open") return False try: # Do we already have an identical copy? if file.size() == fileData.size: if generateChecksum(file) == fileData.digest: self.fileEvent.emit(self, filename, "RECVFILE Size and digest match") print("RECVFILE Size and digest match") return False # User hook if not self.allowReceipt(fileData.filename, fileData.size, fileData.digest): self.fileEvent.emit(self, filename, "RECVFILE User hook") print("RECVFILE User hook") return False file = None self.fileEvent.emit(self, filename, "RECVFILE Success") print("RECVFILE Success") return True finally: if file: file.close() except IOError: pass return False # SIGNAL RESPONSES def _socketConnected(self, socket): """Called when a socket is connected.""" pass def _socketDisconnected(self, socket, errorMessage): """Called when a socket disconnects.""" if socket == self.obj: self.close() elif socket == self.xfer: self._closeXfer() self._updatetransfer() def _socketObject(self, socket, data): """Called when an object is received on a socket.""" if socket == self.obj: self.receive(data) def _socketCommand(self, socket, command, kwargs): """Responds to socket commands.""" if socket == self.obj: if command not in (MESSAGE_ACTIVATE, MESSAGE_GET, MESSAGE_IGNORE): message = "[{0}] Unexpected object command {command}" print(message.format(socket.context, command=command)) return elif socket == self.xfer: if command not in (MESSAGE_ACTIVATE, MESSAGE_PUT, MESSAGE_ACCEPT, MESSAGE_REJECT): message = "[{0}] Unexpected transfer command {command}" print(message.format(socket.context, command=command)) return else: return if ((command == MESSAGE_ACTIVATE) == socket.ready): message = "[{0}] Unexpected command {command}" print(message.format(socket.context, command=command)) return try: kwargs = dict((str(key), val) for key, val in list(kwargs.items())) if command == MESSAGE_ACTIVATE: self._activateSocket(socket, **kwargs) elif command == MESSAGE_GET: self._getFile(socket, **kwargs) elif command == MESSAGE_PUT: self._putFile(socket, **kwargs) elif command == MESSAGE_IGNORE: self._ignoreFile(socket, **kwargs) elif command == MESSAGE_ACCEPT: self._acceptFile(socket, **kwargs) elif command == MESSAGE_REJECT: self._rejectFile(socket, **kwargs) except TypeError as e: message = "[{0}] Invalid parameters to remote command {command}: {parms}; {err}" import traceback print( message.format(socket.context, command=command, parms=repr(kwargs), err=e)) traceback.print_exc() def _activateSocket(self, socket, username): """Activates the socket.""" pass def _getFile(self, socket, filename, size, checksum): """Responds to a file request.""" self.fileEvent.emit(self, filename, "Requested by client") self.sendList.add( fileData(QFile(makeLocalFilename(filename)), filename, size, checksum)) self._updatetransfer() def _putFile(self, socket, filename, size, digest): """Checks whether to accept a sent file.""" if self.receivedfile is not None: message = "[{0}] Remote duplicate PUT; ignoring {filename}" print( message.format(socket.context, filename=self.receivedfile.filename)) self.receivedfile = fileData(QFile(makeLocalFilename(filename)), filename, size, digest) self._updatetransfer() def _ignoreFile(self, socket, filename): """Responds to a file request.""" message = "[{0}] Remote refused to send {filename}" print(message.format(socket.context, filename=filename)) self.fileEvent.emit(self, filename, "Remote refused to send") if filename in self.getList: self.getList.remove(filename) self._fileFailed(filename) self._updatetransfer() def _acceptFile(self, socket, filename): """Starts sending the specified file.""" if not self.sentfile or self.sentfile.filename != filename: message = "[{0}] Attempt to accept unexpected file {filename}" print(message.format(socket.context, filename=filename)) socket._disconnectWithPrejudice() return socket.sendFile(self.sentfile) self.sentfile = None def _rejectFile(self, socket, filename): """Cancels sending the specified file.""" if not self.sentfile or self.sentfile.filename != filename: message = "[{0}] Attempt to reject unexpected file {filename}" print(message.format(socket.context, filename=filename)) return self.sendList.remove(filename) self.sentfile.file.close() self.sentfile = None def _fileSent(self, socket, filename): """Look for more stuff to send.""" self._updatetransfer() def _fileReceived(self, socket, filename): """Look more stuff to send.""" self._updatetransfer() def _fileFailed(self, filename): """Notify that the socket did not send the specified file.""" pass fileEvent = signal(object, BASE_STRING, BASE_STRING, doc="""Called when something happens relating to a file. client -- this client filename -- the filename of the file received event -- a description of the event """) partialTransferEvent = signal(object, BASE_STRING, BASE_STRING, BASE_STRING, doc="""Called when part of a transfer occurs. client -- this client filename -- the filename of the file involved size -- total size of the file processed -- amount of the file transferred so far """)
class JsonServer(object): """A server that processes JSON messages.""" def __init__(self, client): self.clients = {} self._addClient(client) self.unknown = set() self.client = client client.server = self self.tcp = None # Banlist can be manipulated manually; # just add ips self.banlist = set() self.password = None @property def isConnected(self): return bool(self.tcp) def _listen(self, port): """Starts the server. Returns True on success, else False.""" if self.isConnected: raise RuntimeError("Already connected.") self.clients = {} self._addClient(self.client) tcp = QTcpServer(mainWindow) tcp.newConnection.connect(self._newConnection) result = tcp.listen(QHostAddress("0.0.0.0"), port) if result: print("[SERVER] Listening on {0}:{1}".format( tcp.serverAddress().toString(), tcp.serverPort())) self.tcp = tcp else: print("[SERVER] Error on listen attempt; {0}".format( tcp.errorString())) # TODO: Should we delete the server here? tcp.deleteLater() #TODO: Better error reporting? return result def close(self): self.clients = {} self._addClient(self.client) if self.isConnected: tcp = self.tcp self.tcp = None for client in list(self.clients.values()): client.close() for socket in self.unknown: socket.close() self.unknown = set() tcp.close() # TODO: Should we delete the server here? tcp.deleteLater() print("[SERVER] No longer listening.") def send(self, username, data): """Call to send an object over the wire.""" if not self.userExists(username): raise RuntimeError("Invalid username {0}".format(username)) client = self.clients[self._processUsername(username)] client.receive(data) def broadcast(self, data, users=None): """Call to send an object over the wire. users -- users to send to; default is all """ if users: users = set(self._processUsername(username) for username in users) else: users = list(self.clients.keys()) for shortname in users: assert (shortname in self.clients) self.clients[shortname].receive(data) def requestFile(self, username, filename): """Requests a file from the specified user.""" if not self.userExists(username): raise RuntimeError("Invalid username {0}".format(username)) client = self.clients[self._processUsername(username)] return client.requestFile(filename) def receive(self, username, data): """Receive the specified data.""" return self.objectReceived.emit(self, username, data) def allowSend(server, username, filename, size, checksum): """Replacable hook for determining which files should be sent.""" return True def allowReceipt(server, username, filename, size, checksum): """Replacable hook for determining which files should be accepted.""" return True def _processUsername(self, username): """Processes a username to lowercase.""" return UNICODE_STRING(username).lower() def _addClient(self, client): """Adds a client to the list.""" assert (not self.userExists(client.username)) self.clients[self._processUsername(client.username)] = client def userExists(self, username): """Checks whether the given username is taken.""" return (self._processUsername(username) in self.clients) def fullname(self, username): """Returns the correctly capitalized version of the username.""" assert (self.userExists(username)) return self.clients[self._processUsername(username)].username def allowPassword(self, passw): if passw == self.password: return True return False def addBan(self, IP): """Adds the given IP to the banlist.""" self.banlist.add(str(IP)) def clearBanlist(self): """Clears all entries from the banlist.""" self.banlist = set() @property def users(self): """Returns the list of usernames.""" return list(self.clients.keys()) def userIP(self, username): """Gets the IP of an existing client.""" if not self.userExists(username): raise RuntimeError("Invalid username {0}".format(username)) if self.clients[self._processUsername(username)] == self.client: return "127.0.0.1" return UNICODE_STRING(self.clients[self._processUsername( username)].obj.socket.peerAddress()) def baseUsername(server): """Replaceable hook for the base 'guest' username.""" # NOTE: should not localize return 'guest' def allowUsername(server, username): """Replacable hook for determining whether a username is OK. Does not need to determine uniqueness, but the name may be altered to something in the form of '{original}-[A-Za-z0-9]+' """ return bool(VALID_USERNAME.match(username)) def _fixUsername(self, username=None): """Fixes a username so that it's unique.""" if not username: username = self.baseUsername() if self.userExists(username): username = username + ('-' + findRandomAppend()) while self.userExists(username): username += findRandomAppend() return str(username) def setPassword(self, new): self.password = new def rename(self, oldname, newname): """Renames a user to the specified name. Does some quick validity checking. Raises if not accepted; pre: not userExists(newname) and allowUsername(newname) and oldname != newname NOTE: Might cause file transfer to be declined, but will just reconnect. Only renames on this side; you must coordinate it manually. """ oldname = self._processUsername(oldname) newproc = self._processUsername(newname) assert (self.userExists(oldname)) client = self.clients[oldname] assert (self.allowUsername(newname)) assert (client.username != newname) assert (not self.userExists(newname) or newproc == oldname) del self.clients[oldname] client.username = newname self._addClient(client) def kick(self, username): """Kicks a user.""" if not self.isConnected: return username = self._processUsername(username) assert (username in self.clients) client = self.clients[username] assert (client != self.client) client.close() del self.clients[username] self.kicked.emit(self, client.username) def _dropClient(self, username, errorMessage): """Disconnect a remote client.""" if not self.isConnected: return username = self._processUsername(username) assert (self.userExists(username)) assert (username in self.clients) client = self.clients[username] assert (client != self.client) client.close() assert (username in self.clients) del self.clients[username] self.disconnected.emit(self, client.username, errorMessage) def _respondXferDisconnect(self, username, errorMessage): """Attempt to reestablish a lost transfer socket connection.""" assert (self.userExists(username)) assert (username.lower() in self.clients) client = self.clients[username.lower()] assert (client != self.client) self.transferDisconnected.emit(self, client.username, errorMessage) connected = signal(object, BASE_STRING, doc="""Called when a client connects to the server. server -- this server username -- the username of the client """) disconnected = signal( object, BASE_STRING, BASE_STRING, doc="""Called when a client disconnects from the server. Not called when disconnected manually (through close()). Never called when hosting; you will never be disconnected automatically. server -- this server username -- the username of the client errorMessage -- the untranslated reason the connection failed """) transferDisconnected = signal( object, BASE_STRING, BASE_STRING, doc="""Called when a client transfer socket disconnects from the server. Not called when disconnected manually (through close()). Never called when hosting; you will never be disconnected automatically. server -- this server username -- the username of the client errorMessage -- the untranslated reason the connection failed """) kicked = signal(object, BASE_STRING, doc="""Called when a client is kicked from the server. server -- this server username -- the username of the client """) objectReceived = signal( object, BASE_STRING, dict, doc="""Called when an object is received from the client over the wire. server -- this server username -- the username of the client data -- the data received """) fileReceived = signal(object, BASE_STRING, BASE_STRING, doc="""Called when a file is received over the wire. server -- this server username -- the username of the client filename -- the filename of the file received """) fileFailed = signal(object, BASE_STRING, BASE_STRING, doc="""Called when a file fails to come over the wire. server -- this server username -- the username of the client filename -- the filename of the file received """) fileEvent = signal(object, BASE_STRING, BASE_STRING, doc="""Called when a file fails to come over the wire. username -- the username of the client filename -- the filename of the file received event -- a description of the event """) partialTransferEvent = signal(object, BASE_STRING, BASE_STRING, BASE_STRING, doc="""Called when part of a transfer occurs. username -- the username of the client filename -- the filename of the file involved size -- total size of the file processed -- amount of the file transferred so far """) def passFileEvent(self, clientName, filename, eventDescription): self.fileEvent.emit(clientName, filename, eventDescription) def passPartialTransferEvent(self, clientName, filename, size, processed): self.partialTransferEvent.emit(clientName, filename, size, processed) def _newConnection(self): """Responds to a new connection occurring.""" if not self.tcp: return while self.tcp.hasPendingConnections(): socket = self.tcp.nextPendingConnection() if str(socket.peerAddress().toString()) in self.banlist: socket.close() print("[SERVER] Banned client attempted to connect: {1}:{2}". format(socket.peerName(), socket.peerAddress().toString(), socket.peerPort())) return assert (socket) print("[SERVER] New client connected: {1}:{2}".format( socket.peerName(), socket.peerAddress().toString(), socket.peerPort())) socket = statefulSocket(socket=socket) self.unknown.add(socket) socket.disconnected.connect(self._socketDisconnected) socket.commandReceived.connect(self._socketCommand) socket.objectReceived.connect(self._socketObject) print("Connections successful for ", str(socket)) def _detachUnknown(self, socket): self.unknown.discard(socket) socket.disconnected.disconnect() socket.commandReceived.disconnect() socket.objectReceived.disconnect() def _socketDisconnected(self, socket, reason): message = "Socket closed before identification: {reason}" message = message.format(reason=reason) self._forbidSocket(socket, message) def _forbidSocket(self, socket, text): message = "[{0}] {1}" print(message.format(socket.context, text)) self._detachUnknown(socket) socket.close() def _socketObject(self, socket, obj): message = "Disallowed object data received" self._forbidSocket(socket, message) return def _socketCommand(self, socket, command, kwargs): if command != MESSAGE_IDENTIFY: message = "Disallowed initial remote command {command}" message = message.format(command=command) self._forbidSocket(socket, message) return try: kwargs = dict((str(key), val) for key, val in list(kwargs.items())) self._socketIdentified(socket, **kwargs) except TypeError as e: message = "Invalid parameters to initial remote command {command}: {parms}; {err}" message = message.format(command=command, parms=repr(kwargs), err=e) self._forbidSocket(socket, message) def _socketIdentified(self, socket, protocol, username, passw=None, networkVersion="1"): """Socket identifies itself with protocol and username.""" if protocol not in (PROTOCOL_OBJECT, PROTOCOL_TRANSFER): message = "Disallowed protocol identified {protocol} ({username})" message = message.format(protocol=protocol, username=username) self._forbidSocket(socket, message) return if networkVersion not in COMPATIBLE_NETWORK_VERSIONS: message = "Incompatible network version (client should update RGG): {networkVersion} ({username})" message = message.format(networkVersion=networkVersion, username=username) self._forbidSocket(socket, message) return self._detachUnknown(socket) if protocol == PROTOCOL_OBJECT: # Make it unique and valid if not self.allowUsername(username): username = None username = self._fixUsername(username) assert (not self.userExists(username)) if self.password is not None and len(self.password) > 0: if not self.allowPassword(passw): message = "Password error {protocol} ({username})" message = message.format(protocol=protocol, username=username) self._forbidSocket(socket, message) return socket.imbueName("S-OBJ") socket.activate() client = RemoteClient(username, self, socket) self._addClient(client) client.fileEvent.connect(self.passFileEvent) client.partialTransferEvent.connect(self.passPartialTransferEvent) socket.sendMessage(MESSAGE_ACTIVATE, username=username) self.connected.emit(self, username) else: # Make sure identities match username = self._processUsername(username) if username not in self.clients: message = "Transfer protocol username does not match {username}" message = message.format(username=username) self._forbidSocket(socket, message) return client = self.clients[username] if client == self.client: message = "Transfer protocol attempt to match server user." message = message.format(username=username) self._forbidSocket(socket, message) return assert (client.ready) if client.obj.socket.peerAddress() != socket.socket.peerAddress(): message = "Transfer protocol attempt to match user from different IP." message = message.format(username=username) self._forbidSocket(socket, message) return socket.activate() socket.imbueName("S-XFR") client._openXfer(socket) socket.sendMessage(MESSAGE_ACTIVATE, username=username) message = "[{0}:{1}] Transfer socket connected to {username}" print( message.format(socket.context, client.obj.context, username=username)) client._updatetransfer()
class JsonClient(BaseClient): """A client that communicates with a server.""" # CONNECTION @property def isHosting(self): return self.server.isConnected @property def isConnected(self): return self.obj or self.server.isConnected def host(self, port): """Starts the network as a server.""" if self.isConnected: raise RuntimeError("Already connected.") assert (self.username) result = self.server._listen(port) if result: self.port = port return result def join(self, hostname, port): """Starts the network as a client.""" if self.isConnected: raise RuntimeError("Already connected.") self.obj = statefulSocket(name="C-OBJ", hostname=hostname, port=port) self.hostname = hostname self.port = port self.obj.connected.connect(self._socketConnected) self.obj.disconnected.connect(self._socketDisconnected) self.obj.objectReceived.connect(self._socketObject) self.obj.commandReceived.connect(self._socketCommand) assert (self.username) def close(self): """Closes the server as well as this client.""" super(JsonClient, self).close() if self.server.isConnected: self.server.close() def _openXfer(self): """Open the transfer socket.""" BaseClient._openXfer( self, statefulSocket(name="C-XFR", hostname=self.hostname, port=self.port)) def send(self, data): """Call to send an object over the wire.""" if self.ready: self.obj.sendObject(data) else: self.server.receive(self.username, data) def requestFile(self, filename): if self.ready: return BaseClient.requestFile(self, filename) else: return False def receive(self, obj): """Make the client receive some data.""" self.objectReceived.emit(self, obj) def _updateSendReceive(self): """Determines the order of updates to avoid deadlock.""" self.fileEvent.emit(self, "", "CHECKING SEND/RECEIVE") # Even if there's a transfer waiting, prioritize sending stuff first self._updateSend() # This check ensures the client priortizes sending over receiving if self.sentfile: self.fileEvent.emit(self, "", "SENT FILE") return self._updateReceive() # SIGNALS connected = signal(object, BASE_STRING, doc="""Called when the client is ready to start sending; when isConnected becomes True. Never called when hosting; can send immediately in that case. client -- this client username -- the username the server is using """) disconnected = signal( object, BASE_STRING, doc="""Called when the client disconnects or fails to connect. Not called when disconnected manually (through close()). Never called when hosting; you will never be disconnected automatically. client -- this client errorMessage -- the untranslated reason the connection failed """) transferDisconnected = signal( object, BASE_STRING, doc="""Called when the transfer socket disconnects or fails to connect. Not called when disconnected manually (through close()). Never called when hosting; you will never be disconnected automatically. client -- this client errorMessage -- the untranslated reason the connection failed """) objectReceived = signal( object, dict, doc="""Called when an object is received over the wire. client -- this client data -- the data received """) fileReceived = signal(object, BASE_STRING, doc="""Called when a file is received over the wire. client -- this client filename -- the filename of the file received """) fileFailed = signal(object, BASE_STRING, doc="""Called when a file fails to come over the wire. client -- this client filename -- the filename of the file received """) def setPassword(self, new): self.password = new # SIGNAL RESPONSES def _socketConnected(self, socket): """Called when a socket is connected.""" if socket == self.obj: self.obj.sendMessage(MESSAGE_IDENTIFY, protocol=PROTOCOL_OBJECT, username=self.username, passw=self.password, networkVersion=NETWORK_VERSION) elif socket == self.xfer: self.xfer.sendMessage(MESSAGE_IDENTIFY, protocol=PROTOCOL_TRANSFER, username=self.username) def _socketDisconnected(self, socket, errorMessage): """Called when a socket disconnects.""" if socket == self.obj: self.disconnected.emit(self, errorMessage) if socket == self.xfer: self.transferDisconnected.emit(self, errorMessage) super(JsonClient, self)._socketDisconnected(socket, errorMessage) def _activateSocket(self, socket, username): """Activates the socket.""" if socket == self.xfer: socket.activate() self._updatetransfer() elif socket == self.obj: socket.activate() self.connected.emit(self, username) self._updatetransfer() super(JsonClient, self)._activateSocket(socket, username) def _fileReceived(self, socket, filename): """Emit and look for more stuff to send.""" if socket == self.xfer: self.fileReceived.emit(self, filename) super(JsonClient, self)._fileReceived(socket, filename) def _fileFailed(self, filename): """Notify that the socket did not send the specified file.""" self.fileFailed.emit(self, filename)
class ICChatWidget(QDockWidget): def __init__(self, mainWindow): super(QDockWidget, self).__init__(mainWindow) self.setToolTip(self.tr("A widget for in-character chat.")) self.setWindowTitle(self.tr("IC Chat")) self.widgetEditor = QTextBrowser(mainWindow) self.widgetLineInput = chatLineEdit(mainWindow) self.widgetLineInput.setToolTip( self.tr( "Type text here and press Enter or Return to transmit it.")) self.widget = QWidget(mainWindow) self.widgetEditor.setReadOnly(True) self.widgetEditor.setOpenLinks(False) self.characterPreview = QLabel(mainWindow) self.characterSelector = QComboBox(mainWindow) self.characterSelector.setToolTip( self. tr("Select the character to be displayed as the speaker of entered text." )) self.characterAddButton = QPushButton(self.tr("Add New"), mainWindow) self.characterAddButton.setToolTip( self.tr("Add a new in-character chat character via a dialog box.")) self.characterDeleteButton = QPushButton(self.tr("Delete"), mainWindow) self.characterDeleteButton.setToolTip( self.tr( "Delete the currently selected in-character chat character.")) self.characterClearButton = QPushButton(self.tr("Clear"), mainWindow) self.characterClearButton.setToolTip( self.tr("Deletes all in-character chat characters.")) self.layout = QGridLayout() self.layout.addWidget(self.widgetEditor, 0, 0, 1, 4) self.layout.addWidget(self.widgetLineInput, 1, 1, 1, 3) self.layout.addWidget(self.characterPreview, 1, 0, 2, 1) self.layout.addWidget(self.characterDeleteButton, 2, 3, 1, 1) self.layout.addWidget(self.characterClearButton, 3, 3, 1, 1) self.layout.addWidget(self.characterAddButton, 2, 2, 1, 1) self.layout.addWidget(self.characterSelector, 2, 1, 1, 1) self.widget.setLayout(self.layout) self.setWidget(self.widget) self.setObjectName("IC Chat Widget") self.messageCache = [] self.setAcceptDrops(True) mainWindow.addDockWidget(Qt.LeftDockWidgetArea, self) #TODO: Store and access characters in a better fashion. try: self.load(jsonload(ospath.join(CHAR_DIR, "autosave.rgc"))) except: self.characters = [] self.widgetLineInput.returnPressed.connect(self.processInput) self.characterAddButton.clicked.connect(self.newCharacter) self.characterDeleteButton.clicked.connect(self.deleteCharacter) self.characterClearButton.clicked.connect(self.clearCharacters) self.characterSelector.currentIndexChanged.connect( self.setCharacterPreview) self.updateDeleteButton() self.setCharacterPreview() def toggleDarkBackgroundSupport(self, dark): if dark: self.widgetEditor.document().setDefaultStyleSheet( "a {color: cyan; }") else: self.widgetEditor.document().setDefaultStyleSheet( "a {color: blue; }") self.refreshMessages() def refreshMessages(self): '''Clear the text display and re-add all messages with current style settings etc.''' self.widgetEditor.clear() for message in self.messageCache: self.widgetEditor.append(message) def updateDeleteButton(self): self.characterDeleteButton.setEnabled(self.hasCharacters()) self.characterClearButton.setEnabled(self.hasCharacters()) self.characterSelector.setEnabled(self.hasCharacters()) self.widgetLineInput.setEnabled(self.hasCharacters()) def setCharacterPreview(self, newIndex=-1): try: preview = QPixmap( ospath.join( UNICODE_STRING(PORTRAIT_DIR), UNICODE_STRING(self.characters[ self.characterSelector.currentIndex()].portrait))) if preview.isNull( ): #Sadly, we have to check ahead, because Qt is dumb and prints an error about the scaling instead of raising one we can catch. raise TypeError preview = preview.scaled(min(preview.width(), 64), min(preview.height(), 64)) self.characterPreview.setPixmap(preview) except: self.characterPreview.clear() def insertMessage(self, mes): self.scroll = (self.widgetEditor.verticalScrollBar().value() == self.widgetEditor.verticalScrollBar().maximum()) self.messageCache.append(mes) self.widgetEditor.append(mes) if self.scroll: self.widgetEditor.verticalScrollBar().setValue( self.widgetEditor.verticalScrollBar().maximum()) try: try: self.logfile = open( ospath.join(LOG_DIR, strftime("%b_%d_%Y.log", localtime())), 'a') self.logfile.write(mes + "\n") finally: self.logfile.close() except: pass def dragEnterEvent(self, event): if event.mimeData().hasImage(): event.acceptProposedAction() def dropEvent(self, event): if event.mimeData().hasImage(): dat = event.mimeData().imageData() img = QImage(dat) filename = promptSaveFile('Save Portrait', 'Portrait files (*.png)', PORTRAIT_DIR) if filename is not None: img.save(filename, "PNG") event.acceptProposedAction() def newCharacter(self): dialog = newCharacterDialog() def accept(): valid = dialog.is_valid() if not valid: showErrorMessage(dialog.error) return valid if dialog.exec_(self.parentWidget(), accept): newchardat = dialog.save() newchar = ICChar(*newchardat) self.characterSelector.addItem(newchar.id) self.characters.append(newchar) jsondump(self.dump(), ospath.join(CHAR_DIR, "autosave.rgc")) self.characterSelector.setCurrentIndex( self.characterSelector.count() - 1) self.updateDeleteButton() self.setCharacterPreview() def _newChar(self, char): self.characterSelector.addItem(char.id) self.characters.append(char) jsondump(self.dump(), ospath.join(CHAR_DIR, "autosave.rgc")) def deleteCharacter(self): if self.hasCharacters(): self.characters.pop(self.characterSelector.currentIndex()) self.characterSelector.removeItem( self.characterSelector.currentIndex()) jsondump(self.dump(), ospath.join(CHAR_DIR, "autosave.rgc")) self.updateDeleteButton() def clearCharacters(self): if promptYesNo('Really clear all characters?') == 16384: self.characters = [] self.characterSelector.clear() jsondump(self.dump(), ospath.join(CHAR_DIR, "autosave.rgc")) self.updateDeleteButton() self.setCharacterPreview() def processTags(self, message): message = message.replace("<", "<").replace(">", ">") for validTag in ("i", "b", "u", "s"): message = message.replace("".join(("[", validTag, "]")), "".join( ("<", validTag, ">"))) message = message.replace("".join(("[", "/", validTag, "]")), "".join(("<", "/", validTag, ">"))) return message def processInput(self): self.newmes = UNICODE_STRING(self.widgetLineInput.text()) self.newmes = self.processTags(self.newmes) self.widgetLineInput.clear() self.widgetLineInput.addMessage(self.newmes) self.ICChatInput.emit( self.newmes, UNICODE_STRING( self.characters[self.characterSelector.currentIndex()].name), UNICODE_STRING(self.characters[ self.characterSelector.currentIndex()].portrait)) def hasCharacters(self): return len(self.characters) > 0 def dump(self): """Serialize to an object valid for JSON dumping.""" return dict(chars=dict([(i, char.dump()) for i, char in enumerate(self.characters)])) def load(self, obj): """Deserialize set of IC characters from a dictionary.""" self.characters = [] self.characterSelector.clear() chars = loadObject('ICChatWidget.chars', obj.get('chars')) chartemp = [None] * len(list(chars.keys())) for ID, char in list(chars.items()): chartemp[int(ID)] = char for char in chartemp: loaded = ICChar.load(char) self._newChar(loaded) self.updateDeleteButton() self.setCharacterPreview() ICChatInput = signal( BASE_STRING, BASE_STRING, BASE_STRING, doc="""Called when in-character chat input is received. charname -- the character name currently selected text -- the message entered portrait -- the portrait path, relative to data/portraits """)
class GLWidget(QGLWidget): ''' Widget for drawing everything, and for catching mouse presses and similar ''' mousePressSignal = pyqtSignal(int, int, int) #x, y, button mouseReleaseSignal = pyqtSignal(int, int, int) #x, y, button mouseMoveSignal = pyqtSignal(int, int) #x, y keyPressSignal = pyqtSignal(int) #key keyReleaseSignal = pyqtSignal(int) #key def __init__(self, parent): QGLWidget.__init__(self, parent) self.setMinimumSize(320, 240) self.w = 640 self.h = 480 self.images = dict() self.allimgs = [] self.lastMousePos = [0, 0] self.camera = [0, 0] self.layers = [] self.zoom = 1 self.offset = 0 self.ctrl = False self.shift = False self.qimages = {} self.texext = GL_TEXTURE_2D self.lines = dict() self.previewLines = dict() self.selectionCircles = dict() self.rectangles = {1: []} self.error = False self.texts = [] self.textid = 0 self.logoon = "Off" self.setAcceptDrops(True) #settings, as used in SAVE_DIR/gfx_settings.rgs self.npot = 3 self.anifilt = 0 self.magfilter = GL_NEAREST self.mipminfilter = GL_NEAREST_MIPMAP_NEAREST self.minfilter = GL_NEAREST self.setFocusPolicy(Qt.StrongFocus) self.setMouseTracking( True) #this may be the fix for a weird problem with leaveevents #GL functions def paintGL(self): ''' Drawing routine ''' glClear(GL_COLOR_BUFFER_BIT) glPushMatrix() glTranslatef(self.camera[0], self.camera[1], 0) glScaled(self.zoom, self.zoom, 1) glColor4f(1.0, 1.0, 1.0, 1.0) for layer in self.layers: for img in self.images[layer]: self.drawImage(img) glDisable(self.texext) for layer in self.lines: glLineWidth(layer) glBegin(GL_LINES) for line in self.lines[layer]: glColor3f(line[4], line[5], line[6]) glVertex2f(line[0], line[1]) glVertex2f(line[2], line[3]) glEnd() for layer in self.previewLines: glLineWidth(layer) glBegin(GL_LINES) for line in self.previewLines[layer]: glColor3f(line[4], line[5], line[6]) glVertex2f(line[0], line[1]) glVertex2f(line[2], line[3]) glEnd() if -1 in self.selectionCircles: glLineWidth(3) glColor3f(0.0, 1.0, 0.0) for circle in self.selectionCircles[-1]: glBegin(GL_LINE_LOOP) for r in range(0, 360, 3): glVertex2f(circle[0] + cos(r * 0.01745329) * circle[2], circle[1] + sin(r * 0.01745329) * circle[2]) glEnd() for rectangle in self.rectangles[1]: glLineWidth(2) glColor3f(rectangle[4], rectangle[5], rectangle[6]) glBegin(GL_LINE_LOOP) glVertex2d(rectangle[0], rectangle[1]) glVertex2d(rectangle[2], rectangle[1]) glVertex2d(rectangle[2], rectangle[3]) glVertex2d(rectangle[0], rectangle[3]) glEnd() glEnable(self.texext) glColor4f(1.0, 1.0, 1.0, 1.0) for text in self.texts: _split = text[1].split("\n") brk = lambda x, n, acc=[]: brk(x[n:], n, acc + [(x[:n])] ) if x else acc split = [] for item in _split: split.extend(brk(item, 35)) if len(split[0]) == 0: split.pop(0) pos = -16 * (len(split) - 1) for t in split: if len(t) == 0: continue self.renderText(float(text[2][0]), float(text[2][1]) + pos, 0, t) pos += 16 glPopMatrix() def addSelectionCircle(self, splasifarcity, x, y, radius): if not splasifarcity in self.selectionCircles: self.selectionCircles[splasifarcity] = [] self.selectionCircles[splasifarcity].append( (float(x), float(y), float(radius))) def clearSelectionCircles(self): self.selectionCircles.clear() def addLine(self, thickness, x, y, w, h, r, g, b): if not thickness in self.lines: self.lines[thickness] = [] self.lines[thickness].append((float(x), float(y), float(w), float(h), float(r), float(g), float(b))) def deleteLine(self, thickness, x, y, w, h): for thickness in self.lines: new_list = [] for line in self.lines[thickness]: if not self.pointIntersectRect((line[0], line[1]), (x, y, w, h)) \ and not self.pointIntersectRect((line[2], line[3]), (x, y, w, h)): new_list.append(line) self.lines[thickness] = new_list def addPreviewLine(self, thickness, x, y, w, h, r, g, b): if not thickness in self.previewLines: self.previewLines[thickness] = [] self.previewLines[thickness].append( (float(x), float(y), float(w), float(h), float(r), float(g), float(b))) def clearLines(self): self.lines.clear() def clearPreviewLines(self): self.previewLines.clear() def addRectangle(self, x, y, w, h, r, g, b): self.rectangles[1].append((float(x), float(y), float(w), float(h), float(r), float(g), float(b))) def clearRectangles(self): self.rectangles = {1: []} def pointIntersectRect(self, point, rect): #point: (x, y) #rect: (x, y, w, h) if point[0] < rect[0] or point[0] > rect[0] + rect[2]: return False if point[1] < rect[1] or point[1] > rect[1] + rect[3]: return False return True def resizeGL(self, w, h): ''' Resize the GL window ''' glViewport(0, 0, w, h) glMatrixMode(GL_PROJECTION) glLoadIdentity() glOrtho(0, w, h, 0, -1, 1) glMatrixMode(GL_MODELVIEW) glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST) self.w = w self.h = h if self.logoon == "On": if self.logo[0] and len(self.logo) == 5: self.logo[4].setDrawRect([0, 0, w, h]) #ugly conversion function :( def interpretString(self, string): if string == "GL_NEAREST": return GL_NEAREST if string == "GL_LINEAR": return GL_LINEAR if string == "GL_NEAREST_MIPMAP_NEAREST": return GL_NEAREST_MIPMAP_NEAREST if string == "GL_NEAREST_MIPMAP_LINEAR": return GL_NEAREST_MIPMAP_LINEAR if string == "GL_LINEAR_MIPMAP_NEAREST": return GL_LINEAR_MIPMAP_NEAREST if string == "GL_LINEAR_MIPMAP_LINEAR": return GL_LINEAR_MIPMAP_LINEAR return string def initializeGL(self): ''' Initialize GL ''' self.fieldtemp = [ 1.0, "GL_NEAREST", "GL_NEAREST", "GL_NEAREST_MIPMAP_NEAREST", "On" ] try: js = jsonload(path.join(SAVE_DIR, "gfx_settings.rgs")) self.fieldtemp[0] = loadFloat('gfx.anifilt', js.get('anifilt')) self.fieldtemp[1] = loadString('gfx.minfilter', js.get('minfilter')) self.fieldtemp[2] = loadString('gfx.magfilter', js.get('magfilter')) self.fieldtemp[3] = loadString('gfx.mipminfilter', js.get('mipminfilter')) self.fieldtemp[4] = loadString('gfx.FSAA', js.get('FSAA')) except: #print("no settings detected") pass #mipmap support and NPOT texture support block if not hasGLExtension("GL_ARB_framebuffer_object"): #print("GL_ARB_framebuffer_object not supported, switching to GL_GENERATE_MIPMAP") self.npot = 2 version = glGetString(GL_VERSION) if int(version[0]) == 1 and int( version[2]) < 4: #no opengl 1.4 support #print("GL_GENERATE_MIPMAP not supported, not using mipmapping") self.npot = 1 if not hasGLExtension("GL_ARB_texture_rectangle"): #print("GL_TEXTURE_RECTANGLE_ARB not supported, switching to GL_TEXTURE_2D") self.texext = GL_TEXTURE_2D self.npot = 0 #assorted settings block if hasGLExtension("GL_EXT_texture_filter_anisotropic" ) and self.fieldtemp[0] > 1.0: self.anifilt = self.fieldtemp[0] #print("using " + str(self.fieldtemp[0]) + "x anisotropic texture filtering. max: " + str(glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT))) self.minfilter = self.interpretString(self.fieldtemp[1]) self.magfilter = self.interpretString(self.fieldtemp[2]) self.mipminfilter = self.interpretString(self.fieldtemp[3]) if self.mipminfilter == "Off": self.mipminfilter = -1 if self.format().sampleBuffers() and self.fieldtemp[4] == "On": #print("enabling " + str(self.format().samples()) + "x FSAA") glEnable(GL_MULTISAMPLE) else: pass #print("FSAA not supported and/or disabled") glEnable(self.texext) glEnable(GL_BLEND) glDisable(GL_DEPTH_TEST) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glViewport(0, 0, self.width(), self.height()) glClearColor(0.0, 0.0, 0.0, 0.0) #util functions def createImage(self, qimagepath, layer, textureRect, drawRect, hidden=False, dynamicity=GL_STATIC_DRAW_ARB): ''' Creates an rggTile instance, uploads the correct image to GPU if not in cache, and some other helpful things. ''' #print "requested to create", qimagepath, layer, textureRect, drawRect, hidden layer = int(layer) texture = None found = False if qimagepath in self.qimages: qimg = self.qimages[qimagepath][0] if self.qimages[qimagepath][2] > 0: texture = self.qimages[qimagepath][1] found = True else: qimg = QImage(qimagepath) #print("created", qimagepath) if textureRect[2] == -1: textureRect[2] = qimg.width() if textureRect[3] == -1: textureRect[3] = qimg.height() if drawRect[2] == -1: drawRect[2] = qimg.width() if drawRect[3] == -1: drawRect[3] = qimg.height() image = tile(qimagepath, textureRect, drawRect, layer, hidden, dynamicity, self) if found == False: img = None if self.npot == 0: w = nextPowerOfTwo(qimg.width()) h = nextPowerOfTwo(qimg.height()) if w != qimg.width() or h != qimg.height(): img = self.convertToGLFormat(qimg.scaled(w, h)) else: img = self.convertToGLFormat(qimg) else: img = self.convertToGLFormat(qimg) texture = int(glGenTextures(1)) try: imgdata = img.bits().asstring(img.byteCount()) except Exception as e: print(e) print("requested to create", qimagepath, layer, textureRect, drawRect, hidden) for x in [0, 1, 2, 3]: f_code = _getframe( x ).f_code #really bad hack to get the filename and number print("Doing it wrong in " + f_code.co_filename + ":" + str(f_code.co_firstlineno)) print("Error: " + e) #print("created texture", texture) glBindTexture(self.texext, texture) if self.anifilt > 1.0: glTexParameterf(self.texext, GL_TEXTURE_MAX_ANISOTROPY_EXT, self.anifilt) if self.npot == 3 and self.mipminfilter != -1: glTexParameteri(self.texext, GL_TEXTURE_MIN_FILTER, self.mipminfilter) glTexParameteri(self.texext, GL_TEXTURE_MAG_FILTER, self.magfilter) elif self.npot == 2 and self.mipminfilter != -1: glTexParameteri(self.texext, GL_TEXTURE_MIN_FILTER, self.mipminfilter) glTexParameteri(self.texext, GL_TEXTURE_MAG_FILTER, self.magfilter) glTexParameteri(self.texext, GL_GENERATE_MIPMAP, GL_TRUE) else: glTexParameteri(self.texext, GL_TEXTURE_MIN_FILTER, self.minfilter) glTexParameteri(self.texext, GL_TEXTURE_MAG_FILTER, self.magfilter) glTexParameteri(self.texext, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) glTexParameteri(self.texext, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE) glTexParameteri(self.texext, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) glTexImage2D(self.texext, 0, GL_RGBA, img.width(), img.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, imgdata) if self.npot == 3 and self.mipminfilter != -1: glEnable(GL_TEXTURE_2D) glGenerateMipmap(GL_TEXTURE_2D) self.qimages[qimagepath] = [qimg, texture, 1] #texture, reference count else: self.qimages[qimagepath][2] += 1 image.textureId = texture if layer not in self.images: self.images[layer] = [] self.layers = list(self.images.keys()) self.layers.sort() image.createLayer = True self.images[layer].append(image) self.allimgs.append(image) return image def deleteImage(self, image): ''' Decreases the reference count of the texture by one, and deletes it if nothing is using it anymore ''' self.qimages[image.imagepath][2] -= 1 if self.qimages[image.imagepath][2] <= 0: #print("deleting texture", image.textureId) glDeleteTextures(image.textureId) del self.qimages[image.imagepath] self.images[image.layer].remove(image) self.allimgs.remove(image) image = None def deleteImages(self, imageArray): ''' Decreases the reference count of the texture of each image by one, and deletes it if nothing is using it anymore ''' for image in imageArray: self.qimages[image.imagepath][2] -= 1 if self.qimages[image.imagepath][2] <= 0: #print("deleting texture", image.textureId) glDeleteTextures(image.textureId) del self.qimages[image.imagepath] self.images[image.layer].remove(image) self.allimgs.remove(image) def drawImage(self, image): if image.hidden: return x, y, w, h = image.textureRect dx, dy, dw, dh = image.drawRect r = float(image.rotation) cx, cy = self.camera # Culling if (dx * self.zoom > self.w - cx) or (dy * self.zoom > self.h - cy) or ( (dx + dw) * self.zoom < 0 - cx) or ( (dy + dh) * self.zoom < 0 - cy): return self.drawTexture(image.textureId, dx, dy, dw, dh, x, y, w, h, r) def drawTexture(self, texture, dx, dy, dw, dh, x, y, w, h, r): ''' texture is an int textureRect is a list of size 4, determines which square to take from the texture drawRect is a list of size 4, is used to determine the drawing size ''' glBindTexture(self.texext, texture) glPushMatrix() glTranslatef(dx + dw / 2, dy + dh / 2, 0) glRotatef(r, 0, 0, 1.0) glTranslatef(-1 * (dx + dw / 2), -1 * (dy + dh / 2), 0) glBegin(GL_QUADS) #Top-left vertex (corner) glTexCoord2f(x, y + h) #image/texture glVertex3f(dx, dy, 0) #screen coordinates #Bottom-left vertex (corner) glTexCoord2f(x + w, y + h) glVertex3f((dx + dw), dy, 0) #Bottom-right vertex (corner) glTexCoord2f(x + w, y) glVertex3f((dx + dw), (dy + dh), 0) #Top-right vertex (corner) glTexCoord2f(x, y) glVertex3f(dx, (dy + dh), 0) glEnd() glPopMatrix() def hideImage(self, image, hide): ''' This function should only be called from image.py Use Image.hide() instead. ''' pass def setLayer(self, image, newLayer): ''' This function should only be called from image.py Use Image.layer instead. ''' oldLayer = image._layer image._layer = newLayer if newLayer not in self.images: self.images[newLayer] = [] self.layers = list(self.images.keys()) self.layers.sort() image.createLayer = True self.images[oldLayer].remove(image) self.images[newLayer].append(image) def getImageSize(self, image): qimg = None if image in self.qimages: qimg = self.qimages[image][0] else: qimg = QImage(image) return qimg.size() def addText(self, text, pos): self.texts.append([self.textid, text, pos]) self.textid += 1 return self.textid - 1 def removeText(self, id): for i, t in enumerate(self.texts): if t[0] == id: self.texts.pop(i) return def setTextPos(self, id, pos): for t in self.texts: if t[0] == id: t[2] = pos def mouseMoveEvent(self, mouse): self.mouseMoveSignal.emit(mouse.pos().x(), mouse.pos().y()) mouse.accept() def mousePressEvent(self, mouse): button = 0 if self.ctrl: button += 3 if self.shift: button += 6 if mouse.button() == Qt.LeftButton: button += 0 elif mouse.button() == Qt.RightButton: button += 2 elif mouse.button() == Qt.MidButton: button += 1 self.mousePressSignal.emit(mouse.pos().x(), mouse.pos().y(), button) mouse.accept() def mouseReleaseEvent(self, mouse): button = 0 if self.ctrl: button += 3 if self.shift: button += 6 if mouse.button() == Qt.LeftButton: button += 0 elif mouse.button() == Qt.RightButton: button += 2 elif mouse.button() == Qt.MidButton: button += 1 self.mouseReleaseSignal.emit(mouse.pos().x(), mouse.pos().y(), button) mouse.accept() def keyPressEvent(self, event): if event.key() == Qt.Key_Control: self.ctrl = True elif event.key() == Qt.Key_Shift: self.shift = True elif event.key() == Qt.Key_Plus or event.key() == Qt.Key_Equal: self.zoom += 0.15 if self.zoom > 4: self.zoom = 4 elif event.key() == Qt.Key_Minus: self.zoom -= 0.15 if self.zoom < 0.30: self.zoom = 0.30 elif event.key() == Qt.Key_0: self.zoom = 1 elif event.key() == Qt.Key_Up: self.camera[1] += (50 * self.zoom) elif event.key() == Qt.Key_Down: self.camera[1] -= (50 * self.zoom) elif event.key() == Qt.Key_Left: self.camera[0] += (50 * self.zoom) elif event.key() == Qt.Key_Right: self.camera[0] -= (50 * self.zoom) else: self.keyPressSignal.emit(event.key()) def keyReleaseEvent(self, event): if event.key() == Qt.Key_Control: self.ctrl = False elif event.key() == Qt.Key_Shift: self.shift = False else: self.keyReleaseSignal.emit(event.key()) def wheelEvent(self, mouse): oldCoord = [mouse.pos().x(), mouse.pos().y()] oldCoord[0] *= float(1) / self.zoom oldCoord[1] *= float(1) / self.zoom oldCoord2 = self.camera oldCoord2[0] *= float(1) / self.zoom oldCoord2[1] *= float(1) / self.zoom try: delta = mouse.angleDelta().y( ) #let's not worry about 2-dimensional wheels. except AttributeError: delta = mouse.delta() if delta < 0: self.zoom -= 0.5 elif delta > 0: self.zoom += 0.5 if self.zoom < 0.60: self.zoom = 0.5 elif self.zoom > 4: self.zoom = 4 self.camera[0] = oldCoord2[0] * self.zoom - ( (oldCoord[0] * self.zoom) - mouse.pos().x()) self.camera[1] = oldCoord2[1] * self.zoom - ( (oldCoord[1] * self.zoom) - mouse.pos().y()) mouse.accept() def leaveEvent(self, event): self.ctrl = False self.shift = False def dragEnterEvent(self, event): if event.mimeData().hasImage(): event.acceptProposedAction() elif event.mimeData().hasText(): event.acceptProposedAction() def dropEvent(self, event): if event.mimeData().hasImage(): dat = event.mimeData().imageData() img = QImage(dat) filename = promptSaveFile('Save Pog', 'Pog files (*.png)', POG_DIR) if filename is not None: img.save(filename, "PNG") event.acceptProposedAction() elif event.mimeData().hasText(): self.pogPlace.emit(event.pos().x(), event.pos().y(), str(event.mimeData().text())) pogPlace = signal(int, int, BASE_STRING, doc="""Called to request pog placement on the map.""")
class diceRoller(QDockWidget): def __init__(self, mainWindow): super(QDockWidget, self).__init__(mainWindow) self.setWindowTitle(self.tr("Dice")) self.realwidget = QWidget( mainWindow ) #I messed up on the initial setup and was too lazy to rename everything. self.widget = QGridLayout() self.diceArea = QListWidget(mainWindow) try: self.load(jsonload(ospath.join(SAVE_DIR, "dice.rgd"))) except: self.macros = [ QListWidgetItem(QIcon('data/dice.png'), "Sample: 2d6"), QListWidgetItem(QIcon('data/dice.png'), "Sample: 4k2"), QListWidgetItem(QIcon('data/dice.png'), "Sample: 1dn3") ] for m in self.macros: self.diceArea.addItem(m) self.diceArea.currentRowChanged.connect(self.changeCurrentMacro) self.rollbutton = QPushButton(self.tr("Roll"), mainWindow) self.rollbutton.setToolTip( self.tr("Roll dice according to the selected macro.")) self.addmacrobutton = QPushButton(self.tr("Add Macro"), mainWindow) self.addmacrobutton.setToolTip( self.tr("Add a new macro via a dialog box.")) self.removemacrobutton = QPushButton(self.tr("Delete Macro"), mainWindow) self.removemacrobutton.setToolTip( self.tr("Remove the currently selected macro.")) self.rollbutton.clicked.connect(self.rollDice) self.addmacrobutton.clicked.connect(self.summonMacro) self.removemacrobutton.clicked.connect(self.removeCurrentMacro) self.widget.addWidget(self.diceArea, 0, 0) self.widget.addWidget(self.rollbutton, 1, 0) self.widget.addWidget(self.addmacrobutton, 2, 0) self.widget.addWidget(self.removemacrobutton, 3, 0) self.realwidget.setLayout(self.widget) self.setWidget(self.realwidget) self.setObjectName("Dice Widget") mainWindow.addDockWidget(Qt.BottomDockWidgetArea, self) self.close() self.currentMacro = -1 def changeCurrentMacro(self, n): self.currentMacro = n def rollDice(self): current = self.diceArea.item(self.currentMacro) if current is not None: text = UNICODE_STRING(current.text()) self.rollRequested.emit(text[text.rfind(':') + 1:]) def _addMacro(self, macro): self.macros.append(QListWidgetItem(QIcon('data/dice.png'), macro)) self.diceArea.addItem(self.macros[len(self.macros) - 1]) def addMacro(self, mac, macname): self.macros.append( QListWidgetItem(QIcon('data/dice.png'), macname + ': ' + mac)) self.diceArea.addItem(self.macros[len(self.macros) - 1]) jsondump(self.dump(), ospath.join(SAVE_DIR, "dice.rgd")) def removeCurrentMacro(self): if self.diceArea.item(self.currentMacro) != self.diceArea.currentItem( ): #This SHOULD, probably, only occur if there are two items and the first is deleted. Probably. self.diceArea.takeItem(0) return self.diceArea.takeItem(self.currentMacro) jsondump(self.dump(), ospath.join(SAVE_DIR, "dice.rgd")) def summonMacro(self): self.macroRequested.emit() def load(self, obj): """Deserialize set of macros from a dictionary.""" self.macros = [] macroz = loadObject('diceRoller.macros', obj.get('macros')) for ID, macro in list(macroz.items()): self._addMacro(macro) def dump(self): """Serialize to an object valid for JSON dumping.""" macroz = [] for i in range(0, self.diceArea.count()): macroz.append(UNICODE_STRING(self.diceArea.item(i).text())) return dict(macros=dict([(i, macro) for i, macro in enumerate(macroz)])) rollRequested = signal(BASE_STRING, doc="""Called when the roll button is hit. roll -- the dice to be rolled """) macroRequested = signal( doc="""Called when the add macro button is pressed.""")
class scratchPadWidget(QDockWidget): """A mutually-editable shared text widget.""" def __init__(self, mainWindow): """Initializes the user list.""" super(QDockWidget, self).__init__(mainWindow) self.setWindowTitle(self.tr("Scratch Pad")) self.widget = QWidget(mainWindow) self.textArea = QTextEdit(mainWindow) self.currentEditingLabel = QLabel("No one is editing.") self.getLockButton = QPushButton("Edit") self.releaseLockButton = QPushButton("Confirm") #self.saveToFileButton = QPushButton("Save...") #self.loadButton = QPushButton("Load") self.layout = QGridLayout() self.layout.addWidget(self.currentEditingLabel, 0, 0, 1, 2) self.layout.addWidget(self.textArea, 1, 0, 1, 2) self.layout.addWidget(self.getLockButton, 2, 0) self.layout.addWidget(self.releaseLockButton, 2, 1) self.releaseLockButton.pressed.connect(self._releaseLock) self.getLockButton.pressed.connect(self._getLock) self.widget.setLayout(self.layout) self.setWidget(self.widget) self.setObjectName("Scratch Pad Widget") self.hasLock = False self.updateButtonStatus() try: with open(ospath.join(SAVE_DIR, "scratchpad.txt"), 'r') as autosave: self.textArea.setText(autosave.read()) except: pass mainWindow.addDockWidget(Qt.BottomDockWidgetArea, self) @property def currentText(self): return self.textArea.toPlainText() def updateButtonStatus(self): self.getLockButton.setEnabled(not self.hasLock) self.textArea.setReadOnly(not self.hasLock) self.releaseLockButton.setEnabled(self.hasLock) def _getLock(self): self.getScratchPadLock.emit() def _releaseLock(self): self.updateScratchPad.emit() self.releaseScratchPadLock.emit() def getLock(self): self.hasLock = True self.updateButtonStatus() def releaseLock(self, name): self.hasLock = False if name: self.currentEditingLabel.setText("%s is editing." % name) else: self.currentEditingLabel.setText("No one is editing.") self.updateButtonStatus() def updateText(self, txt): if len(txt) > 100000: print("Excessively long scratchpad data received. Might be an attack or just a bug.") return self.textArea.setText(txt) try: with open(ospath.join(SAVE_DIR, "scratchpad.txt"), 'w') as autosave: autosave.write(txt) except: pass getScratchPadLock = signal(doc= """Called to request lock for scratch pad.""" ) releaseScratchPadLock = signal(doc= """Called to indicate relinquishment of scratch pad lock.""" ) updateScratchPad = signal(doc= """Called to request that the scratchpad be updated.""" )