eric7/Cooperation/ChatWidget.py

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

eric ide

mercurial