|
| 1 | +import datetime, asyncio |
| 2 | + |
| 3 | +from PyQt4 import QtCore, QtGui |
| 4 | + |
| 5 | +import hangups |
| 6 | +from hangups.utils import get_conv_name |
| 7 | + |
| 8 | +from qhangups.utils import text_to_segments, message_to_html |
| 9 | +from qhangups.ui_qhangupsconversationwidget import Ui_QHangupsConversationWidget |
| 10 | + |
| 11 | + |
| 12 | +class HTMLDelegate(QtGui.QStyledItemDelegate): |
| 13 | + """QStyledItemDelegate implementation - draws HTML instead of plain text |
| 14 | + http://stackoverflow.com/questions/1956542/how-to-make-item-view-render-rich-html-text-in-qt/1956781#1956781 |
| 15 | + """ |
| 16 | + def paint(self, painter, option, index): |
| 17 | + """QStyledItemDelegate.paint implementation""" |
| 18 | + option.state &= ~QtGui.QStyle.State_HasFocus # never draw focus rect |
| 19 | + |
| 20 | + options = QtGui.QStyleOptionViewItemV4(option) |
| 21 | + self.initStyleOption(options, index) |
| 22 | + |
| 23 | + style = QtGui.QApplication.style() if options.widget is None else options.widget.style() |
| 24 | + |
| 25 | + doc = QtGui.QTextDocument() |
| 26 | + doc.setDocumentMargin(1) |
| 27 | + doc.setHtml(options.text) |
| 28 | + if options.widget is not None: |
| 29 | + doc.setDefaultFont(options.widget.font()) |
| 30 | + # bad long (multiline) strings processing |
| 31 | + doc.setTextWidth(options.rect.width()) |
| 32 | + |
| 33 | + options.text = "" |
| 34 | + style.drawControl(QtGui.QStyle.CE_ItemViewItem, options, painter) |
| 35 | + |
| 36 | + ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() |
| 37 | + |
| 38 | + # Highlighting text if item is selected |
| 39 | + if option.state & QtGui.QStyle.State_Selected: |
| 40 | + ctx.palette.setColor(QtGui.QPalette.Text, option.palette.color(QtGui.QPalette.Active, |
| 41 | + QtGui.QPalette.HighlightedText)) |
| 42 | + |
| 43 | + textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, options) |
| 44 | + painter.save() |
| 45 | + painter.translate(textRect.topLeft()) |
| 46 | + # Original example contained line |
| 47 | + # painter.setClipRect(textRect.translated(-textRect.topLeft())) |
| 48 | + # but text is drawn clipped with it on kubuntu 12.04 |
| 49 | + doc.documentLayout().draw(painter, ctx) |
| 50 | + |
| 51 | + painter.restore() |
| 52 | + |
| 53 | + def sizeHint(self, option, index): |
| 54 | + """QStyledItemDelegate.sizeHint implementation""" |
| 55 | + options = QtGui.QStyleOptionViewItemV4(option) |
| 56 | + self.initStyleOption(options, index) |
| 57 | + |
| 58 | + doc = QtGui.QTextDocument() |
| 59 | + doc.setDocumentMargin(1) |
| 60 | + # bad long (multiline) strings processing |
| 61 | + doc.setTextWidth(options.rect.width()) |
| 62 | + doc.setHtml(options.text) |
| 63 | + return QtCore.QSize(doc.idealWidth(), |
| 64 | + QtGui.QStyledItemDelegate.sizeHint(self, option, index).height()) |
| 65 | + |
| 66 | + |
| 67 | +class QHangupsConversationWidget(QtGui.QWidget, Ui_QHangupsConversationWidget): |
| 68 | + """Conversation tab""" |
| 69 | + def __init__(self, tab_parent, client, conv, parent=None): |
| 70 | + super().__init__(parent) |
| 71 | + self.setupUi(self) |
| 72 | + self.messagesListWidget.setItemDelegate(HTMLDelegate(self.messagesListWidget)) |
| 73 | + |
| 74 | + self.tab_parent = tab_parent |
| 75 | + self.client = client |
| 76 | + self.conv = conv |
| 77 | + |
| 78 | + self.client.on_disconnect.add_observer(self.on_disconnect) |
| 79 | + self.client.on_reconnect.add_observer(self.on_reconnect) |
| 80 | + self.conv.on_event.add_observer(self.on_event) |
| 81 | + |
| 82 | + self.sendButton.clicked.connect(self.on_send_clicked) |
| 83 | + |
| 84 | + self.num_unread = 0 |
| 85 | + for event in self.conv.events: |
| 86 | + self.on_event(event, set_title=False, set_unread=False) |
| 87 | + |
| 88 | + def set_title(self): |
| 89 | + """Update this conversation's tab title.""" |
| 90 | + title = get_conv_name(self.conv, truncate=True) |
| 91 | + conv_widget_id = self.tab_parent.conversationsTabWidget.indexOf(self) |
| 92 | + if self.num_unread > 0: |
| 93 | + title += ' ({})'.format(self.num_unread) |
| 94 | + self.tab_parent.conversationsTabWidget.tabBar().setTabTextColor(conv_widget_id, QtCore.Qt.darkBlue) |
| 95 | + else: |
| 96 | + self.tab_parent.conversationsTabWidget.tabBar().setTabTextColor(conv_widget_id, QtGui.QColor()) |
| 97 | + self.tab_parent.conversationsTabWidget.setTabText(conv_widget_id, title) |
| 98 | + self.tab_parent.conversationsTabWidget.setTabToolTip(conv_widget_id, title) |
| 99 | + |
| 100 | + def add_message(self, timestamp, text, username=None): |
| 101 | + """Add new message to list of messages""" |
| 102 | + datestr = "%d.%m. %H:%M" if timestamp.astimezone(tz=None).date() < datetime.date.today() else "%H:%M" |
| 103 | + message = "<b>{}{}:</b><br>\n{}<br>\n".format(timestamp.astimezone(tz=None).strftime(datestr), |
| 104 | + " | {}".format(username) if username is not None else "", |
| 105 | + text) |
| 106 | + item = QtGui.QListWidgetItem(message) |
| 107 | + self.messagesListWidget.addItem(item) |
| 108 | + self.messagesListWidget.scrollToBottom() |
| 109 | + |
| 110 | + def is_current(self): |
| 111 | + """Is this conversation in current tab?""" |
| 112 | + return self.tab_parent.conversationsTabWidget.currentWidget() is self |
| 113 | + |
| 114 | + def on_send_clicked(self): |
| 115 | + """Send button pressed (callback)""" |
| 116 | + text = self.messageTextEdit.toPlainText() |
| 117 | + if not text.strip(): |
| 118 | + return |
| 119 | + |
| 120 | + self.messageTextEdit.setEnabled(False) |
| 121 | + self.sendButton.setEnabled(False) |
| 122 | + |
| 123 | + segments = text_to_segments(text) |
| 124 | + asyncio.async( |
| 125 | + self.conv.send_message(segments) |
| 126 | + ).add_done_callback(self.on_message_sent) |
| 127 | + |
| 128 | + def on_message_sent(self, future): |
| 129 | + """Handle showing an error if a message fails to send (callback)""" |
| 130 | + try: |
| 131 | + future.result() |
| 132 | + except hangups.NetworkError: |
| 133 | + QtGui.QMessageBox.warning(self, self.tr("QHangups - Warning"), |
| 134 | + self.tr("Failed to send message!")) |
| 135 | + else: |
| 136 | + self.messageTextEdit.clear() |
| 137 | + finally: |
| 138 | + self.messageTextEdit.setEnabled(True) |
| 139 | + self.sendButton.setEnabled(True) |
| 140 | + |
| 141 | + def on_disconnect(self): |
| 142 | + """Show that Hangups has disconnected from server (callback)""" |
| 143 | + self.add_message(datetime.datetime.now(tz=datetime.timezone.utc), "<i>*** disconnected ***</i>") |
| 144 | + |
| 145 | + def on_reconnect(self): |
| 146 | + """Show that Hangups has reconnected to server (callback)""" |
| 147 | + self.add_message(datetime.datetime.now(tz=datetime.timezone.utc), "<i>*** connected ***</i>") |
| 148 | + |
| 149 | + def on_event(self, conv_event, set_title=True, set_unread=True): |
| 150 | + """Hangups event received (callback)""" |
| 151 | + user = self.conv.get_user(conv_event.user_id) |
| 152 | + |
| 153 | + if isinstance(conv_event, hangups.ChatMessageEvent): |
| 154 | + self.handle_message(conv_event, user, set_unread=set_unread) |
| 155 | + elif isinstance(conv_event, hangups.RenameEvent): |
| 156 | + self.handle_rename(conv_event, user) |
| 157 | + elif isinstance(conv_event, hangups.MembershipChangeEvent): |
| 158 | + self.handle_membership_change(conv_event, user) |
| 159 | + |
| 160 | + # Update the title in case unread count or conversation name changed. |
| 161 | + if set_title: |
| 162 | + self.set_title() |
| 163 | + |
| 164 | + def handle_message(self, conv_event, user, set_unread=True): |
| 165 | + """Handle received chat message""" |
| 166 | + self.add_message(conv_event.timestamp, message_to_html(conv_event), user.full_name) |
| 167 | + # Update the count of unread messages. |
| 168 | + if not user.is_self and set_unread and not self.is_current(): |
| 169 | + self.num_unread += 1 |
| 170 | + |
| 171 | + def handle_rename(self, conv_event, user): |
| 172 | + """Handle received rename event""" |
| 173 | + if conv_event.new_name == '': |
| 174 | + text = '<i>*** cleared the conversation name ***</i>' |
| 175 | + else: |
| 176 | + text = '<i>*** renamed the conversation to {} ***</i>'.format(conv_event.new_name) |
| 177 | + self.add_message(conv_event.timestamp, text, user.full_name) |
| 178 | + |
| 179 | + def handle_membership_change(self, conv_event, user): |
| 180 | + """Handle received membership change event""" |
| 181 | + event_users = [self.conv.get_user(user_id) for user_id in conv_event.participant_ids] |
| 182 | + names = ', '.join(user.full_name for user in event_users) |
| 183 | + if conv_event.type_ == hangups.MembershipChangeType.JOIN: |
| 184 | + self.add_message(conv_event.timestamp, |
| 185 | + '<i>*** added {} to the conversation ***</i>'.format(names), |
| 186 | + user.full_name) |
| 187 | + else: |
| 188 | + for name in names: |
| 189 | + self.add_message(conv_event.timestamp, |
| 190 | + '<i>*** left the conversation ***</i>', |
| 191 | + user.full_name) |
0 commit comments