diff -r 810e9c7b61e3 -r 1fc32bd13be3 Network/IRC/IrcChannelWidget.py --- a/Network/IRC/IrcChannelWidget.py Fri Dec 07 18:35:54 2012 +0100 +++ b/Network/IRC/IrcChannelWidget.py Fri Dec 07 19:48:23 2012 +0100 @@ -9,10 +9,10 @@ import re -from PyQt4.QtCore import pyqtSlot, pyqtSignal, QDateTime, QPoint -from PyQt4.QtGui import QWidget, QListWidgetItem, QIcon, QPainter, QMenu +from PyQt4.QtCore import pyqtSlot, pyqtSignal, QDateTime, QPoint, QFileInfo +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 @@ -142,9 +142,11 @@ @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\-\@?^=%&/~\+#])?)""") @@ -163,14 +165,6 @@ # * 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: Check away indication in the user list def __init__(self, parent=None): """ @@ -184,13 +178,17 @@ self.__ui = e5App().getObject("UserInterface") self.__initMessagesMenu() + self.__initUsersMenu() self.__name = "" self.__userName = "" self.__partMessage = "" self.__prefixToPrivilege = {} + self.__private = False + self.__privatePartner = "" self.__markerLine = "" + self.__hidden = True self.__patterns = [ # :foo_!n=foo@foohost.bar.net PRIVMSG #eric-ide :some long message @@ -211,6 +209,8 @@ # :barty!n=foo@foohost.bar.net MODE #eric-ide +o foo_ (re.compile(r":([^!]+).*\sMODE\s([^ ]+)\s([^ ]+)\s([^ ]+).*"), self.__setUserPrivilege), + # :sturgeon.freenode.net 301 foo_ bar :Gone away for now + (re.compile(r":.*\s301\s([^ ]+)\s([^ ]+)\s:(.+)"), self.__userAway), # :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/ @@ -225,6 +225,8 @@ (re.compile(r":.*\s353\s.*\s.\s([^ ]+)\s:(.*)"), self.__userList), # :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), ] @pyqtSlot() @@ -240,7 +242,24 @@ Preferences.getIrc("ChannelMessageColour"), Preferences.getIrc("OwnNickColour"), ircTimestamp(), self.__userName, Utilities.html_encode(msg))) - self.sendData.emit("PRIVMSG " + self.__name + " :" + 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() def requestLeave(self): @@ -311,6 +330,16 @@ """ 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 handleMessage(self, line): """ Public method to handle the message sent by the server. @@ -335,7 +364,7 @@ """ if match.group(2).lower() == self.__name: msg = ircFilter(match.group(3)) - self.messages.append( + self.__appendMessage( '<font color="{0}">{2} <b><</b><font color="{1}">{3}</font>' '<b>></b> {4}</font>'.format( Preferences.getIrc("ChannelMessageColour"), @@ -353,6 +382,17 @@ return False + 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. @@ -478,6 +518,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. @@ -605,6 +659,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. @@ -661,10 +726,18 @@ 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 == "": + self.setMarkerLine() + self.messages.append(message) + def setMarkerLine(self): """ Public method to draw a line to mark the current position. @@ -691,58 +764,209 @@ 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.__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. + """ + # TODO: not implemented yet + return + + 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) """ - # TODO: not implemented yet - return + 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