Mon, 10 Dec 2012 18:40:10 +0100
Merged with Spanish translations update done by Jaime.
--- a/Cooperation/ChatWidget.py Sun Dec 02 18:53:39 2012 +0100 +++ b/Cooperation/ChatWidget.py Mon Dec 10 18:40:10 2012 +0100 @@ -504,6 +504,8 @@ self.__chatMenu.addAction( UI.PixmapCache.getIcon("fileSave.png"), self.trUtf8("Save"), self.__saveChat) + + self.on_chatEdit_copyAvailable(False) @pyqtSlot(bool) def on_chatEdit_copyAvailable(self, yes):
--- a/IconEditor/cursors/cursors_rc.py Sun Dec 02 18:53:39 2012 +0100 +++ b/IconEditor/cursors/cursors_rc.py Mon Dec 10 18:40:10 2012 +0100 @@ -2,8 +2,8 @@ # Resource object code # -# Created: Do. Dez 31 16:47:09 2009 -# by: The Resource Compiler for PyQt (Qt v4.5.3) +# Created: Fr. Dez 7 18:40:19 2012 +# by: The Resource Compiler for PyQt (Qt v4.8.3) # # WARNING! All changes made in this file will be lost! @@ -62,50 +62,16 @@ \x2e\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\x23\x69\x23\x2e\x2e\x2e\x2e\ \x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x22\ \x7d\x3b\x0a\ -\x00\x00\x02\x93\ -\x2f\ -\x2a\x20\x58\x50\x4d\x20\x2a\x2f\x0a\x73\x74\x61\x74\x69\x63\x20\ -\x63\x68\x61\x72\x20\x2a\x61\x69\x6d\x5b\x5d\x3d\x7b\x0a\x22\x32\ -\x32\x20\x32\x32\x20\x33\x20\x31\x22\x2c\x0a\x22\x2e\x20\x63\x20\ -\x4e\x6f\x6e\x65\x22\x2c\x0a\x22\x61\x20\x63\x20\x23\x30\x30\x30\ -\x30\x30\x30\x22\x2c\x0a\x22\x23\x20\x63\x20\x23\x66\x66\x66\x66\ -\x66\x66\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\ -\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\ -\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x23\x2e\x2e\x2e\x2e\x2e\ -\x2e\x2e\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\ -\x2e\x2e\x2e\x23\x61\x23\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\ -\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x23\x61\x23\ -\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\ -\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x23\x61\x23\x2e\x2e\x2e\x2e\x2e\x2e\ -\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\ -\x2e\x23\x61\x23\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x22\x2c\ -\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x23\x61\x23\x2e\x2e\ -\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\ -\x2e\x2e\x2e\x2e\x2e\x23\x61\x23\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\ -\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x23\ -\x61\x23\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\ -\x2e\x2e\x23\x23\x23\x23\x23\x23\x23\x2e\x2e\x2e\x23\x23\x23\x23\ -\x23\x23\x23\x2e\x2e\x2e\x22\x2c\x0a\x22\x2e\x23\x61\x61\x61\x61\ -\x61\x61\x61\x2e\x2e\x2e\x61\x61\x61\x61\x61\x61\x61\x23\x2e\x2e\ -\x22\x2c\x0a\x22\x2e\x2e\x23\x23\x23\x23\x23\x23\x23\x2e\x2e\x2e\ -\x23\x23\x23\x23\x23\x23\x23\x2e\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\ -\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x61\x2e\x2e\x2e\x2e\x2e\x2e\x2e\ -\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\ -\x2e\x23\x61\x23\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x22\x2c\ -\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x23\x61\x23\x2e\x2e\ -\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\ -\x2e\x2e\x2e\x2e\x2e\x23\x61\x23\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\ -\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x23\ -\x61\x23\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\ -\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x23\x61\x23\x2e\x2e\x2e\x2e\ -\x2e\x2e\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\ -\x2e\x2e\x2e\x23\x61\x23\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\ -\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x23\x2e\ -\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\ -\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\ -\x2e\x2e\x2e\x2e\x22\x2c\x0a\x22\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\ -\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x2e\x22\x7d\ -\x3b\x0a\ +\x00\x00\x00\x7d\ +\x00\ +\x00\x02\x93\x78\x9c\xd3\xd7\x52\x88\x08\xf0\x55\xd0\xd2\xe7\x2a\ +\x2e\x49\x2c\xc9\x4c\x56\x48\xce\x48\x2c\x52\xd0\x4a\xcc\xcc\x8d\ +\x8e\xb5\xad\xe6\x52\x32\x32\x52\x00\x22\x63\x05\x43\x25\x1d\x2e\ +\x25\x3d\x85\x64\x05\xbf\xfc\xbc\x54\x10\x3b\x11\xc8\x56\x36\x00\ +\x03\x10\x57\x19\xc4\x4d\x03\x03\xb0\x4a\xac\x00\x55\x46\x19\x97\ +\x8c\x72\xa2\xf2\x50\x92\x51\x86\x00\x3d\x64\x16\x58\x46\x39\x11\ +\x02\x80\x7c\x28\x4b\x99\xa0\x1e\x38\x48\x1c\x8a\xa1\x83\x3b\x4e\ +\x51\x00\x1e\x99\x5a\x6b\x2e\x00\x12\x9a\x79\xb2\ \x00\x00\x02\xe1\ \x2f\ \x2a\x20\x58\x50\x4d\x20\x2a\x2f\x0a\x73\x74\x61\x74\x69\x63\x20\ @@ -311,19 +277,17 @@ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\x00\x01\ -\x00\x00\x00\x54\x00\x00\x00\x00\x00\x01\x00\x00\x05\xaf\ -\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xa5\ -\x00\x00\x00\x32\x00\x00\x00\x00\x00\x01\x00\x00\x03\x18\ -\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xb0\ +\x00\x00\x00\x54\x00\x00\x00\x00\x00\x01\x00\x00\x03\x99\ +\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x01\x00\x00\x09\x8f\ +\x00\x00\x00\x32\x00\x01\x00\x00\x00\x01\x00\x00\x03\x18\ +\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x9a\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x08\x94\ +\x00\x00\x00\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x06\x7e\ " - def qInitResources(): QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - def qCleanupResources(): QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
--- a/Network/IRC/IrcChannelWidget.py Sun Dec 02 18:53:39 2012 +0100 +++ b/Network/IRC/IrcChannelWidget.py Mon Dec 10 18:40:10 2012 +0100 @@ -9,10 +9,10 @@ import re -from PyQt4.QtCore import pyqtSlot, pyqtSignal, QDateTime -from PyQt4.QtGui import QWidget, QListWidgetItem, QIcon, QPainter +from PyQt4.QtCore import pyqtSlot, pyqtSignal, QDateTime, QPoint, QFileInfo, QTimer +from PyQt4.QtGui import QWidget, QListWidgetItem, QIcon, QPainter, QMenu, QApplication -from E5Gui import E5MessageBox +from E5Gui import E5MessageBox, E5FileDialog from E5Gui.E5Application import e5App from .Ui_IrcChannelWidget import Ui_IrcChannelWidget @@ -134,14 +134,43 @@ painter.drawPixmap(0, 0, pix2) painter.end() return QIcon(pix1) + + def parseWhoFlags(self, flags): + """ + Public method to parse the user flags reported by a WHO command. + + @param flags user flags as reported by WHO (string) + """ + # H The user is not away. + # G The user is set away. + # * The user is an IRC operator. + # @ The user is a channel op in the channel listed in the first field. + # + The user is voiced in the channel listed. + if flags.endswith("@"): + privilege = IrcUserItem.Operator + elif flags.endswith("+"): + privilege = IrcUserItem.Voice + else: + privilege = IrcUserItem.Normal + if "*" in flags: + privilege = IrcUserItem.Admin + if flags.startswith("G"): + privilege |= IrcUserItem.Away + self.__privilege = privilege + self.__setIcon() class IrcChannelWidget(QWidget, Ui_IrcChannelWidget): """ Class implementing the IRC channel widget. + + @signal sendData(str) emitted to send a message to the channel + @signal channelClosed(str) emitted after the user has left the channel + @signal openPrivateChat(str) emitted to open a "channel" for private messages """ sendData = pyqtSignal(str) channelClosed = pyqtSignal(str) + openPrivateChat = pyqtSignal(str) UrlRe = re.compile(r"""((?:http|ftp|https):\/\/[\w\-_]+(?:\.[\w\-_]+)+""" r"""(?:[\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?)""") @@ -150,27 +179,6 @@ LeaveIndicator = "<--" MessageIndicator = "***" - # TODO: add context menu to users list with these entries: - # Whois - # Private Message - # TODO: add "Auto WHO" functionality (WHO <channel> %nf) - # The possible combinations for this field are listed below: - # H The user is not away. - # G The user is set away. - # * The user is an IRC operator. - # @ The user is a channel op in the channel listed in the first field. - # + The user is voiced in the channel listed. - # TODO: add context menu to messages pane with these entries: - # Copy - # Copy Link Location - # Copy All - # Clear - # Save - # Remember Position - # TODO: Remember current position with <hr/> when widget is invisible - # TODO: Remember current position with <hr/> upon user request (only one such line) - # TODO: Remember current position with <hr/> when away and configured accordingly - # TODO: Check away indication in the user list def __init__(self, parent=None): """ Constructor @@ -181,17 +189,27 @@ self.setupUi(self) self.__ui = e5App().getObject("UserInterface") + self.__ircWidget = parent + + self.__initMessagesMenu() + self.__initUsersMenu() self.__name = "" self.__userName = "" self.__partMessage = "" self.__prefixToPrivilege = {} + self.__private = False + self.__privatePartner = "" + self.__whoIsNick = "" + + self.__markerLine = "" + self.__hidden = True self.__patterns = [ # :foo_!n=foo@foohost.bar.net PRIVMSG #eric-ide :some long message - (re.compile(r":([^!]+).*\sPRIVMSG\s([^ ]+)\s:(.*)"), self.__message), + # :foo_!n=foo@foohost.bar.net PRIVMSG bar_ :some long message + (re.compile(r":([^!]+)!([^ ]+)\sPRIVMSG\s([^ ]+)\s:(.*)"), self.__message), # :foo_!n=foo@foohost.bar.net JOIN :#eric-ide - # :detlev_!~detlev@mnch-5d876cfa.pool.mediaWays.net JOIN #eric-ide (re.compile(r":([^!]+)!([^ ]+)\sJOIN\s:?([^ ]+)"), self.__userJoin), # :foo_!n=foo@foohost.bar.net PART #eric-ide :part message (re.compile(r":([^!]+).*\sPART\s([^ ]+)\s:(.*)"), self.__userPart), @@ -203,9 +221,15 @@ (re.compile(r":([^!]+).*\sQUIT\s*"), self.__userQuit), # :foo_!n=foo@foohost.bar.net NICK :newnick (re.compile(r":([^!]+).*\sNICK\s:(.*)"), self.__userNickChange), - # :barty!n=foo@foohost.bar.net MODE #eric-ide +o foo_ - (re.compile(r":([^!]+).*\sMODE\s([^ ]+)\s([^ ]+)\s([^ ]+).*"), + # :foo_!n=foo@foohost.bar.net MODE #eric-ide +o foo_ + (re.compile(r":([^!]+).*\sMODE\s([^ ]+)\s([+-][ovO]+)\s([^ ]+).*"), self.__setUserPrivilege), + # :cameron.freenode.net MODE #testeric +ns + (re.compile(r":([^ ]+)\sMODE\s([^ ]+)\s(.+)"), self.__updateChannelModes), + # :sturgeon.freenode.net 301 foo_ bar :Gone away for now + (re.compile(r":.*\s301\s([^ ]+)\s([^ ]+)\s:(.+)"), self.__userAway), + # :sturgeon.freenode.net 315 foo_ #eric-ide :End of /WHO list. + (re.compile(r":.*\s315\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoEnd), # :zelazny.freenode.net 324 foo_ #eric-ide +cnt (re.compile(r":.*\s324\s.*\s([^ ]+)\s(.+)"), self.__channelModes), # :zelazny.freenode.net 328 foo_ #eric-ide :http://www.buggeroff.com/ @@ -216,11 +240,61 @@ (re.compile(r":.*\s332\s.*\s([^ ]+)\s:(.*)"), self.__setTopic), # :zelazny.freenode.net foo_ 333 #eric-ide foo 1353089020 (re.compile(r":.*\s333\s.*\s([^ ]+)\s([^ ]+)\s(\d+)"), self.__topicCreated), + # :cameron.freenode.net 352 detlev_ #eric-ide ~foo foohost.bar.net cameron.freenode.net foo_ H :0 Foo Bar + (re.compile(r":.*\s352\s[^ ]+\s([^ ]+)\s([^ ]+)\s([^ ]+)\s[^ ]+\s([^ ]+)" + r"\s([^ ]+)\s:\d+\s(.*)"), self.__whoEntry), # :zelazny.freenode.net 353 foo_ @ #eric-ide :@user1 +user2 user3 (re.compile(r":.*\s353\s.*\s.\s([^ ]+)\s:(.*)"), self.__userList), + # :sturgeon.freenode.net 354 foo_ 42 ChanServ H@ + (re.compile(r":.*\s354\s[^ ]+\s42\s([^ ]+)\s(.*)"), self.__autoWhoEntry), # :zelazny.freenode.net 366 foo_ #eric-ide :End of /NAMES list. (re.compile(r":.*\s366\s.*\s([^ ]+)\s:(.*)"), self.__ignore), + # :sturgeon.freenode.net 704 foo_ index :Help topics available to users: + (re.compile(r":.*\s70[456]\s[^ ]+\s([^ ]+)\s:(.*)"), self.__help), + + # WHOIS replies + # :sturgeon.freenode.net 311 foo_ bar ~bar barhost.foo.net * :Bar User + (re.compile(r":.*\s311\s[^ ]+\s([^ ]+)\s([^ ]+)\s([^ ]+)\s\*\s:(.*)"), + self.__whoIsUser), + # :sturgeon.freenode.net 319 foo_ bar :@#eric-ide + (re.compile(r":.*\s319\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsChannels), + # :sturgeon.freenode.net 312 foo_ bar sturgeon.freenode.net :London, UK + (re.compile(r":.*\s312\s[^ ]+\s([^ ]+)\s([^ ]+)\s:(.*)"), self.__whoIsServer), + # :sturgeon.freenode.net 671 foo_ bar :is using a secure connection + (re.compile(r":.*\s671\s[^ ]+\s([^ ]+)\s:.*"), self.__whoIsSecure), + # :sturgeon.freenode.net 317 foo_ bar 3758 1355046912 :seconds idle, signon time + (re.compile(r":.*\s317\s[^ ]+\s([^ ]+)\s(\d+)\s(\d+)\s:.*"), + self.__whoIsIdle), + # :sturgeon.freenode.net 330 foo_ bar bar :is logged in as + (re.compile(r":.*\s330\s[^ ]+\s([^ ]+)\s([^ ]+)\s:.*"), self.__whoIsAccount), + # :sturgeon.freenode.net 318 foo_ bar :End of /WHOIS list. + (re.compile(r":.*\s318\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsEnd), + # :sturgeon.freenode.net 307 foo_ bar :is an identified user + (re.compile(r":.*\s307\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsIdentify), + # :sturgeon.freenode.net 320 foo_ bar :is an identified user + (re.compile(r":.*\s320\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsIdentify), + # :sturgeon.freenode.net 310 foo_ bar :is available for help + (re.compile(r":.*\s310\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsHelper), + # :sturgeon.freenode.net 338 foo_ bar real.ident@real.host 12.34.56.78 :Actual user@host, Actual IP + (re.compile(r":.*\s338\s[^ ]+\s([^ ]+)\s([^ ]+)\s([^ ]+)\s:.*"), + self.__whoIsActually), + # :sturgeon.freenode.net 313 foo_ bar :is an IRC Operator + (re.compile(r":.*\s313\s[^ ]+\s([^ ]+)\s:(.*)"), self.__whoIsOperator), + # :sturgeon.freenode.net 378 foo_ bar :is connecting from *@mnch-4d044d5a.pool.mediaWays.net 77.4.77.90 + (re.compile(r":.*\s311\s[^ ]+\s([^ ]+)\s:.*\s([^ ]+)\s([^ ]+)"), + self.__whoIsConnection), ] + # group(1) nick + # group(2) host name + # group(3) IP + # group(4) real name + # group(5) + + self.__autoWhoTemplate = "WHO {0} %tnf,42" + self.__autoWhoTimer = QTimer() + self.__autoWhoTimer.setSingleShot(True) + self.__autoWhoTimer.timeout.connect(self.__sendAutoWhoCommand) + self.__autoWhoRequested = False @pyqtSlot() def on_messageEdit_returnPressed(self): @@ -228,14 +302,33 @@ Private slot to send a message to the channel. """ msg = self.messageEdit.text() - self.messages.append( - '<font color="{0}">{2} <b><</b><font color="{1}">{3}</font>' - '<b>></b> {4}</font>'.format( - Preferences.getIrc("ChannelMessageColour"), - Preferences.getIrc("OwnNickColour"), - ircTimestamp(), self.__userName, Utilities.html_encode(msg))) - self.sendData.emit("PRIVMSG " + self.__name + " :" + msg) - self.messageEdit.clear() + if msg: + self.messages.append( + '<font color="{0}">{2} <b><</b><font color="{1}">{3}</font>' + '<b>></b> {4}</font>'.format( + Preferences.getIrc("ChannelMessageColour"), + Preferences.getIrc("OwnNickColour"), + ircTimestamp(), self.__userName, Utilities.html_encode(msg))) + if msg.startswith("/"): + if self.__private: + E5MessageBox.information(self, + self.trUtf8("Send Message"), + self.trUtf8("""Messages starting with a '/' are not allowed""" + """ in private chats.""")) + else: + msgList = msg.split(None, 1) + cmd = msgList[0][1:].upper() + if cmd == "MSG": + cmd = "PRIVMSG" + msgList[0] = cmd + self.sendData.emit(" ".join(msgList)) + else: + if self.__private: + self.sendData.emit("PRIVMSG " + self.__privatePartner + " :" + msg) + else: + self.sendData.emit("PRIVMSG " + self.__name + " :" + msg) + self.messageEdit.clear() + self.unsetMarkerLine() def requestLeave(self): """ @@ -246,7 +339,8 @@ self.trUtf8("""Do you really want to leave the IRC channel <b>{0}</b>?""")\ .format(self.__name)) if ok: - self.sendData.emit("PART " + self.__name + " :" + self.__partMessage) + if not self.__private: + self.sendData.emit("PART " + self.__name + " :" + self.__partMessage) self.channelClosed.emit(self.__name) def name(self): @@ -287,7 +381,7 @@ @param name user name for the channel (string) """ - self.__userName = name.lower() + self.__userName = name def partMessage(self): """ @@ -305,6 +399,25 @@ """ self.__partMessage = message + def setPrivate(self, private, partner=""): + """ + Public method to set the private chat mode. + + @param private flag indicating private chat mode (boolean) + @param partner name of the partner user (string) + """ + self.__private = private + self.__privatePartner = partner + + def setPrivateInfo(self, infoText): + """ + Public method to set some info text for private chat mode. + + @param infoText info text to be shown (string) + """ + if self.__private: + self.topicLabel.setText(infoText) + def handleMessage(self, line): """ Public method to handle the message sent by the server. @@ -327,26 +440,51 @@ @param match match object that matched the pattern @return flag indicating whether the message was handled (boolean) """ - if match.group(2).lower() == self.__name: - msg = ircFilter(match.group(3)) - self.messages.append( - '<font color="{0}">{2} <b><</b><font color="{1}">{3}</font>' - '<b>></b> {4}</font>'.format( - Preferences.getIrc("ChannelMessageColour"), - Preferences.getIrc("NickColour"), - ircTimestamp(), match.group(1), - msg)) - if Preferences.getIrc("ShowNotifications"): - if Preferences.getIrc("NotifyMessage"): - self.__ui.showNotification(UI.PixmapCache.getPixmap("irc48.png"), - self.trUtf8("Channel Message"), msg) - elif Preferences.getIrc("NotifyNick") and self.__userName in msg: - self.__ui.showNotification(UI.PixmapCache.getPixmap("irc48.png"), - self.trUtf8("Nick mentioned"), msg) + # group(1) sender user name + # group(2) sender user@host + # group(3) target nick + # group(4) message + if match.group(3).lower() == self.__name: + self.addMessage(match.group(1), match.group(4)) + if self.__private and not self.topicLabel.text(): + self.setPrivateInfo("{0} - {1}".format(match.group(1), match.group(2))) return True return False + def addMessage(self, sender, msg): + """ + Public method to add a message from external. + + @param sender nick name of the sender (string) + @param msg message received from sender (string) + """ + self.__appendMessage( + '<font color="{0}">{2} <b><</b><font color="{1}">{3}</font>' + '<b>></b> {4}</font>'.format( + Preferences.getIrc("ChannelMessageColour"), + Preferences.getIrc("NickColour"), + ircTimestamp(), sender, ircFilter(msg))) + if Preferences.getIrc("ShowNotifications"): + if Preferences.getIrc("NotifyMessage"): + self.__ui.showNotification(UI.PixmapCache.getPixmap("irc48.png"), + self.trUtf8("Channel Message"), msg) + elif Preferences.getIrc("NotifyNick") and \ + self.__userName.lower() in msg.lower(): + self.__ui.showNotification(UI.PixmapCache.getPixmap("irc48.png"), + self.trUtf8("Nick mentioned"), msg) + + def addUsers(self, users): + """ + Public method to add users to the channel. + + @param users list of user names to add (list of string) + """ + for user in users: + itm = self.__findUser(user) + if itm is None: + IrcUserItem(user, self.usersList) + def __userJoin(self, match): """ Private method to handle a user joining the channel. @@ -472,6 +610,20 @@ return False + def __userAway(self, match): + """ + Private method to handle a topic change of the channel. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + if match.group(1).lower() == self.__name: + self.__addManagementMessage(self.trUtf8("Away"), + self.trUtf8("{0} is away: {1}").format(match.group(2), match.group(3))) + return True + + return False + def __setTopic(self, match): """ Private method to handle a topic change of the channel. @@ -569,6 +721,151 @@ return False + def __updateChannelModes(self, match): + """ + Private method to handle a message reporting the channel modes. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) user or server + # group(2) channel + # group(3) modes and parameter list + if match.group(2).lower() == self.__name: + nick = match.group(1) + modesParameters = match.group(3).split() + modeString = modesParameters.pop(0) + isPlus = True + message = "" + for mode in modeString: + if mode == "+": + isPlus = True + continue + elif mode == "-": + isPlus = False + continue + elif mode == "a": + if isPlus: + message = self.trUtf8( + "{0} sets the channel mode to 'anonymous'.").format(nick) + else: + message = self.trUtf8( + "{0} removes the 'anonymous' mode from the channel.").format( + nick) + elif mode == "b": + if isPlus: + message = self.trUtf8( + "{0} sets a ban on {1}.").format( + nick, modesParameters.pop(0)) + else: + message = self.trUtf8( + "{0} removes the ban on {1}.").format( + nick, modesParameters.pop(0)) + elif mode == "c": + if isPlus: + message = self.trUtf8( + "{0} sets the channel mode to 'no colors allowed'.").format( + nick) + else: + message = self.trUtf8( + "{0} sets the channel mode to 'allow color codes'.").format( + nick) + elif mode == "e": + if isPlus: + message = self.trUtf8( + "{0} sets a ban exception on {1}.").format( + nick, modesParameters.pop(0)) + else: + message = self.trUtf8( + "{0} removes the ban exception on {1}.").format( + nick, modesParameters.pop(0)) + elif mode == "i": + if isPlus: + message = self.trUtf8( + "{0} sets the channel mode to 'invite only'.").format(nick) + else: + message = self.trUtf8( + "{0} removes the 'invite only' mode from the channel." + ).format(nick) + elif mode == "k": + if isPlus: + message = self.trUtf8( + "{0} sets the channel key to '{1}'.").format( + nick, modesParameters.pop(0)) + else: + message = self.trUtf8( + "{0} removes the channel key.").format(nick) + elif mode == "l": + if isPlus: + message = self.trUtf8( + "{0} sets the channel limit to %n nick(s).", "", + int(modesParameters.pop(0))).format(nick) + else: + message = self.trUtf8( + "{0} removes the channel limit.").format(nick) + elif mode == "m": + if isPlus: + message = self.trUtf8( + "{0} sets the channel mode to 'moderated'.").format(nick) + else: + message = self.trUtf8( + "{0} sets the channel mode to 'unmoderated'.").format(nick) + elif mode == "n": + if isPlus: + message = self.trUtf8( + "{0} sets the channel mode to 'no messages from outside'." + ).format(nick) + else: + message = self.trUtf8( + "{0} sets the channel mode to 'allow messages from outside'." + ).format(nick) + elif mode == "p": + if isPlus: + message = self.trUtf8( + "{0} sets the channel mode to 'private'.").format(nick) + else: + message = self.trUtf8( + "{0} sets the channel mode to 'public'.").format(nick) + elif mode == "q": + if isPlus: + message = self.trUtf8( + "{0} sets the channel mode to 'quiet'.").format(nick) + else: + message = self.trUtf8( + "{0} removes the 'quiet' mode from the channel.").format( + nick) + elif mode == "r": + continue + elif mode == "s": + if isPlus: + message = self.trUtf8( + "{0} sets the channel mode to 'secret'.").format(nick) + else: + message = self.trUtf8( + "{0} sets the channel mode to 'visible'.").format(nick) + elif mode == "t": + if isPlus: + message = self.trUtf8( + "{0} switches on 'topic protection'.").format(nick) + else: + message = self.trUtf8( + "{0} switches off 'topic protection'.").format(nick) + elif mode == "I": + if isPlus: + message = self.trUtf8( + "{0} sets invitation mask {1}.").format( + nick, modesParameters.pop(0)) + else: + message = self.trUtf8( + "{0} removes the invitation mask {1}.").format( + nick, modesParameters.pop(0)) + + self.__addManagementMessage(self.trUtf8("Mode"), message) + + return True + + return False + def __setUserPrivilege(self, match): """ Private method to handle a change of user privileges for the channel. @@ -580,9 +877,9 @@ itm = self.__findUser(match.group(4)) if itm: itm.changePrivilege(match.group(3)) - self.__addManagementMessage(IrcChannelWidget.MessageIndicator, - self.trUtf8("{0} sets mode for {1}: {2}.").format( - match.group(1), match.group(4), match.group(3))) + self.__addManagementMessage(IrcChannelWidget.MessageIndicator, + self.trUtf8("{0} sets mode for {1}: {2}.").format( + match.group(1), match.group(4), match.group(3))) return True return False @@ -599,6 +896,17 @@ return False + def __help(self, match): + """ + Private method to handle a help message. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + self.__addManagementMessage(self.trUtf8("Help"), + "{0} {1}".format(match.group(1), ircFilter(match.group(2)))) + return True + def setUserPrivilegePrefix(self, prefixes): """ Public method to set the user privilege to prefix mapping. @@ -655,6 +963,628 @@ color = Preferences.getIrc("LeaveChannelColour") else: color = Preferences.getIrc("ChannelInfoColour") - self.messages.append( + self.__appendMessage( '<font color="{0}">{1} <b>[</b>{2}<b>]</b> {3}</font>'.format( color, ircTimestamp(), indicator, message)) + + def __appendMessage(self, message): + """ + Private slot to append a message. + """ + if self.__hidden and \ + self.__markerLine == "" and \ + Preferences.getIrc("MarkPositionWhenHidden"): + self.setMarkerLine() + self.messages.append(message) + + def setMarkerLine(self): + """ + Public method to draw a line to mark the current position. + """ + self.unsetMarkerLine() + self.__markerLine = \ + '<span style=" color:{0}; background-color:{1};">{2}</span>'.format( + Preferences.getIrc("MarkerLineForegroundColour"), + Preferences.getIrc("MarkerLineBackgroundColour"), + self.trUtf8('--- New From Here ---')) + self.messages.append(self.__markerLine) + + def unsetMarkerLine(self): + """ + Public method to remove the marker line. + """ + if self.__markerLine: + txt = self.messages.toHtml() + if txt.endswith(self.__markerLine + "</p></body></html>"): + # remove empty last paragraph + pos = txt.rfind("<p") + txt = txt[:pos] + "</body></html>" + else: + txt = txt.replace(self.__markerLine, "") + self.messages.setHtml(txt) + self.__markerLine = "" + + def __clearMessages(self): + """ + Private slot to clear the contents of the messages display. + """ + self.messages.clear() + + def __copyMessages(self): + """ + Private slot to copy the selection of the messages display to the clipboard. + """ + self.messages.copy() + + def __cutMessages(self): + """ + Private slot to cut the selection of the messages display to the clipboard. + """ + self.messages.cut() + + def __copyAllMessages(self): + """ + Private slot to copy the contents of the messages display to the clipboard. + """ + txt = self.messages.toPlainText() + if txt: + cb = QApplication.clipboard() + cb.setText(txt) + + def __cutAllMessages(self): + """ + Private slot to cut the contents of the messages display to the clipboard. + """ + txt = self.messages.toPlainText() + if txt: + cb = QApplication.clipboard() + cb.setText(txt) + self.messages.clear() + + def __saveMessages(self): + """ + Private slot to save the contents of the messages display. + """ + hasText = not self.messages.document().isEmpty() + if hasText: + if Utilities.isWindowsPlatform(): + htmlExtension = "htm" + else: + htmlExtension = "html" + fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( + self, + self.trUtf8("Save Messages"), + "", + self.trUtf8( + "HTML Files (*.{0});;Text Files (*.txt);;All Files (*)").format( + htmlExtension), + None, + E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) + if fname: + ext = QFileInfo(fname).suffix() + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fname += ex + ext = QFileInfo(fname).suffix() + if QFileInfo(fname).exists(): + res = E5MessageBox.yesNo(self, + self.trUtf8("Save Messages"), + self.trUtf8("<p>The file <b>{0}</b> already exists." + " Overwrite it?</p>").format(fname), + icon=E5MessageBox.Warning) + if not res: + return + fname = Utilities.toNativeSeparators(fname) + + try: + if ext.lower() in ["htm", "html"]: + txt = self.messages.toHtml() + else: + txt = self.messages.toPlainText() + f = open(fname, "w", encoding="utf-8") + f.write(txt) + f.close() + except IOError as err: + E5MessageBox.critical(self, + self.trUtf8("Error saving Messages"), + self.trUtf8("""<p>The messages contents could not be written""" + """ to <b>{0}</b></p><p>Reason: {1}</p>""")\ + .format(fname, str(err))) + + def __initMessagesMenu(self): + """ + Private slot to initialize the context menu of the messages pane. + """ + self.__messagesMenu = QMenu(self) + self.__cutMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("editCut.png"), + self.trUtf8("Cut"), self.__cutMessages) + self.__copyMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("editCopy.png"), + self.trUtf8("Copy"), self.__copyMessages) + self.__messagesMenu.addSeparator() + self.__cutAllMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("editCut.png"), + self.trUtf8("Cut all"), self.__cutAllMessages) + self.__copyAllMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("editCopy.png"), + self.trUtf8("Copy all"), self.__copyAllMessages) + self.__messagesMenu.addSeparator() + self.__clearMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("editDelete.png"), + self.trUtf8("Clear"), self.__clearMessages) + self.__messagesMenu.addSeparator() + self.__saveMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("fileSave.png"), + self.trUtf8("Save"), self.__saveMessages) + self.__messagesMenu.addSeparator() + self.__setMarkerMessagesAct = \ + self.__messagesMenu.addAction(self.trUtf8("Mark Current Position"), + self.setMarkerLine) + self.__unsetMarkerMessagesAct = \ + self.__messagesMenu.addAction(self.trUtf8("Remove Position Marker"), + self.unsetMarkerLine) + + self.on_messages_copyAvailable(False) + + @pyqtSlot(bool) + def on_messages_copyAvailable(self, yes): + """ + Private slot to react to text selection/deselection of the messages edit. + + @param yes flag signaling the availability of selected text (boolean) + """ + self.__copyMessagesAct.setEnabled(yes) + self.__cutMessagesAct.setEnabled(yes) + + @pyqtSlot(QPoint) + def on_messages_customContextMenuRequested(self, pos): + """ + Private slot to show the context menu of the messages pane. + + @param pos the position of the mouse pointer (QPoint) + """ + enable = not self.messages.document().isEmpty() + self.__cutAllMessagesAct.setEnabled(enable) + self.__copyAllMessagesAct.setEnabled(enable) + self.__saveMessagesAct.setEnabled(enable) + self.__setMarkerMessagesAct.setEnabled(self.__markerLine == "") + self.__unsetMarkerMessagesAct.setEnabled(self.__markerLine != "") + self.__messagesMenu.popup(self.messages.mapToGlobal(pos)) + + def __whoIs(self): + """ + Private slot to get information about the selected user. + """ + self.__whoIsNick = self.usersList.selectedItems()[0].text() + self.sendData.emit("WHOIS " + self.__whoIsNick) + + def __openPrivateChat(self): + """ + Private slot to open a chat with the selected user. + """ + user = self.usersList.selectedItems()[0].text() + self.openPrivateChat.emit(user) + + def __initUsersMenu(self): + """ + Private slot to initialize the users list context menu. + """ + self.__usersMenu = QMenu(self) + self.__usersMenu.addAction(self.trUtf8("Who Is"), self.__whoIs) + self.__usersMenu.addSeparator() + self.__privateChatAct = \ + self.__usersMenu.addAction(self.trUtf8("Private Chat"), + self.__openPrivateChat) + + @pyqtSlot(QPoint) + def on_usersList_customContextMenuRequested(self, pos): + """ + Private slot to show the context menu of the users list. + + @param pos the position of the mouse pointer (QPoint) + """ + self.__privateChatAct.setEnabled(not self.__private) + if len(self.usersList.selectedItems()) > 0: + self.__usersMenu.popup(self.usersList.mapToGlobal(pos)) + + def hideEvent(self, evt): + """ + Protected method handling hide events. + + @param evt reference to the hide event (QHideEvent) + """ + self.__hidden = True + + def showEvent(self, evt): + """ + Protected method handling show events. + + @param evt reference to the show event (QShowEvent) + """ + self.__hidden = False + + def initAutoWho(self): + """ + Public method to initialize the Auto Who system. + """ + if Preferences.getIrc("AutoUserInfoLookup"): + self.__autoWhoTimer.setInterval( + Preferences.getIrc("AutoUserInfoInterval") * 1000) + self.__autoWhoTimer.start() + + @pyqtSlot() + def __sendAutoWhoCommand(self): + """ + Private slot to send the WHO command to update the users list. + """ + if self.usersList.count() <= Preferences.getIrc("AutoUserInfoMax"): + self.__autoWhoRequested = True + self.sendData.emit(self.__autoWhoTemplate.format(self.__name)) + + def __autoWhoEntry(self, match): + """ + Private method to handle a WHO entry returned by the server as requested + automatically. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) user flags + if self.__autoWhoRequested: + itm = self.__findUser(match.group(1)) + if itm: + itm.parseWhoFlags(match.group(2)) + return True + + return False + + def __whoEnd(self, match): + """ + Private method to handle the end of the WHO list. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + if match.group(1).lower() == self.__name: + if self.__autoWhoRequested: + self.__autoWhoRequested = False + self.initAutoWho() + else: + self.__addManagementMessage(self.trUtf8("Who"), + self.trUtf8("End of WHO list for {0}.").format(match.group(1))) + return True + + return False + + def __whoEntry(self, match): + """ + Private method to handle a WHO entry returned by the server as requested + manually. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) channel + # group(2) user + # group(3) host + # group(4) nick + # group(5) user flags + # group(6) real name + if match.group(2).lower() == self.__name: + away = self.trUtf8(" (Away)") if match.group(5).startswith("G") else "" + self.__addManagementMessage(self.trUtf8("Who"), + self.trUtf8("{0} is {1}@{2} ({3}){4}").format( + match.group(4), match.group(2), match.group(3), match.group(6), away)) + return True + + return False + + def __whoIsUser(self, match): + """ + Private method to handle the WHOIS user reply. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) user + # group(3) host + # group(4) real name + if match.group(1) == self.__whoIsNick: + realName = match.group(4).replace("<", "<").replace(">", ">") + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is {1}@{2} ({3}).").format(match.group(1), + match.group(2), match.group(3), realName)) + return True + + return False + + def __whoIsChannels(self, match): + """ + Private method to handle the WHOIS channels reply. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) channels + if match.group(1) == self.__whoIsNick: + userChannels = [] + voiceChannels = [] + opChannels = [] + halfopChannels = [] + ownerChannels = [] + adminChannels = [] + + # generate the list of channels the user is in + channelList = match.group(2).split() + for channel in channelList: + if channel.startswith(("*", "&")): + adminChannels.append(channel[1:]) + elif channel.startswith(("!", "~")) and \ + self.__ircWidget.isChannelName(channel[1:]): + ownerChannels.append(channel[1:]) + elif channel.startswith("@+"): + opChannels.append(channel[2:]) + elif channel.startswith("@"): + opChannels.append(channel[1:]) + elif channel.startswith("%"): + halfopChannels.append(channel[1:]) + elif channel.startswith("+"): + voiceChannels.append(channel[1:]) + else: + userChannels.append(channel) + + # show messages + if userChannels: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is a user on channels: {1}").format( + match.group(1), " ".join(userChannels))) + if voiceChannels: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} has voice on channels: {1}").format( + match.group(1), " ".join(voiceChannels))) + if halfopChannels: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is a halfop on channels: {1}").format( + match.group(1), " ".join(halfopChannels))) + if opChannels: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is an operator on channels: {1}").format( + match.group(1), " ".join(opChannels))) + if ownerChannels: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is owner of channels: {1}").format( + match.group(1), " ".join(ownerChannels))) + if adminChannels: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is admin on channels: {1}").format( + match.group(1), " ".join(adminChannels))) + return True + + return False + + def __whoIsServer(self, match): + """ + Private method to handle the WHOIS server reply. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) server + # group(3) server info + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is online via {1} ({2}).").format(match.group(1), + match.group(2), match.group(3))) + return True + + return False + + def __whoIsOperator(self, match): + """ + Private method to handle the WHOIS operator reply. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) message + if match.group(1) == self.__whoIsNick: + if match.group(2).lower().startswith("is an irc operator"): + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is an IRC Operator.").format(match.group(1))) + else: + self.__addManagementMessage(self.trUtf8("Whois"), + "{0} {1}".format(match.group(1), match.group(2))) + return True + + return False + + def __whoIsIdle(self, match): + """ + Private method to handle the WHOIS idle reply. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) idle seconds + # group(3) signon time + if match.group(1) == self.__whoIsNick: + seconds = int(match.group(2)) + minutes = seconds // 60 + hours = minutes // 60 + days = hours // 24 + + signonTimestamp = int(match.group(3)) + signonTime = QDateTime() + signonTime.setTime_t(signonTimestamp) + + if days: + daysString = self.trUtf8("%n day(s)", "", days) + hoursString = self.trUtf8("%n hour(s)", "", hours) + minutesString = self.trUtf8("%n minute(s)", "", minutes) + secondsString = self.trUtf8("%n second(s)", "", seconds) + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} has been idle for {1}, {2}, {3}, and {4}.", + "{0} = name of person, {1} = (x days), {2} = (x hours)," + " {3} = (x minutes), {4} = (x seconds)").format( + match.group(1), daysString, hoursString, minutesString, + secondsString)) + elif hours: + hoursString = self.trUtf8("%n hour(s)", "", hours) + minutesString = self.trUtf8("%n minute(s)", "", minutes) + secondsString = self.trUtf8("%n second(s)", "", seconds) + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} has been idle for {1}, {2}, and {3}.", + "{0} = name of person, {1} = (x hours), " + "{2} = (x minutes), {3} = (x seconds)").format( + match.group(1), hoursString, minutesString, secondsString)) + elif minutes: + minutesString = self.trUtf8("%n minute(s)", "", minutes) + secondsString = self.trUtf8("%n second(s)", "", seconds) + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} has been idle for {1} and {2}.", + "{0} = name of person, {1} = (x minutes), " + "{3} = (x seconds)").format( + match.group(1), minutesString, secondsString)) + else: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} has been idle for %n second(s).", "", + seconds).format(match.group(1))) + + if not signonTime.isNull(): + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} has been online since {1}.").format( + match.group(1), signonTime.toString("yyyy-MM-dd, hh:mm:ss"))) + return True + + return False + + def __whoIsEnd(self, match): + """ + Private method to handle the end of WHOIS reply. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) end message + if match.group(1) == self.__whoIsNick: + self.__whoIsNick = "" + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("End of WHOIS list for {0}.").format(match.group(1))) + return True + + return False + + def __whoIsIdentify(self, match): + """ + Private method to handle the WHOIS identify and identified replies. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) identified message + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is an identified user.").format(match.group(1))) + return True + + return False + + def __whoIsHelper(self, match): + """ + Private method to handle the WHOIS helper reply. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) helper message + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is available for help.").format(match.group(1))) + return True + + return False + + def __whoIsAccount(self, match): + """ + Private method to handle the WHOIS account reply. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) login name + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is logged in as {1}.").format(match.group(1), + match.group(2))) + return True + + return False + + def __whoIsActually(self, match): + """ + Private method to handle the WHOIS actually reply. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) actual user@host + # group(3) actual IP + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is actually using the host {1} (IP: {2}).").format( + match.group(1), match.group(2), match.group(3))) + return True + + return False + + def __whoIsSecure(self, match): + """ + Private method to handle the WHOIS secure reply. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is using a secure connection.").format(match.group(1))) + return True + + return False + + def __whoIsConnection(self, match): + """ + Private method to handle the WHOIS connection reply. + + @param match match object that matched the pattern + @return flag indicating whether the message was handled (boolean) + """ + # group(1) nick + # group(2) host name + # group(3) IP + if match.group(1) == self.__whoIsNick: + self.__addManagementMessage(self.trUtf8("Whois"), + self.trUtf8("{0} is connecting from {1} (IP: {2}).").format( + match.group(1), match.group(2), match.group(3))) + return True + + return False
--- a/Network/IRC/IrcChannelWidget.ui Sun Dec 02 18:53:39 2012 +0100 +++ b/Network/IRC/IrcChannelWidget.ui Mon Dec 10 18:40:10 2012 +0100 @@ -33,6 +33,9 @@ <enum>Qt::Vertical</enum> </property> <widget class="QListWidget" name="usersList"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> <property name="toolTip"> <string>Shows the list of users</string> </property> @@ -50,6 +53,9 @@ <verstretch>1</verstretch> </sizepolicy> </property> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> <property name="toolTip"> <string>Shows the channel messages</string> </property> @@ -63,7 +69,7 @@ </widget> </item> <item> - <widget class="E5ClearableLineEdit" name="messageEdit"> + <widget class="IrcMessageEdit" name="messageEdit"> <property name="toolTip"> <string>Enter a message, send by pressing Return or Enter</string> </property> @@ -76,9 +82,9 @@ </widget> <customwidgets> <customwidget> - <class>E5ClearableLineEdit</class> + <class>IrcMessageEdit</class> <extends>QLineEdit</extends> - <header>E5Gui/E5LineEdit.h</header> + <header>Network/IRC/IrcMessageEdit.h</header> </customwidget> </customwidgets> <tabstops>
--- a/Network/IRC/IrcIdentitiesEditDialog.py Sun Dec 02 18:53:39 2012 +0100 +++ b/Network/IRC/IrcIdentitiesEditDialog.py Mon Dec 10 18:40:10 2012 +0100 @@ -117,9 +117,6 @@ self.rememberPosOnAwayCheckBox.setChecked( self.__currentIdentity.rememberAwayPosition()) self.awayEdit.setText(self.__currentIdentity.getAwayMessage()) - self.autoAwayGroup.setChecked(self.__currentIdentity.autoAway()) - self.inactivitySpinBox.setValue(self.__currentIdentity.getAutoAwayTimeout()) - self.autoReturnCheckBox.setChecked(self.__currentIdentity.autoReturn()) # Advanced Tab self.identEdit.setText(self.__currentIdentity.getIdent()) @@ -150,9 +147,6 @@ self.__currentIdentity.setRememberAwayPosition( self.rememberPosOnAwayCheckBox.isChecked()) self.__currentIdentity.setAwayMessage(self.awayEdit.text()) - self.__currentIdentity.setAutoAway(self.autoAwayGroup.isChecked()) - self.__currentIdentity.setAutoAwayTimeout(self.inactivitySpinBox.value()) - self.__currentIdentity.setAutoReturn(self.autoReturnCheckBox.isChecked()) # Advanced Tab self.__currentIdentity.setIdent(self.identEdit.text())
--- a/Network/IRC/IrcIdentitiesEditDialog.ui Sun Dec 02 18:53:39 2012 +0100 +++ b/Network/IRC/IrcIdentitiesEditDialog.ui Mon Dec 10 18:40:10 2012 +0100 @@ -237,71 +237,7 @@ </property> </widget> </item> - <item row="2" column="0" colspan="2"> - <widget class="QGroupBox" name="autoAwayGroup"> - <property name="toolTip"> - <string>Select to send an AWAY message when inactive</string> - </property> - <property name="title"> - <string>Auto Away</string> - </property> - <property name="checkable"> - <bool>true</bool> - </property> - <layout class="QGridLayout" name="gridLayout_4"> - <item row="0" column="0"> - <widget class="QLabel" name="label_9"> - <property name="text"> - <string>Time of Inactivity:</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QSpinBox" name="inactivitySpinBox"> - <property name="toolTip"> - <string>Enter the time of inactivity to send the automatic AWAY message</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - <property name="suffix"> - <string> min</string> - </property> - <property name="minimum"> - <number>1</number> - </property> - <property name="maximum"> - <number>120</number> - </property> - </widget> - </item> - <item row="0" column="2"> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>321</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item row="1" column="0" colspan="3"> - <widget class="QCheckBox" name="autoReturnCheckBox"> - <property name="toolTip"> - <string>Select to return automatically when activity is detected</string> - </property> - <property name="text"> - <string>Automatically return on activity</string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item row="3" column="1"> + <item row="2" column="1"> <spacer name="verticalSpacer_3"> <property name="orientation"> <enum>Qt::Vertical</enum> @@ -410,9 +346,6 @@ <tabstop>passwordEdit</tabstop> <tabstop>rememberPosOnAwayCheckBox</tabstop> <tabstop>awayEdit</tabstop> - <tabstop>autoAwayGroup</tabstop> - <tabstop>inactivitySpinBox</tabstop> - <tabstop>autoReturnCheckBox</tabstop> <tabstop>identEdit</tabstop> <tabstop>quitEdit</tabstop> <tabstop>partEdit</tabstop>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Network/IRC/IrcMessageEdit.py Mon Dec 10 18:40:10 2012 +0100 @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a specialized line edit for entering IRC messages. +""" + +from PyQt4.QtCore import Qt + +from E5Gui.E5LineEdit import E5LineEdit, E5ClearableLineEdit + + +class IrcMessageEdit(E5ClearableLineEdit): + """ + Class implementing a specialized line edit for entering IRC messages. + """ + MaxHistory = 100 + + def __init__(self, parent=None, inactiveText="", side=E5LineEdit.RightSide): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + @keyparam inactiveText text to be shown on inactivity (string) + @keyparam side side the clear button should be shown at (E5LineEdit.RightSide, + E5LineEdit.LeftSide) + """ + super().__init__(parent, inactiveText, side) + + self.__historyList = [""] # initialize with one empty line + self.__historyLine = 0 + + def setText(self, text): + """ + Public method to set the text. + + Note: This reimplementation ensures, that the cursor is at the end of the text. + + @param text text to be set (string) + """ + super().setText(text) + self.setCursorPosition(len(text)) + + def keyPressEvent(self, evt): + """ + Protected method implementing special key handling. + + @param evt reference to the event (QKeyEvent) + """ + key = evt.key() + if key == Qt.Key_Up: + self.__getHistory(True) + return + elif key == Qt.Key_Down: + self.__getHistory(False) + return + elif key in [Qt.Key_Return, Qt.Key_Enter]: + if self.text(): + self.__addHistory(self.text()) + elif evt.text() == chr(21): + # ^U: clear the text + self.setText("") + + super().keyPressEvent(evt) + + def wheelEvent(self, evt): + """ + Protected slot to support wheel events. + + @param reference to the wheel event (QWheelEvent) + """ + if evt.delta() > 0: + self.__getHistory(True) + elif evt.delta() < 0: + self.__getHistory(False) + + super().wheelEvent(evt) + + def __addHistory(self, txt): + """ + Private method to add an entry to the history. + + @param txt text to be added to the history (string) + """ + # Only add the entry, if it is not the same as last time + if len(self.__historyList) == 1 or \ + (len(self.__historyList) > 1 and self.__historyList[1] != txt): + # Replace empty first entry and add new empty first entry + self.__historyList[0] = txt + self.__historyList.insert(0, "") + # Keep history below the defined limit + del self.__historyList[IrcMessageEdit.MaxHistory:] + + self.__historyLine = 0 + + def __getHistory(self, up): + """ + Private method to move in the history. + + @param up flag indicating the direction (boolean) + """ + # preserve the current text, if it is not empty + if self.text(): + self.__historyList[self.__historyLine] = self.text() + + if up: + self.__historyLine += 1 + # If the position was moved past the end of the history, go to the last entry + if self.__historyLine == len(self.__historyList): + self.__historyLine -= 1 + return + else: + # If the position is at the top of the history, arrow-down shall add the text + # to the history and clear the line edit for new input + if self.__historyLine == 0: + if self.text(): + self.__addHistory(self.text()) + self.setText("") + else: + # If the position is not at the top of the history, decrement it + self.__historyLine -= 1 + + # replace the text of the line edit with the selected history entry + self.setText(self.__historyList[self.__historyLine])
--- a/Network/IRC/IrcNetworkManager.py Sun Dec 02 18:53:39 2012 +0100 +++ b/Network/IRC/IrcNetworkManager.py Mon Dec 10 18:40:10 2012 +0100 @@ -45,9 +45,6 @@ self.__rememberPosOnAway = True self.__awayMessage = IrcIdentity.DefaultAwayMessage - self.__autoAway = False - self.__autoAwayTimeout = 1 - self.__autoReturn = False self.__quitMessage = IrcIdentity.DefaultQuitMessage self.__partMessage = IrcIdentity.DefaultPartMessage @@ -68,9 +65,6 @@ settings.setValue("PartMessage", self.__partMessage) settings.setValue("RememberAwayPosition", self.__rememberPosOnAway) settings.setValue("AwayMessage", self.__awayMessage) - settings.setValue("AutoAway", self.__autoAway) - settings.setValue("AwayTimeout", self.__autoAwayTimeout) - settings.setValue("AutoReturn", self.__autoReturn) def load(self, settings): """ @@ -88,9 +82,6 @@ self.__rememberPosOnAway = Preferences.toBool( settings.value("RememberAwayPosition", True)) self.__awayMessage = settings.value("AwayMessage", IrcIdentity.DefaultAwayMessage) - self.__autoAway = Preferences.toBool(settings.value("AutoAway", False)) - self.__autoAwayTimeout = int(settings.value("AwayTimeout", 1)) - self.__autoReturn = Preferences.toBool(settings.value("AutoReturn", False)) def setName(self, name): """ @@ -261,54 +252,6 @@ """ return self.__awayMessage - def setAutoAway(self, on): - """ - Public method to set the auto away function. - - @param on flag indicating to enable the auto away function (boolean) - """ - self.__autoAway = on - - def autoAway(self): - """ - Public method to get the auto away flag. - - @return auto away flag (boolean) - """ - return self.__autoAway - - def setAutoAwayTimeout(self, minutes): - """ - Public method to set the auto away timeout. - - @param minutes auto away timeout in minutes (integer) - """ - self.__autoAwayTimeout = minutes - - def getAutoAwayTimeout(self): - """ - Public method to get the auto away timeout. - - @return auto away timeout in minutes (integer) - """ - return self.__autoAwayTimeout - - def setAutoReturn(self, on): - """ - Public method to set the auto return function. - - @param on flag indicating to enable the auto return function (boolean) - """ - self.__autoReturn = on - - def autoReturn(self): - """ - Public method to get the auto return flag. - - @return auto return flag (boolean) - """ - return self.__autoReturn - @classmethod def createDefaultIdentity(cls): """
--- a/Network/IRC/IrcNetworkWidget.py Sun Dec 02 18:53:39 2012 +0100 +++ b/Network/IRC/IrcNetworkWidget.py Mon Dec 10 18:40:10 2012 +0100 @@ -7,8 +7,10 @@ Module implementing the network part of the IRC widget. """ -from PyQt4.QtCore import pyqtSlot, pyqtSignal -from PyQt4.QtGui import QWidget +from PyQt4.QtCore import pyqtSlot, pyqtSignal, QPoint, QFileInfo +from PyQt4.QtGui import QWidget, QApplication, QMenu + +from E5Gui import E5MessageBox, E5FileDialog from .Ui_IrcNetworkWidget import Ui_IrcNetworkWidget @@ -16,6 +18,7 @@ import UI.PixmapCache import Preferences +import Utilities class IrcNetworkWidget(QWidget, Ui_IrcNetworkWidget): @@ -26,20 +29,18 @@ @signal editNetwork(str) emitted to edit a network configuration @signal joinChannel(str) emitted to join a channel @signal nickChanged(str) emitted to change the nick name + @signal sendData(str) emitted to send a message to the channel + @signal away(bool) emitted to indicate the away status + @signal autoConnected() emitted after an automatic connection was initiated """ connectNetwork = pyqtSignal(str, bool) editNetwork = pyqtSignal(str) joinChannel = pyqtSignal(str) nickChanged = pyqtSignal(str) - + sendData = pyqtSignal(str) + away = pyqtSignal(bool) + autoConnected = pyqtSignal() - # TODO: add context menu to messages pane with these entries: - # Copy - # Copy Link Location - # Copy All - # Clear - # Save - # TODO: add AWAY support (toolbutton in widget) def __init__(self, parent=None): """ Constructor @@ -52,10 +53,22 @@ self.connectButton.setIcon(UI.PixmapCache.getIcon("ircConnect.png")) self.editButton.setIcon(UI.PixmapCache.getIcon("ircConfigure.png")) self.joinButton.setIcon(UI.PixmapCache.getIcon("ircJoinChannel.png")) + self.awayButton.setIcon(UI.PixmapCache.getIcon("ircUserPresent.png")) + self.joinButton.setEnabled(False) + self.nickCombo.setEnabled(False) + self.awayButton.setEnabled(False) + + self.channelCombo.lineEdit().returnPressed.connect(self.on_joinButton_clicked) + self.nickCombo.lineEdit().returnPressed.connect( + self.on_nickCombo_currentIndexChanged) + + self.__initMessagesMenu() self.__manager = None self.__connected = False + self.__registered = False + self.__away = False def initialize(self, manager): """ @@ -78,6 +91,7 @@ row = self.networkCombo.findText(networkName) self.networkCombo.setCurrentIndex(row) self.on_connectButton_clicked() + self.autoConnected.emit() break @pyqtSlot() @@ -106,6 +120,24 @@ self.connectNetwork.emit(network, not self.__connected) @pyqtSlot() + def on_awayButton_clicked(self): + """ + Private slot to toggle the away status. + """ + if self.__away: + self.sendData.emit("AWAY") + self.awayButton.setIcon(UI.PixmapCache.getIcon("ircUserPresent.png")) + self.__away = False + else: + networkName = self.networkCombo.currentText() + identityName = self.__manager.getNetwork(networkName).getIdentityName() + awayMessage = self.__manager.getIdentity(identityName).getAwayMessage() + self.sendData.emit("AWAY :" + awayMessage) + self.awayButton.setIcon(UI.PixmapCache.getIcon("ircUserAway.png")) + self.__away = True + self.away.emit(self.__away) + + @pyqtSlot() def on_editButton_clicked(self): """ Private slot to edit a network. @@ -120,7 +152,7 @@ @param txt current text of the channel combo (string) """ - on = bool(txt) and self.__connected + on = bool(txt) and self.__registered self.joinButton.setEnabled(on) @pyqtSlot() @@ -150,10 +182,8 @@ network.getIdentityName()) if identity: self.nickCombo.addItems(identity.getNickNames()) - self.nickCombo.setEnabled(True) else: self.channelCombo.setEnabled(False) - self.nickCombo.setEnabled(False) def getNetworkChannels(self): """ @@ -167,7 +197,7 @@ return network.getChannels() @pyqtSlot(str) - def on_nickCombo_currentIndexChanged(self, nick): + def on_nickCombo_currentIndexChanged(self, nick=""): """ Private slot to use another nick name. @@ -252,5 +282,161 @@ else: self.connectButton.setIcon(UI.PixmapCache.getIcon("ircConnect.png")) - on = bool(self.channelCombo.currentText()) and self.__connected + def setRegistered(self, registered): + """ + Public slot to set the registered state. + + @param connected flag indicating the connection state (boolean) + """ + self.__registered = registered + on = bool(self.channelCombo.currentText()) and self.__registered self.joinButton.setEnabled(on) + self.nickCombo.setEnabled(registered) + self.awayButton.setEnabled(registered) + if registered: + self.awayButton.setIcon(UI.PixmapCache.getIcon("ircUserPresent.png")) + self.__away = False + + def __clearMessages(self): + """ + Private slot to clear the contents of the messages display. + """ + self.messages.clear() + + def __copyMessages(self): + """ + Private slot to copy the selection of the messages display to the clipboard. + """ + self.messages.copy() + + def __cutMessages(self): + """ + Private slot to cut the selection of the messages display to the clipboard. + """ + self.messages.cut() + + def __copyAllMessages(self): + """ + Private slot to copy the contents of the messages display to the clipboard. + """ + txt = self.messages.toPlainText() + if txt: + cb = QApplication.clipboard() + cb.setText(txt) + + def __cutAllMessages(self): + """ + Private slot to cut the contents of the messages display to the clipboard. + """ + txt = self.messages.toPlainText() + if txt: + cb = QApplication.clipboard() + cb.setText(txt) + self.messages.clear() + + def __saveMessages(self): + """ + Private slot to save the contents of the messages display. + """ + hasText = not self.messages.document().isEmpty() + if hasText: + if Utilities.isWindowsPlatform(): + htmlExtension = "htm" + else: + htmlExtension = "html" + fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( + self, + self.trUtf8("Save Messages"), + "", + self.trUtf8( + "HTML Files (*.{0});;Text Files (*.txt);;All Files (*)").format( + htmlExtension), + None, + E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) + if fname: + ext = QFileInfo(fname).suffix() + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fname += ex + ext = QFileInfo(fname).suffix() + if QFileInfo(fname).exists(): + res = E5MessageBox.yesNo(self, + self.trUtf8("Save Messages"), + self.trUtf8("<p>The file <b>{0}</b> already exists." + " Overwrite it?</p>").format(fname), + icon=E5MessageBox.Warning) + if not res: + return + fname = Utilities.toNativeSeparators(fname) + + try: + if ext.lower() in ["htm", "html"]: + txt = self.messages.toHtml() + else: + txt = self.messages.toPlainText() + f = open(fname, "w", encoding="utf-8") + f.write(txt) + f.close() + except IOError as err: + E5MessageBox.critical(self, + self.trUtf8("Error saving Messages"), + self.trUtf8("""<p>The messages contents could not be written""" + """ to <b>{0}</b></p><p>Reason: {1}</p>""")\ + .format(fname, str(err))) + + def __initMessagesMenu(self): + """ + Private slot to initialize the context menu of the messages pane. + """ + self.__messagesMenu = QMenu(self) + self.__cutMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("editCut.png"), + self.trUtf8("Cut"), self.__cutMessages) + self.__copyMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("editCopy.png"), + self.trUtf8("Copy"), self.__copyMessages) + self.__messagesMenu.addSeparator() + self.__cutAllMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("editCut.png"), + self.trUtf8("Cut all"), self.__cutAllMessages) + self.__copyAllMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("editCopy.png"), + self.trUtf8("Copy all"), self.__copyAllMessages) + self.__messagesMenu.addSeparator() + self.__clearMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("editDelete.png"), + self.trUtf8("Clear"), self.__clearMessages) + self.__messagesMenu.addSeparator() + self.__saveMessagesAct = \ + self.__messagesMenu.addAction( + UI.PixmapCache.getIcon("fileSave.png"), + self.trUtf8("Save"), self.__saveMessages) + + self.on_messages_copyAvailable(False) + + @pyqtSlot(bool) + def on_messages_copyAvailable(self, yes): + """ + Private slot to react to text selection/deselection of the messages edit. + + @param yes flag signaling the availability of selected text (boolean) + """ + self.__copyMessagesAct.setEnabled(yes) + self.__cutMessagesAct.setEnabled(yes) + + @pyqtSlot(QPoint) + def on_messages_customContextMenuRequested(self, pos): + """ + Private slot to show the context menu of the messages pane. + """ + enable = not self.messages.document().isEmpty() + self.__cutAllMessagesAct.setEnabled(enable) + self.__copyAllMessagesAct.setEnabled(enable) + self.__saveMessagesAct.setEnabled(enable) + self.__messagesMenu.popup(self.messages.mapToGlobal(pos))
--- a/Network/IRC/IrcNetworkWidget.ui Sun Dec 02 18:53:39 2012 +0100 +++ b/Network/IRC/IrcNetworkWidget.ui Mon Dec 10 18:40:10 2012 +0100 @@ -14,8 +14,14 @@ <string/> </property> <layout class="QVBoxLayout" name="verticalLayout"> + <property name="margin"> + <number>0</number> + </property> <item> <widget class="QTextBrowser" name="messages"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> <property name="toolTip"> <string>Shows the network messages</string> </property> @@ -47,6 +53,16 @@ </widget> </item> <item> + <widget class="QToolButton" name="awayButton"> + <property name="toolTip"> + <string>Press to set the user status to AWAY</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> <widget class="QToolButton" name="editButton"> <property name="toolTip"> <string>Press to edit the networks</string> @@ -122,6 +138,7 @@ <tabstops> <tabstop>networkCombo</tabstop> <tabstop>connectButton</tabstop> + <tabstop>awayButton</tabstop> <tabstop>editButton</tabstop> <tabstop>nickCombo</tabstop> <tabstop>channelCombo</tabstop>
--- a/Network/IRC/IrcUtilities.py Sun Dec 02 18:53:39 2012 +0100 +++ b/Network/IRC/IrcUtilities.py Mon Dec 10 18:40:10 2012 +0100 @@ -146,16 +146,21 @@ global __channelModesDict modesDict = { - "t": QCoreApplication.translate("IrcUtilities", "topic protection"), + "a": QCoreApplication.translate("IrcUtilities", "anonymous"), + "b": QCoreApplication.translate("IrcUtilities", "ban mask"), + "c": QCoreApplication.translate("IrcUtilities", "no colors allowed"), + "e": QCoreApplication.translate("IrcUtilities", "ban exception mask"), + "i": QCoreApplication.translate("IrcUtilities", "invite only"), + "k": QCoreApplication.translate("IrcUtilities", "password protected"), + "l": QCoreApplication.translate("IrcUtilities", "user limit"), + "m": QCoreApplication.translate("IrcUtilities", "moderated"), "n": QCoreApplication.translate("IrcUtilities", "no messages from outside"), - "s": QCoreApplication.translate("IrcUtilities", "secret"), - "i": QCoreApplication.translate("IrcUtilities", "invite only"), "p": QCoreApplication.translate("IrcUtilities", "private"), - "m": QCoreApplication.translate("IrcUtilities", "moderated"), - "k": QCoreApplication.translate("IrcUtilities", "password protected"), - "a": QCoreApplication.translate("IrcUtilities", "anonymous"), - "c": QCoreApplication.translate("IrcUtilities", "no colors allowed"), - "l": QCoreApplication.translate("IrcUtilities", "user limit"), + "q": QCoreApplication.translate("IrcUtilities", "quit"), + "r": QCoreApplication.translate("IrcUtilities", "reop channel"), + "s": QCoreApplication.translate("IrcUtilities", "secret"), + "t": QCoreApplication.translate("IrcUtilities", "topic protection"), + "I": QCoreApplication.translate("IrcUtilities", "invitation mask"), } __channelModesDict = modesDict
--- a/Network/IRC/IrcWidget.py Sun Dec 02 18:53:39 2012 +0100 +++ b/Network/IRC/IrcWidget.py Mon Dec 10 18:40:10 2012 +0100 @@ -10,7 +10,7 @@ import re import logging -from PyQt4.QtCore import pyqtSlot, Qt, QByteArray, QTimer +from PyQt4.QtCore import pyqtSlot, pyqtSignal, Qt, QByteArray, QTimer from PyQt4.QtGui import QWidget, QToolButton, QLabel from PyQt4.QtNetwork import QTcpSocket, QAbstractSocket try: @@ -34,12 +34,15 @@ class IrcWidget(QWidget, Ui_IrcWidget): """ Class implementing the IRC window. + + @signal autoConnected() emitted after an automatic connection was initiated """ + autoConnected = pyqtSignal() + ServerDisconnected = 1 ServerConnected = 2 ServerConnecting = 3 - # TODO: Implement the Auto Away functionality def __init__(self, parent=None): """ Constructor @@ -50,7 +53,6 @@ self.setupUi(self) self.__ircNetworkManager = IrcNetworkManager(self) - self.__ircNetworkManager.dataChanged.connect(self.__networkDataChanged) self.__leaveButton = QToolButton(self) self.__leaveButton.setIcon(UI.PixmapCache.getIcon("ircCloseChannel.png")) @@ -60,12 +62,6 @@ self.channelsWidget.setCornerWidget(self.__leaveButton, Qt.BottomRightCorner) self.channelsWidget.setTabsClosable(False) - self.networkWidget.initialize(self.__ircNetworkManager) - self.networkWidget.connectNetwork.connect(self.__connectNetwork) - self.networkWidget.editNetwork.connect(self.__editNetwork) - self.networkWidget.joinChannel.connect(self.__joinChannel) - self.networkWidget.nickChanged.connect(self.__changeNick) - self.__channelList = [] self.__channelTypePrefixes = "" self.__userName = "" @@ -85,6 +81,8 @@ self.__socket = None self.__patterns = [ + # :foo_!n=foo@foohost.bar.net PRIVMSG bar_ :some long message + (re.compile(r":([^!]+)!([^ ]+)\sPRIVMSG\s([^ ]+)\s:(.*)"), self.__query), # :foo.bar.net COMMAND some message (re.compile(r""":([^ ]+)\s+([A-Z]+)\s+(.+)"""), self.__handleNamedMessage), # :foo.bar.net 123 * :info @@ -101,6 +99,16 @@ self.__emptyLabel.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) self.channelsWidget.addTab(self.__emptyLabel, "") + # all initialized, do connections now + self.__ircNetworkManager.dataChanged.connect(self.__networkDataChanged) + self.networkWidget.initialize(self.__ircNetworkManager) + self.networkWidget.connectNetwork.connect(self.__connectNetwork) + self.networkWidget.editNetwork.connect(self.__editNetwork) + self.networkWidget.joinChannel.connect(self.__joinChannel) + self.networkWidget.nickChanged.connect(self.__changeNick) + self.networkWidget.sendData.connect(self.__send) + self.networkWidget.away.connect(self.__away) + self.networkWidget.autoConnected.connect(self.autoConnected) def shutdown(self): """ @@ -244,12 +252,15 @@ identity = self.__ircNetworkManager.getIdentity(self.__identityName) channel.setPartMessage(identity.getPartMessage()) channel.setUserPrivilegePrefix(self.__userPrefix) + channel.initAutoWho() channel.sendData.connect(self.__send) channel.channelClosed.connect(self.__closeChannel) + channel.openPrivateChat.connect(self.__openPrivate) self.channelsWidget.addTab(channel, name) self.__channelList.append(channel) + self.channelsWidget.setCurrentWidget(channel) joinCommand = ["JOIN", name] if key: @@ -263,6 +274,48 @@ self.__leaveButton.setEnabled(True) self.channelsWidget.setTabsClosable(True) + def __query(self, match): + """ + Private method to handle a new private connection. + + @param reference to the match object + @return flag indicating, if the message was handled (boolean) + """ + # group(1) sender user name + # group(2) sender user@host + # group(3) target nick + # group(4) message + self.__openPrivate(match.group(1)) + # the above call sets the new channel as the current widget + channel = self.channelsWidget.currentWidget() + channel.addMessage(match.group(1), match.group(4)) + channel.setPrivateInfo("{0} - {1}".format(match.group(1), match.group(2))) + + return True + + @pyqtSlot(str) + def __openPrivate(self, name): + """ + Private slot to open a private chat with the given user. + + @param name name of the user (string) + """ + channel = IrcChannelWidget(self) + channel.setName(self.__nickName) + channel.setUserName(self.__nickName) + identity = self.__ircNetworkManager.getIdentity(self.__identityName) + channel.setPartMessage(identity.getPartMessage()) + channel.setUserPrivilegePrefix(self.__userPrefix) + channel.setPrivate(True, name) + channel.addUsers([name, self.__nickName]) + + channel.sendData.connect(self.__send) + channel.channelClosed.connect(self.__closeChannel) + + self.channelsWidget.addTab(channel, name) + self.__channelList.append(channel) + self.channelsWidget.setCurrentWidget(channel) + @pyqtSlot() def __leaveChannel(self): """ @@ -351,6 +404,7 @@ """ self.networkWidget.addServerMessage(self.trUtf8("Info"), self.trUtf8("Server disconnected.")) + self.networkWidget.setRegistered(False) self.networkWidget.setConnected(False) self.__server = None self.__nickName = "" @@ -422,7 +476,7 @@ # :detlev_ MODE detlev_ :+i name, modes = match.group(3).split(" :") sourceNick = match.group(1) - if not self.__isChannelName(name): + if not self.isChannelName(name): if name == self.__nickName: if sourceNick == self.__nickName: msg = self.trUtf8( @@ -459,6 +513,10 @@ self.trUtf8("User {0} is now known as {1}.").format( oldNick, newNick)) return True + elif name == "ERROR": + self.networkWidget.addErrorMessage( + self.trUtf8("Server Error"), match.group(3).split(":", 1)[1]) + return True return False @@ -512,6 +570,8 @@ msgType = self.trUtf8("User") elif code in [372, 375, 376]: msgType = self.trUtf8("MOTD") + elif code in [305, 306]: + msgType = self.trUtf8("Away") else: msgType = self.trUtf8("Info ({0})").format(code) @@ -532,6 +592,10 @@ parts = message.strip().split() message = self.trUtf8("Current users on the network: {0}, max. {1}").format( parts[1], parts[2]) + elif code == 305: + message = self.trUtf8("You are no longer marked as being away.") + elif code == 306: + message = self.trUtf8("You have been marked as being away.") else: first, message = message.split(None, 1) if message.startswith(":"): @@ -545,6 +609,7 @@ # register with services after the welcome message self.__connectionState = IrcWidget.ServerConnected self.__registerWithServices() + self.networkWidget.setRegistered(True) QTimer.singleShot(1000, self.__autoJoinChannels) elif code == 5: # extract the user privilege prefixes @@ -725,9 +790,9 @@ """ self.__channelTypePrefixes = prefixes - def __isChannelName(self, name): + def isChannelName(self, name): """ - Private method to check, if the given name is a channel name. + PublicisChannelName method to check, if the given name is a channel name. @return flag indicating a channel name (boolean) """ @@ -738,3 +803,15 @@ return name[0] in self.__channelTypePrefixes else: return name[0] in "#&" + + def __away(self, isAway): + """ + Private slot handling the change of the away state. + + @param isAway flag indicating the current away state (boolean) + """ + if isAway and self.__identityName: + identity = self.__ircNetworkManager.getIdentity(self.__identityName) + if identity.rememberAwayPosition(): + for channel in self.__channelList: + channel.setMarkerLine()
--- a/Preferences/ConfigurationPages/IrcPage.py Sun Dec 02 18:53:39 2012 +0100 +++ b/Preferences/ConfigurationPages/IrcPage.py Mon Dec 10 18:40:10 2012 +0100 @@ -21,7 +21,6 @@ DateFormats = ["yyyy-MM-dd", "dd.MM.yyyy", "MM/dd/yyyy", "yyyy MMM. dd", "dd MMM. yyyy", "MMM. dd, yyyy"] - # TODO: add config entries for auto WHO def __init__(self): """ Constructor @@ -105,6 +104,19 @@ Preferences.getIrc, byName=True) self.initColour("IrcColor15", self.ircColor15Button, Preferences.getIrc, byName=True) + + # Automatic User Information Lookup + self.whoGroup.setChecked(Preferences.getIrc("AutoUserInfoLookup")) + self.whoUsersSpinBox.setValue(Preferences.getIrc("AutoUserInfoMax")) + self.whoIntervalSpinBox.setValue(Preferences.getIrc("AutoUserInfoInterval")) + + # Markers + self.markWhenHiddenCheckBox.setChecked( + Preferences.getIrc("MarkPositionWhenHidden")) + self.initColour("MarkerLineForegroundColour", self.markerForegroundButton, + Preferences.getIrc, byName=True) + self.initColour("MarkerLineBackgroundColour", self.markerBackgroundButton, + Preferences.getIrc, byName=True) def save(self): """ @@ -122,6 +134,15 @@ Preferences.setIrc("NotifyMessage", self.messageCheckBox.isChecked()) Preferences.setIrc("NotifyNick", self.ownNickCheckBox.isChecked()) + # Automatic User Information Lookup + Preferences.setIrc("AutoUserInfoLookup", self.whoGroup.isChecked()) + Preferences.setIrc("AutoUserInfoMax", self.whoUsersSpinBox.value()) + Preferences.setIrc("AutoUserInfoInterval", self.whoIntervalSpinBox.value()) + + # Markers + Preferences.setIrc("MarkPositionWhenHidden", + self.markWhenHiddenCheckBox.isChecked()) + # colours self.saveColours(Preferences.setIrc)
--- a/Preferences/ConfigurationPages/IrcPage.ui Sun Dec 02 18:53:39 2012 +0100 +++ b/Preferences/ConfigurationPages/IrcPage.ui Mon Dec 10 18:40:10 2012 +0100 @@ -10,7 +10,7 @@ <height>853</height> </rect> </property> - <layout class="QVBoxLayout" name="verticalLayout"> + <layout class="QVBoxLayout" name="verticalLayout_2"> <item> <widget class="QLabel" name="headerLabel"> <property name="text"> @@ -708,6 +708,155 @@ </widget> </item> <item> + <widget class="QGroupBox" name="whoGroup"> + <property name="toolTip"> + <string>Select this to enable the automatic lookup of user information for joined channels</string> + </property> + <property name="title"> + <string>Enable Automatic User Information Lookup (/WHO)</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QGridLayout" name="gridLayout_5"> + <item row="0" column="0"> + <widget class="QLabel" name="label_31"> + <property name="text"> + <string>Max. Number of Users in Channel:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="whoUsersSpinBox"> + <property name="toolTip"> + <string>Enter the maximum numbers of users in a channel allowed for this function</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="maximum"> + <number>999</number> + </property> + </widget> + </item> + <item row="0" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>174</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_32"> + <property name="text"> + <string>Update Interval:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="whoIntervalSpinBox"> + <property name="toolTip"> + <string>Enter the user information update interval</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string> s</string> + </property> + <property name="minimum"> + <number>30</number> + </property> + <property name="maximum"> + <number>600</number> + </property> + <property name="singleStep"> + <number>10</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="markerGroup"> + <property name="title"> + <string>Marker</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QCheckBox" name="markWhenHiddenCheckBox"> + <property name="toolTip"> + <string>Select to mark the current position, when the chat window is hidden</string> + </property> + <property name="text"> + <string>Mark Current Position When Hidden</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label_33"> + <property name="text"> + <string>Marker Foreground:</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="markerForegroundButton"> + <property name="minimumSize"> + <size> + <width>100</width> + <height>0</height> + </size> + </property> + <property name="toolTip"> + <string>Select the foreground colour for the marker</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_34"> + <property name="text"> + <string>Marker Background:</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="markerBackgroundButton"> + <property name="minimumSize"> + <size> + <width>100</width> + <height>0</height> + </size> + </property> + <property name="toolTip"> + <string>Select the background colour for the marker</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> <spacer name="verticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum>
--- a/Preferences/__init__.py Sun Dec 02 18:53:39 2012 +0100 +++ b/Preferences/__init__.py Mon Dec 10 18:40:10 2012 +0100 @@ -953,6 +953,14 @@ "NotifyJoinPart": True, "NotifyMessage": False, "NotifyNick": False, + + "AutoUserInfoLookup": True, + "AutoUserInfoMax": 200, + "AutoUserInfoInterval": 90, + + "MarkPositionWhenHidden": True, + "MarkerLineForegroundColour": "#000000", # Black on + "MarkerLineBackgroundColour": "#ffff00", # Yellow } @@ -2587,9 +2595,13 @@ @return the requested user setting """ if key in ["TimestampIncludeDate", "ShowTimestamps", "ShowNotifications", - "NotifyJoinPart", "NotifyMessage", "NotifyNick", "EnableIrcColours"]: + "NotifyJoinPart", "NotifyMessage", "NotifyNick", "EnableIrcColours", + "AutoUserInfoLookup", "MarkPositionWhenHidden"]: return toBool(prefClass.settings.value("IRC/" + key, prefClass.ircDefaults[key])) + elif key in ["AutoUserInfoMax", "AutoUserInfoInterval"]: + return int(prefClass.settings.value("IRC/" + key, + prefClass.ircDefaults[key])) else: return prefClass.settings.value("IRC/" + key, prefClass.ircDefaults[key])
--- a/UI/UserInterface.py Sun Dec 02 18:53:39 2012 +0100 +++ b/UI/UserInterface.py Mon Dec 10 18:40:10 2012 +0100 @@ -462,6 +462,8 @@ self.numbersViewer.insertNumber.connect(self.viewmanager.insertNumber) + self.irc.autoConnected.connect(self.__ircAutoConnected) + # create the toolbar manager object self.toolbarManager = E5ToolBarManager(self, self) self.toolbarManager.setMainWindow(self) @@ -5605,3 +5607,9 @@ Public method to initiate the IRC auto connection. """ self.irc.autoConnect() + + def __ircAutoConnected(self): + """ + Private slot handling the automatic connection of the IRC client. + """ + self.__activateIRC()
--- a/eric5.e4p Sun Dec 02 18:53:39 2012 +0100 +++ b/eric5.e4p Mon Dec 10 18:40:10 2012 +0100 @@ -1067,6 +1067,7 @@ <Source>Network/IRC/IrcChannelEditDialog.py</Source> <Source>Network/IRC/IrcServerEditDialog.py</Source> <Source>Network/IRC/IrcIdentitiesEditDialog.py</Source> + <Source>Network/IRC/IrcMessageEdit.py</Source> </Sources> <Forms> <Form>PyUnit/UnittestDialog.ui</Form>