]>
Commit | Line | Data |
---|---|---|
7dcf23b1 SH |
1 | #!/usr/bin/python |
2 | # -*- coding: utf-8 -*- | |
3 | # | |
beaa8775 | 4 | # Copyright (C) 2011-2012 Sebastien Helleu <flashcode@flashtux.org> |
7dcf23b1 SH |
5 | # |
6 | # This file is part of QWeeChat, a Qt remote GUI for WeeChat. | |
7 | # | |
8 | # QWeeChat is free software; you can redistribute it and/or modify | |
9 | # it under the terms of the GNU General Public License as published by | |
10 | # the Free Software Foundation; either version 3 of the License, or | |
11 | # (at your option) any later version. | |
12 | # | |
13 | # QWeeChat is distributed in the hope that it will be useful, | |
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | # GNU General Public License for more details. | |
17 | # | |
18 | # You should have received a copy of the GNU General Public License | |
19 | # along with QWeeChat. If not, see <http://www.gnu.org/licenses/>. | |
20 | # | |
21 | ||
22 | # | |
23 | # QWeeChat - WeeChat remote GUI using Qt toolkit. | |
24 | # (this script requires WeeChat 0.3.7 or newer, running on local or remote host) | |
25 | # | |
26 | # History: | |
27 | # | |
28 | # 2011-05-27, Sebastien Helleu <flashcode@flashtux.org>: | |
29 | # start dev | |
30 | # | |
31 | ||
32 | import sys, struct | |
33 | import qt_compat | |
34 | QtCore = qt_compat.import_module('QtCore') | |
35 | QtGui = qt_compat.import_module('QtGui') | |
36 | import config | |
37 | import weechat.protocol as protocol | |
7dcf23b1 SH |
38 | from network import Network |
39 | from connection import ConnectionDialog | |
40 | from buffer import BufferListWidget, Buffer | |
41 | from debug import DebugDialog | |
42 | from about import AboutDialog | |
43 | ||
f4848c2e | 44 | NAME = 'QWeeChat' |
8320f3a8 | 45 | VERSION = '0.0.1-dev' |
7dcf23b1 SH |
46 | AUTHOR = 'Sébastien Helleu' |
47 | AUTHOR_MAIL= 'flashcode@flashtux.org' | |
48 | WEECHAT_SITE = 'http://www.weechat.org/' | |
49 | ||
cc80cd81 SH |
50 | # number of lines in buffer for debug window |
51 | DEBUG_NUM_LINES = 50 | |
52 | ||
7dcf23b1 SH |
53 | |
54 | class MainWindow(QtGui.QMainWindow): | |
55 | """Main window.""" | |
56 | ||
57 | def __init__(self, *args): | |
67ae3204 | 58 | QtGui.QMainWindow.__init__(*(self,) + args) |
7dcf23b1 SH |
59 | |
60 | self.config = config.read() | |
61 | ||
7dcf23b1 SH |
62 | self.resize(1000, 600) |
63 | self.setWindowTitle(NAME) | |
64 | ||
cc80cd81 SH |
65 | self.debug_dialog = None |
66 | self.debug_lines = [] | |
67 | ||
7dcf23b1 SH |
68 | # network |
69 | self.network = Network() | |
70 | self.network.statusChanged.connect(self.network_status_changed) | |
71 | self.network.messageFromWeechat.connect(self.network_message_from_weechat) | |
72 | ||
73 | # list of buffers | |
74 | self.list_buffers = BufferListWidget() | |
75 | self.list_buffers.currentRowChanged.connect(self.buffer_switch) | |
76 | ||
77 | # default buffer | |
78 | self.buffers = [Buffer()] | |
79 | self.stacked_buffers = QtGui.QStackedWidget() | |
80 | self.stacked_buffers.addWidget(self.buffers[0].widget) | |
81 | ||
82 | # splitter with buffers + chat/input | |
83 | splitter = QtGui.QSplitter() | |
84 | splitter.addWidget(self.list_buffers) | |
85 | splitter.addWidget(self.stacked_buffers) | |
86 | ||
87 | self.setCentralWidget(splitter) | |
88 | ||
89 | if self.config.getboolean('look', 'statusbar'): | |
90 | self.statusBar().visible = True | |
91 | ||
92 | # actions for menu and toolbar | |
93 | actions_def = {'connect' : ['network-connect.png', 'Connect to WeeChat', 'Ctrl+O', self.open_connection_dialog], | |
94 | 'disconnect' : ['network-disconnect.png', 'Disconnect from WeeChat', 'Ctrl+D', self.network.disconnect_weechat], | |
95 | 'debug' : ['edit-find.png', 'Debug console window', 'Ctrl+B', self.open_debug_dialog], | |
96 | 'preferences': ['preferences-other.png', 'Preferences', 'Ctrl+P', self.open_preferences_dialog], | |
97 | 'about' : ['help-about.png', 'About', 'Ctrl+H', self.open_about_dialog], | |
98 | 'quit' : ['application-exit.png', 'Quit application', 'Ctrl+Q', self.close], | |
99 | } | |
100 | self.actions = {} | |
773ee7bb | 101 | for name, action in list(actions_def.items()): |
7dcf23b1 SH |
102 | self.actions[name] = QtGui.QAction(QtGui.QIcon('data/icons/%s' % action[0]), name.capitalize(), self) |
103 | self.actions[name].setStatusTip(action[1]) | |
104 | self.actions[name].setShortcut(action[2]) | |
105 | self.actions[name].triggered.connect(action[3]) | |
106 | ||
107 | # menu | |
108 | self.menu = self.menuBar() | |
109 | menu_file = self.menu.addMenu('&File') | |
110 | menu_file.addActions([self.actions['connect'], self.actions['disconnect'], | |
111 | self.actions['preferences'], self.actions['quit']]) | |
112 | menu_window = self.menu.addMenu('&Window') | |
113 | menu_window.addAction(self.actions['debug']) | |
114 | menu_help = self.menu.addMenu('&Help') | |
115 | menu_help.addAction(self.actions['about']) | |
116 | self.network_status = QtGui.QLabel() | |
117 | self.network_status.setFixedHeight(20) | |
118 | self.network_status.setFixedWidth(200) | |
119 | self.network_status.setContentsMargins(0, 0, 10, 0) | |
120 | self.network_status.setAlignment(QtCore.Qt.AlignRight) | |
121 | if hasattr(self.menu, 'setCornerWidget'): | |
122 | self.menu.setCornerWidget(self.network_status, QtCore.Qt.TopRightCorner) | |
123 | self.network_status_set(self.network.status_disconnected, None) | |
124 | ||
125 | # toolbar | |
126 | toolbar = self.addToolBar('toolBar') | |
127 | toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) | |
128 | toolbar.addActions([self.actions['connect'], self.actions['disconnect'], | |
129 | self.actions['debug'], self.actions['preferences'], | |
130 | self.actions['about'], self.actions['quit']]) | |
131 | ||
132 | self.buffers[0].widget.input.setFocus() | |
133 | ||
134 | # open debug dialog | |
135 | if self.config.getboolean('look', 'debug'): | |
136 | self.open_debug_dialog() | |
137 | ||
138 | # auto-connect to relay | |
139 | if self.config.getboolean('relay', 'autoconnect'): | |
140 | self.network.connect_weechat(self.config.get('relay', 'server'), | |
141 | self.config.get('relay', 'port'), | |
77b25057 | 142 | self.config.get('relay', 'ssl') == 'on', |
7dcf23b1 SH |
143 | self.config.get('relay', 'password')) |
144 | ||
145 | self.show() | |
146 | ||
147 | def buffer_switch(self, index): | |
148 | if index >= 0: | |
149 | self.stacked_buffers.setCurrentIndex(index) | |
150 | self.stacked_buffers.widget(index).input.setFocus() | |
151 | ||
152 | def buffer_input(self, full_name, text): | |
153 | if self.network.is_connected(): | |
154 | message = 'input %s %s\n' % (full_name, text) | |
155 | self.network.send_to_weechat(message) | |
3a5ec0c1 | 156 | self.debug_display(0, '<==', message, forcecolor='#AA0000') |
7dcf23b1 SH |
157 | |
158 | def open_preferences_dialog(self): | |
159 | pass # TODO | |
160 | ||
cc80cd81 SH |
161 | def debug_display(self, *args, **kwargs): |
162 | self.debug_lines.append((args, kwargs)) | |
163 | self.debug_lines = self.debug_lines[-DEBUG_NUM_LINES:] | |
164 | if self.debug_dialog: | |
165 | self.debug_dialog.chat.display(*args, **kwargs) | |
166 | ||
7dcf23b1 | 167 | def open_debug_dialog(self): |
cc80cd81 SH |
168 | if not self.debug_dialog: |
169 | self.debug_dialog = DebugDialog(self) | |
170 | self.debug_dialog.input.textSent.connect(self.debug_input_text_sent) | |
171 | self.debug_dialog.finished.connect(self.debug_dialog_closed) | |
172 | self.debug_dialog.display_lines(self.debug_lines) | |
173 | self.debug_dialog.chat.scroll_bottom() | |
7dcf23b1 SH |
174 | |
175 | def debug_input_text_sent(self, text): | |
176 | if self.network.is_connected(): | |
177 | text = str(text) | |
178 | pos = text.find(')') | |
179 | if text.startswith('(') and pos >= 0: | |
180 | text = '(debug_%s)%s' % (text[1:pos], text[pos+1:]) | |
181 | else: | |
182 | text = '(debug) %s' % text | |
3a5ec0c1 | 183 | self.debug_display(0, '<==', text, forcecolor='#AA0000') |
7dcf23b1 | 184 | self.network.send_to_weechat(text + '\n') |
7dcf23b1 SH |
185 | |
186 | def debug_dialog_closed(self, result): | |
cc80cd81 | 187 | self.debug_dialog = None |
7dcf23b1 SH |
188 | |
189 | def open_about_dialog(self): | |
190 | messages = ['<b>%s</b> %s' % (NAME, VERSION), | |
d1b4884d | 191 | '© 2011-2012 %s <<a href="mailto:%s">%s</a>>' % (AUTHOR, AUTHOR_MAIL, AUTHOR_MAIL), |
7dcf23b1 SH |
192 | '', |
193 | 'WeeChat site: <a href="%s">%s</a>' % (WEECHAT_SITE, WEECHAT_SITE), | |
194 | ''] | |
195 | self.about_dialog = AboutDialog(NAME, messages, self) | |
196 | ||
197 | def open_connection_dialog(self): | |
198 | values = {} | |
77b25057 | 199 | for option in ('server', 'port', 'ssl', 'password'): |
7dcf23b1 SH |
200 | values[option] = self.config.get('relay', option) |
201 | self.connection_dialog = ConnectionDialog(values, self) | |
202 | self.connection_dialog.dialog_buttons.accepted.connect(self.connect_weechat) | |
203 | ||
204 | def connect_weechat(self): | |
205 | self.network.connect_weechat(self.connection_dialog.fields['server'].text(), | |
206 | self.connection_dialog.fields['port'].text(), | |
77b25057 | 207 | self.connection_dialog.fields['ssl'].isChecked(), |
7dcf23b1 SH |
208 | self.connection_dialog.fields['password'].text()) |
209 | self.connection_dialog.close() | |
210 | ||
211 | def network_status_changed(self, status, extra): | |
212 | if self.config.getboolean('look', 'statusbar'): | |
213 | self.statusBar().showMessage(status) | |
3a5ec0c1 | 214 | self.debug_display(0, '', status, forcecolor='#0000AA') |
7dcf23b1 SH |
215 | self.network_status_set(status, extra) |
216 | ||
217 | def network_status_set(self, status, extra): | |
218 | pal = self.network_status.palette() | |
77b25057 | 219 | if status == self.network.status_connected: |
7dcf23b1 SH |
220 | pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('green')) |
221 | else: | |
222 | pal.setColor(self.network_status.foregroundRole(), QtGui.QColor('#aa0000')) | |
77b25057 | 223 | ssl = ' (SSL)' if status != self.network.status_disconnected and self.network.is_ssl() else '' |
7dcf23b1 SH |
224 | self.network_status.setPalette(pal) |
225 | icon = self.network.status_icon(status) | |
226 | if icon: | |
77b25057 | 227 | self.network_status.setText('<img src="data/icons/%s"> %s' % (icon, status.capitalize() + ssl)) |
7dcf23b1 SH |
228 | else: |
229 | self.network_status.setText(status.capitalize()) | |
230 | if status == self.network.status_disconnected: | |
231 | self.actions['connect'].setEnabled(True) | |
232 | self.actions['disconnect'].setEnabled(False) | |
233 | else: | |
234 | self.actions['connect'].setEnabled(False) | |
235 | self.actions['disconnect'].setEnabled(True) | |
236 | ||
237 | def network_message_from_weechat(self, message): | |
cc80cd81 SH |
238 | self.debug_display(0, '==>', |
239 | 'message (%d bytes):\n%s' | |
240 | % (len(message), protocol.hex_and_ascii(message, 20)), | |
3a5ec0c1 | 241 | forcecolor='#008800') |
77b25057 SH |
242 | try: |
243 | proto = protocol.Protocol() | |
244 | message = proto.decode(str(message)) | |
245 | if message.uncompressed: | |
246 | self.debug_display(0, '==>', | |
247 | 'message uncompressed (%d bytes):\n%s' | |
248 | % (message.size_uncompressed, | |
249 | protocol.hex_and_ascii(message.uncompressed, 20)), | |
250 | forcecolor='#008800') | |
251 | self.debug_display(0, '', 'Message: %s' % message) | |
252 | self.parse_message(message) | |
253 | except: | |
254 | print("Error while decoding message from WeeChat") | |
255 | self.network.disconnect_weechat() | |
7dcf23b1 SH |
256 | |
257 | def parse_message(self, message): | |
258 | if message.msgid.startswith('debug'): | |
cc80cd81 | 259 | self.debug_display(0, '', '(debug message, ignored)') |
7dcf23b1 SH |
260 | return |
261 | if message.msgid == 'listbuffers': | |
262 | for obj in message.objects: | |
263 | if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer': | |
264 | self.list_buffers.clear() | |
265 | while self.stacked_buffers.count() > 0: | |
266 | buf = self.stacked_buffers.widget(0) | |
267 | self.stacked_buffers.removeWidget(buf) | |
268 | self.buffers = [] | |
269 | for item in obj.value['items']: | |
ca67c3a7 SH |
270 | buf = self.create_buffer(item) |
271 | self.insert_buffer(len(self.buffers), buf) | |
7dcf23b1 SH |
272 | self.list_buffers.setCurrentRow(0) |
273 | self.buffers[0].widget.input.setFocus() | |
ca67c3a7 | 274 | elif message.msgid in ('listlines', '_buffer_line_added'): |
7dcf23b1 SH |
275 | for obj in message.objects: |
276 | if obj.objtype == 'hda' and obj.value['path'][-1] == 'line_data': | |
277 | for item in obj.value['items']: | |
ca67c3a7 SH |
278 | if message.msgid == 'listlines': |
279 | ptrbuf = item['__path'][0] | |
280 | else: | |
281 | ptrbuf = item['buffer'] | |
282 | index = [i for i, b in enumerate(self.buffers) if b.pointer() == ptrbuf] | |
283 | if index: | |
284 | self.buffers[index[0]].widget.chat.display(item['date'], | |
3a5ec0c1 SH |
285 | item['prefix'], |
286 | item['message']) | |
ca67c3a7 SH |
287 | elif message.msgid in ('_nicklist', 'nicklist'): |
288 | buffer_nicklist = {} | |
7dcf23b1 | 289 | for obj in message.objects: |
ca67c3a7 | 290 | if obj.objtype == 'hda' and obj.value['path'][-1] == 'nicklist_item': |
7dcf23b1 | 291 | for item in obj.value['items']: |
ca67c3a7 SH |
292 | index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]] |
293 | if index: | |
294 | if not item['__path'][0] in buffer_nicklist: | |
295 | self.buffers[index[0]].remove_all_nicks() | |
296 | buffer_nicklist[item['__path'][0]] = True | |
297 | if not item['group'] and item['visible']: | |
298 | self.buffers[index[0]].add_nick(item['prefix'], item['name']) | |
299 | elif message.msgid == '_buffer_opened': | |
300 | for obj in message.objects: | |
301 | if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer': | |
302 | for item in obj.value['items']: | |
303 | buf = self.create_buffer(item) | |
304 | index = self.find_buffer_index_for_insert(item['next_buffer']) | |
305 | self.insert_buffer(index, buf) | |
306 | elif message.msgid.startswith('_buffer_'): | |
307 | for obj in message.objects: | |
308 | if obj.objtype == 'hda' and obj.value['path'][-1] == 'buffer': | |
309 | for item in obj.value['items']: | |
310 | index = [i for i, b in enumerate(self.buffers) if b.pointer() == item['__path'][0]] | |
311 | if index: | |
312 | index = index[0] | |
97936653 SH |
313 | if message.msgid == '_buffer_type_changed': |
314 | self.buffers[index].data['type'] = item['type'] | |
315 | elif message.msgid in ('_buffer_moved', '_buffer_merged', '_buffer_unmerged'): | |
ca67c3a7 SH |
316 | buf = self.buffers[index] |
317 | buf.data['number'] = item['number'] | |
318 | self.remove_buffer(index) | |
319 | index2 = self.find_buffer_index_for_insert(item['next_buffer']) | |
320 | self.insert_buffer(index2, buf) | |
321 | elif message.msgid == '_buffer_renamed': | |
322 | self.buffers[index].data['full_name'] = item['full_name'] | |
323 | self.buffers[index].data['short_name'] = item['short_name'] | |
324 | elif message.msgid == '_buffer_title_changed': | |
325 | self.buffers[index].data['title'] = item['title'] | |
c728febd SH |
326 | self.buffers[index].update_title() |
327 | elif message.msgid.startswith('_buffer_localvar_'): | |
328 | self.buffers[index].data['local_variables'] = item['local_variables'] | |
329 | self.buffers[index].update_prompt() | |
ca67c3a7 SH |
330 | elif message.msgid == '_buffer_closing': |
331 | self.remove_buffer(index) | |
beaa8775 SH |
332 | elif message.msgid == '_upgrade': |
333 | self.network.desync_weechat() | |
334 | elif message.msgid == '_upgrade_ended': | |
335 | self.network.sync_weechat() | |
ca67c3a7 SH |
336 | |
337 | def create_buffer(self, item): | |
338 | buf = Buffer(item) | |
339 | buf.bufferInput.connect(self.buffer_input) | |
340 | buf.widget.input.bufferSwitchPrev.connect(self.list_buffers.switch_prev_buffer) | |
341 | buf.widget.input.bufferSwitchNext.connect(self.list_buffers.switch_next_buffer) | |
342 | return buf | |
343 | ||
344 | def insert_buffer(self, index, buf): | |
345 | self.buffers.insert(index, buf) | |
346 | self.list_buffers.insertItem(index, '%d. %s' % (buf.data['number'], buf.data['full_name'])) | |
347 | self.stacked_buffers.insertWidget(index, buf.widget) | |
348 | ||
349 | def remove_buffer(self, index): | |
350 | if self.list_buffers.currentRow == index and index > 0: | |
351 | self.list_buffers.setCurrentRow(index - 1) | |
352 | self.list_buffers.takeItem(index) | |
353 | self.stacked_buffers.removeWidget(self.stacked_buffers.widget(index)) | |
354 | self.buffers.pop(index) | |
355 | ||
356 | def find_buffer_index_for_insert(self, next_buffer): | |
357 | index = -1 | |
358 | if next_buffer == '0x0': | |
359 | index = len(self.buffers) | |
360 | else: | |
361 | index = [i for i, b in enumerate(self.buffers) if b.pointer() == next_buffer] | |
362 | if index: | |
363 | index = index[0] | |
364 | if index < 0: | |
365 | print('Warning: unable to find position for buffer, using end of list by default') | |
366 | index = len(self.buffers) | |
367 | return index | |
7dcf23b1 SH |
368 | |
369 | def closeEvent(self, event): | |
370 | self.network.disconnect_weechat() | |
cc80cd81 SH |
371 | if self.debug_dialog: |
372 | self.debug_dialog.close() | |
7dcf23b1 SH |
373 | config.write(self.config) |
374 | QtGui.QMainWindow.closeEvent(self, event) | |
375 | ||
376 | ||
377 | app = QtGui.QApplication(sys.argv) | |
378 | app.setStyle(QtGui.QStyleFactory.create('Cleanlooks')) | |
93865c21 | 379 | app.setWindowIcon(QtGui.QIcon('data/icons/weechat_icon_32.png')) |
7dcf23b1 SH |
380 | main = MainWindow() |
381 | sys.exit(app.exec_()) |