Skip to content

Commit 366a443

Browse files
committed
Initial commit
0 parents  commit 366a443

29 files changed

+2239
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
__pycache__/
2+
*.py[cod]
3+
4+
hangups
5+
purplex
6+
aiohttp
7+
quamash

LICENSE

+674
Large diffs are not rendered by default.

PKGBUILD

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Maintainer: Michal Krenek (Mikos) <[email protected]>
2+
pkgname=qhangups
3+
pkgver=1.0
4+
pkgrel=1
5+
pkgdesc="Alternative client for Google Hangouts written in PyQt"
6+
arch=('any')
7+
url="https://github.com/xmikos/qhangups"
8+
license=('GPL3')
9+
depends=('hangups-git' 'python-pyqt' 'python-appdirs')
10+
source=(https://github.com/xmikos/qhangups/archive/v$pkgver.tar.gz)
11+
12+
build() {
13+
cd "$srcdir/$pkgname-$pkgver"
14+
python2 setup.py build
15+
}
16+
17+
package() {
18+
cd "$srcdir/$pkgname-$pkgver"
19+
python2 setup.py install --root="$pkgdir"
20+
}
21+
22+
# vim:set ts=2 sw=2 et:

README.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
QHangups
2+
========
3+
4+
Alternative client for Google Hangouts written in PyQt
5+
6+
Requirements
7+
------------
8+
9+
- Python >= 3.3
10+
- PyQt >= 4.5
11+
- hangups (https://github.com/tdryer/hangups)
12+
- asyncio (https://pypi.python.org/pypi/asyncio) for Python < 3.4
13+
14+
Usage
15+
-----
16+
17+
Run `qhangups --help` to see all available options.
18+
Start QHangups by running `qhangups`.
19+
20+
The first time you start QHangups, you will be prompted to log into your
21+
Google account. Your credentials will only be sent to Google, and only
22+
session cookies will be stored locally. If you have trouble logging in,
23+
try logging in through a browser first.
24+
25+
Help
26+
----
27+
28+
usage: qhangups [-h] [-d] [--log LOG] [--cookies COOKIES]
29+
30+
optional arguments:
31+
-h, --help show this help message and exit
32+
-d, --debug log detailed debugging messages (default: False)
33+
--log LOG log file path (default:
34+
~/.local/share/QHangups/hangups.log)
35+
--cookies COOKIES cookie storage path (default:
36+
~/.local/share/QHangups/cookies.json)

flake8_test.sh

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash
2+
flake8 --exclude="qrc_*,ui_*" --ignore=E401 --max-line-length 119 --max-complexity 6 --show-source "$@"

qhangups.desktop

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[Desktop Entry]
2+
Encoding=UTF-8
3+
Version=1.0
4+
Name=QHangups
5+
GenericName=Google Hangouts client
6+
Comment=Alternative client for Google Hangouts written in PyQt
7+
Exec=qhangups
8+
Icon=qhangups
9+
StartupNotify=false
10+
Terminal=false
11+
Type=Application
12+
Categories=Qt;Network;InstantMessaging;

qhangups.png

1.45 KB
Loading

qhangups/__init__.py

Whitespace-only changes.

qhangups/conversations.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from PyQt4 import QtCore, QtGui
2+
3+
from qhangups.conversationwidget import QHangupsConversationWidget
4+
from qhangups.ui_qhangupsconversations import Ui_QHangupsConversations
5+
6+
7+
class QHangupsConversations(QtGui.QDialog, Ui_QHangupsConversations):
8+
"""Tabbed window with opened conversations"""
9+
def __init__(self, client, conv_list, parent=None):
10+
super().__init__(parent)
11+
self.setupUi(self)
12+
self.client = client
13+
self.conv_list = conv_list
14+
self.conv_widgets = {}
15+
16+
self.conversationsTabWidget.currentChanged.connect(self.on_tab_current_changed)
17+
self.conversationsTabWidget.tabCloseRequested.connect(self.on_tab_close_requested)
18+
19+
def get_conv_widget(self, conv_id):
20+
"""Return an existing or new QHangupsConversationWidget"""
21+
if conv_id not in self.conv_widgets:
22+
conv = self.conv_list.get(conv_id)
23+
conv_widget = QHangupsConversationWidget(self, self.client, conv)
24+
self.conv_widgets[conv_id] = conv_widget
25+
self.conversationsTabWidget.addTab(conv_widget, "")
26+
conv_widget.set_title()
27+
return self.conv_widgets[conv_id]
28+
29+
def set_conv_tab(self, conv_id, switch=False, title=None):
30+
"""Add conversation tab (if not present) and optionally switch to it"""
31+
conv_widget = self.get_conv_widget(conv_id)
32+
conv_widget_id = self.conversationsTabWidget.indexOf(conv_widget)
33+
34+
if switch:
35+
self.conversationsTabWidget.setCurrentWidget(conv_widget)
36+
37+
if title:
38+
self.conversationsTabWidget.setTabText(conv_widget_id, title)
39+
self.conversationsTabWidget.setTabToolTip(conv_widget_id, title)
40+
41+
def on_tab_current_changed(self, conv_widget_id):
42+
"""Current tab changed (callback)"""
43+
conv_widget = self.conversationsTabWidget.widget(conv_widget_id)
44+
if conv_widget:
45+
conv_widget.num_unread = 0
46+
conv_widget.set_title()
47+
48+
def on_tab_close_requested(self, conv_widget_id):
49+
"""Tab close button clicked (callback)"""
50+
conv_widget = self.conversationsTabWidget.widget(conv_widget_id)
51+
self.conversationsTabWidget.removeTab(conv_widget_id)
52+
del self.conv_widgets[conv_widget.conv.id_]
53+
if not self.conv_widgets:
54+
self.close()

qhangups/conversationslist.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from PyQt4 import QtCore, QtGui
2+
3+
import hangups
4+
from hangups.utils import get_conv_name
5+
6+
from qhangups.ui_qhangupsconversationslist import Ui_QHangupsConversationsList
7+
8+
9+
class QHangupsConversationsList(QtGui.QDialog, Ui_QHangupsConversationsList):
10+
"""Window with list of conversations"""
11+
def __init__(self, client, conv_list, parent=None):
12+
super().__init__(parent)
13+
self.setupUi(self)
14+
self.client = client
15+
self.conv_list = conv_list
16+
17+
self.conv_list.on_event.add_observer(self.on_event)
18+
self.conversationsListWidget.itemActivated.connect(self.on_item_activated)
19+
20+
self.update_conversations()
21+
22+
def update_conversations(self):
23+
"""Update list of conversations"""
24+
self.conversationsListWidget.clear()
25+
for conv in sorted(self.conv_list.get_all(), reverse=True, key=lambda c: c.last_modified):
26+
item = QtGui.QListWidgetItem(get_conv_name(conv, truncate=True))
27+
item.setToolTip(get_conv_name(conv))
28+
item.setData(QtCore.Qt.UserRole, conv.id_)
29+
self.conversationsListWidget.addItem(item)
30+
31+
def on_item_activated(self, item):
32+
"""List item activated (callback)"""
33+
self.parent().open_messages_dialog(item.data(QtCore.Qt.UserRole))
34+
35+
def on_event(self, conv_event):
36+
"""Hangups event received (callback)"""
37+
if isinstance(conv_event, hangups.RenameEvent):
38+
self.update_conversations()

qhangups/conversationwidget.py

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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)

qhangups/languages/qhangups_cs.qm

2.36 KB
Binary file not shown.

0 commit comments

Comments
 (0)