eric6/Cooperation/ChatWidget.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2010 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the chat dialog.
8 """
9
10 from __future__ import unicode_literals
11
12 from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QDateTime, QPoint, QFileInfo
13 from PyQt5.QtGui import QColor
14 from PyQt5.QtWidgets import QWidget, QListWidgetItem, QMenu, QApplication
15
16 from E5Gui.E5Application import e5App
17 from E5Gui import E5MessageBox, E5FileDialog
18
19 from Globals import recentNameHosts
20
21 from .CooperationClient import CooperationClient
22
23 from .Ui_ChatWidget import Ui_ChatWidget
24
25 import Preferences
26 import Utilities
27 import UI.PixmapCache
28
29
30 class ChatWidget(QWidget, Ui_ChatWidget):
31 """
32 Class implementing the chat dialog.
33
34 @signal connected(connected) emitted to signal a change of the connected
35 state (bool)
36 @signal editorCommand(hashStr, filename, message) emitted when an editor
37 command has been received (string, string, string)
38 @signal shareEditor(share) emitted to signal a share is requested (bool)
39 @signal startEdit() emitted to start a shared edit session
40 @signal sendEdit() emitted to send a shared edit session
41 @signal cancelEdit() emitted to cancel a shared edit session
42 """
43 connected = pyqtSignal(bool)
44 editorCommand = pyqtSignal(str, str, str)
45
46 shareEditor = pyqtSignal(bool)
47 startEdit = pyqtSignal()
48 sendEdit = pyqtSignal()
49 cancelEdit = pyqtSignal()
50
51 def __init__(self, ui, port=-1, parent=None):
52 """
53 Constructor
54
55 @param ui reference to the user interface object (UserInterface)
56 @param port port to be used for the cooperation server (integer)
57 @param parent reference to the parent widget (QWidget)
58 """
59 super(ChatWidget, self).__init__(parent)
60 self.setupUi(self)
61
62 self.shareButton.setIcon(
63 UI.PixmapCache.getIcon("sharedEditDisconnected.png"))
64 self.startEditButton.setIcon(
65 UI.PixmapCache.getIcon("sharedEditStart.png"))
66 self.sendEditButton.setIcon(
67 UI.PixmapCache.getIcon("sharedEditSend.png"))
68 self.cancelEditButton.setIcon(
69 UI.PixmapCache.getIcon("sharedEditCancel.png"))
70
71 self.__ui = ui
72 self.__client = CooperationClient(self)
73 self.__myNickName = self.__client.nickName()
74
75 self.__initChatMenu()
76 self.__initUsersMenu()
77
78 self.messageEdit.returnPressed.connect(self.__handleMessage)
79 self.sendButton.clicked.connect(self.__handleMessage)
80 self.__client.newMessage.connect(self.appendMessage)
81 self.__client.newParticipant.connect(self.__newParticipant)
82 self.__client.participantLeft.connect(self.__participantLeft)
83 self.__client.connectionError.connect(self.__showErrorMessage)
84 self.__client.cannotConnect.connect(self.__initialConnectionRefused)
85 self.__client.editorCommand.connect(self.__editorCommandMessage)
86
87 self.serverButton.setText(self.tr("Start Server"))
88 self.serverLed.setColor(QColor(Qt.red))
89 if port == -1:
90 port = Preferences.getCooperation("ServerPort")
91
92 self.serverPortSpin.setValue(port)
93
94 self.__setConnected(False)
95
96 if Preferences.getCooperation("AutoStartServer"):
97 self.on_serverButton_clicked()
98
99 self.recent = []
100 self.__loadHostsHistory()
101
102 def __loadHostsHistory(self):
103 """
104 Private method to load the recently connected hosts.
105 """
106 self.__recent = []
107 Preferences.Prefs.rsettings.sync()
108 rh = Preferences.Prefs.rsettings.value(recentNameHosts)
109 if rh is not None:
110 self.__recent = rh[:20]
111 self.hostEdit.clear()
112 self.hostEdit.addItems(self.__recent)
113 self.hostEdit.clearEditText()
114
115 def __saveHostsHistory(self):
116 """
117 Private method to save the list of recently connected hosts.
118 """
119 Preferences.Prefs.rsettings.setValue(recentNameHosts, self.__recent)
120 Preferences.Prefs.rsettings.sync()
121
122 def __setHostsHistory(self, host):
123 """
124 Private method to remember the given host as the most recent entry.
125
126 @param host host entry to remember (string)
127 """
128 if host in self.__recent:
129 self.__recent.remove(host)
130 self.__recent.insert(0, host)
131 self.__saveHostsHistory()
132 self.hostEdit.clear()
133 self.hostEdit.addItems(self.__recent)
134
135 def __clearHostsHistory(self):
136 """
137 Private slot to clear the hosts history.
138 """
139 self.__recent = []
140 self.__saveHostsHistory()
141 self.hostEdit.clear()
142 self.hostEdit.addItems(self.__recent)
143
144 def __handleMessage(self):
145 """
146 Private slot handling the Return key pressed in the message edit.
147 """
148 text = self.messageEdit.text()
149 if text == "":
150 return
151
152 if text.startswith("/"):
153 self.__showErrorMessage(
154 self.tr("! Unknown command: {0}\n")
155 .format(text.split()[0]))
156 else:
157 self.__client.sendMessage(text)
158 self.appendMessage(self.__myNickName, text)
159
160 self.messageEdit.clear()
161
162 def __newParticipant(self, nick):
163 """
164 Private slot handling a new participant joining.
165
166 @param nick nick name of the new participant (string)
167 """
168 if nick == "":
169 return
170
171 color = self.chatEdit.textColor()
172 self.chatEdit.setTextColor(Qt.gray)
173 self.chatEdit.append(
174 QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) +
175 ":")
176 self.chatEdit.append(self.tr("* {0} has joined.\n").format(nick))
177 self.chatEdit.setTextColor(color)
178
179 QListWidgetItem(
180 UI.PixmapCache.getIcon(
181 "chatUser{0}.png".format(1 + self.usersList.count() % 6)),
182 nick, self.usersList)
183
184 if not self.__connected:
185 self.__setConnected(True)
186
187 if not self.isVisible():
188 self.__ui.showNotification(
189 UI.PixmapCache.getPixmap("cooperation48.png"),
190 self.tr("New User"), self.tr("{0} has joined.")
191 .format(nick))
192
193 def __participantLeft(self, nick):
194 """
195 Private slot handling a participant leaving the session.
196
197 @param nick nick name of the participant (string)
198 """
199 if nick == "":
200 return
201
202 items = self.usersList.findItems(nick, Qt.MatchExactly)
203 for item in items:
204 self.usersList.takeItem(self.usersList.row(item))
205 del item
206
207 color = self.chatEdit.textColor()
208 self.chatEdit.setTextColor(Qt.gray)
209 self.chatEdit.append(
210 QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) +
211 ":")
212 self.chatEdit.append(self.tr("* {0} has left.\n").format(nick))
213 self.chatEdit.setTextColor(color)
214
215 if not self.__client.hasConnections():
216 self.__setConnected(False)
217
218 if not self.isVisible():
219 self.__ui.showNotification(
220 UI.PixmapCache.getPixmap("cooperation48.png"),
221 self.tr("User Left"), self.tr("{0} has left.")
222 .format(nick))
223
224 def appendMessage(self, from_, message):
225 """
226 Public slot to append a message to the display.
227
228 @param from_ originator of the message (string)
229 @param message message to be appended (string)
230 """
231 if from_ == "" or message == "":
232 return
233
234 self.chatEdit.append(
235 QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) +
236 " <" + from_ + ">:")
237 self.chatEdit.append(message + "\n")
238 bar = self.chatEdit.verticalScrollBar()
239 bar.setValue(bar.maximum())
240
241 if not self.isVisible():
242 self.__ui.showNotification(
243 UI.PixmapCache.getPixmap("cooperation48.png"),
244 self.tr("Message from <{0}>").format(from_), message)
245
246 @pyqtSlot(str)
247 def on_hostEdit_editTextChanged(self, host):
248 """
249 Private slot handling the entry of a host to connect to.
250
251 @param host host to connect to (string)
252 """
253 if not self.__connected:
254 self.connectButton.setEnabled(host != "")
255
256 def __getConnectionParameters(self):
257 """
258 Private method to determine the connection parameters.
259
260 @return tuple with hostname and port (string, integer)
261 """
262 hostEntry = self.hostEdit.currentText()
263 if "@" in hostEntry:
264 host, port = hostEntry.split("@")
265 try:
266 port = int(port)
267 except ValueError:
268 port = Preferences.getCooperation("ServerPort")
269 self.hostEdit.setEditText("{0}@{1}".format(host, port))
270 else:
271 host = hostEntry
272 port = Preferences.getCooperation("ServerPort")
273 self.hostEdit.setEditText("{0}@{1}".format(host, port))
274 return host, port
275
276 @pyqtSlot()
277 def on_connectButton_clicked(self):
278 """
279 Private slot initiating the connection.
280 """
281 if not self.__connected:
282 host, port = self.__getConnectionParameters()
283 self.__setHostsHistory(self.hostEdit.currentText())
284 if not self.__client.isListening():
285 self.on_serverButton_clicked()
286 if self.__client.isListening():
287 self.__client.connectToHost(host, port)
288 self.__setConnected(True)
289 else:
290 self.__client.disconnectConnections()
291 self.__setConnected(False)
292
293 @pyqtSlot()
294 def on_clearHostsButton_clicked(self):
295 """
296 Private slot to clear the hosts list.
297 """
298 self.__clearHostsHistory()
299
300 @pyqtSlot()
301 def on_serverButton_clicked(self):
302 """
303 Private slot to start the server.
304 """
305 if self.__client.isListening():
306 self.__client.close()
307 self.serverButton.setText(self.tr("Start Server"))
308 self.serverPortSpin.setEnabled(True)
309 if (self.serverPortSpin.value() !=
310 Preferences.getCooperation("ServerPort")):
311 self.serverPortSpin.setValue(
312 Preferences.getCooperation("ServerPort"))
313 self.serverLed.setColor(QColor(Qt.red))
314 else:
315 res, port = self.__client.startListening(
316 self.serverPortSpin.value())
317 if res:
318 self.serverButton.setText(self.tr("Stop Server"))
319 self.serverPortSpin.setValue(port)
320 self.serverPortSpin.setEnabled(False)
321 self.serverLed.setColor(QColor(Qt.green))
322 else:
323 self.__showErrorMessage(
324 self.tr("! Server Error: {0}\n").format(
325 self.__client.errorString())
326 )
327
328 def __setConnected(self, connected):
329 """
330 Private slot to set the connected state.
331
332 @param connected new connected state (boolean)
333 """
334 if connected:
335 self.connectButton.setText(self.tr("Disconnect"))
336 self.connectButton.setEnabled(True)
337 self.connectionLed.setColor(QColor(Qt.green))
338 else:
339 self.connectButton.setText(self.tr("Connect"))
340 self.connectButton.setEnabled(self.hostEdit.currentText() != "")
341 self.connectionLed.setColor(QColor(Qt.red))
342 self.on_cancelEditButton_clicked()
343 self.shareButton.setChecked(False)
344 self.on_shareButton_clicked(False)
345 self.__connected = connected
346 self.hostEdit.setEnabled(not connected)
347 self.serverButton.setEnabled(not connected)
348 self.sharingGroup.setEnabled(connected)
349
350 if connected:
351 vm = e5App().getObject("ViewManager")
352 aw = vm.activeWindow()
353 if aw:
354 self.checkEditorActions(aw)
355
356 def __showErrorMessage(self, message):
357 """
358 Private slot to show an error message.
359
360 @param message error message to show (string)
361 """
362 color = self.chatEdit.textColor()
363 self.chatEdit.setTextColor(Qt.red)
364 self.chatEdit.append(
365 QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) +
366 ":")
367 self.chatEdit.append(message + "\n")
368 self.chatEdit.setTextColor(color)
369
370 def __initialConnectionRefused(self):
371 """
372 Private slot to handle the refusal of the initial connection.
373 """
374 self.__setConnected(False)
375
376 def preferencesChanged(self):
377 """
378 Public slot to handle a change of preferences.
379 """
380 if not self.__client.isListening():
381 self.serverPortSpin.setValue(
382 Preferences.getCooperation("ServerPort"))
383 if Preferences.getCooperation("AutoStartServer"):
384 self.on_serverButton_clicked()
385
386 def getClient(self):
387 """
388 Public method to get a reference to the cooperation client.
389
390 @return reference to the cooperation client (CooperationClient)
391 """
392 return self.__client
393
394 def __editorCommandMessage(self, hashStr, fileName, message):
395 """
396 Private slot to handle editor command messages from the client.
397
398 @param hashStr hash of the project (string)
399 @param fileName project relative file name of the editor (string)
400 @param message command message (string)
401 """
402 self.editorCommand.emit(hashStr, fileName, message)
403
404 from QScintilla.Editor import Editor
405 if message.startswith(Editor.StartEditToken + Editor.Separator) or \
406 message.startswith(Editor.EndEditToken + Editor.Separator):
407 vm = e5App().getObject("ViewManager")
408 aw = vm.activeWindow()
409 if aw:
410 self.checkEditorActions(aw)
411
412 @pyqtSlot(bool)
413 def on_shareButton_clicked(self, checked):
414 """
415 Private slot to share the current editor.
416
417 @param checked flag indicating the button state (boolean)
418 """
419 if checked:
420 self.shareButton.setIcon(
421 UI.PixmapCache.getIcon("sharedEditConnected.png"))
422 else:
423 self.shareButton.setIcon(
424 UI.PixmapCache.getIcon("sharedEditDisconnected.png"))
425 self.startEditButton.setEnabled(checked)
426
427 self.shareEditor.emit(checked)
428
429 @pyqtSlot(bool)
430 def on_startEditButton_clicked(self, checked):
431 """
432 Private slot to start a shared edit session.
433
434 @param checked flag indicating the button state (boolean)
435 """
436 if checked:
437 self.sendEditButton.setEnabled(True)
438 self.cancelEditButton.setEnabled(True)
439 self.shareButton.setEnabled(False)
440 self.startEditButton.setEnabled(False)
441
442 self.startEdit.emit()
443
444 @pyqtSlot()
445 def on_sendEditButton_clicked(self):
446 """
447 Private slot to end a shared edit session and send the changes.
448 """
449 self.sendEditButton.setEnabled(False)
450 self.cancelEditButton.setEnabled(False)
451 self.shareButton.setEnabled(True)
452 self.startEditButton.setEnabled(True)
453 self.startEditButton.setChecked(False)
454
455 self.sendEdit.emit()
456
457 @pyqtSlot()
458 def on_cancelEditButton_clicked(self):
459 """
460 Private slot to cancel a shared edit session.
461 """
462 self.sendEditButton.setEnabled(False)
463 self.cancelEditButton.setEnabled(False)
464 self.shareButton.setEnabled(True)
465 self.startEditButton.setEnabled(True)
466 self.startEditButton.setChecked(False)
467
468 self.cancelEdit.emit()
469
470 def checkEditorActions(self, editor):
471 """
472 Public slot to set action according to an editor's state.
473
474 @param editor reference to the editor (Editor)
475 """
476 shareable, sharing, editing, remoteEditing = editor.getSharingStatus()
477
478 self.shareButton.setChecked(sharing)
479 if sharing:
480 self.shareButton.setIcon(
481 UI.PixmapCache.getIcon("sharedEditConnected.png"))
482 else:
483 self.shareButton.setIcon(
484 UI.PixmapCache.getIcon("sharedEditDisconnected.png"))
485 self.startEditButton.setChecked(editing)
486
487 self.shareButton.setEnabled(shareable and not editing)
488 self.startEditButton.setEnabled(
489 sharing and not editing and not remoteEditing)
490 self.sendEditButton.setEnabled(editing)
491 self.cancelEditButton.setEnabled(editing)
492
493 def __initChatMenu(self):
494 """
495 Private slot to initialize the chat edit context menu.
496 """
497 self.__chatMenu = QMenu(self)
498 self.__copyChatAct = \
499 self.__chatMenu.addAction(
500 UI.PixmapCache.getIcon("editCopy.png"),
501 self.tr("Copy"), self.__copyChat)
502 self.__chatMenu.addSeparator()
503 self.__cutAllChatAct = \
504 self.__chatMenu.addAction(
505 UI.PixmapCache.getIcon("editCut.png"),
506 self.tr("Cut all"), self.__cutAllChat)
507 self.__copyAllChatAct = \
508 self.__chatMenu.addAction(
509 UI.PixmapCache.getIcon("editCopy.png"),
510 self.tr("Copy all"), self.__copyAllChat)
511 self.__chatMenu.addSeparator()
512 self.__clearChatAct = \
513 self.__chatMenu.addAction(
514 UI.PixmapCache.getIcon("editDelete.png"),
515 self.tr("Clear"), self.__clearChat)
516 self.__chatMenu.addSeparator()
517 self.__saveChatAct = \
518 self.__chatMenu.addAction(
519 UI.PixmapCache.getIcon("fileSave.png"),
520 self.tr("Save"), self.__saveChat)
521
522 self.on_chatEdit_copyAvailable(False)
523
524 @pyqtSlot(bool)
525 def on_chatEdit_copyAvailable(self, yes):
526 """
527 Private slot to react to text selection/deselection of the chat edit.
528
529 @param yes flag signaling the availability of selected text (boolean)
530 """
531 self.__copyChatAct.setEnabled(yes)
532
533 @pyqtSlot(QPoint)
534 def on_chatEdit_customContextMenuRequested(self, pos):
535 """
536 Private slot to show the context menu for the chat.
537
538 @param pos the position of the mouse pointer (QPoint)
539 """
540 enable = self.chatEdit.toPlainText() != ""
541 self.__saveChatAct.setEnabled(enable)
542 self.__copyAllChatAct.setEnabled(enable)
543 self.__cutAllChatAct.setEnabled(enable)
544 self.__chatMenu.popup(self.chatEdit.mapToGlobal(pos))
545
546 def __clearChat(self):
547 """
548 Private slot to clear the contents of the chat display.
549 """
550 self.chatEdit.clear()
551
552 def __saveChat(self):
553 """
554 Private slot to save the contents of the chat display.
555 """
556 txt = self.chatEdit.toPlainText()
557 if txt:
558 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
559 self,
560 self.tr("Save Chat"),
561 "",
562 self.tr("Text Files (*.txt);;All Files (*)"),
563 None,
564 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
565 if fname:
566 ext = QFileInfo(fname).suffix()
567 if not ext:
568 ex = selectedFilter.split("(*")[1].split(")")[0]
569 if ex:
570 fname += ex
571 if QFileInfo(fname).exists():
572 res = E5MessageBox.yesNo(
573 self,
574 self.tr("Save Chat"),
575 self.tr("<p>The file <b>{0}</b> already exists."
576 " Overwrite it?</p>").format(fname),
577 icon=E5MessageBox.Warning)
578 if not res:
579 return
580 fname = Utilities.toNativeSeparators(fname)
581
582 try:
583 f = open(fname, "w", encoding="utf-8")
584 f.write(txt)
585 f.close()
586 except IOError as err:
587 E5MessageBox.critical(
588 self,
589 self.tr("Error saving Chat"),
590 self.tr("""<p>The chat contents could not be"""
591 """ written to <b>{0}</b></p>"""
592 """<p>Reason: {1}</p>""") .format(
593 fname, str(err)))
594
595 def __copyChat(self):
596 """
597 Private slot to copy the contents of the chat display to the clipboard.
598 """
599 self.chatEdit.copy()
600
601 def __copyAllChat(self):
602 """
603 Private slot to copy the contents of the chat display to the clipboard.
604 """
605 txt = self.chatEdit.toPlainText()
606 if txt:
607 cb = QApplication.clipboard()
608 cb.setText(txt)
609
610 def __cutAllChat(self):
611 """
612 Private slot to cut the contents of the chat display to the clipboard.
613 """
614 txt = self.chatEdit.toPlainText()
615 if txt:
616 cb = QApplication.clipboard()
617 cb.setText(txt)
618 self.chatEdit.clear()
619
620 def __initUsersMenu(self):
621 """
622 Private slot to initialize the users list context menu.
623 """
624 self.__usersMenu = QMenu(self)
625 self.__kickUserAct = \
626 self.__usersMenu.addAction(
627 UI.PixmapCache.getIcon("chatKickUser.png"),
628 self.tr("Kick User"), self.__kickUser)
629 self.__banUserAct = \
630 self.__usersMenu.addAction(
631 UI.PixmapCache.getIcon("chatBanUser.png"),
632 self.tr("Ban User"), self.__banUser)
633 self.__banKickUserAct = \
634 self.__usersMenu.addAction(
635 UI.PixmapCache.getIcon("chatBanKickUser.png"),
636 self.tr("Ban and Kick User"), self.__banKickUser)
637
638 @pyqtSlot(QPoint)
639 def on_usersList_customContextMenuRequested(self, pos):
640 """
641 Private slot to show the context menu for the users list.
642
643 @param pos the position of the mouse pointer (QPoint)
644 """
645 itm = self.usersList.itemAt(pos)
646 self.__kickUserAct.setEnabled(itm is not None)
647 self.__banUserAct.setEnabled(itm is not None)
648 self.__banKickUserAct.setEnabled(itm is not None)
649 self.__usersMenu.popup(self.usersList.mapToGlobal(pos))
650
651 def __kickUser(self):
652 """
653 Private slot to disconnect a user.
654 """
655 itm = self.usersList.currentItem()
656 self.__client.kickUser(itm.text())
657
658 color = self.chatEdit.textColor()
659 self.chatEdit.setTextColor(Qt.darkYellow)
660 self.chatEdit.append(
661 QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) +
662 ":")
663 self.chatEdit.append(self.tr("* {0} has been kicked.\n").format(
664 itm.text().split("@")[0]))
665 self.chatEdit.setTextColor(color)
666
667 def __banUser(self):
668 """
669 Private slot to ban a user.
670 """
671 itm = self.usersList.currentItem()
672 self.__client.banUser(itm.text())
673
674 color = self.chatEdit.textColor()
675 self.chatEdit.setTextColor(Qt.darkYellow)
676 self.chatEdit.append(
677 QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) +
678 ":")
679 self.chatEdit.append(self.tr("* {0} has been banned.\n").format(
680 itm.text().split("@")[0]))
681 self.chatEdit.setTextColor(color)
682
683 def __banKickUser(self):
684 """
685 Private slot to ban and kick a user.
686 """
687 itm = self.usersList.currentItem()
688 self.__client.banKickUser(itm.text())
689
690 color = self.chatEdit.textColor()
691 self.chatEdit.setTextColor(Qt.darkYellow)
692 self.chatEdit.append(
693 QDateTime.currentDateTime().toString(Qt.SystemLocaleLongDate) +
694 ":")
695 self.chatEdit.append(
696 self.tr("* {0} has been banned and kicked.\n")
697 .format(itm.text().split("@")[0]))
698 self.chatEdit.setTextColor(color)
699
700 def shutdown(self):
701 """
702 Public method to shut down the cooperation system.
703 """
704 self.__client.disconnectConnections()
705 self.__setConnected(False)

eric ide

mercurial