src/eric7/Cooperation/ChatWidget.py

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

eric ide

mercurial