Cooperation/ChatWidget.py

Sun, 04 Jul 2010 10:24:09 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 04 Jul 2010 10:24:09 +0200
branch
5_0_x
changeset 379
97bf7543c58a
parent 165
3302a726fd1e
child 501
5c615a85241a
child 792
a13346916170
permissions
-rw-r--r--

Prepared first formal release.

# -*- coding: utf-8 -*-

# Copyright (c) 2010 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the chat dialog.
"""

from PyQt4.QtCore import Qt, pyqtSlot, pyqtSignal, QDateTime, QPoint, QFileInfo
from PyQt4.QtGui import QWidget, QColor, QListWidgetItem, QMenu, QFileDialog, \
    QMessageBox, QApplication

from E5Gui.E5Application import e5App

from Globals import recentNameHosts

from QScintilla.Editor import Editor

from .CooperationClient import CooperationClient

from .Ui_ChatWidget import Ui_ChatWidget

import Preferences
import Utilities
import UI.PixmapCache

class ChatWidget(QWidget, Ui_ChatWidget):
    """
    Class implementing the chat dialog.
    
    @signal connected(connected) emitted to signal a change of the connected
            state (boole)
    @signal editorCommand(hash, filename, message) emitted when an editor command
            has been received (string, string, string)
    @signal shareEditor(share) emitted to signal a share is requested (bool)
    @signal startEdit() emitted to start a shared edit session
    @signal sendEdit() emitted to send a shared edit session
    @signal cancelEdit() emitted to cancel a shared edit session
    """
    connected = pyqtSignal(bool)
    editorCommand = pyqtSignal(str, str, str)
    
    shareEditor = pyqtSignal(bool)
    startEdit = pyqtSignal()
    sendEdit = pyqtSignal()
    cancelEdit = pyqtSignal()
    
    def __init__(self, port = -1, parent = None):
        """
        Constructor
        
        @param port port to be used for the cooperation server (integer)
        @param parent reference to the parent widget (QWidget)
        """
        QWidget.__init__(self, parent)
        self.setupUi(self)
        
        self.shareButton.setIcon(
            UI.PixmapCache.getIcon("sharedEditDisconnected.png"))
        self.startEditButton.setIcon(
            UI.PixmapCache.getIcon("sharedEditStart.png"))
        self.sendEditButton.setIcon(
            UI.PixmapCache.getIcon("sharedEditSend.png"))
        self.cancelEditButton.setIcon(
            UI.PixmapCache.getIcon("sharedEditCancel.png"))
        self.clearMessageButton.setIcon(
            UI.PixmapCache.getIcon("clearLeft.png"))
        self.clearHostButton.setIcon(
            UI.PixmapCache.getIcon("clearLeft.png"))
        
        self.__client = CooperationClient(self)
        self.__myNickName = self.__client.nickName()
        
        self.__initChatMenu()
        self.__initUsersMenu()
        
        self.messageEdit.returnPressed.connect(self.__handleMessage)
        self.sendButton.clicked.connect(self.__handleMessage)
        self.__client.newMessage.connect(self.appendMessage)
        self.__client.newParticipant.connect(self.__newParticipant)
        self.__client.participantLeft.connect(self.__participantLeft)
        self.__client.connectionError.connect(self.__showErrorMessage)
        self.__client.cannotConnect.connect(self.__initialConnectionRefused)
        self.__client.editorCommand.connect(self.__editorCommandMessage)
        
        self.serverButton.setText(self.trUtf8("Start Server"))
        self.serverLed.setColor(QColor(Qt.red))
        if port == -1:
            port = Preferences.getCooperation("ServerPort")
        
        self.serverPortSpin.setValue(port)
        
        self.__setConnected(False)
        
        if Preferences.getCooperation("AutoStartServer"):
            self.on_serverButton_clicked()
        
        self.recent = []
        self.__loadHostsHistory()
    
    def __loadHostsHistory(self):
        """
        Private method to load the recently connected hosts.
        """
        self.__recent = []
        Preferences.Prefs.rsettings.sync()
        rh = Preferences.Prefs.rsettings.value(recentNameHosts)
        if rh is not None:
            self.__recent = rh[:20]
            self.hostEdit.clear()
            self.hostEdit.addItems(self.__recent)
            self.hostEdit.clearEditText()
    
    def __saveHostsHistory(self):
        """
        Private method to save the list of recently connected hosts.
        """
        Preferences.Prefs.rsettings.setValue(recentNameHosts, self.__recent)
        Preferences.Prefs.rsettings.sync()
    
    def __setHostsHistory(self, host):
        """
        Private method to remember the given host as the most recent entry.
        
        @param host host entry to remember (string)
        """
        if host in self.__recent:
            self.__recent.remove(host)
        self.__recent.insert(0, host)
        self.__saveHostsHistory()
        self.hostEdit.clear()
        self.hostEdit.addItems(self.__recent)
    
    def __clearHostsHistory(self):
        """
        Private slot to clear the hosts history.
        """
        self.__recent = []
        self.__saveHostsHistory()
        self.hostEdit.clear()
        self.hostEdit.addItems(self.__recent)
    
    def __handleMessage(self):
        """
        Private slot handling the Return key pressed in the message edit.
        """
        text = self.messageEdit.text()
        if text == "":
            return
        
        if text.startswith("/"):
            self.__showErrorMessage(
                self.trUtf8("! Unknown command: {0}\n").format(text.split()[0]))
        else:
            self.__client.sendMessage(text)
            self.appendMessage(self.__myNickName, text)
        
        self.messageEdit.clear()
    
    def __newParticipant(self, nick):
        """
        Private slot handling a new participant joining.
        
        @param nick nick name of the new participant (string)
        """
        if nick == "":
            return
        
        color = self.chatEdit.textColor()
        self.chatEdit.setTextColor(Qt.gray)
        self.chatEdit.append(
            QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) + ":")
        self.chatEdit.append(self.trUtf8("* {0} has joined.\n").format(nick))
        self.chatEdit.setTextColor(color)
        
        QListWidgetItem(
            UI.PixmapCache.getIcon(
                "chatUser{0}.png".format(1 + self.usersList.count() % 6)), 
            nick, self.usersList)
        
        if not self.__connected:
            self.__setConnected(True)
    
    def __participantLeft(self, nick):
        """
        Private slot handling a participant leaving the session.
        
        @param nick nick name of the participant (string)
        """
        if nick == "":
            return
        
        items = self.usersList.findItems(nick, Qt.MatchExactly)
        for item in items:
            self.usersList.takeItem(self.usersList.row(item))
            del item
            
            color = self.chatEdit.textColor()
            self.chatEdit.setTextColor(Qt.gray)
            self.chatEdit.append(
                QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) + ":")
            self.chatEdit.append(self.trUtf8("* {0} has left.\n").format(nick))
            self.chatEdit.setTextColor(color)
        
        if not self.__client.hasConnections():
            self.__setConnected(False)
    
    def appendMessage(self, from_, message):
        """
        Public slot to append a message to the display.
        
        @param from_ originator of the message (string)
        @param message message to be appended (string)
        """
        if from_ == "" or message == "":
            return
        
        self.chatEdit.append(
            QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) + \
            " <" + from_ + ">:")
        self.chatEdit.append(message + "\n")
        bar = self.chatEdit.verticalScrollBar()
        bar.setValue(bar.maximum())
    
    @pyqtSlot(str)
    def on_hostEdit_editTextChanged(self, host):
        """
        Private slot handling the entry of a host to connect to.
        
        @param host host to connect to (string)
        """
        if not self.__connected:
            self.connectButton.setEnabled(host != "")
    
    def __getConnectionParameters(self):
        """
        Private method to determine the connection parameters.
        
        @return tuple with hostname and port (string, integer)
        """
        hostEntry = self.hostEdit.currentText()
        if ":" in hostEntry:
            host, port = hostEntry.split(":")
            try:
                port = int(port)
            except ValueError:
                port = Preferences.getCooperation("ServerPort")
                self.hostEdit.setEditText("{0}:{1}".format(host, port))
        else:
            host = hostEntry
            port = Preferences.getCooperation("ServerPort")
            self.hostEdit.setEditText("{0}:{1}".format(host, port))
        return host, port
    
    @pyqtSlot()
    def on_connectButton_clicked(self):
        """
        Private slot initiating the connection.
        """
        if not self.__connected:
            host, port = self.__getConnectionParameters()
            self.__setHostsHistory(self.hostEdit.currentText())
            if not self.__client.server().isListening():
                self.on_serverButton_clicked()
            if self.__client.server().isListening():
                self.__client.connectToHost(host, port)
                self.__setConnected(True)
        else:
            self.__client.disconnectConnections()
            self.__setConnected(False)
    
    @pyqtSlot()
    def on_clearHostsButton_clicked(self):
        """
        Private slot to clear the hosts list.
        """
        self.__clearHostsHistory()
    
    @pyqtSlot()
    def on_serverButton_clicked(self):
        """
        Private slot to start the server.
        """
        if self.__client.server().isListening():
            self.__client.server().close()
            self.serverButton.setText(self.trUtf8("Start Server"))
            self.serverPortSpin.setEnabled(True)
            if self.serverPortSpin.value() != Preferences.getCooperation("ServerPort"):
                self.serverPortSpin.setValue(Preferences.getCooperation("ServerPort"))
            self.serverLed.setColor(QColor(Qt.red))
        else:
            res, port = self.__client.server().startListening(self.serverPortSpin.value())
            if res:
                self.serverButton.setText(self.trUtf8("Stop Server"))
                self.serverPortSpin.setValue(port)
                self.serverPortSpin.setEnabled(False)
                self.serverLed.setColor(QColor(Qt.green))
            else:
                self.__showErrorMessage(
                    self.trUtf8("! Server Error: {0}\n").format(
                    self.__client.server().errorString())
                )
    
    def __setConnected(self, connected):
        """
        Private slot to set the connected state.
        
        @param connected new connected state (boolean)
        """
        if connected:
            self.connectButton.setText(self.trUtf8("Disconnect"))
            self.connectButton.setEnabled(True)
            self.connectionLed.setColor(QColor(Qt.green))
        else:
            self.connectButton.setText(self.trUtf8("Connect"))
            self.connectButton.setEnabled(self.hostEdit.currentText() != "")
            self.connectionLed.setColor(QColor(Qt.red))
            self.on_cancelEditButton_clicked()
            self.shareButton.setChecked(False)
            self.on_shareButton_clicked(False)
        self.__connected = connected
        self.hostEdit.setEnabled(not connected)
        self.serverButton.setEnabled(not connected)
        self.sharingGroup.setEnabled(connected)
        
        if connected:
            vm = e5App().getObject("ViewManager")
            aw = vm.activeWindow()
            if aw:
                self.checkEditorActions(aw)
    
    def __showErrorMessage(self, message):
        """
        Private slot to show an error message.
        
        @param message error message to show (string)
        """
        color = self.chatEdit.textColor()
        self.chatEdit.setTextColor(Qt.red)
        self.chatEdit.append(
            QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) + ":")
        self.chatEdit.append(message + "\n")
        self.chatEdit.setTextColor(color)
    
    def __initialConnectionRefused(self):
        """
        Private slot to handle the refusal of the initial connection.
        """
        self.__setConnected(False)
    
    def preferencesChanged(self):
        """
        Public slot to handle a change of preferences.
        """
        if not self.__client.server().isListening():
            self.serverPortSpin.setValue(Preferences.getCooperation("ServerPort"))
            if Preferences.getCooperation("AutoStartServer"):
                self.on_serverButton_clicked()
    
    def getClient(self):
        """
        Public method to get a reference to the cooperation client.
        """
        return self.__client
    
    def __editorCommandMessage(self, hash, fileName, message):
        """
        Private slot to handle editor command messages from the client.
        
        @param hash hash of the project (string)
        @param fileName project relative file name of the editor (string)
        @param message command message (string)
        """
        self.editorCommand.emit(hash, fileName, message)
        
        if message.startswith(Editor.StartEditToken + Editor.Separator) or \
           message.startswith(Editor.EndEditToken + Editor.Separator):
            vm = e5App().getObject("ViewManager")
            aw = vm.activeWindow()
            if aw:
                self.checkEditorActions(aw)
    
    @pyqtSlot(bool)
    def on_shareButton_clicked(self, checked):
        """
        Private slot to share the current editor.
        
        @param checked flag indicating the button state (boolean)
        """
        if checked:
            self.shareButton.setIcon(
                UI.PixmapCache.getIcon("sharedEditConnected.png"))
        else:
            self.shareButton.setIcon(
                UI.PixmapCache.getIcon("sharedEditDisconnected.png"))
        self.startEditButton.setEnabled(checked)
        
        self.shareEditor.emit(checked)
    
    @pyqtSlot(bool)
    def on_startEditButton_clicked(self, checked):
        """
        Private slot to start a shared edit session.
        
        @param checked flag indicating the button state (boolean)
        """
        if checked:
            self.sendEditButton.setEnabled(True)
            self.cancelEditButton.setEnabled(True)
            self.shareButton.setEnabled(False)
            self.startEditButton.setEnabled(False)
            
            self.startEdit.emit()
    
    @pyqtSlot()
    def on_sendEditButton_clicked(self):
        """
        Private slot to end a shared edit session and send the changes.
        """
        self.sendEditButton.setEnabled(False)
        self.cancelEditButton.setEnabled(False)
        self.shareButton.setEnabled(True)
        self.startEditButton.setEnabled(True)
        self.startEditButton.setChecked(False)
        
        self.sendEdit.emit()
    
    @pyqtSlot()
    def on_cancelEditButton_clicked(self):
        """
        Private slot to cancel a shared edit session.
        """
        self.sendEditButton.setEnabled(False)
        self.cancelEditButton.setEnabled(False)
        self.shareButton.setEnabled(True)
        self.startEditButton.setEnabled(True)
        self.startEditButton.setChecked(False)
        
        self.cancelEdit.emit()
    
    def checkEditorActions(self, editor):
        """
        Public slot to set action according to an editor's state.
        
        @param editor reference to the editor (Editor)
        """
        shareable, sharing, editing, remoteEditing = editor.getSharingStatus()
        
        self.shareButton.setChecked(sharing)
        if sharing:
            self.shareButton.setIcon(
                UI.PixmapCache.getIcon("sharedEditConnected.png"))
        else:
            self.shareButton.setIcon(
                UI.PixmapCache.getIcon("sharedEditDisconnected.png"))
        self.startEditButton.setChecked(editing)
        
        self.shareButton.setEnabled(shareable and not editing)
        self.startEditButton.setEnabled(sharing and not editing and not remoteEditing)
        self.sendEditButton.setEnabled(editing)
        self.cancelEditButton.setEnabled(editing)
    
    def __initChatMenu(self):
        """
        Private slot to initialize the chat edit context menu.
        """
        self.__chatMenu = QMenu(self)
        self.__cutChatAct = \
            self.__chatMenu.addAction(
                UI.PixmapCache.getIcon("editCut.png"), 
                self.trUtf8("Cut"), self.__cutChat)
        self.__copyChatAct = \
            self.__chatMenu.addAction(
                UI.PixmapCache.getIcon("editCopy.png"), 
                self.trUtf8("Copy"), self.__copyChat)
        self.__chatMenu.addSeparator()
        self.__cutAllChatAct = \
            self.__chatMenu.addAction(
                UI.PixmapCache.getIcon("editCut.png"), 
                self.trUtf8("Cut all"), self.__cutAllChat)
        self.__copyAllChatAct = \
            self.__chatMenu.addAction(
                UI.PixmapCache.getIcon("editCopy.png"), 
                self.trUtf8("Copy all"), self.__copyAllChat)
        self.__chatMenu.addSeparator()
        self.__clearChatAct = \
            self.__chatMenu.addAction(
                UI.PixmapCache.getIcon("editDelete.png"), 
                self.trUtf8("Clear"), self.__clearChat)
        self.__chatMenu.addSeparator()
        self.__saveChatAct = \
            self.__chatMenu.addAction(
                UI.PixmapCache.getIcon("fileSave.png"), 
                self.trUtf8("Save"), self.__saveChat)
    
    @pyqtSlot(bool)
    def on_chatEdit_copyAvailable(self, yes):
        """
        Private slot to react to text selection/deselection of the chat edit.
        
        @param yes flag signaling the availability of selected text (boolean)
        """
        self.__copyChatAct.setEnabled(yes)
        self.__cutChatAct.setEnabled(yes)
    
    @pyqtSlot(QPoint)
    def on_chatEdit_customContextMenuRequested(self, pos):
        """
        Private slot to show the context menu for the chat.
        
        @param pos the position of the mouse pointer (QPoint)
        """
        enable = self.chatEdit.toPlainText() != ""
        self.__saveChatAct.setEnabled(enable)
        self.__copyAllChatAct.setEnabled(enable)
        self.__cutAllChatAct.setEnabled(enable)
        self.__chatMenu.popup(self.chatEdit.mapToGlobal(pos))
    
    def __clearChat(self):
        """
        Private slot to clear the contents of the chat display.
        """
        self.chatEdit.clear()
    
    def __saveChat(self):
        """
        Private slot to save the contents of the chat display.
        """
        txt = self.chatEdit.toPlainText()
        if txt:
            fname, selectedFilter = QFileDialog.getSaveFileNameAndFilter(\
                self,
                self.trUtf8("Save Chat"),
                "",
                self.trUtf8("Text Files (*.txt);;All Files (*)"),
                None,
                QFileDialog.Options(QFileDialog.DontConfirmOverwrite))
            if fname:
                ext = QFileInfo(fname).suffix()
                if not ext:
                    ex = selectedFilter.split("(*")[1].split(")")[0]
                    if ex:
                        fname += ex
                if QFileInfo(fname).exists():
                    res = QMessageBox.warning(self,
                        self.trUtf8("Save Chat"),
                        self.trUtf8("<p>The file <b>{0}</b> already exists.</p>")
                            .format(fname),
                        QMessageBox.StandardButtons(\
                            QMessageBox.Abort | \
                            QMessageBox.Save),
                        QMessageBox.Abort)
                    if res != QMessageBox.Save:
                        return
                    fname = Utilities.toNativeSeparators(fname)
                
                try:
                    f = open(fname, "w", encoding = "utf-8")
                    f.write(txt)
                    f.close()
                except IOError as err:
                    QMessageBox.critical(self,
                        self.trUtf8("Error saving Chat"),
                        self.trUtf8("""<p>The chat contents could not be written"""
                                    """ to <b>{0}</b></p><p>Reason: {1}</p>""")\
                            .format(fname, str(err)))
    
    def __copyChat(self):
        """
        Private slot to copy the contents of the chat display to the clipboard.
        """
        self.chatEdit.copy()
    
    def __cutChat(self):
        """
        Private slot to cut the contents of the chat display to the clipboard.
        """
        self.chatEdit.cut()
    
    def __copyAllChat(self):
        """
        Private slot to copy the contents of the chat display to the clipboard.
        """
        txt = self.chatEdit.toPlainText()
        if txt:
            cb = QApplication.clipboard()
            cb.setText(txt)
    
    def __cutAllChat(self):
        """
        Private slot to cut the contents of the chat display to the clipboard.
        """
        txt = self.chatEdit.toPlainText()
        if txt:
            cb = QApplication.clipboard()
            cb.setText(txt)
        self.chatEdit.clear()
    
    def __initUsersMenu(self):
        """
        Private slot to initialize the users list context menu.
        """
        self.__usersMenu = QMenu(self)
        self.__kickUserAct = \
            self.__usersMenu.addAction(
                UI.PixmapCache.getIcon("chatKickUser.png"), 
                self.trUtf8("Kick User"), self.__kickUser)
        self.__banUserAct = \
            self.__usersMenu.addAction(
                UI.PixmapCache.getIcon("chatBanUser.png"), 
                self.trUtf8("Ban User"), self.__banUser)
        self.__banKickUserAct = \
            self.__usersMenu.addAction(
                UI.PixmapCache.getIcon("chatBanKickUser.png"), 
                self.trUtf8("Ban and Kick User"), self.__banKickUser)
    
    @pyqtSlot(QPoint)
    def on_usersList_customContextMenuRequested(self, pos):
        """
        Private slot to show the context menu for the users list.
        
        @param pos the position of the mouse pointer (QPoint)
        """
        itm = self.usersList.itemAt(pos)
        self.__kickUserAct.setEnabled(itm is not None)
        self.__banUserAct.setEnabled(itm is not None)
        self.__banKickUserAct.setEnabled(itm is not None)
        self.__usersMenu.popup(self.usersList.mapToGlobal(pos))
    
    def __kickUser(self):
        """
        Private slot to disconnect a user.
        """
        itm = self.usersList.currentItem()
        self.__client.kickUser(itm.text())
        
        color = self.chatEdit.textColor()
        self.chatEdit.setTextColor(Qt.darkYellow)
        self.chatEdit.append(
            QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) + ":")
        self.chatEdit.append(self.trUtf8("* {0} has been kicked.\n").format(
            itm.text().split(":")[0]))
        self.chatEdit.setTextColor(color)
    
    def __banUser(self):
        """
        Private slot to ban a user.
        """
        itm = self.usersList.currentItem()
        self.__client.banUser(itm.text())
        
        color = self.chatEdit.textColor()
        self.chatEdit.setTextColor(Qt.darkYellow)
        self.chatEdit.append(
            QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) + ":")
        self.chatEdit.append(self.trUtf8("* {0} has been banned.\n").format(
            itm.text().split(":")[0]))
        self.chatEdit.setTextColor(color)
    
    def __banKickUser(self):
        """
        Private slot to ban and kick a user.
        """
        itm = self.usersList.currentItem()
        self.__client.banKickUser(itm.text())
        
        color = self.chatEdit.textColor()
        self.chatEdit.setTextColor(Qt.darkYellow)
        self.chatEdit.append(
            QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) + ":")
        self.chatEdit.append(self.trUtf8("* {0} has been banned and kicked.\n").format(
            itm.text().split(":")[0]))
        self.chatEdit.setTextColor(color)

eric ide

mercurial