import logging from string import Template from time import strftime from datetime import datetime, timedelta try: from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtGui import QShortcut, QAction except ImportError: print("PyQt5 fallback (convo.py)") from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtWidgets import QAction, QShortcut from dataobjs import PesterHistory from parsetools import convertTags, lexMessage, mecmd, colorBegin, colorEnd, smiledict import parsetools PchumLog = logging.getLogger("pchumLogger") class PesterTabWindow(QtWidgets.QFrame): def __init__(self, mainwindow, parent=None, convo="convo"): super().__init__(parent) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_QuitOnClose, False) self.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) self.mainwindow = mainwindow self.tabs = QtWidgets.QTabBar(self) self.tabs.setMovable(True) self.tabs.setTabsClosable(True) self.tabs.currentChanged[int].connect(self.changeTab) self.tabs.tabCloseRequested[int].connect(self.tabClose) self.tabs.tabMoved[int, int].connect(self.tabMoved) self.shortcuts = {} self.shortcuts["tabNext"] = QShortcut( QtGui.QKeySequence("Ctrl+j"), self, context=QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut, ) self.shortcuts["tabLast"] = QShortcut( QtGui.QKeySequence("Ctrl+k"), self, context=QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut, ) # Note that we use reversed keys here. self.shortcuts["tabUp"] = QShortcut( QtGui.QKeySequence("Ctrl+PgDown"), self, context=QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut, ) self.shortcuts["tabDn"] = QShortcut( QtGui.QKeySequence("Ctrl+PgUp"), self, context=QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut, ) self.shortcuts["tabNext"].activated.connect(self.nudgeTabNext) self.shortcuts["tabUp"].activated.connect(self.nudgeTabNext) self.shortcuts["tabLast"].activated.connect(self.nudgeTabLast) self.shortcuts["tabDn"].activated.connect(self.nudgeTabLast) self.initTheme(self.mainwindow.theme) self.layout = QtWidgets.QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.tabs) self.setLayout(self.layout) self.convos = {} self.tabIndices = {} self.currentConvo = None self.changedTab = False self.softclose = False self.type = convo # get default tab color i guess self.defaultTabTextColor = self.getTabTextColor() def getTabTextColor(self): # ugly, ugly hack self.changedTab = True i = self.tabs.addTab(".") c = self.tabs.tabTextColor(i) self.tabs.removeTab(i) self.changedTab = False return c def addChat(self, convo): self.convos[convo.title()] = convo # either addTab or setCurrentIndex will trigger changed() newindex = self.tabs.addTab(convo.title()) self.tabIndices[convo.title()] = newindex self.tabs.setCurrentIndex(newindex) self.tabs.setTabIcon(newindex, convo.icon()) def showChat(self, handle): tabi = self.tabIndices[handle] if self.tabs.currentIndex() == tabi: self.activateWindow() self.raise_() self.convos[handle].raiseChat() else: self.tabs.setCurrentIndex(tabi) """ There are two instances of "convoHasFocus" for some reason? This one seems to just get redefined. def convoHasFocus(self, convo): if ((self.hasFocus() or self.tabs.hasFocus()) and self.tabs.tabText(self.tabs.currentIndex()) == convo.title()): return True """ def isBot(self, *args, **kwargs): return self.mainwindow.isBot(*args, **kwargs) def keyPressEvent(self, event): # TODO: Clean this up. Our text areas now call this. keypress = event.key() mods = event.modifiers() if ( mods & QtCore.Qt.KeyboardModifier.ControlModifier ) and keypress == QtCore.Qt.Key.Key_Tab: handles = list(self.convos.keys()) waiting = self.mainwindow.waitingMessages.waitingHandles() waitinghandles = list(set(handles) & set(waiting)) if len(waitinghandles) > 0: nexti = self.tabIndices[waitinghandles[0]] else: nexti = ( self.tabIndices[self.currentConvo.title()] + 1 ) % self.tabs.count() self.tabs.setCurrentIndex(nexti) @QtCore.pyqtSlot() def nudgeTabNext(self): return self.nudgeTabIndex(+1) @QtCore.pyqtSlot() def nudgeTabLast(self): return self.nudgeTabIndex(-1) def nudgeTabIndex(self, direction): # Inverted controls. Might add an option for this if people want # it. # ~if keypress == QtCore.Qt.Key.Key_PageDown: # ~ direction = 1 # ~elif keypress == QtCore.Qt.Key.Key_PageUp: # ~ direction = -1 # ...Processing... tabs = self.tabs # Pick our new index by sliding up or down the tab range. # NOTE: This feels like it could error. In fact, it /will/ if # there are no tabs, but...that shouldn't happen, should it? # There are probably other scenarios, too, so we'll have to # check on this later. # # Calculate the new index. ct = tabs.count() cind = tabs.currentIndex() nind = cind + direction if nind > (ct - 1): # The new index would be higher than the maximum; loop. nind = nind % ct # Otherwise, negative syntax should get it for us. nind = list(range(ct))[nind] # Change to the selected tab. # Note that this will send out the usual callbacks that handle # focusing and such. tabs.setCurrentIndex(nind) def contextMenuEvent(self, event): # ~if event.reason() == QtGui.QContextMenuEvent.Reason.Mouse: tabi = self.tabs.tabAt(event.pos()) if tabi < 0: tabi = self.tabs.currentIndex() for h, i in list(self.tabIndices.items()): if i == tabi: # Our index matches, grab the object using our handle. convo = self.convos[h] break else: # No matches return # Pop up the options menu of the relevant tab. convo.contextMenuEvent(event) def closeSoft(self): self.softclose = True self.close() def updateBlocked(self, handle): i = self.tabIndices[handle] icon = QtGui.QIcon(self.mainwindow.theme["main/chums/moods/blocked/icon"]) self.tabs.setTabIcon(i, icon) if self.tabs.currentIndex() == i: self.setWindowIcon(icon) def updateMood(self, handle, mood, unblocked=False): i = self.tabIndices[handle] if handle in self.mainwindow.config.getBlocklist() and not unblocked: icon = QtGui.QIcon(self.mainwindow.theme["main/chums/moods/blocked/icon"]) else: icon = mood.icon(self.mainwindow.theme) self.tabs.setTabIcon(i, icon) if self.tabs.currentIndex() == i: self.setWindowIcon(icon) def closeEvent(self, event): if not self.softclose: while self.tabs.count() > 0: self.tabClose(0) self.windowClosed.emit() def focusInEvent(self, event): # make sure we're not switching tabs! i = self.tabs.tabAt(self.mapFromGlobal(QtGui.QCursor.pos())) if i == -1: i = self.tabs.currentIndex() handle = self.tabs.tabText(i) self.clearNewMessage(handle) def convoHasFocus(self, handle): i = self.tabIndices[handle] if self.tabs.currentIndex() == i and (self.hasFocus() or self.tabs.hasFocus()): return True else: return False def notifyNewMessage(self, handle): i = self.tabIndices[handle] self.tabs.setTabTextColor( i, QtGui.QColor(self.mainwindow.theme["%s/tabs/newmsgcolor" % (self.type)]) ) convo = self.convos[handle] # Create a function for the icon to use # TODO: Let us disable this. def func(): convo.showChat() self.mainwindow.waitingMessages.addMessage(handle, func) # set system tray def clearNewMessage(self, handle): try: i = self.tabIndices[handle] self.tabs.setTabTextColor(i, self.defaultTabTextColor) except KeyError: pass self.mainwindow.waitingMessages.messageAnswered(handle) def initTheme(self, theme): self.resize(*theme["convo/size"]) self.setStyleSheet(theme["convo/tabwindow/style"]) self.tabs.setShape(QtWidgets.QTabBar.Shape(theme["convo/tabs/tabstyle"])) self.tabs.setStyleSheet( "QTabBar::tab{ %s } QTabBar::tab:selected { %s }" % (theme["convo/tabs/style"], theme["convo/tabs/selectedstyle"]) ) def changeTheme(self, theme): self.initTheme(theme) for c in list(self.convos.values()): tabi = self.tabIndices[c.title()] self.tabs.setTabIcon(tabi, c.icon()) currenttabi = self.tabs.currentIndex() if currenttabi >= 0: currentHandle = self.tabs.tabText(self.tabs.currentIndex()) self.setWindowIcon(self.convos[currentHandle].icon()) self.defaultTabTextColor = self.getTabTextColor() @QtCore.pyqtSlot(int) def tabClose(self, i): handle = self.tabs.tabText(i) self.mainwindow.waitingMessages.messageAnswered(handle) # print(self.convos.keys()) # I, legit don' t know why this is an issue, but, uh, yeah- try: convo = self.convos[handle] except: # handle = handle.replace("&","") handle = "".join(handle.split("&", 1)) convo = self.convos[handle] del self.convos[handle] del self.tabIndices[handle] self.tabs.removeTab(i) for h, j in self.tabIndices.items(): if j > i: self.tabIndices[h] = j - 1 self.layout.removeWidget(convo) convo.close() if self.tabs.count() == 0: self.close() return if self.currentConvo == convo: currenti = self.tabs.currentIndex() currenth = self.tabs.tabText(currenti) self.currentConvo = self.convos[currenth] self.currentConvo.raiseChat() @QtCore.pyqtSlot(int) def changeTab(self, i): if i < 0: return if self.changedTab: self.changedTab = False return handle = self.tabs.tabText(i) convo = self.convos[handle] if self.currentConvo: self.layout.removeWidget(self.currentConvo) self.currentConvo = convo self.layout.addWidget(convo) self.setWindowIcon(convo.icon()) self.setWindowTitle(convo.title()) self.activateWindow() self.raise_() convo.raiseChat() @QtCore.pyqtSlot(int, int) def tabMoved(self, to, fr): l = self.tabIndices for i in l: if l[i] == fr: oldpos = i if l[i] == to: newpos = i l[oldpos] = to l[newpos] = fr windowClosed = QtCore.pyqtSignal() class PesterMovie(QtGui.QMovie): def __init__(self, parent): super().__init__(parent) self.textwindow = parent @QtCore.pyqtSlot(int) def animate(self, frame): text = self.textwindow if text.mainwindow.config.animations(): movie = self url = text.urls[movie].toString() html = text.toHtml() if html.find(url) != -1: try: # PyQt6 resource_type = QtGui.QTextDocument.ResourceType.ImageResource.value except AttributeError: # PyQt5 resource_type = QtGui.QTextDocument.ResourceType.ImageResource if text.hasTabs: i = text.tabobject.tabIndices[text.parent().title()] if text.tabobject.tabs.currentIndex() == i: text.document().addResource( resource_type, text.urls[movie], movie.currentPixmap(), ) text.setLineWrapColumnOrWidth(text.lineWrapColumnOrWidth()) else: text.document().addResource( resource_type, text.urls[movie], movie.currentPixmap(), ) text.setLineWrapColumnOrWidth(text.lineWrapColumnOrWidth()) class PesterText(QtWidgets.QTextEdit): def __init__(self, theme, parent=None): super().__init__(parent) if hasattr(self.parent(), "mainwindow"): self.mainwindow = self.parent().mainwindow else: self.mainwindow = self.parent() if isinstance(parent.parent, PesterTabWindow): self.tabobject = parent.parent() self.hasTabs = True else: self.hasTabs = False self.initTheme(theme) self.setReadOnly(True) self.setMouseTracking(True) self.textSelected = False self.copyAvailable.connect(self.textReady) # (bool yes) self.urls = {} self.lastmsg = None for k in smiledict: self.addAnimation( QtCore.QUrl("smilies/%s" % (smiledict[k])), "smilies/%s" % (smiledict[k]), ) # self.mainwindow.animationSetting[bool].connect(self.animateChanged) def addAnimation(self, url, fileName): # We don't need to treat images formats like .png as animation, # this always opens a file handler otherwise, "movie.frameCount() > 1" isn't sufficient. # Might be useful to use QImageReader's supportsAnimation function for this? # As long as we're only using gifs there's no advantage to that though. if not fileName.endswith(".gif"): return movie = PesterMovie(self) movie.setFileName(fileName) self.urls[movie] = url movie.frameChanged.connect(movie.animate) # (int frameNumber) """ @QtCore.pyqtSlot(bool) def animateChanged(self, animate): PchumLog.warning("aaa") if animate: for m in self.urls: html = str(self.toHtml()) if html.find(self.urls[m].toString()) != -1: if m.frameCount() > 1: m.start() else: for m in self.urls: html = str(self.toHtml()) if html.find(self.urls[m].toString()) != -1: if m.frameCount() > 1: m.stop() """ @QtCore.pyqtSlot(bool) def textReady(self, ready): self.textSelected = ready def initTheme(self, theme): if "convo/scrollbar" in theme: self.setStyleSheet( "QTextEdit { %s }" "QScrollBar:vertical { %s }" "QScrollBar::handle:vertical { %s }" "QScrollBar::add-line:vertical { %s }" "QScrollBar::sub-line:vertical { %s }" "QScrollBar:up-arrow:vertical { %s }" "QScrollBar:down-arrow:vertical { %s }" % ( theme["convo/textarea/style"], theme["convo/scrollbar/style"], theme["convo/scrollbar/handle"], theme["convo/scrollbar/downarrow"], theme["convo/scrollbar/uparrow"], theme["convo/scrollbar/uarrowstyle"], theme["convo/scrollbar/darrowstyle"], ) ) else: self.setStyleSheet("QTextEdit { %s }" % (theme["convo/textarea/style"])) def addMessage(self, lexmsg, chum): if len(lexmsg) == 0: return color = chum.colorcmd() systemColor = QtGui.QColor( self.parent().mainwindow.theme["convo/systemMsgColor"] ) initials = chum.initials() parent = self.parent() window = parent.mainwindow me = window.profile() if self.mainwindow.config.animations(): for m in self.urls: if convertTags(lexmsg).find(self.urls[m].toString()) != -1: if m.state() == QtGui.QMovie.MovieState.NotRunning: m.start() if self.parent().mainwindow.config.showTimeStamps(): if self.parent().mainwindow.config.time12Format(): time = strftime("[%I:%M") else: time = strftime("[%H:%M") if self.parent().mainwindow.config.showSeconds(): time += strftime(":%S] ") else: time += "] " else: time = "" if lexmsg[0] == "PESTERCHUM:BEGIN": parent.setChumOpen(True) pmsg = chum.pestermsg( me, systemColor, window.theme["convo/text/beganpester"] ) window.chatlog.log(chum.handle, pmsg) self.append(convertTags(pmsg)) elif lexmsg[0] == "PESTERCHUM:CEASE": parent.setChumOpen(False) pmsg = chum.pestermsg( me, systemColor, window.theme["convo/text/ceasepester"] ) window.chatlog.log(chum.handle, pmsg) self.append(convertTags(pmsg)) elif lexmsg[0] == "PESTERCHUM:BLOCK": pmsg = chum.pestermsg(me, systemColor, window.theme["convo/text/blocked"]) window.chatlog.log(chum.handle, pmsg) self.append(convertTags(pmsg)) elif lexmsg[0] == "PESTERCHUM:UNBLOCK": pmsg = chum.pestermsg(me, systemColor, window.theme["convo/text/unblocked"]) window.chatlog.log(chum.handle, pmsg) self.append(convertTags(pmsg)) elif lexmsg[0] == "PESTERCHUM:BLOCKED": pmsg = chum.pestermsg( me, systemColor, window.theme["convo/text/blockedmsg"] ) window.chatlog.log(chum.handle, pmsg) self.append(convertTags(pmsg)) elif lexmsg[0] == "PESTERCHUM:IDLE": imsg = chum.idlemsg(systemColor, window.theme["convo/text/idle"]) window.chatlog.log(chum.handle, imsg) self.append(convertTags(imsg)) elif isinstance(lexmsg[0], mecmd): memsg = chum.memsg(systemColor, lexmsg) if chum is me: window.chatlog.log(parent.chum.handle, memsg) else: window.chatlog.log(chum.handle, memsg) self.append(time + convertTags(memsg)) else: if not parent.chumopen and chum is not me: beginmsg = chum.pestermsg( me, systemColor, window.theme["convo/text/beganpester"] ) parent.setChumOpen(True) window.chatlog.log(chum.handle, beginmsg) self.append(convertTags(beginmsg)) lexmsg[0:0] = [colorBegin("" % (color), color), "%s: " % (initials)] lexmsg.append(colorEnd("")) self.append( '' + time + convertTags(lexmsg) + "" ) # self.append('' # + '' # + '' # + ''); if chum is me: window.chatlog.log(parent.chum.handle, lexmsg) else: if ( (window.idler["auto"] or window.idler["manual"]) and parent.chumopen and not parent.isBot(chum.handle) ): idlethreshhold = 60 do_idle_send = False if self.lastmsg is None: do_idle_send = True else: if datetime.now() - self.lastmsg > timedelta(0, idlethreshhold): do_idle_send = True if do_idle_send: verb = window.theme["convo/text/idle"] idlemsg = me.idlemsg(systemColor, verb) parent.textArea.append(convertTags(idlemsg)) window.chatlog.log(chum.handle, idlemsg) parent.messageSent.emit("PESTERCHUM:IDLE", parent.title()) self.lastmsg = datetime.now() window.chatlog.log(chum.handle, lexmsg) def changeTheme(self, theme): self.initTheme(theme) sb = self.verticalScrollBar() sb.setValue(sb.maximum()) def focusInEvent(self, event): self.parent().clearNewMessage() QtWidgets.QTextEdit.focusInEvent(self, event) def isBot(self, *args, **kwargs): return self.parent().isBot(*args, **kwargs) def keyPressEvent(self, event): # First parent is the PesterConvo containing this. # Second parent is the PesterTabWindow containing *it*. pass_to_super = ( QtCore.Qt.Key.Key_PageUp, QtCore.Qt.Key.Key_PageDown, QtCore.Qt.Key.Key_Up, QtCore.Qt.Key.Key_Down, ) parent = self.parent() key = event.key() # keymods = event.modifiers() if hasattr(parent, "textInput") and key not in pass_to_super: # TODO: Shift focus here on bare (no modifiers) alphanumerics. parent.textInput.keyPressEvent(event) # Pass to the normal handler. super().keyPressEvent(event) def mousePressEvent(self, event): if event.button() == QtCore.Qt.MouseButton.LeftButton: try: # PyQt6 url = self.anchorAt(event.position().toPoint()) except AttributeError: # PyQt5 url = self.anchorAt(event.pos()) if url != "": if url[0] == "#" and url != "#pesterchum": self.parent().mainwindow.showMemos(url[1:]) elif url[0] == "@": handle = url[1:] self.parent().mainwindow.newConversation(handle) else: if event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier: QtWidgets.QApplication.clipboard().setText(url) else: QtGui.QDesktopServices.openUrl( QtCore.QUrl(url, QtCore.QUrl.ParsingMode.TolerantMode) ) QtWidgets.QTextEdit.mousePressEvent(self, event) def mouseMoveEvent(self, event): QtWidgets.QTextEdit.mouseMoveEvent(self, event) try: # PyQt6 pos = event.position().toPoint() except AttributeError: # PyQt5 pos = event.pos() if self.anchorAt(pos): if ( self.viewport().cursor().shape != QtCore.Qt.CursorShape.PointingHandCursor ): self.viewport().setCursor( QtGui.QCursor(QtCore.Qt.CursorShape.PointingHandCursor) ) else: self.viewport().setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.IBeamCursor)) def contextMenuEvent(self, event): textMenu = self.createStandardContextMenu() textMenu.exec(event.globalPos()) class PesterInput(QtWidgets.QLineEdit): stylesheet_path = "convo/input/style" def __init__(self, theme, parent=None): super().__init__(parent) self.changeTheme(theme) def changeTheme(self, theme): # Explicitly set color if not already set. # (Some platforms seem to default to white instead of black.) StyleSheet = theme[self.stylesheet_path] if "color:" not in theme[self.stylesheet_path].replace(" ", ""): StyleSheet = "color: black; " + StyleSheet self.setStyleSheet(StyleSheet) def focusInEvent(self, event): self.parent().clearNewMessage() self.parent().textArea.textCursor().clearSelection() super().focusInEvent(event) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key.Key_Up: text = self.text() next = self.parent().history.next(text) if next is not None: self.setText(next) elif event.key() == QtCore.Qt.Key.Key_Down: prev = self.parent().history.prev() if prev is not None: self.setText(prev) elif event.key() in [QtCore.Qt.Key.Key_PageUp, QtCore.Qt.Key.Key_PageDown]: self.parent().textArea.keyPressEvent(event) self.parent().mainwindow.idler["time"] = 0 super().keyPressEvent(event) class PesterConvo(QtWidgets.QFrame): def __init__(self, chum, initiated, mainwindow, parent=None): super().__init__(parent) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_QuitOnClose, False) self.setObjectName(chum.handle) self.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) self.chum = chum self.mainwindow = mainwindow theme = self.mainwindow.theme self.resize(*theme["convo/size"]) self.setStyleSheet( "QtWidgets.QFrame#{} {{ {} }}".format(chum.handle, theme["convo/style"]) ) self.setWindowIcon(self.icon()) self.setWindowTitle(self.title()) t = Template(self.mainwindow.theme["convo/chumlabel/text"]) self.chumLabel = QtWidgets.QLabel(t.safe_substitute(handle=chum.handle), self) self.chumLabel.setStyleSheet(self.mainwindow.theme["convo/chumlabel/style"]) self.chumLabel.setAlignment( self.aligndict["h"][self.mainwindow.theme["convo/chumlabel/align/h"]] | self.aligndict["v"][self.mainwindow.theme["convo/chumlabel/align/v"]] ) self.chumLabel.setMaximumHeight( self.mainwindow.theme["convo/chumlabel/maxheight"] ) self.chumLabel.setMinimumHeight( self.mainwindow.theme["convo/chumlabel/minheight"] ) self.chumLabel.setSizePolicy( QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, ) ) self.textArea = PesterText(self.mainwindow.theme, self) self.textInput = PesterInput(self.mainwindow.theme, self) self.textInput.setFocus() self.textInput.returnPressed.connect(self.sentMessage) self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.chumLabel) self.layout.addWidget(self.textArea) self.layout.addWidget(self.textInput) self.layout.setSpacing(0) margins = self.mainwindow.theme["convo/margins"] self.layout.setContentsMargins( margins["left"], margins["top"], margins["right"], margins["bottom"] ) self.setLayout(self.layout) self.optionsMenu = QtWidgets.QMenu(self) self.optionsMenu.setStyleSheet( self.mainwindow.theme["main/defaultwindow/style"] ) self.addChumAction = QAction( self.mainwindow.theme["main/menus/rclickchumlist/addchum"], self ) self.addChumAction.triggered.connect(self.addThisChum) self.blockAction = QAction( self.mainwindow.theme["main/menus/rclickchumlist/blockchum"], self ) self.blockAction.triggered.connect(self.blockThisChum) self.quirksOff = QAction( self.mainwindow.theme["main/menus/rclickchumlist/quirksoff"], self ) self.quirksOff.setCheckable(True) self.quirksOff.toggled[bool].connect(self.toggleQuirks) self.oocToggle = QAction( self.mainwindow.theme["main/menus/rclickchumlist/ooc"], self ) self.oocToggle.setCheckable(True) self.oocToggle.toggled[bool].connect(self.toggleOOC) self.unblockchum = QAction( self.mainwindow.theme["main/menus/rclickchumlist/unblockchum"], self ) self.unblockchum.triggered.connect(self.unblockChumSlot) self.reportchum = QAction( self.mainwindow.theme["main/menus/rclickchumlist/report"], self ) self.reportchum.triggered.connect(self.reportThisChum) self.logchum = QAction( self.mainwindow.theme["main/menus/rclickchumlist/viewlog"], self ) self.logchum.triggered.connect(self.openChumLogs) # For this, we'll want to use setChecked to toggle these so they match # the user's setting. Alternately (better), use a tristate checkbox, so # that they start semi-checked? # Easiest solution: Implement a 'Mute' option that overrides all # notifications for that window, save for mentions. # TODO: Look into setting up theme support here. # Theme support :3c # if self.mainwindow.theme.has_key("main/menus/rclickchumlist/beeponmessage"): try: self._beepToggle = QAction( self.mainwindow.theme["main/menus/rclickchumlist/beeponmessage"], self ) except: self._beepToggle = QAction("BEEP ON MESSAGE", self) self._beepToggle.setCheckable(True) self._beepToggle.toggled[bool].connect(self.toggleBeep) # if self.mainwindow.theme.has_key("main/menus/rclickchumlist/flashonmessage"): try: self._flashToggle = QAction( self.mainwindow.theme["main/menus/rclickchumlist/flashonmessage"], self ) except: self._flashToggle = QAction("FLASH ON MESSAGE", self) self._flashToggle.setCheckable(True) self._flashToggle.toggled[bool].connect(self.toggleFlash) # if self.mainwindow.theme.has_key("main/menus/rclickchumlist/mutenotifications"): try: self._muteToggle = QAction( self.mainwindow.theme["main/menus/rclickchumlist/mutenotifications"], self, ) except: self._muteToggle = QAction("MUTE NOTIFICATIONS", self) self._muteToggle.setCheckable(True) self._muteToggle.toggled[bool].connect(self.toggleMute) self.optionsMenu.addAction(self.quirksOff) self.optionsMenu.addAction(self.oocToggle) self.optionsMenu.addAction(self._beepToggle) self.optionsMenu.addAction(self._flashToggle) self.optionsMenu.addAction(self._muteToggle) self.optionsMenu.addAction(self.logchum) self.optionsMenu.addAction(self.addChumAction) self.optionsMenu.addAction(self.blockAction) self.optionsMenu.addAction(self.reportchum) self.chumopen = False self.applyquirks = True self.ooc = False self.always_beep = False self.always_flash = False self.notifications_muted = False if parent: parent.addChat(self) if initiated: msg = self.mainwindow.profile().pestermsg( self.chum, QtGui.QColor(self.mainwindow.theme["convo/systemMsgColor"]), self.mainwindow.theme["convo/text/beganpester"], ) self.setChumOpen(True) self.textArea.append(convertTags(msg)) self.mainwindow.chatlog.log(self.title(), msg) self.newmessage = False self.history = PesterHistory() def title(self): return self.chum.handle def icon(self): return self.chum.mood.icon(self.mainwindow.theme) def myUpdateMood(self, mood): chum = self.mainwindow.profile() syscolor = QtGui.QColor(self.mainwindow.theme["convo/systemMsgColor"]) msg = chum.moodmsg(mood, syscolor, self.mainwindow.theme) self.textArea.append(convertTags(msg)) self.mainwindow.chatlog.log(self.title(), msg) def isBot(self, *args, **kwargs): return self.parent().isBot(*args, **kwargs) def updateMood(self, mood, unblocked=False, old=None): syscolor = QtGui.QColor(self.mainwindow.theme["convo/systemMsgColor"]) # ~ if mood.name() == "offline" and self.chumopen == True and not unblocked: # ~ self.mainwindow.ceasesound.play() # ~ msg = self.chum.pestermsg(self.mainwindow.profile(), syscolor, self.mainwindow.theme["convo/text/ceasepester"]) # ~ self.textArea.append(convertTags(msg)) # ~ self.mainwindow.chatlog.log(self.title(), msg) # ~ self.chumopen = False if old and old.name() != mood.name(): msg = self.chum.moodmsg(mood, syscolor, self.mainwindow.theme) self.textArea.append(convertTags(msg)) self.mainwindow.chatlog.log(self.title(), msg) if self.parent(): self.parent().updateMood(self.title(), mood, unblocked) else: if self.chum.blocked(self.mainwindow.config) and not unblocked: self.setWindowIcon( QtGui.QIcon(self.mainwindow.theme["main/chums/moods/blocked/icon"]) ) self.optionsMenu.addAction(self.unblockchum) self.optionsMenu.removeAction(self.blockAction) else: self.setWindowIcon(mood.icon(self.mainwindow.theme)) self.optionsMenu.removeAction(self.unblockchum) self.optionsMenu.addAction(self.blockAction) # print mood update? def updateBlocked(self): if self.parent(): self.parent().updateBlocked(self.title()) else: self.setWindowIcon( QtGui.QIcon(self.mainwindow.theme["main/chums/moods/blocked/icon"]) ) self.optionsMenu.addAction(self.unblockchum) self.optionsMenu.removeAction(self.blockAction) def updateColor(self, color): PchumLog.debug("convo updateColor: %s", color) self.chum.color = color def addMessage(self, msg, me=True): if isinstance(msg, str): lexmsg = lexMessage(msg) else: lexmsg = msg if me: chum = self.mainwindow.profile() else: chum = self.chum self.notifyNewMessage() self.textArea.addMessage(lexmsg, chum) def notifyNewMessage(self): # Our imports have to be here to prevent circular import issues. from memos import PesterMemo, MemoTabWindow # first see if this conversation HASS the focus title = self.title() parent = self.parent() memoblink = pesterblink = self.mainwindow.config.blink() memoblink &= self.mainwindow.config.MBLINK pesterblink &= self.mainwindow.config.PBLINK mutednots = self.notifications_muted # mtsrc = self if parent: try: mutednots = parent.notifications_muted # mtsrc = parent except: pass if not ( self.hasFocus() or self.textArea.hasFocus() or self.textInput.hasFocus() or (parent and parent.convoHasFocus(title)) ): # ok if it has a tabconvo parent, send that the notify. if parent: # Just let the icon highlight normally. # This function *also* highlights the tab, mind. parent.notifyNewMessage(title) if not mutednots: # Remember that these two are descended from one another. # TODO: Make these obey subclassing rules...ugh. # They should really just use the class's function and do # the checks there. # PesterTabWindow -> MemoTabWindow if isinstance(parent, MemoTabWindow): if self.always_flash or memoblink: self.mainwindow.gainAttention.emit(parent) elif isinstance(parent, PesterTabWindow): if self.always_flash or pesterblink: self.mainwindow.gainAttention.emit(parent) # if not change the window title and update system tray else: self.newmessage = True self.setWindowTitle(title + "*") # karxi: The order of execution here is a bit unclear...I'm not # entirely sure how much of this directly affects what we see. def func(): self.showChat() self.mainwindow.waitingMessages.addMessage(title, func) if not mutednots: # Once again, PesterMemo inherits from PesterConvo. if isinstance(self, PesterMemo): if self.always_flash or memoblink: self.mainwindow.gainAttention.emit(self) elif isinstance(self, PesterConvo): if self.always_flash or pesterblink: self.mainwindow.gainAttention.emit(self) def clearNewMessage(self): if self.parent(): self.parent().clearNewMessage(self.title()) elif self.newmessage: self.newmessage = False self.setWindowTitle(self.title()) self.mainwindow.waitingMessages.messageAnswered(self.title()) # reset system tray def focusInEvent(self, event): self.clearNewMessage() self.textInput.setFocus() def raiseChat(self): self.activateWindow() self.raise_() self.textInput.setFocus() def showChat(self): if self.parent(): self.parent().showChat(self.title()) self.raiseChat() def contextMenuEvent(self, event): if event.reason() == QtGui.QContextMenuEvent.Reason.Mouse: self.optionsMenu.popup(event.globalPos()) def closeEvent(self, event): self.mainwindow.waitingMessages.messageAnswered(self.title()) for movie in self.textArea.urls.copy(): movie.setFileName("") # Required, sometimes, for some reason. . . movie.stop() del movie self.windowClosed.emit(self.title()) def setChumOpen(self, o): self.chumopen = o def changeTheme(self, theme): self.resize(*theme["convo/size"]) self.setStyleSheet( "QtWidgets.QFrame#{} {{ {} }}".format( self.chum.handle, theme["convo/style"] ) ) margins = theme["convo/margins"] self.layout.setContentsMargins( margins["left"], margins["top"], margins["right"], margins["bottom"] ) self.setWindowIcon(self.icon()) t = Template(self.mainwindow.theme["convo/chumlabel/text"]) self.chumLabel.setText(t.safe_substitute(handle=self.title())) self.chumLabel.setStyleSheet(theme["convo/chumlabel/style"]) self.chumLabel.setAlignment( self.aligndict["h"][self.mainwindow.theme["convo/chumlabel/align/h"]] | self.aligndict["v"][self.mainwindow.theme["convo/chumlabel/align/v"]] ) self.chumLabel.setMaximumHeight( self.mainwindow.theme["convo/chumlabel/maxheight"] ) self.chumLabel.setMinimumHeight( self.mainwindow.theme["convo/chumlabel/minheight"] ) self.chumLabel.setSizePolicy( QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Expanding, ) ) self.quirksOff.setText( self.mainwindow.theme["main/menus/rclickchumlist/quirksoff"] ) self.addChumAction.setText( self.mainwindow.theme["main/menus/rclickchumlist/addchum"] ) self.blockAction.setText( self.mainwindow.theme["main/menus/rclickchumlist/blockchum"] ) self.unblockchum.setText( self.mainwindow.theme["main/menus/rclickchumlist/unblockchum"] ) self.logchum.setText(self.mainwindow.theme["main/menus/rclickchumlist/viewlog"]) # if self.mainwindow.theme.has_key("main/menus/rclickchumlist/beeponmessage"): try: self._beepToggle.setText( self.mainwindow.theme["main/menus/rclickchumlist/beeponmessage"] ) except: self._beepToggle.setText("BEEP ON MESSAGE") # if self.mainwindow.theme.has_key("main/menus/rclickchumlist/flashonmessage"): try: self._flashToggle.setText( self.mainwindow.theme["main/menus/rclickchumlist/flashonmessage"] ) except: self._flashToggle.setText("FLASH ON MESSAGE", self) # if self.mainwindow.theme.has_key("main/menus/rclickchumlist/mutenotifications"): try: self._muteToggle.setText( self.mainwindow.theme["main/menus/rclickchumlist/mutenotifications"] ) except: self._muteToggle.setText("MUTE NOTIFICATIONS") # if self.mainwindow.theme.has_key("main/menus/rclickchumlist/report"): try: self.reportchum.setText( self.mainwindow.theme["main/menus/rclickchumlist/report"] ) except: pass self.textArea.changeTheme(theme) self.textInput.changeTheme(theme) @QtCore.pyqtSlot() def sentMessage(self): """Offloaded to another function, like its sisters. Fetch the raw text from the input box. """ return parsetools.kxhandleInput( self, self.textInput.text(), flavor="convo", irc_compatible=self.mainwindow.config.irc_compatibility_mode(), ) @QtCore.pyqtSlot() def addThisChum(self): self.mainwindow.addChum(self.chum) @QtCore.pyqtSlot() def blockThisChum(self): self.mainwindow.blockChum(self.chum.handle) @QtCore.pyqtSlot() def reportThisChum(self): self.mainwindow.reportChum(self.chum.handle) @QtCore.pyqtSlot() def unblockChumSlot(self): self.mainwindow.unblockChum(self.chum.handle) @QtCore.pyqtSlot(bool) def toggleQuirks(self, toggled): self.applyquirks = not toggled @QtCore.pyqtSlot(bool) def toggleOOC(self, toggled): self.ooc = toggled @QtCore.pyqtSlot() def openChumLogs(self): currentChum = self.chum.handle self.mainwindow.chumList.pesterlogviewer = PesterLogViewer( currentChum, self.mainwindow.config, self.mainwindow.theme, self.mainwindow ) self.mainwindow.chumList.pesterlogviewer.rejected.connect( self.mainwindow.chumList.closeActiveLog ) self.mainwindow.chumList.pesterlogviewer.show() self.mainwindow.chumList.pesterlogviewer.raise_() self.mainwindow.chumList.pesterlogviewer.activateWindow() @QtCore.pyqtSlot(bool) def toggleBeep(self, toggled): self.always_beep = toggled @QtCore.pyqtSlot(bool) def toggleFlash(self, toggled): self.always_flash = toggled @QtCore.pyqtSlot(bool) def toggleMute(self, toggled): self.notifications_muted = toggled messageSent = QtCore.pyqtSignal(str, str) windowClosed = QtCore.pyqtSignal(str) aligndict = { "h": { "center": QtCore.Qt.AlignmentFlag.AlignHCenter, "left": QtCore.Qt.AlignmentFlag.AlignLeft, "right": QtCore.Qt.AlignmentFlag.AlignRight, }, "v": { "center": QtCore.Qt.AlignmentFlag.AlignVCenter, "top": QtCore.Qt.AlignmentFlag.AlignTop, "bottom": QtCore.Qt.AlignmentFlag.AlignBottom, }, } # the import is way down here to avoid recursive imports from logviewer import PesterLogViewer