Example #1
0
class Snakefire(object):
    DOMAIN = "www.snakefire.org"
    NAME = "Snakefire"
    DESCRIPTION = "Snakefire: Campfire Linux Client"
    VERSION = "1.0.3"
    ICON = "snakefire.png"
    MAC_TRAY_ICON = "snakefire-gray.png"
    COLORS = {
        "normal": None,
        "new": QtGui.QColor(0, 0, 255),
        "alert": QtGui.QColor(255, 0, 0)
    }

    def __init__(self):
        self.DESCRIPTION = self._(self.DESCRIPTION)
        self._pingTimer = None
        self._idleTimer = None
        self._idle = False
        self._lastIdleAnswer = None
        self._worker = None
        self._settings = {}
        self._canConnect = False
        self._cfDisconnected()

        if len(sys.argv) > 1:
            self._qsettings = QtCore.QSettings(
                sys.argv[1],
                QtCore.QSettings.IniFormat if sys.platform.find("win") == 0
                else QtCore.QSettings.NativeFormat)
        else:
            self._qsettings = QtCore.QSettings(self.NAME, self.NAME)

        self._icon = QtGui.QIcon(":/icons/{icon}".format(icon=self.ICON))
        if platform.system() == "Darwin":
            self._trayIconIcon = QtGui.QIcon(
                ":/icons/{icon}".format(icon=self.MAC_TRAY_ICON))
        else:
            self._trayIconIcon = self._icon
        self.setWindowIcon(self._icon)
        self.setAcceptDrops(True)
        self._setupUI()

        settings = self.getSettings("connection")

        self._canConnect = False
        if settings["subdomain"] and settings["user"] and settings["password"]:
            self._canConnect = True

        self._updateLayout()

        if not self._canConnect:
            self.options()
        elif settings["connect"]:
            self.connectNow()

    def showEvent(self, event):
        if self._trayIcon.isVisible():
            if self._trayIcon.isAlerting():
                self._trayIcon.stopAlert()
            return
        self._trayIcon.show()

    def dragEnterEvent(self, event):
        room = self.getCurrentRoom()
        canUpload = not self._rooms[room.id]["upload"] if room else False
        if canUpload and self._getDropFile(event):
            event.acceptProposedAction()

    def dropEvent(self, event):
        room = self.getCurrentRoom()
        path = self._getDropFile(event)
        if room and path:
            self._upload(room, path)

    def _getDropFile(self, event):
        files = []
        urls = event.mimeData().urls()
        if urls:
            for url in urls:
                path = url.path()
                if path and os.path.exists(path) and os.path.isfile(path):
                    try:
                        handle = open(str(path))
                        handle.close()
                        files.append(str(path))
                    except:
                        pass
            if len(files) > 1:
                files = []
        return files[0] if files else None

    def getSetting(self, group, setting):
        settings = self.getSettings(group, asString=False)
        return settings[setting] if setting in settings else None

    def setSetting(self, group, setting, value):
        self._qsettings.beginGroup(group)
        self._qsettings.setValue(setting, value)
        self._qsettings.endGroup()

    def getSettings(self, group, asString=True, reload=False):
        try:
            spell_language = SpellTextEditor.defaultLanguage()
        except enchant.errors.Error:
            spell_language = None
        defaults = {
            "connection": {
                "subdomain": None,
                "user": None,
                "password": None,
                "ssl": False,
                "connect": False,
                "join": False,
                "rooms": []
            },
            "program": {
                "minimize":
                False,
                "spell_language":
                spell_language,
                "away":
                True,
                "away_time":
                10,
                "away_time_between_messages":
                5,
                "away_message":
                self._("I am currently away from {name}").format(
                    name=self.NAME)
            },
            "display": {
                "theme": "default",
                "size": 100,
                "show_join_message": True,
                "show_part_message": True,
                "show_message_timestamps": True
            },
            "alerts": {
                "notify_ping": True,
                "notify_inactive_tab": False,
                "notify_blink": True,
                "notify_notify": True
            },
            "matches": []
        }

        if reload or not group in self._settings:
            settings = defaults[group] if group in defaults else {}

            if group == "matches":
                settings = []
                size = self._qsettings.beginReadArray("matches")
                for i in range(size):
                    self._qsettings.setArrayIndex(i)
                    isRegex = False
                    try:
                        isRegex = True if ["true", "1"].index(
                            str(self._qsettings.value(
                                "regex").toPyObject()).lower()) >= 0 else False
                    except:
                        pass

                    settings.append({
                        'regex':
                        isRegex,
                        'match':
                        self._qsettings.value("match").toPyObject()
                    })
                self._qsettings.endArray()
            else:
                self._qsettings.beginGroup(group)
                for setting in self._qsettings.childKeys():
                    settings[str(setting)] = self._qsettings.value(
                        setting).toPyObject()
                self._qsettings.endGroup()

                boolSettings = []
                if group == "connection":
                    boolSettings += ["ssl", "connect", "join"]
                elif group == "program":
                    boolSettings += ["away", "minimize"]
                elif group == "display":
                    boolSettings += [
                        "show_join_message", "show_part_message",
                        "show_message_timestamps"
                    ]
                elif group == "alerts":
                    boolSettings += [
                        "notify_ping", "notify_inactive_tab", "notify_blink",
                        "notify_notify"
                    ]

                for boolSetting in boolSettings:
                    try:
                        settings[boolSetting] = True if ["true", "1"].index(
                            str(settings[boolSetting]).lower()) >= 0 else False
                    except:
                        settings[boolSetting] = False

                if group == "connection" and settings[
                        "subdomain"] and settings["user"]:
                    settings["password"] = keyring.get_password(
                        self.NAME,
                        str(settings["subdomain"]) + "_" +
                        str(settings["user"]))

            self._settings[group] = settings

        settings = self._settings[group]
        if asString:
            if isinstance(settings, list):
                for i, row in enumerate(settings):
                    for setting in row:
                        if not isinstance(row[setting], bool):
                            settings[i][setting] = str(
                                row[setting]) if row[setting] else ""
            else:
                for setting in settings:
                    if not isinstance(settings[setting], bool):
                        settings[setting] = str(
                            settings[setting]) if settings[setting] else ""

        return settings

    def setSettings(self, group, settings):
        self._settings[group] = settings

        if group == "matches":
            self._qsettings.beginWriteArray("matches")
            for i, setting in enumerate(settings):
                self._qsettings.setArrayIndex(i)
                self._qsettings.setValue("regex", setting["regex"])
                self._qsettings.setValue("match", setting["match"])
            self._qsettings.endArray()
        else:
            self._qsettings.beginGroup(group)
            for setting in self._settings[group]:
                if group != "connection" or setting != "password":
                    self._qsettings.setValue(setting, settings[setting])
                elif settings["subdomain"] and settings["user"]:
                    keyring.set_password(
                        self.NAME,
                        settings["subdomain"] + "_" + settings["user"],
                        settings[setting])
            self._qsettings.endGroup()

            if group == "connection":
                self._canConnect = False
                if settings["subdomain"] and settings["user"] and settings[
                        "password"]:
                    self._canConnect = True
                self._updateLayout()
            elif group == "program":
                if settings["away"] and self._connected:
                    self._setUpIdleTracker()
                else:
                    self._setUpIdleTracker(False)
                if self._editor:
                    if settings["spell_language"]:
                        self._editor.enableSpell(settings["spell_language"])
                    else:
                        self._editor.disableSpell()
            elif group == "display":
                for roomId in self._rooms.keys():
                    if roomId in self._rooms and self._rooms[roomId]["view"]:
                        self._rooms[roomId]["view"].updateTheme(
                            settings["theme"], settings["size"])

    def exit(self):
        self._forceClose = True
        self.close()

    def changeEvent(self, event):
        if self.getSetting("program", "minimize") and event.type(
        ) == QtCore.QEvent.WindowStateChange and self.isMinimized():
            self.hide()
            event.ignore()
        else:
            event.accept()

    def closeEvent(self, event):
        if (not hasattr(self, "_forceClose")
                or not self._forceClose) and self.getSetting(
                    "program", "minimize"):
            self.hide()
            event.ignore()
        else:
            if self.getSetting("connection", "join"):
                self.setSetting(
                    "connection", "rooms",
                    ",".join([str(roomId) for roomId in self._rooms.keys()]))

            self.disconnectNow()

            if hasattr(self, "_workers") and self._workers:
                for worker in self._workers:
                    worker.terminate()
                    worker.wait()

            if hasattr(self, "_worker") and self._worker:
                self._worker.terminate()
                self._worker.wait()

            self.setSetting("window", "size", self.size())
            self.setSetting("window", "position", self.pos())

            event.accept()

    def alerts(self):
        dialog = AlertsDialog(self)
        dialog.open()

    def options(self):
        dialog = OptionsDialog(self)
        dialog.open()

    def about(self):
        dialog = AboutDialog(self)
        dialog.open()

    def connectNow(self):
        if not self._canConnect:
            return

        self._connecting = True
        self.statusBar().showMessage(self._("Connecting with Campfire..."))
        self._updateLayout()

        settings = self.getSettings("connection")

        self._worker = CampfireWorker(settings["subdomain"], settings["user"],
                                      settings["password"], settings["ssl"],
                                      self)
        self._connectWorkerSignals(self._worker)
        self._worker.connect()

    def disconnectNow(self):
        self.statusBar().showMessage(self._("Disconnecting from Campfire..."))
        if self._worker and hasattr(self, "_rooms"):
            # Using keys() since the dict could be changed (by _cfRoomLeft())
            # while iterating on it
            for roomId in self._rooms.keys():
                if roomId in self._rooms and self._rooms[roomId]["room"]:
                    self._worker.leave(self._rooms[roomId]["room"], False)

        self._cfDisconnected()
        self._updateLayout()

    def joinRoom(self, roomIndex=None):
        room = self._roomInIndex(
            roomIndex if roomIndex else self._toolBar["rooms"].currentIndex())
        if not room:
            return

        self._toolBar["join"].setEnabled(False)
        self.statusBar().showMessage(
            unicode(
                self._("Joining room {room}...").format(room=room["name"])))

        self._rooms[room["id"]] = {
            "room": None,
            "stream": None,
            "upload": None,
            "tab": None,
            "view": None,
            "frame": None,
            "usersList": None,
            "topicLabel": None,
            "filesLabel": None,
            "uploadButton": None,
            "uploadLabel": None,
            "uploadWidget": None,
            "newMessages": 0
        }
        self._getWorker().join(room["id"])

    def ping(self):
        if not self._connected:
            return
        for roomId in self._rooms.keys():
            if roomId in self._rooms and self._rooms[roomId]["room"]:
                self.updateRoomUsers(roomId, pinging=True)

    def speak(self):
        message = self._editor.document().toPlainText()
        room = self.getCurrentRoom()
        if not room or message.trimmed().isEmpty():
            return

        self._editor.document().clear()

        if message[0] == '/':
            command = QtCore.QString(message)
            separatorIndex = command.indexOf(QtCore.QRegExp('\\s'))
            handled = self.command(
                command.mid(1, separatorIndex - 1),
                command.mid(separatorIndex +
                            1 if separatorIndex >= 0 else command.length()))
            if handled:
                return

        self.statusBar().showMessage(
            unicode(
                self._("Sending message to {room}...").format(room=room.name)))
        self._getWorker().speak(room, unicode(message))

    def command(self, command, args):
        if command.compare(QtCore.QString("away"),
                           QtCore.Qt.CaseInsensitive) == 0:
            self.toggleAway()
            return True

    def uploadFile(self):
        room = self.getCurrentRoom()
        if not room:
            return

        path = QtGui.QFileDialog.getOpenFileName(
            self, self._("Select file to upload"))
        if path:
            self._upload(room, str(path))

    def uploadCancel(self):
        room = self.getCurrentRoom()
        if not room:
            return

        if self._rooms[room.id]["upload"]:
            self._rooms[room.id]["upload"].stop().join()
            self._rooms[room.id]["upload"] = None

        self._rooms[room.id]["uploadWidget"].hide()

    def leaveRoom(self, roomId):
        if roomId in self._rooms:
            self.statusBar().showMessage(
                unicode(
                    self._("Leaving room {room}...").format(
                        room=self._rooms[roomId]["room"].name)))
            self._getWorker().leave(self._rooms[roomId]["room"])

    def changeTopic(self):
        room = self.getCurrentRoom()
        if not room:
            return
        topic, ok = QtGui.QInputDialog.getText(
            self, self._("Change topic"),
            unicode(
                self._("Enter new topic for room {room}").format(
                    room=room.name)), QtGui.QLineEdit.Normal, room.topic)
        if ok:
            self.statusBar().showMessage(
                unicode(
                    self._("Changing topic for room {room}...").format(
                        room=room.name)))
            self._getWorker().changeTopic(room, topic)

    def updateRoomUsers(self, roomId=None, pinging=False):
        if not roomId:
            room = self.getCurrentRoom()
            if room:
                roomId = room.id
        if roomId in self._rooms:
            if not pinging:
                self.statusBar().showMessage(
                    unicode(
                        self._("Getting users in {room}...").format(
                            room=self._rooms[roomId]["room"].name)))
            self._getWorker().users(self._rooms[roomId]["room"], pinging)

    def updateRoomUploads(self, roomId=None):
        if not roomId:
            room = self.getCurrentRoom()
            if room:
                roomId = room.id
        if roomId in self._rooms:
            self.statusBar().showMessage(
                unicode(
                    self._("Getting uploads from {room}...").format(
                        room=self._rooms[roomId]["room"].name)))
            self._getWorker().uploads(self._rooms[roomId]["room"])

    def getCurrentRoom(self):
        index = self._tabs.currentIndex()
        for roomId in self._rooms.keys():
            if roomId in self._rooms and self._rooms[roomId]["tab"] == index:
                return self._rooms[roomId]["room"]

    def toggleAway(self):
        self.setAway(False if self._idle else True)

    def setAway(self, away=True):
        self._idle = away
        self.statusBar().showMessage(
            self._("You are now away")
            if self._idle else self._('You are now active'), 5000)

    def onIdle(self):
        self.setAway(True)

    def onActive(self):
        self.setAway(False)

    def _setUpIdleTracker(self, enable=True):
        if self._idleTimer:
            self._idleTimer.stop()
            self._idleTimer = None

        if enable and IdleTimer.supported():
            self._idleTimer = IdleTimer(
                self,
                int(self.getSetting("program", "away_time")) * 60)
            self.connect(self._idleTimer, QtCore.SIGNAL("idle()"), self.onIdle)
            self.connect(self._idleTimer, QtCore.SIGNAL("active()"),
                         self.onActive)
            self._idleTimer.start()

    def _cfStreamMessage(self, room, message, live=True, updateRoom=True):
        if (not message.user or
            (live and message.is_text() and message.is_by_current_user())
                or not room.id in self._rooms):
            return

        view = self._rooms[room.id]["view"]
        if not view:
            return

        alert = False
        alertIsDirectPing = False

        if message.is_text() and not message.is_by_current_user():
            alertIsDirectPing = (QtCore.QString(message.body).indexOf(
                QtCore.QRegExp(
                    "\\s*\\b{name}\\b".format(name=QtCore.QRegExp.escape(
                        self._worker.getUser().name)),
                    QtCore.Qt.CaseInsensitive)) == 0)
            alert = self.getSetting(
                "alerts",
                "notify_ping") if alertIsDirectPing else self._matchesAlert(
                    message.body)

        maximumImageWidth = int(view.size().width() * 0.4)  # 40% of viewport
        renderer = MessageRenderer(self._worker.getApiToken(),
                                   maximumImageWidth,
                                   room,
                                   message,
                                   live=live,
                                   updateRoom=updateRoom,
                                   showTimestamps=self.getSetting(
                                       "display", "show_message_timestamps"),
                                   alert=alert,
                                   alertIsDirectPing=alertIsDirectPing,
                                   parent=self)

        if renderer.needsThread():
            self.connect(
                renderer,
                QtCore.SIGNAL(
                    "render(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"
                ), self._renderMessage)
            renderer.start()
        else:
            self._renderMessage(renderer.render(),
                                room,
                                message,
                                live=live,
                                updateRoom=updateRoom,
                                alert=alert,
                                alertIsDirectPing=alertIsDirectPing)

    def _renderMessage(self,
                       html,
                       room,
                       message,
                       live=True,
                       updateRoom=True,
                       alert=False,
                       alertIsDirectPing=False):
        if (not room.id in self._rooms):
            return

        frame = self._rooms[room.id]["frame"]
        view = self._rooms[room.id]["view"]
        if not frame or not view:
            return

        if not self.getSetting("display", "show_join_message") and (
                message.is_joining() or message.is_leaving()
                or message.is_kick()):
            return

        if html:
            currentScrollbarValue = frame.scrollPosition()
            autoScroll = (currentScrollbarValue == frame.scrollBarMaximum(
                QtCore.Qt.Vertical))
            frame.setHtml(frame.toHtml() + html)
            view.show()
            if autoScroll:
                frame.scroll(0, frame.scrollBarMaximum(QtCore.Qt.Vertical))
            else:
                frame.scroll(currentScrollbarValue.x(),
                             currentScrollbarValue.y())

            tabIndex = self._rooms[room.id]["tab"]
            tabBar = self._tabs.tabBar()
            isActiveTab = (self.isActiveWindow()
                           and tabIndex == self._tabs.currentIndex())

            if message.is_text() and not isActiveTab:
                self._rooms[room.id]["newMessages"] += 1

            if self._rooms[room.id]["newMessages"] > 0:
                tabBar.setTabText(
                    tabIndex,
                    unicode("{room} ({count})".format(
                        room=room.name,
                        count=self._rooms[room.id]["newMessages"])))

            if not isActiveTab and (
                    alert or self._rooms[room.id]["newMessages"] > 0
            ) and tabBar.tabTextColor(tabIndex) == self.COLORS["normal"]:
                tabBar.setTabTextColor(
                    tabIndex, self.COLORS["alert" if alert else "new"])

            notifyInactiveTab = self.getSetting("alerts",
                                                "notify_inactive_tab")

            if (not isActiveTab and
                (alert or notifyInactiveTab)) and self.getSetting(
                    "alerts", "notify_blink"):
                self._trayIcon.alert()

            if live and (
                (alert or
                 (not isActiveTab and notifyInactiveTab and message.is_text()))
                    and self.getSetting("alerts", "notify_notify")):
                self._notify(
                    room,
                    unicode("{} says: {}".format(message.user.name,
                                                 message.body)), message.user)

        if updateRoom:
            if (message.is_joining() or message.is_leaving()):
                self.updateRoomUsers(room.id)
            elif message.is_upload():
                self.updateRoomUploads(room.id)
            elif message.is_topic_change(
            ) and not message.is_by_current_user():
                self._cfTopicChanged(room, message.body)

        # Respond to direct pings while being away, but only send an auto-response if last one was sent more than 2 minutes ago
        if live and alertIsDirectPing and self.getSetting(
                "program", "away") and self._idle:
            if self._lastIdleAnswer is None or time.time(
            ) - self._lastIdleAnswer >= (int(
                    self.getSetting("program", "away_time_between_messages")) *
                                         60):
                self._lastIdleAnswer = time.time()
                self._getWorker().speak(
                    room,
                    unicode("{user}: {message}".format(user=message.user.name,
                                                       message=self.getSetting(
                                                           "program",
                                                           "away_message"))))

    def _matchesAlert(self, message):
        matches = False
        searchMatches = self.getSettings("matches")
        for match in searchMatches:
            regex = "\\b{word}\\b".format(word=QtCore.QRegExp.escape(
                match['match'])) if not match['regex'] else match['match']
            if QtCore.QString(message).contains(
                    QtCore.QRegExp(regex, QtCore.Qt.CaseInsensitive)):
                matches = True
                break
        return matches

    def _cfConnected(self, user, rooms):
        self._connecting = False
        self._connected = True
        self._rooms = {}

        self._toolBar["rooms"].clear()
        for room in rooms:
            self._toolBar["rooms"].addItem(room["name"], room)

        self.statusBar().showMessage(
            unicode(
                self._("{user} connected to Campfire").format(user=user.name)),
            5000)
        self._updateLayout()

        if not self._pingTimer:
            self._pingTimer = QtCore.QTimer(self)
            self.connect(self._pingTimer, QtCore.SIGNAL("timeout()"),
                         self.ping)
        self._pingTimer.start(60000)  # Ping every minute

        if self.getSetting("program", "away"):
            self._setUpIdleTracker()

        if self.getSetting("connection", "join"):
            rooms = self.getSetting("connection", "rooms")
            if rooms:
                for roomId in rooms.split(","):
                    count = self._toolBar["rooms"].count()
                    if count:
                        roomIndex = None
                        for i in range(count):
                            data = self._toolBar["rooms"].itemData(i)
                            if not data.isNull():
                                data = data.toMap()
                                for key in data:
                                    if str(key) == "id" and str(
                                            data[key].toString()) == roomId:
                                        roomIndex = i
                                        break
                                if roomIndex is not None:
                                    break
                        if roomIndex is not None:
                            self.joinRoom(roomIndex)

    def _cfDisconnected(self):
        if self._pingTimer:
            self._pingTimer.stop()
            self._pingTimer = None

        if self._idleTimer:
            self._setUpIdleTracker(False)

        self._connecting = False
        self._connected = False
        self._rooms = {}
        self._worker = None
        self.statusBar().clearMessage()

    def _cfRoomJoined(self, room, messages=[], rejoined=False):
        if room.id not in self._rooms:
            return

        if not rejoined:
            self._rooms[room.id].update(self._setupRoomUI(room))
            self._rooms[room.id]["room"] = room
        self._rooms[room.id]["stream"] = self._worker.getStream(room)
        self.updateRoomUsers(room.id)
        self.updateRoomUploads(room.id)
        if not rejoined:
            self.statusBar().showMessage(
                unicode(self._("Joined room {room}").format(room=room.name)),
                5000)
        self._updatedRoomsList()
        if not rejoined and messages:
            for message in messages:
                self._cfStreamMessage(room,
                                      message,
                                      live=False,
                                      updateRoom=False)

    def _cfSpoke(self, room, message):
        self._cfStreamMessage(room, message, live=False)
        self.statusBar().clearMessage()

    def _cfRoomLeft(self, room):
        if self._rooms[room.id]["stream"]:
            self._rooms[room.id]["stream"].stop().join()
        if self._rooms[room.id]["upload"]:
            self._rooms[room.id]["upload"].stop().join()

        self._tabs.removeTab(self._rooms[room.id]["tab"])
        del self._rooms[room.id]
        self.statusBar().showMessage(
            unicode(self._("Left room {room}").format(room=room.name)), 5000)
        self._updatedRoomsList()

    def _cfRoomUsers(self, room, users, pinging=False):
        # We may be disconnecting while still processing the list
        if not room.id in self._rooms:
            return

        if not pinging:
            self.statusBar().clearMessage()
        self._rooms[room.id]["usersList"].clear()
        for user in users:
            item = QtGui.QListWidgetItem(user["name"])
            item.setData(QtCore.Qt.UserRole, user)
            self._rooms[room.id]["usersList"].addItem(item)

    def _cfRoomUploads(self, room, uploads):
        # We may be disconnecting while still processing the list
        if not room.id in self._rooms:
            return

        self.statusBar().clearMessage()
        label = self._rooms[room.id]["filesLabel"]
        if uploads:
            html = ""
            for upload in uploads:
                html += "{br}&bull; <a href=\"{url}\">{name}</a>".format(
                    br="<br />" if html else "",
                    url=upload["full_url"],
                    name=upload["name"])
            html = unicode("{text}<br />{html}".format(
                text=self._("Latest uploads:"), html=html))

            label.setText(html)
            if not label.isVisible():
                label.show()
        elif label.isVisible():
            label.setText("")
            label.hide()

    def _cfUploadProgress(self, room, current, total):
        if not room.id in self._rooms:
            return

        progressBar = self._rooms[room.id]["uploadProgressBar"]
        if not self._rooms[room.id]["uploadWidget"].isVisible():
            self._rooms[room.id]["uploadWidget"].show()
            progressBar.setMaximum(total)

        progressBar.setValue(current)

    def _cfUploadFinished(self, room):
        if not room.id in self._rooms:
            return

        self._rooms[room.id]["upload"].join()
        self._rooms[room.id]["upload"] = None
        self._rooms[room.id]["uploadWidget"].hide()

    def _cfTopicChanged(self, room, topic):
        if not room.id in self._rooms:
            return

        self._rooms[room.id]["topicLabel"].setText(topic)
        self.statusBar().clearMessage()

    def _cfConnectError(self, error):
        self._cfDisconnected()
        self._updateLayout()
        self._cfError(error)

    def _cfError(self, error):
        self.statusBar().clearMessage()
        if not self._connected:
            QtGui.QMessageBox.critical(
                self, "Error",
                self._("Error while connecting: {error}".format(
                    error=str(error))))
        else:
            QtGui.QMessageBox.critical(self, "Error", str(error))

    def _cfRoomError(self, error, room):
        self.statusBar().clearMessage()
        if isinstance(error, RuntimeError):
            (code, message) = error
            if code == 401:
                self.statusBar().showMessage(
                    unicode(
                        self._(
                            "Disconnected from room. Rejoining room {room}..."
                        ).format(room=room.name)), 5000)
                self._rooms[room.id]["stream"].stop().join()
                self._getWorker().join(room.id, True)
                return
        QtGui.QMessageBox.critical(self, "Error", str(error))

    def _roomSelected(self, index):
        self._updatedRoomsList(index)

    def _upload(self, room, path):
        self._rooms[room.id]["upload"] = self._worker.upload(room, path)
        self._updateRoomLayout()

    def _roomTabClose(self, tabIndex):
        for roomId in self._rooms:
            if self._rooms[roomId]["tab"] == tabIndex:
                self.leaveRoom(roomId)
                break

    def _roomTabFocused(self):
        tabIndex = self._tabs.currentIndex()
        if tabIndex < 0 or not self.isActiveWindow():
            return

        room = self._roomInTabIndex(tabIndex)
        if not room:
            return

        tabBar = self._tabs.tabBar()

        if self._rooms[room.id]["newMessages"] > 0:
            self._rooms[room.id]["newMessages"] = 0
            tabBar.setTabText(tabIndex, room.name)

        if tabBar.tabTextColor(tabIndex) != self.COLORS["normal"]:
            tabBar.setTabTextColor(tabIndex, self.COLORS["normal"])

        self._updateRoomLayout()

    def _roomInTabIndex(self, index):
        room = None
        for key in self._rooms:
            if self._rooms[key]["tab"] == index:
                room = self._rooms[key]["room"]
                break
        return room

    def _roomInIndex(self, index):
        room = {}
        data = self._toolBar["rooms"].itemData(index)
        if not data.isNull():
            data = data.toMap()
            for key in data:
                room[str(key)] = unicode(data[key].toString())
        return room

    def _connectWorkerSignals(self, worker):
        self.connect(worker, QtCore.SIGNAL("error(PyQt_PyObject)"),
                     self._cfError)
        self.connect(worker,
                     QtCore.SIGNAL("connected(PyQt_PyObject, PyQt_PyObject)"),
                     self._cfConnected)
        self.connect(worker, QtCore.SIGNAL("connectError(PyQt_PyObject)"),
                     self._cfConnectError)
        self.connect(
            worker, QtCore.SIGNAL("streamError(PyQt_PyObject, PyQt_PyObject)"),
            self._cfRoomError)
        self.connect(
            worker, QtCore.SIGNAL("uploadError(PyQt_PyObject, PyQt_PyObject)"),
            self._cfRoomError)
        self.connect(
            worker,
            QtCore.SIGNAL(
                "joined(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"),
            self._cfRoomJoined)
        self.connect(worker,
                     QtCore.SIGNAL("spoke(PyQt_PyObject, PyQt_PyObject)"),
                     self._cfSpoke)
        self.connect(
            worker,
            QtCore.SIGNAL(
                "streamMessage(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"),
            self._cfStreamMessage)
        self.connect(worker, QtCore.SIGNAL("left(PyQt_PyObject)"),
                     self._cfRoomLeft)
        self.connect(
            worker,
            QtCore.SIGNAL(
                "users(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"),
            self._cfRoomUsers)
        self.connect(worker,
                     QtCore.SIGNAL("uploads(PyQt_PyObject, PyQt_PyObject)"),
                     self._cfRoomUploads)
        self.connect(
            worker,
            QtCore.SIGNAL(
                "uploadProgress(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"),
            self._cfUploadProgress)
        self.connect(worker, QtCore.SIGNAL("uploadFinished(PyQt_PyObject)"),
                     self._cfUploadFinished)
        self.connect(
            worker,
            QtCore.SIGNAL("topicChanged(PyQt_PyObject, PyQt_PyObject)"),
            self._cfTopicChanged)

    def _getWorker(self):
        if not hasattr(self, "_workers"):
            self._workers = []

        if self._workers:
            for worker in self._workers:
                if worker.isFinished():
                    return worker

        worker = copy.copy(self._worker)
        self._connectWorkerSignals(worker)
        self._workers.append(worker)
        return worker

    def _updatedRoomsList(self, index=None):
        if not index:
            index = self._toolBar["rooms"].currentIndex()

        room = self._roomInIndex(index)

        self._toolBar["join"].setEnabled(False)
        if not room or room["id"] not in self._rooms:
            self._toolBar["join"].setEnabled(True)

        centralWidget = self.centralWidget()
        if not self._tabs.count():
            centralWidget.hide()
        else:
            centralWidget.show()

    def _notify(self, room, message, user):
        raise NotImplementedError("_notify() must be implemented")

    def _updateRoomLayout(self):
        room = self.getCurrentRoom()
        if room:
            canUpload = not self._rooms[room.id]["upload"]
            uploadButton = self._rooms[room.id]["uploadButton"]
            if ((canUpload and not uploadButton.isEnabled())
                    or (not canUpload and uploadButton.isEnabled())):
                uploadButton.setEnabled(canUpload)

    def _updateLayout(self):
        self._menus["file"]["connect"].setEnabled(not self._connected
                                                  and self._canConnect
                                                  and not self._connecting)
        self._menus["file"]["disconnect"].setEnabled(self._connected)

        roomsEmpty = self._toolBar["rooms"].count(
        ) == 1 and self._toolBar["rooms"].itemData(0).isNull()
        if not roomsEmpty and (not self._connected
                               or not self._toolBar["rooms"].count()):
            self._toolBar["rooms"].clear()
            self._toolBar["rooms"].addItem(self._("No rooms available"))
            self._toolBar["rooms"].setEnabled(False)
        elif not roomsEmpty:
            self._toolBar["rooms"].setEnabled(True)

        self._toolBar["roomsLabel"].setEnabled(
            self._toolBar["rooms"].isEnabled())
        self._toolBar["join"].setEnabled(self._toolBar["rooms"].isEnabled())

    def _setupRoomUI(self, room):
        topic = room.topic if room.topic else ""
        topicLabel = ClickableQLabel(topic)
        topicLabel.setToolTip(self._("Click here to change room's topic"))
        topicLabel.setWordWrap(True)
        self.connect(topicLabel, QtCore.SIGNAL("clicked()"), self.changeTopic)

        view = SnakeFireWebView(self)
        view.setSizePolicy(QtGui.QSizePolicy.Expanding,
                           QtGui.QSizePolicy.Expanding)
        frame = view.page().mainFrame()

        #Send all link clicks to systems web browser
        view.page().setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks)

        def linkClicked(url):
            QtGui.QDesktopServices.openUrl(url)

        view.connect(view, QtCore.SIGNAL("linkClicked (const QUrl&)"),
                     linkClicked)

        # Support auto scroll when needed
        def autoScroll(size):
            frame.scroll(0, size.height())

        frame.connect(frame,
                      QtCore.SIGNAL("contentsSizeChanged (const QSize&)"),
                      autoScroll)

        usersList = QtGui.QListWidget()

        filesLabel = QtGui.QLabel("")
        filesLabel.setOpenExternalLinks(True)
        filesLabel.setWordWrap(True)
        filesLabel.hide()

        uploadButton = QtGui.QPushButton(self._("&Upload new file"))
        self.connect(uploadButton, QtCore.SIGNAL("clicked()"), self.uploadFile)

        uploadProgressBar = QtGui.QProgressBar()
        uploadProgressLabel = QtGui.QLabel(self._("Uploading:"))

        uploadCancelButton = QtGui.QPushButton(self._("Cancel"))
        self.connect(uploadCancelButton, QtCore.SIGNAL("clicked()"),
                     self.uploadCancel)

        uploadLayout = QtGui.QHBoxLayout()
        uploadLayout.addWidget(uploadProgressLabel)
        uploadLayout.addWidget(uploadProgressBar)
        uploadLayout.addWidget(uploadCancelButton)

        uploadWidget = QtGui.QWidget()
        uploadWidget.setLayout(uploadLayout)
        uploadWidget.hide()

        leftFrameLayout = QtGui.QVBoxLayout()
        leftFrameLayout.addWidget(topicLabel)
        leftFrameLayout.addWidget(view)
        leftFrameLayout.addWidget(uploadWidget)

        rightFrameLayout = QtGui.QVBoxLayout()
        rightFrameLayout.addWidget(usersList)
        rightFrameLayout.addWidget(filesLabel)
        rightFrameLayout.addWidget(uploadButton)
        rightFrameLayout.addStretch(1)

        leftFrame = QtGui.QWidget()
        leftFrame.setLayout(leftFrameLayout)

        rightFrame = QtGui.QWidget()
        rightFrame.setLayout(rightFrameLayout)

        splitter = QtGui.QSplitter()
        splitter.addWidget(leftFrame)
        splitter.addWidget(rightFrame)
        splitter.setSizes(
            [splitter.size().width() * 0.75,
             splitter.size().width() * 0.25])

        index = self._tabs.addTab(splitter, room.name)
        self._tabs.setCurrentIndex(index)

        if not self.COLORS["normal"]:
            self.COLORS["normal"] = self._tabs.tabBar().tabTextColor(index)
        else:
            self._tabs.tabBar().setTabTextColor(index, self.COLORS["normal"])

        return {
            "tab": index,
            "view": view,
            "frame": frame,
            "usersList": usersList,
            "topicLabel": topicLabel,
            "filesLabel": filesLabel,
            "uploadButton": uploadButton,
            "uploadWidget": uploadWidget,
            "uploadProgressBar": uploadProgressBar,
            "uploadProgressLabel": uploadProgressLabel
        }

    def _setupUI(self):
        self.setWindowTitle(self.NAME)

        self._addMenu()
        self._addToolbar()

        self._tabs = QtGui.QTabWidget()
        self._tabs.setTabsClosable(True)
        self.connect(self._tabs, QtCore.SIGNAL("currentChanged(int)"),
                     self._roomTabFocused)
        self.connect(self._tabs, QtCore.SIGNAL("tabCloseRequested(int)"),
                     self._roomTabClose)

        self._editor = SpellTextEditor(lang=self.getSetting(
            "program", "spell_language"),
                                       mainFrame=self)

        speakButton = QtGui.QPushButton(self._("&Send"))
        self.connect(speakButton, QtCore.SIGNAL('clicked()'), self.speak)

        grid = QtGui.QGridLayout()
        grid.setRowStretch(0, 1)
        grid.addWidget(self._tabs, 0, 0, 1, -1)
        grid.addWidget(self._editor, 2, 0)
        grid.addWidget(speakButton, 2, 1)

        widget = QtGui.QWidget()
        widget.setLayout(grid)
        self.setCentralWidget(widget)

        tabWidgetFocusEventFilter = TabWidgetFocusEventFilter(self)
        self.connect(tabWidgetFocusEventFilter, QtCore.SIGNAL("tabFocused()"),
                     self._roomTabFocused)
        widget.installEventFilter(tabWidgetFocusEventFilter)

        self.centralWidget().hide()

        size = self.getSetting("window", "size")

        if not size:
            size = QtCore.QSize(640, 480)

        self.resize(size)

        position = self.getSetting("window", "position")
        if not position:
            screen = QtGui.QDesktopWidget().screenGeometry()
            position = QtCore.QPoint((screen.width() - size.width()) / 2,
                                     (screen.height() - size.height()) / 2)

        self.move(position)

        self._updateLayout()

        menu = QtGui.QMenu(self)
        menu.addAction(self._menus["file"]["connect"])
        menu.addAction(self._menus["file"]["disconnect"])
        menu.addSeparator()
        menu.addAction(self._menus["file"]["exit"])

        self._trayIcon = Systray(self._trayIconIcon, self)
        self._trayIcon.setContextMenu(menu)
        self._trayIcon.setToolTip(self.DESCRIPTION)

    def _addMenu(self):
        self._menus = {
            "file": {
                "connect":
                self._createAction(self._("&Connect"),
                                   self.connectNow,
                                   icon="connect.png"),
                "disconnect":
                self._createAction(self._("&Disconnect"),
                                   self.disconnectNow,
                                   icon="disconnect.png"),
                "exit":
                self._createAction(self._("E&xit"), self.exit)
            },
            "settings": {
                "alerts":
                self._createAction(self._("&Alerts..."),
                                   self.alerts,
                                   icon="alerts.png"),
                "options":
                self._createAction(self._("&Options..."), self.options)
            },
            "help": {
                "about": self._createAction(self._("A&bout"), self.about)
            }
        }

        menu = self.menuBar()

        file_menu = menu.addMenu(self._("&File"))
        file_menu.addAction(self._menus["file"]["connect"])
        file_menu.addAction(self._menus["file"]["disconnect"])
        file_menu.addSeparator()
        file_menu.addAction(self._menus["file"]["exit"])

        settings_menu = menu.addMenu(self._("S&ettings"))
        settings_menu.addAction(self._menus["settings"]["alerts"])
        settings_menu.addSeparator()
        settings_menu.addAction(self._menus["settings"]["options"])

        help_menu = menu.addMenu(self._("&Help"))
        help_menu.addAction(self._menus["help"]["about"])

    def _addToolbar(self):
        self._toolBar = {
            "connect":
            self._menus["file"]["connect"],
            "disconnect":
            self._menus["file"]["disconnect"],
            "roomsLabel":
            QtGui.QLabel(self._("Rooms:")),
            "rooms":
            QtGui.QComboBox(),
            "join":
            self._createAction(self._("Join room"),
                               self.joinRoom,
                               icon="join.png"),
            "alerts":
            self._menus["settings"]["alerts"]
        }

        self.connect(self._toolBar["rooms"],
                     QtCore.SIGNAL("currentIndexChanged(int)"),
                     self._roomSelected)

        toolBar = self.toolBar()
        toolBar.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
        toolBar.addAction(self._toolBar["connect"])
        toolBar.addAction(self._toolBar["disconnect"])
        toolBar.addSeparator()
        toolBar.addWidget(self._toolBar["roomsLabel"])
        toolBar.addWidget(self._toolBar["rooms"])
        toolBar.addAction(self._toolBar["join"])
        toolBar.addSeparator()
        toolBar.addAction(self._toolBar["alerts"])

    def _createAction(self,
                      text,
                      slot=None,
                      shortcut=None,
                      icon=None,
                      tip=None,
                      checkable=False,
                      signal="triggered()"):
        """ Create an action """
        action = QtGui.QAction(text, self)
        if icon is not None:
            if not isinstance(icon, QtGui.QIcon):
                action.setIcon(QtGui.QIcon(":/icons/{icon}".format(icon=icon)))
            else:
                action.setIcon(icon)
        if shortcut is not None:
            action.setShortcut(shortcut)
        if tip is not None:
            action.setToolTip(tip)
            action.setStatusTip(tip)
        if slot is not None:
            self.connect(action, QtCore.SIGNAL(signal), slot)
        if checkable:
            action.setCheckable(True)
        return action

    def _(self, string, module=None):
        return str(
            QtCore.QCoreApplication.translate(module or Snakefire.NAME,
                                              string))
Example #2
0
class Snakefire(object):
    DOMAIN = "www.snakefire.org"
    NAME = "Snakefire"
    DESCRIPTION = "Snakefire: Campfire Linux Client"
    VERSION = "1.0.3"
    ICON = "snakefire.png"
    COLORS = {
        "normal": None,
        "new": QtGui.QColor(0, 0, 255),
        "alert": QtGui.QColor(255, 0, 0)
    }

    def __init__(self):
        self.DESCRIPTION = self._(self.DESCRIPTION)
        self._pingTimer = None
        self._idleTimer = None
        self._idle = False
        self._lastIdleAnswer = None
        self._worker = None
        self._settings = {}
        self._canConnect = False
        self._cfDisconnected()

        if len(sys.argv) > 1:
            self._qsettings = QtCore.QSettings(sys.argv[1], QtCore.QSettings.IniFormat if sys.platform.find("win") == 0 else QtCore.QSettings.NativeFormat)
        else:
            self._qsettings = QtCore.QSettings(self.NAME, self.NAME)

        self._icon = QtGui.QIcon(":/icons/{icon}".format(icon=self.ICON))
        self.setWindowIcon(self._icon)
        self.setAcceptDrops(True)
        self._setupUI()

        settings = self.getSettings("connection")

        self._canConnect = False
        if settings["subdomain"] and settings["user"] and settings["password"]:
            self._canConnect = True

        self._updateLayout()

        if not self._canConnect:
            self.options()
        elif settings["connect"]:
            self.connectNow()

    def showEvent(self, event):
        if self._trayIcon.isVisible():
            if self._trayIcon.isAlerting():
                self._trayIcon.stopAlert()
            return
        self._trayIcon.show()

    def dragEnterEvent(self, event):
        room = self.getCurrentRoom()
        canUpload = not self._rooms[room.id]["upload"] if room else False
        if canUpload and self._getDropFile(event):
            event.acceptProposedAction()

    def dropEvent(self, event):
        room = self.getCurrentRoom()
        path = self._getDropFile(event)
        if room and path:
            self._upload(room, path)

    def _getDropFile(self, event):
        files = []
        urls = event.mimeData().urls()
        if urls:
            for url in urls:
                path = url.path()
                if path and os.path.exists(path) and os.path.isfile(path):
                    try:
                        handle = open(str(path))
                        handle.close()
                        files.append(str(path))
                    except:
                        pass
            if len(files) > 1:
               files = []
        return files[0] if files else None

    def getSetting(self, group, setting):
        settings = self.getSettings(group, asString=False);
        return settings[setting] if setting in settings else None

    def setSetting(self, group, setting, value):
        self._qsettings.beginGroup(group);
        self._qsettings.setValue(setting, value)
        self._qsettings.endGroup();

    def getSettings(self, group, asString=True, reload=False):
        defaults = {
            "connection": {
                "subdomain": None,
                "user": None,
                "password": None,
                "ssl": False,
                "connect": False,
                "join": False,
                "rooms": []
            },
            "program": {
                "minimize": False,
                "spell_language": SpellTextEditor.defaultLanguage(),
                "away": True,
                "away_time": 10,
                "away_time_between_messages": 5,
                "away_message": self._("I am currently away from {name}").format(name=self.NAME)
            },
            "display": {
                "theme": "default",
                "size": 100,
                "show_join_message": True,
                "show_part_message": True,
                "show_message_timestamps": True
            },
            "alerts": {
                "notify_ping": True,
                "notify_inactive_tab": False,
                "notify_blink": True,
                "notify_notify": True
            },
            "matches": []
        }

        if reload or not group in self._settings:
            settings = defaults[group] if group in defaults else {}

            if group == "matches":
                settings = []
                size = self._qsettings.beginReadArray("matches")
                for i in range(size):
                    self._qsettings.setArrayIndex(i)
                    isRegex = False
                    try:
                        isRegex = True if ["true", "1"].index(str(self._qsettings.value("regex").toPyObject()).lower()) >= 0 else False
                    except:
                        pass

                    settings.append({
                        'regex': isRegex,
                        'match': self._qsettings.value("match").toPyObject()
                    })
                self._qsettings.endArray()
            else:
                self._qsettings.beginGroup(group);
                for setting in self._qsettings.childKeys():
                    settings[str(setting)] = self._qsettings.value(setting).toPyObject()
                self._qsettings.endGroup();

                boolSettings = []
                if group == "connection":
                    boolSettings += ["ssl", "connect", "join"]
                elif group == "program":
                    boolSettings += ["away", "minimize"]
                elif group == "display":
                    boolSettings += ["show_join_message", "show_part_message", "show_message_timestamps"]
                elif group == "alerts":
                    boolSettings += ["notify_ping", "notify_inactive_tab", "notify_blink", "notify_notify"]

                for boolSetting in boolSettings:
                    try:
                        settings[boolSetting] = True if ["true", "1"].index(str(settings[boolSetting]).lower()) >= 0 else False
                    except:
                        settings[boolSetting] = False

                if group == "connection" and settings["subdomain"] and settings["user"]:
                    settings["password"] = keyring.get_password(self.NAME, str(settings["subdomain"])+"_"+str(settings["user"]))

            self._settings[group] = settings

        settings = self._settings[group]
        if asString:
            if isinstance(settings, list):
                for i, row in enumerate(settings):
                    for setting in row:
                        if not isinstance(row[setting], bool):
                            settings[i][setting] = str(row[setting]) if row[setting] else ""
            else:
                for setting in settings:
                    if not isinstance(settings[setting], bool):
                        settings[setting] = str(settings[setting]) if settings[setting] else ""

        return settings

    def setSettings(self, group, settings):
        self._settings[group] = settings;

        if group == "matches":
            self._qsettings.beginWriteArray("matches")
            for i, setting in enumerate(settings):
                self._qsettings.setArrayIndex(i)
                self._qsettings.setValue("regex", setting["regex"])
                self._qsettings.setValue("match", setting["match"])
            self._qsettings.endArray()
        else:
            self._qsettings.beginGroup(group);
            for setting in self._settings[group]:
                if group != "connection" or setting != "password":
                    self._qsettings.setValue(setting, settings[setting])
                elif settings["subdomain"] and settings["user"]:
                    keyring.set_password(self.NAME, settings["subdomain"]+"_"+settings["user"], settings[setting])
            self._qsettings.endGroup();

            if group == "connection":
                self._canConnect = False
                if settings["subdomain"] and settings["user"] and settings["password"]:
                    self._canConnect = True
                self._updateLayout()
            elif group == "program":
                if settings["away"] and self._connected:
                    self._setUpIdleTracker()
                else:
                    self._setUpIdleTracker(False)
                if self._editor:
                    if settings["spell_language"]:
                        self._editor.enableSpell(settings["spell_language"])
                    else:
                        self._editor.disableSpell()
            elif group == "display":
                for roomId in self._rooms.keys():
                    if roomId in self._rooms and self._rooms[roomId]["view"]:
                        self._rooms[roomId]["view"].updateTheme(settings["theme"], settings["size"])

    def exit(self):
        self._forceClose = True
        self.close()

    def changeEvent(self, event):
        if self.getSetting("program", "minimize") and event.type() == QtCore.QEvent.WindowStateChange and self.isMinimized():
            self.hide()
            event.ignore()
        else:
            event.accept()

    def closeEvent(self, event):
        if (not hasattr(self, "_forceClose") or not self._forceClose) and self.getSetting("program", "minimize"):
            self.hide()
            event.ignore()
        else:
            if self.getSetting("connection", "join"):
                self.setSetting("connection", "rooms", ",".join([str(roomId) for roomId in self._rooms.keys()]))

            self.disconnectNow()

            if hasattr(self, "_workers") and self._workers:
                for worker in self._workers:
                    worker.terminate()
                    worker.wait()

            if hasattr(self, "_worker") and self._worker:
                self._worker.terminate()
                self._worker.wait()

            self.setSetting("window", "size", self.size())
            self.setSetting("window", "position", self.pos())

            event.accept()

    def alerts(self):
        dialog = AlertsDialog(self)
        dialog.open()

    def options(self):
        dialog = OptionsDialog(self)
        dialog.open()

    def about(self):
        dialog = AboutDialog(self)
        dialog.open()

    def connectNow(self):
        if not self._canConnect:
            return

        self._connecting = True
        self.statusBar().showMessage(self._("Connecting with Campfire..."))
        self._updateLayout()

        settings = self.getSettings("connection")

        self._worker = CampfireWorker(settings["subdomain"], settings["user"], settings["password"], settings["ssl"], self)
        self._connectWorkerSignals(self._worker)
        self._worker.connect()

    def disconnectNow(self):
        self.statusBar().showMessage(self._("Disconnecting from Campfire..."))
        if self._worker and hasattr(self, "_rooms"):
            # Using keys() since the dict could be changed (by _cfRoomLeft())
            # while iterating on it
            for roomId in self._rooms.keys():
                if roomId in self._rooms and self._rooms[roomId]["room"]:
                    self._worker.leave(self._rooms[roomId]["room"], False)

        self._cfDisconnected()
        self._updateLayout()

    def joinRoom(self, roomIndex=None):
        room = self._roomInIndex(roomIndex if roomIndex else self._toolBar["rooms"].currentIndex())
        if not room:
            return

        self._toolBar["join"].setEnabled(False)
        self.statusBar().showMessage(unicode(self._("Joining room {room}...").format(room=room["name"])))

        self._rooms[room["id"]] = {
            "room": None,
            "stream": None,
            "upload": None,
            "tab": None,
            "view": None,
            "frame": None,
            "usersList": None,
            "topicLabel": None,
            "filesLabel": None,
            "uploadButton": None,
            "uploadLabel": None,
            "uploadWidget": None,
            "newMessages": 0
        }
        self._getWorker().join(room["id"])

    def ping(self):
        if not self._connected:
            return
        for roomId in self._rooms.keys():
            if roomId in self._rooms and self._rooms[roomId]["room"]:
                self.updateRoomUsers(roomId, pinging=True)

    def speak(self):
        message = self._editor.document().toPlainText()
        room = self.getCurrentRoom()
        if not room or message.trimmed().isEmpty():
            return

        self._editor.document().clear()

        if message[0] == '/':
            command = QtCore.QString(message)
            separatorIndex = command.indexOf(QtCore.QRegExp('\\s'));
            handled = self.command(command.mid(1, separatorIndex-1), command.mid(separatorIndex + 1 if separatorIndex >= 0 else command.length()))
            if handled:
                return

        self.statusBar().showMessage(unicode(self._("Sending message to {room}...").format(room=room.name)))
        self._getWorker().speak(room, unicode(message))

    def command(self, command, args):
        if command.compare(QtCore.QString("away"), QtCore.Qt.CaseInsensitive) == 0:
            self.toggleAway()
            return True

    def uploadFile(self):
        room = self.getCurrentRoom()
        if not room:
            return

        path = QtGui.QFileDialog.getOpenFileName(self, self._("Select file to upload"))
        if path:
            self._upload(room, str(path))

    def uploadCancel(self):
        room = self.getCurrentRoom()
        if not room:
            return

        if self._rooms[room.id]["upload"]:
            self._rooms[room.id]["upload"].stop().join()
            self._rooms[room.id]["upload"] = None

        self._rooms[room.id]["uploadWidget"].hide()

    def leaveRoom(self, roomId):
        if roomId in self._rooms:
            self.statusBar().showMessage(unicode(self._("Leaving room {room}...").format(room=self._rooms[roomId]["room"].name)))
            self._getWorker().leave(self._rooms[roomId]["room"])

    def changeTopic(self):
        room = self.getCurrentRoom()
        if not room:
            return
        topic, ok = QtGui.QInputDialog.getText(self,
            self._("Change topic"),
            unicode(self._("Enter new topic for room {room}").format(room=room.name)),
            QtGui.QLineEdit.Normal,
            room.topic
        )
        if ok:
            self.statusBar().showMessage(unicode(self._("Changing topic for room {room}...").format(room=room.name)))
            self._getWorker().changeTopic(room, topic)

    def updateRoomUsers(self, roomId = None, pinging = False):
        if not roomId:
            room = self.getCurrentRoom()
            if room:
                roomId = room.id
        if roomId in self._rooms:
            if not pinging:
                self.statusBar().showMessage(unicode(self._("Getting users in {room}...").format(room=self._rooms[roomId]["room"].name)))
            self._getWorker().users(self._rooms[roomId]["room"], pinging)

    def updateRoomUploads(self, roomId = None):
        if not roomId:
            room = self.getCurrentRoom()
            if room:
                roomId = room.id
        if roomId in self._rooms:
            self.statusBar().showMessage(unicode(self._("Getting uploads from {room}...").format(room=self._rooms[roomId]["room"].name)))
            self._getWorker().uploads(self._rooms[roomId]["room"])

    def getCurrentRoom(self):
        index = self._tabs.currentIndex()
        for roomId in self._rooms.keys():
            if roomId in self._rooms and self._rooms[roomId]["tab"] == index:
                return self._rooms[roomId]["room"]

    def toggleAway(self):
        self.setAway(False if self._idle else True)

    def setAway(self, away=True):
        self._idle = away
        self.statusBar().showMessage(self._("You are now away") if self._idle else self._('You are now active'), 5000)

    def onIdle(self):
        self.setAway(True)

    def onActive(self):
        self.setAway(False)

    def _setUpIdleTracker(self, enable=True):
        if self._idleTimer:
            self._idleTimer.stop()
            self._idleTimer = None

        if enable and IdleTimer.supported():
            self._idleTimer = IdleTimer(self, int(self.getSetting("program", "away_time")) * 60)
            self.connect(self._idleTimer, QtCore.SIGNAL("idle()"), self.onIdle)
            self.connect(self._idleTimer, QtCore.SIGNAL("active()"), self.onActive)
            self._idleTimer.start()

    def _cfStreamMessage(self, room, message, live=True, updateRoom=True):
        if (
            not message.user or
            (live and message.is_text() and message.is_by_current_user()) or
            not room.id in self._rooms
        ):
            return

        view = self._rooms[room.id]["view"]
        if not view:
            return

        alert = False
        alertIsDirectPing = False

        if message.is_text() and not message.is_by_current_user():
            alertIsDirectPing = (QtCore.QString(message.body).indexOf(QtCore.QRegExp("\\s*\\b{name}\\b".format(name=QtCore.QRegExp.escape(self._worker.getUser().name)), QtCore.Qt.CaseInsensitive)) == 0)
            alert = self.getSetting("alerts", "notify_ping") if alertIsDirectPing else self._matchesAlert(message.body)

        maximumImageWidth = int(view.size().width() * 0.4) # 40% of viewport
        renderer = MessageRenderer(
            self._worker.getApiToken(),
            maximumImageWidth,
            room,
            message,
            live=live,
            updateRoom=updateRoom,
            showTimestamps = self.getSetting("display", "show_message_timestamps"),
            alert=alert,
            alertIsDirectPing=alertIsDirectPing,
            parent=self
        )

        if renderer.needsThread():
            self.connect(renderer, QtCore.SIGNAL("render(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"), self._renderMessage)
            renderer.start()
        else:
            self._renderMessage(renderer.render(), room, message, live=live, updateRoom=updateRoom, alert=alert, alertIsDirectPing=alertIsDirectPing)

    def _renderMessage(self, html, room, message, live=True, updateRoom=True, alert=False, alertIsDirectPing=False):
        if (not room.id in self._rooms):
            return

        frame = self._rooms[room.id]["frame"]
        view = self._rooms[room.id]["view"]
        if not frame or not view:
            return

        if not self.getSetting("display", "show_join_message") and (message.is_joining() or message.is_leaving() or message.is_kick()):
            return

        if html:
            currentScrollbarValue = frame.scrollPosition()
            autoScroll = (currentScrollbarValue == frame.scrollBarMaximum(QtCore.Qt.Vertical))
            frame.setHtml(frame.toHtml() + html)
            view.show()
            if autoScroll:
                frame.scroll(0, frame.scrollBarMaximum(QtCore.Qt.Vertical))
            else:
                frame.scroll(currentScrollbarValue.x(), currentScrollbarValue.y())

            tabIndex = self._rooms[room.id]["tab"]
            tabBar = self._tabs.tabBar()
            isActiveTab = (self.isActiveWindow() and tabIndex == self._tabs.currentIndex())

            if message.is_text() and not isActiveTab:
                self._rooms[room.id]["newMessages"] += 1

            if self._rooms[room.id]["newMessages"] > 0:
                tabBar.setTabText(tabIndex, unicode("{room} ({count})".format(room = room.name, count = self._rooms[room.id]["newMessages"])))

            if not isActiveTab and (alert or self._rooms[room.id]["newMessages"] > 0) and tabBar.tabTextColor(tabIndex) == self.COLORS["normal"]:
                tabBar.setTabTextColor(tabIndex, self.COLORS["alert" if alert else "new"])

            notifyInactiveTab = self.getSetting("alerts", "notify_inactive_tab")

            if (not isActiveTab and (alert or notifyInactiveTab)) and self.getSetting("alerts", "notify_blink"):
                self._trayIcon.alert()

            if live and ((alert or (not isActiveTab and notifyInactiveTab and message.is_text())) and self.getSetting("alerts", "notify_notify")):
                self._notify(room, unicode("{} says: {}".format(message.user.name, message.body)), message.user)

        if updateRoom:
            if (message.is_joining() or message.is_leaving()):
                self.updateRoomUsers(room.id)
            elif message.is_upload():
                self.updateRoomUploads(room.id)
            elif message.is_topic_change() and not message.is_by_current_user():
                self._cfTopicChanged(room, message.body)

        # Respond to direct pings while being away, but only send an auto-response if last one was sent more than 2 minutes ago
        if live and alertIsDirectPing and self.getSetting("program", "away") and self._idle:
            if self._lastIdleAnswer is None or time.time() - self._lastIdleAnswer >= (int(self.getSetting("program", "away_time_between_messages")) * 60):
                self._lastIdleAnswer = time.time()
                self._getWorker().speak(room, unicode("{user}: {message}".format(
                    user = message.user.name,
                    message = self.getSetting("program", "away_message")
                )))

    def _matchesAlert(self, message):
        matches = False
        searchMatches = self.getSettings("matches")
        for match in searchMatches:
            regex = "\\b{word}\\b".format(word=QtCore.QRegExp.escape(match['match'])) if not match['regex'] else match['match']
            if QtCore.QString(message).contains(QtCore.QRegExp(regex, QtCore.Qt.CaseInsensitive)):
                matches = True
                break
        return matches

    def _cfConnected(self, user, rooms):
        self._connecting = False
        self._connected = True
        self._rooms = {}

        self._toolBar["rooms"].clear()
        for room in rooms:
            self._toolBar["rooms"].addItem(room["name"], room)

        self.statusBar().showMessage(unicode(self._("{user} connected to Campfire").format(user=user.name)), 5000)
        self._updateLayout()

        if not self._pingTimer:
            self._pingTimer = QtCore.QTimer(self)
            self.connect(self._pingTimer, QtCore.SIGNAL("timeout()"), self.ping)
        self._pingTimer.start(60000) # Ping every minute

        if self.getSetting("program", "away"):
            self._setUpIdleTracker()

        if self.getSetting("connection", "join"):
            rooms = self.getSetting("connection", "rooms")
            if rooms:
                for roomId in rooms.split(","):
                    count = self._toolBar["rooms"].count()
                    if count:
                        roomIndex = None
                        for i in range(count):
                            data = self._toolBar["rooms"].itemData(i)
                            if not data.isNull():
                                data = data.toMap()
                                for key in data:
                                    if str(key) == "id" and str(data[key].toString()) == roomId:
                                        roomIndex = i
                                        break;
                                if roomIndex is not None:
                                    break
                        if roomIndex is not None:
                            self.joinRoom(roomIndex)

    def _cfDisconnected(self):
        if self._pingTimer:
            self._pingTimer.stop()
            self._pingTimer = None

        if self._idleTimer:
            self._setUpIdleTracker(False)

        self._connecting = False
        self._connected = False
        self._rooms = {}
        self._worker = None
        self.statusBar().clearMessage()

    def _cfRoomJoined(self, room, messages=[], rejoined=False):
        if room.id not in self._rooms:
            return

        if not rejoined:
            self._rooms[room.id].update(self._setupRoomUI(room))
            self._rooms[room.id]["room"] = room
        self._rooms[room.id]["stream"] = self._worker.getStream(room)
        self.updateRoomUsers(room.id)
        self.updateRoomUploads(room.id)
        if not rejoined:
            self.statusBar().showMessage(unicode(self._("Joined room {room}").format(room=room.name)), 5000)
        self._updatedRoomsList()
        if not rejoined and messages:
            for message in messages:
                self._cfStreamMessage(room, message, live=False, updateRoom=False)

    def _cfSpoke(self, room, message):
        self._cfStreamMessage(room, message, live=False)
        self.statusBar().clearMessage()

    def _cfRoomLeft(self, room):
        if self._rooms[room.id]["stream"]:
            self._rooms[room.id]["stream"].stop().join()
        if self._rooms[room.id]["upload"]:
            self._rooms[room.id]["upload"].stop().join()

        self._tabs.removeTab(self._rooms[room.id]["tab"])
        del self._rooms[room.id]
        self.statusBar().showMessage(unicode(self._("Left room {room}").format(room=room.name)), 5000)
        self._updatedRoomsList()

    def _cfRoomUsers(self, room, users, pinging=False):
        # We may be disconnecting while still processing the list
        if not room.id in self._rooms:
            return

        if not pinging:
            self.statusBar().clearMessage()
        self._rooms[room.id]["usersList"].clear()
        for user in users:
            item = QtGui.QListWidgetItem(user["name"])
            item.setData(QtCore.Qt.UserRole, user)
            self._rooms[room.id]["usersList"].addItem(item)

    def _cfRoomUploads(self, room, uploads):
        # We may be disconnecting while still processing the list
        if not room.id in self._rooms:
            return

        self.statusBar().clearMessage()
        label = self._rooms[room.id]["filesLabel"]
        if uploads:
            html = ""
            for upload in uploads:
                html += "{br}&bull; <a href=\"{url}\">{name}</a>".format(
                    br = "<br />" if html else "",
                    url = upload["full_url"],
                    name = upload["name"]
                )
            html = unicode("{text}<br />{html}".format(
                text = self._("Latest uploads:"),
                html = html
            ))

            label.setText(html)
            if not label.isVisible():
                label.show()
        elif label.isVisible():
            label.setText("")
            label.hide()

    def _cfUploadProgress(self, room, current, total):
        if not room.id in self._rooms:
            return

        progressBar = self._rooms[room.id]["uploadProgressBar"]
        if not self._rooms[room.id]["uploadWidget"].isVisible():
            self._rooms[room.id]["uploadWidget"].show()
            progressBar.setMaximum(total)

        progressBar.setValue(current)

    def _cfUploadFinished(self, room):
        if not room.id in self._rooms:
            return

        self._rooms[room.id]["upload"].join()
        self._rooms[room.id]["upload"] = None
        self._rooms[room.id]["uploadWidget"].hide()

    def _cfTopicChanged(self, room, topic):
        if not room.id in self._rooms:
            return

        self._rooms[room.id]["topicLabel"].setText(topic)
        self.statusBar().clearMessage()

    def _cfConnectError(self, error):
        self._cfDisconnected()
        self._updateLayout()
        self._cfError(error)

    def _cfError(self, error):
        self.statusBar().clearMessage()
        if not self._connected:
            QtGui.QMessageBox.critical(self, "Error", self._("Error while connecting: {error}".format(error = str(error))))
        else:
            QtGui.QMessageBox.critical(self, "Error", str(error))

    def _cfRoomError(self, error, room):
        self.statusBar().clearMessage()
        if isinstance(error, RuntimeError):
            (code, message) = error
            if code == 401:
                self.statusBar().showMessage(unicode(self._("Disconnected from room. Rejoining room {room}...").format(room=room.name)), 5000)
                self._rooms[room.id]["stream"].stop().join()
                self._getWorker().join(room.id, True)
                return
        QtGui.QMessageBox.critical(self, "Error", str(error))

    def _roomSelected(self, index):
        self._updatedRoomsList(index)

    def _upload(self, room, path):
        self._rooms[room.id]["upload"] = self._worker.upload(room, path)
        self._updateRoomLayout()

    def _roomTabClose(self, tabIndex):
        for roomId in self._rooms:
            if self._rooms[roomId]["tab"] == tabIndex:
                self.leaveRoom(roomId)
                break

    def _roomTabFocused(self):
        tabIndex = self._tabs.currentIndex()
        if tabIndex < 0 or not self.isActiveWindow():
            return

        room = self._roomInTabIndex(tabIndex)
        if not room:
            return

        tabBar = self._tabs.tabBar()

        if self._rooms[room.id]["newMessages"] > 0:
            self._rooms[room.id]["newMessages"] = 0
            tabBar.setTabText(tabIndex, room.name)

        if tabBar.tabTextColor(tabIndex) != self.COLORS["normal"]:
            tabBar.setTabTextColor(tabIndex, self.COLORS["normal"])

        self._updateRoomLayout()

    def _roomInTabIndex(self, index):
        room = None
        for key in self._rooms:
            if self._rooms[key]["tab"] == index:
                room = self._rooms[key]["room"]
                break
        return room

    def _roomInIndex(self, index):
        room = {}
        data = self._toolBar["rooms"].itemData(index)
        if not data.isNull():
            data = data.toMap()
            for key in data:
                room[str(key)] = unicode(data[key].toString())
        return room

    def _connectWorkerSignals(self, worker):
        self.connect(worker, QtCore.SIGNAL("error(PyQt_PyObject)"), self._cfError)
        self.connect(worker, QtCore.SIGNAL("connected(PyQt_PyObject, PyQt_PyObject)"), self._cfConnected)
        self.connect(worker, QtCore.SIGNAL("connectError(PyQt_PyObject)"), self._cfConnectError)
        self.connect(worker, QtCore.SIGNAL("streamError(PyQt_PyObject, PyQt_PyObject)"), self._cfRoomError)
        self.connect(worker, QtCore.SIGNAL("uploadError(PyQt_PyObject, PyQt_PyObject)"), self._cfRoomError)
        self.connect(worker, QtCore.SIGNAL("joined(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"), self._cfRoomJoined)
        self.connect(worker, QtCore.SIGNAL("spoke(PyQt_PyObject, PyQt_PyObject)"), self._cfSpoke)
        self.connect(worker, QtCore.SIGNAL("streamMessage(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"), self._cfStreamMessage)
        self.connect(worker, QtCore.SIGNAL("left(PyQt_PyObject)"), self._cfRoomLeft)
        self.connect(worker, QtCore.SIGNAL("users(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"), self._cfRoomUsers)
        self.connect(worker, QtCore.SIGNAL("uploads(PyQt_PyObject, PyQt_PyObject)"), self._cfRoomUploads)
        self.connect(worker, QtCore.SIGNAL("uploadProgress(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"), self._cfUploadProgress)
        self.connect(worker, QtCore.SIGNAL("uploadFinished(PyQt_PyObject)"), self._cfUploadFinished)
        self.connect(worker, QtCore.SIGNAL("topicChanged(PyQt_PyObject, PyQt_PyObject)"), self._cfTopicChanged)

    def _getWorker(self):
        if not hasattr(self, "_workers"):
            self._workers = []

        if self._workers:
            for worker in self._workers:
                if worker.isFinished():
                    return worker

        worker = copy.copy(self._worker)
        self._connectWorkerSignals(worker)
        self._workers.append(worker)
        return worker

    def _updatedRoomsList(self, index=None):
        if not index:
            index = self._toolBar["rooms"].currentIndex()

        room = self._roomInIndex(index)

        self._toolBar["join"].setEnabled(False)
        if not room or room["id"] not in self._rooms:
            self._toolBar["join"].setEnabled(True)

        centralWidget = self.centralWidget()
        if not self._tabs.count():
            centralWidget.hide()
        else:
            centralWidget.show()

    def _notify(self, room, message, user):
        raise NotImplementedError("_notify() must be implemented")

    def _updateRoomLayout(self):
        room = self.getCurrentRoom()
        if room:
            canUpload = not self._rooms[room.id]["upload"]
            uploadButton = self._rooms[room.id]["uploadButton"]
            if (
                (canUpload and not uploadButton.isEnabled()) or
                (not canUpload and uploadButton.isEnabled())
            ):
                uploadButton.setEnabled(canUpload)

    def _updateLayout(self):
        self._menus["file"]["connect"].setEnabled(not self._connected and self._canConnect and not self._connecting)
        self._menus["file"]["disconnect"].setEnabled(self._connected)

        roomsEmpty = self._toolBar["rooms"].count() == 1 and self._toolBar["rooms"].itemData(0).isNull()
        if not roomsEmpty and (not self._connected or not self._toolBar["rooms"].count()):
            self._toolBar["rooms"].clear()
            self._toolBar["rooms"].addItem(self._("No rooms available"))
            self._toolBar["rooms"].setEnabled(False)
        elif not roomsEmpty:
            self._toolBar["rooms"].setEnabled(True)

        self._toolBar["roomsLabel"].setEnabled(self._toolBar["rooms"].isEnabled())
        self._toolBar["join"].setEnabled(self._toolBar["rooms"].isEnabled())

    def _setupRoomUI(self, room):
        topic = room.topic if room.topic else ""
        topicLabel = ClickableQLabel(topic)
        topicLabel.setToolTip(self._("Click here to change room's topic"))
        topicLabel.setWordWrap(True)
        self.connect(topicLabel, QtCore.SIGNAL("clicked()"), self.changeTopic)

        view = SnakeFireWebView(self)
        view.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
        frame = view.page().mainFrame()

        #Send all link clicks to systems web browser
        view.page().setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks)
        def linkClicked(url):
            QtGui.QDesktopServices.openUrl(url)
        view.connect(view, QtCore.SIGNAL("linkClicked (const QUrl&)"), linkClicked)

        # Support auto scroll when needed
        def autoScroll(size):
            frame.scroll(0, size.height())
        frame.connect(frame, QtCore.SIGNAL("contentsSizeChanged (const QSize&)"), autoScroll)

        usersList = QtGui.QListWidget()

        filesLabel = QtGui.QLabel("")
        filesLabel.setOpenExternalLinks(True)
        filesLabel.setWordWrap(True)
        filesLabel.hide()

        uploadButton = QtGui.QPushButton(self._("&Upload new file"))
        self.connect(uploadButton, QtCore.SIGNAL("clicked()"), self.uploadFile)

        uploadProgressBar = QtGui.QProgressBar()
        uploadProgressLabel = QtGui.QLabel(self._("Uploading:"))

        uploadCancelButton = QtGui.QPushButton(self._("Cancel"))
        self.connect(uploadCancelButton, QtCore.SIGNAL("clicked()"), self.uploadCancel)

        uploadLayout = QtGui.QHBoxLayout()
        uploadLayout.addWidget(uploadProgressLabel)
        uploadLayout.addWidget(uploadProgressBar)
        uploadLayout.addWidget(uploadCancelButton)

        uploadWidget = QtGui.QWidget()
        uploadWidget.setLayout(uploadLayout)
        uploadWidget.hide()

        leftFrameLayout = QtGui.QVBoxLayout()
        leftFrameLayout.addWidget(topicLabel)
        leftFrameLayout.addWidget(view)
        leftFrameLayout.addWidget(uploadWidget)

        rightFrameLayout = QtGui.QVBoxLayout()
        rightFrameLayout.addWidget(usersList)
        rightFrameLayout.addWidget(filesLabel)
        rightFrameLayout.addWidget(uploadButton)
        rightFrameLayout.addStretch(1)

        leftFrame = QtGui.QWidget()
        leftFrame.setLayout(leftFrameLayout)

        rightFrame = QtGui.QWidget()
        rightFrame.setLayout(rightFrameLayout)

        splitter = QtGui.QSplitter()
        splitter.addWidget(leftFrame)
        splitter.addWidget(rightFrame)
        splitter.setSizes([splitter.size().width() * 0.75, splitter.size().width() * 0.25])

        index = self._tabs.addTab(splitter, room.name)
        self._tabs.setCurrentIndex(index)

        if not self.COLORS["normal"]:
            self.COLORS["normal"] = self._tabs.tabBar().tabTextColor(index)
        else:
            self._tabs.tabBar().setTabTextColor(index, self.COLORS["normal"])

        return {
            "tab": index,
            "view": view,
            "frame": frame,
            "usersList": usersList,
            "topicLabel": topicLabel,
            "filesLabel": filesLabel,
            "uploadButton": uploadButton,
            "uploadWidget": uploadWidget,
            "uploadProgressBar": uploadProgressBar,
            "uploadProgressLabel": uploadProgressLabel
        }

    def _setupUI(self):
        self.setWindowTitle(self.NAME)

        self._addMenu()
        self._addToolbar()

        self._tabs = QtGui.QTabWidget()
        self._tabs.setTabsClosable(True)
        self.connect(self._tabs, QtCore.SIGNAL("currentChanged(int)"), self._roomTabFocused)
        self.connect(self._tabs, QtCore.SIGNAL("tabCloseRequested(int)"), self._roomTabClose)

        self._editor = SpellTextEditor(lang=self.getSetting("program", "spell_language"), mainFrame=self)

        speakButton = QtGui.QPushButton(self._("&Send"))
        self.connect(speakButton, QtCore.SIGNAL('clicked()'), self.speak)

        grid = QtGui.QGridLayout()
        grid.setRowStretch(0, 1)
        grid.addWidget(self._tabs, 0, 0, 1, -1)
        grid.addWidget(self._editor, 2, 0)
        grid.addWidget(speakButton, 2, 1)

        widget = QtGui.QWidget()
        widget.setLayout(grid)
        self.setCentralWidget(widget)

        tabWidgetFocusEventFilter = TabWidgetFocusEventFilter(self)
        self.connect(tabWidgetFocusEventFilter, QtCore.SIGNAL("tabFocused()"), self._roomTabFocused)
        widget.installEventFilter(tabWidgetFocusEventFilter)

        self.centralWidget().hide()

        size = self.getSetting("window", "size")

        if not size:
            size = QtCore.QSize(640, 480)

        self.resize(size)

        position = self.getSetting("window", "position")
        if not position:
            screen = QtGui.QDesktopWidget().screenGeometry()
            position = QtCore.QPoint((screen.width()-size.width())/2, (screen.height()-size.height())/2)

        self.move(position)

        self._updateLayout()

        menu = QtGui.QMenu(self)
        menu.addAction(self._menus["file"]["connect"])
        menu.addAction(self._menus["file"]["disconnect"])
        menu.addSeparator()
        menu.addAction(self._menus["file"]["exit"])

        self._trayIcon = Systray(self._icon, self)
        self._trayIcon.setContextMenu(menu)
        self._trayIcon.setToolTip(self.DESCRIPTION)

    def _addMenu(self):
        self._menus = {
            "file": {
                "connect": self._createAction(self._("&Connect"), self.connectNow, icon="connect.png"),
                "disconnect": self._createAction(self._("&Disconnect"), self.disconnectNow, icon="disconnect.png"),
                "exit": self._createAction(self._("E&xit"), self.exit)
            },
            "settings": {
                "alerts": self._createAction(self._("&Alerts..."), self.alerts, icon="alerts.png"),
                "options": self._createAction(self._("&Options..."), self.options)
            },
            "help": {
                "about": self._createAction(self._("A&bout"), self.about)
            }
        }

        menu = self.menuBar()

        file_menu = menu.addMenu(self._("&File"))
        file_menu.addAction(self._menus["file"]["connect"])
        file_menu.addAction(self._menus["file"]["disconnect"])
        file_menu.addSeparator()
        file_menu.addAction(self._menus["file"]["exit"])

        settings_menu = menu.addMenu(self._("S&ettings"))
        settings_menu.addAction(self._menus["settings"]["alerts"])
        settings_menu.addSeparator()
        settings_menu.addAction(self._menus["settings"]["options"])

        help_menu = menu.addMenu(self._("&Help"))
        help_menu.addAction(self._menus["help"]["about"])

    def _addToolbar(self):
        self._toolBar = {
            "connect": self._menus["file"]["connect"],
            "disconnect": self._menus["file"]["disconnect"],
            "roomsLabel": QtGui.QLabel(self._("Rooms:")),
            "rooms": QtGui.QComboBox(),
            "join": self._createAction(self._("Join room"), self.joinRoom, icon="join.png"),
            "alerts": self._menus["settings"]["alerts"]
        }

        self.connect(self._toolBar["rooms"], QtCore.SIGNAL("currentIndexChanged(int)"), self._roomSelected)

        toolBar = self.toolBar()
        toolBar.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
        toolBar.addAction(self._toolBar["connect"])
        toolBar.addAction(self._toolBar["disconnect"])
        toolBar.addSeparator();
        toolBar.addWidget(self._toolBar["roomsLabel"])
        toolBar.addWidget(self._toolBar["rooms"])
        toolBar.addAction(self._toolBar["join"])
        toolBar.addSeparator();
        toolBar.addAction(self._toolBar["alerts"])

    def _createAction(self, text, slot=None, shortcut=None, icon=None,
        tip=None, checkable=False, signal="triggered()"):
        """ Create an action """
        action = QtGui.QAction(text, self)
        if icon is not None:
            if not isinstance(icon, QtGui.QIcon):
                action.setIcon(QtGui.QIcon(":/icons/{icon}".format(icon=icon)))
            else:
                action.setIcon(icon)
        if shortcut is not None:
            action.setShortcut(shortcut)
        if tip is not None:
            action.setToolTip(tip)
            action.setStatusTip(tip)
        if slot is not None:
            self.connect(action, QtCore.SIGNAL(signal), slot)
        if checkable:
            action.setCheckable(True)
        return action

    def _(self, string, module=None):
        return str(QtCore.QCoreApplication.translate(module or Snakefire.NAME, string))
Example #3
0
class Snakefire(object):
    DOMAIN = "snakefire.org"
    NAME = "Snakefire"
    DESCRIPTION = "Snakefire: Campfire Linux Client"
    VERSION = "1.0.1"
    ICON = "snakefire.png"
    COLORS = {
        "normal": None,
        "new": QtGui.QColor(0, 0, 255),
        "alert": QtGui.QColor(255, 0, 0)
    }
    MESSAGES = {
        "alert": '<div class="alert"><span class="time">[{time}]</span> <span class="author">{user}</span>: {message}</div>',
        "image": '<span class="upload image"><a href="{url}"><img src="data:image/{type};base64,{data}" title="{name}" {attribs} /></a></span>',
        "join": '<div class="joined">--&gt; {user} joined {room}</div>',
        "leave": '<div class="left">&lt;-- {user} has left {room}</div>',
        "message_self": '<div class="message"><span class="time">[{time}]</span> <span class="author self">{user}</span>: {message}</div>',
        "message": '<div class="message"><span class="time">[{time}]</span> <span class="author">{user}</span>: {message}</div>',
        "paste": '<div class="paste">{message}</div>',
        "upload": '<span class="upload"><a href="{url}">{name}</a></span>',
        "topic": '<div class="topic">{user} changed topic to <span class="new_topic">{topic}</span></div>',
        "tweet": '<div class="tweet"><a href="{url_user}">{user}</a> <a href="{url}">tweeted</a>: {message}</div>'
    }

    def __init__(self):
        self.DESCRIPTION = self._(self.DESCRIPTION)
        self._pingTimer = None
        self._idleTimer = None
        self._idle = False
        self._worker = None
        self._settings = {}
        self._canConnect = False
        self._cfDisconnected()
        self._qsettings = QtCore.QSettings()
        self._icon = QtGui.QIcon(":/icons/{icon}".format(icon=self.ICON))
        self.setWindowIcon(self._icon)
        self.setAcceptDrops(True)
        self._setupUI()

        settings = self.getSettings("connection")

        self._canConnect = False
        if settings["subdomain"] and settings["user"] and settings["password"]:
            self._canConnect = True

        self._updateLayout()

        if settings["connect"]:
            self.connectNow()

    def showEvent(self, event):
        if self._trayIcon.isVisible():
            if self._trayIcon.isAlerting():
                self._trayIcon.stopAlert()
            return
        self._trayIcon.show()

    def dragEnterEvent(self, event):
        room = self.getCurrentRoom()
        canUpload = not self._rooms[room.id]["upload"] if room else False
        if canUpload and self._getDropFile(event):
            event.acceptProposedAction()

    def dropEvent(self, event):
        room = self.getCurrentRoom()
        path = self._getDropFile(event)
        if room and path:
            self._upload(room, path)

    def _getDropFile(self, event):
        files = []
        urls = event.mimeData().urls()
        if urls:
            for url in urls:
                path = url.path()
                if path and os.path.exists(path) and os.path.isfile(path):
                    try:
                        handle = open(str(path))
                        handle.close()
                        files.append(str(path))
                    except Exception as e:
                        pass
            if len(files) > 1:
               files = []
        return files[0] if files else None

    def getSetting(self, group, setting):
        settings = self.getSettings(group, asString=False);
        return settings[setting] if setting in settings else None

    def setSetting(self, group, setting, value):
        self._qsettings.beginGroup(group);
        self._qsettings.setValue(setting, value)
        self._qsettings.endGroup();

    def getSettings(self, group, asString=True, reload=False):
        defaults = {
            "connection": {
                "subdomain": None,
                "user": None,
                "password": None,
                "ssl": False,
                "connect": False,
                "join": False,
                "rooms": []
            },
            "program": {
                "minimize": False,
                "away": True,
                "away_time": 10,
                "away_message": self._("I am currently away from {name}").format(name=self.NAME)
            },
            "display": {
                "theme": "default",
                "size": 100,
                "show_join_message": True,
                "show_part_message": True
            },
            "alerts": {
                "notify_inactive_tab": False,
                "matches": "Snakefire;python"
            }
        }

        if reload or not group in self._settings:
            settings = defaults[group] if group in defaults else {}

            self._qsettings.beginGroup(group);
            for setting in self._qsettings.childKeys():
                settings[str(setting)] = self._qsettings.value(setting).toPyObject()
            self._qsettings.endGroup();

            boolSettings = []
            if group == "connection":
                boolSettings += ["ssl", "connect", "join"]
            elif group == "program":
                boolSettings += ["away", "minimize"]
            elif group == "display":
                boolSettings += ["show_join_message", "show_part_message"]
            elif group == "alerts":
                boolSettings += ["notify_inactive_tab"]

            for boolSetting in boolSettings:
                try:
                    settings[boolSetting] = True if ["true", "1"].index(str(settings[boolSetting]).lower()) >= 0 else False
                except:
                    settings[boolSetting] = False

            if group == "connection" and settings["subdomain"] and settings["user"]:
                settings["password"] = keyring.get_password(self.NAME, str(settings["subdomain"])+"_"+str(settings["user"])) 

            self._settings[group] = settings

        settings = self._settings[group]
        if asString:
            for setting in settings:
                if not isinstance(settings[setting], bool):
                    settings[setting] = str(settings[setting]) if settings[setting] else ""
        return settings

    def setSettings(self, group, settings):
        self._settings[group] = settings;

        self._qsettings.beginGroup(group);
        for setting in self._settings[group]:
            if group != "connection" or setting != "password":
                self._qsettings.setValue(setting, settings[setting])
            elif settings["subdomain"] and settings["user"]:
                keyring.set_password(self.NAME, settings["subdomain"]+"_"+settings["user"], settings[setting]) 
        self._qsettings.endGroup();

        if group == "connection":
            self._canConnect = False
            if settings["subdomain"] and settings["user"] and settings["password"]:
                self._canConnect = True
            self._updateLayout()
        elif group == "program":
            if settings["away"] and self._connected:
                self._setUpIdleTracker()
            else:
                self._setUpIdleTracker(False)
        elif group == "display":
            for roomId in self._rooms.keys():
                if roomId in self._rooms and self._rooms[roomId]["view"]:
                    self._rooms[roomId]["view"].updateTheme(settings["theme"], settings["size"])

    def exit(self):
        self._forceClose = True
        self.close()

    def changeEvent(self, event):
        if self.getSetting("program", "minimize") and event.type() == QtCore.QEvent.WindowStateChange and self.isMinimized():
            self.hide()
            event.ignore()
        else:
            event.accept()

    def closeEvent(self, event):
        if (not hasattr(self, "_forceClose") or not self._forceClose) and self.getSetting("program", "minimize"):
            self.hide()
            event.ignore()
        else:
            if self.getSetting("connection", "join"):
                self.setSetting("connection", "rooms", ",".join([str(roomId) for roomId in self._rooms.keys()]))

            self.disconnectNow()

            if hasattr(self, "_workers") and self._workers:
                for worker in self._workers:
                    worker.terminate()
                    worker.wait()

            if hasattr(self, "_worker") and self._worker:
                self._worker.terminate()
                self._worker.wait()

            self.setSetting("window", "size", self.size())
            self.setSetting("window", "position", self.pos())

            event.accept()

    def alerts(self):
        dialog = AlertsDialog(self)
        dialog.open()

    def options(self):
        dialog = OptionsDialog(self)
        dialog.open()

    def connectNow(self):
        if not self._canConnect:
            return

        self._connecting = True
        self.statusBar().showMessage(self._("Connecting with Campfire..."))
        self._updateLayout()

        settings = self.getSettings("connection")

        self._worker = CampfireWorker(settings["subdomain"], settings["user"], settings["password"], settings["ssl"], self)
        self._connectWorkerSignals(self._worker)
        self._worker.connect()

    def disconnectNow(self):
        self.statusBar().showMessage(self._("Disconnecting from Campfire..."))
        if self._worker and hasattr(self, "_rooms"):
            # Using keys() since the dict could be changed (by _cfRoomLeft())
            # while iterating on it
            for roomId in self._rooms.keys():
                if roomId in self._rooms and self._rooms[roomId]["room"]:
                    self._worker.leave(self._rooms[roomId]["room"], False)
         
        self._cfDisconnected()
        self._updateLayout()

    def joinRoom(self, roomIndex=None):
        room = self._roomInIndex(roomIndex if roomIndex else self._toolBar["rooms"].currentIndex())
        if not room:
            return

        self._toolBar["join"].setEnabled(False)
        self.statusBar().showMessage(self._("Joining room {room}...").format(room=room["name"]))

        self._rooms[room["id"]] = {
            "room": None,
            "stream": None,
            "upload": None,
            "tab": None,
            "view": None,
            "frame": None,
            "usersList": None,
            "topicLabel": None,
            "filesLabel": None,
            "uploadButton": None,
            "uploadLabel": None,
            "uploadWidget": None,
            "newMessages": 0
        }
        self._getWorker().join(room["id"])

    def ping(self):
        if not self._connected:
            return
        for roomId in self._rooms.keys():
            if roomId in self._rooms and self._rooms[roomId]["room"]:
                self.updateRoomUsers(roomId, pinging=True)

    def speak(self):
        message = self._editor.document().toPlainText()
        room = self.getCurrentRoom()
        if not room or message.trimmed().isEmpty():
            return

        self._editor.document().clear()

        if message[0] == '/':
            command = QtCore.QString(message)
            separatorIndex = command.indexOf(QtCore.QRegExp('\\s'));
            handled = self.command(command.mid(1, separatorIndex-1), command.mid(separatorIndex + 1 if separatorIndex >= 0 else command.length()))
            if handled:
                return

        self.statusBar().showMessage(self._("Sending message to {room}...").format(room=room.name))
        self._getWorker().speak(room, unicode(message))

    def command(self, command, args):
        if command.compare(QtCore.QString("away"), QtCore.Qt.CaseInsensitive) == 0:
            self.toggleAway()
            return True

    def uploadFile(self):
        room = self.getCurrentRoom()
        if not room:
            return

        path = QtGui.QFileDialog.getOpenFileName(self, self._("Select file to upload"))
        if path:
            self._upload(room, str(path))

    def uploadCancel(self):
        room = self.getCurrentRoom()
        if not room:
            return

        if self._rooms[room.id]["upload"]:
            self._rooms[room.id]["upload"].stop().join()
            self._rooms[room.id]["upload"] = None

        self._rooms[room.id]["uploadWidget"].hide()

    def leaveRoom(self, roomId):
        if roomId in self._rooms:
            self.statusBar().showMessage(self._("Leaving room {room}...").format(room=self._rooms[roomId]["room"].name))
            self._getWorker().leave(self._rooms[roomId]["room"])

    def changeTopic(self):
        room = self.getCurrentRoom()
        if not room:
            return
        topic, ok = QtGui.QInputDialog.getText(self,
            self._("Change topic"),
            self._("Enter new topic for room {room}").format(room=room.name),
            QtGui.QLineEdit.Normal,
            room.topic
        )
        if ok:
            self.statusBar().showMessage(self._("Changing topic for room {room}...").format(room=room.name))
            self._getWorker().changeTopic(room, topic)

    def updateRoomUsers(self, roomId = None, pinging = False):
        if not roomId:
            room = self.getCurrentRoom()
            if room:
                roomId = room.id
        if roomId in self._rooms:
            if not pinging:
                self.statusBar().showMessage(self._("Getting users in {room}...").format(room=self._rooms[roomId]["room"].name))
            self._getWorker().users(self._rooms[roomId]["room"], pinging)

    def updateRoomUploads(self, roomId = None):
        if not roomId:
            room = self.getCurrentRoom()
            if room:
                roomId = room.id
        if roomId in self._rooms:
            self.statusBar().showMessage(self._("Getting uploads from {room}...").format(room=self._rooms[roomId]["room"].name))
            self._getWorker().uploads(self._rooms[roomId]["room"])

    def getCurrentRoom(self):
        index = self._tabs.currentIndex()
        for roomId in self._rooms.keys():
            if roomId in self._rooms and self._rooms[roomId]["tab"] == index:
                return self._rooms[roomId]["room"]

    def toggleAway(self):
        self.setAway(False if self._idle else True)

    def setAway(self, away=True):
        self._idle = away
        self.statusBar().showMessage(self._("You are now away") if self._idle else self._('You are now active'), 5000)

    def _idle(self):
        self.setAway(True)

    def _active(self):
        self.setAway(False)

    def _setUpIdleTracker(self, enable=True):
        if self._idleTimer:
            self._idleTimer.stop().wait()
            self._idleTimer = None

        if enable:
            try:
                self._idleTimer = IdleTimer(self, self.getSetting("program", "away_time") * 60)
                self.connect(self._idleTimer, QtCore.SIGNAL("idle()"), self._idle)
                self.connect(self._idleTimer, QtCore.SIGNAL("active()"), self._active)
                self._idleTimer.start()
            except:
                self._idleTimer = None

    def _cfStreamMessage(self, room, message, live=True, updateRoom=True):
        if (
            not message.user or 
            (live and message.is_text() and message.is_by_current_user()) or
            not room.id in self._rooms
        ):
            return

        view = self._rooms[room.id]["view"]
        frame = self._rooms[room.id]["frame"]
        if not view and frame:
            return

        notify = True
        alert = False
        alertIsDirectPing = False

        if message.is_text() and not message.is_by_current_user():
            alertIsDirectPing = QtCore.QString(message.body).contains(QtCore.QRegExp("\\b{name}\\b".format(name=self._worker.getUser().name), QtCore.Qt.CaseInsensitive))
            alert = True if alertIsDirectPing else self._matchesAlert(message.body)

        html = None
        if message.is_joining() and self.getSetting("display", "show_join_message"):
            html = self.MESSAGES["join"].format(user=message.user.name, room=room.name)
        elif message.is_leaving() and self.getSetting("display", "show_join_message"):
            html = self.MESSAGES["leave"].format(user=message.user.name, room=room.name)
        elif message.is_text() or message.is_upload():
            if message.body:
                body = self._plainTextToHTML(message.tweet["tweet"] if message.is_tweet() else message.body)

            if message.is_tweet():
                body = self.MESSAGES["tweet"].format(
                    url_user = "******".format(user=message.tweet["user"]),
                    user = message.tweet["user"], 
                    url = message.tweet["url"],
                    message = body
                )
            elif message.is_paste():
                body = self.MESSAGES["paste"].format(message=body)
            elif message.is_upload():
                body = self._displayUpload(view, message)
            else:
                body = self._autoLink(body)

            created = QtCore.QDateTime(
                message.created_at.year,
                message.created_at.month,
                message.created_at.day,
                message.created_at.hour,
                message.created_at.minute,
                message.created_at.second
            )
            created.setTimeSpec(QtCore.Qt.UTC)

            createdFormat = "h:mm ap"
            if created.daysTo(QtCore.QDateTime.currentDateTime()):
                createdFormat = "MMM d,  {createdFormat}".format(createdFormat=createdFormat)

            key = "message"
            if message.is_by_current_user():
                key = "message_self"
            elif alert:
                key = "alert"

            html = self.MESSAGES[key].format(
                time = created.toLocalTime().toString(createdFormat),
                user = message.user.name,
                message = body
            )
        elif message.is_topic_change():
            html = self.MESSAGES["topic"].format(user=message.user.name, topic=message.body)

        if html:
            currentScrollbarValue = frame.scrollPosition()
            autoScroll = (currentScrollbarValue == frame.scrollBarMaximum(QtCore.Qt.Vertical))
            frame.setHtml(frame.toHtml() + html)
            view.show()
            if autoScroll:
                frame.scroll(0, frame.scrollBarMaximum(QtCore.Qt.Vertical))
            else:
                frame.scroll(currentScrollbarValue.x(), currentScrollbarValue.y())

            tabIndex = self._rooms[room.id]["tab"]
            tabBar = self._tabs.tabBar()
            isActiveTab = (self.isActiveWindow() and tabIndex == self._tabs.currentIndex())
            
            if message.is_text() and not isActiveTab:
                self._rooms[room.id]["newMessages"] += 1

            if self._rooms[room.id]["newMessages"] > 0:
                tabBar.setTabText(tabIndex, "{room} ({count})".format(room = room.name, count = self._rooms[room.id]["newMessages"]))

            if not isActiveTab and (alert or self._rooms[room.id]["newMessages"] > 0) and tabBar.tabTextColor(tabIndex) == self.COLORS["normal"]:
                tabBar.setTabTextColor(tabIndex, self.COLORS["alert" if alert else "new"])

            notifyInactiveTab = self.getSetting("alerts", "notify_inactive_tab")

            if not isActiveTab and (alert or notifyInactiveTab):
                self._trayIcon.alert()

            if (alert and notify) or (not isActiveTab and notifyInactiveTab and message.is_text()):
                self._notify(room, "{} says: {}".format(message.user.name, message.body))

        if updateRoom:
            if (message.is_joining() or message.is_leaving()):
                self.updateRoomUsers(room.id)
            elif message.is_upload():
                self.updateRoomUploads(room.id)
            elif message.is_topic_change() and not message.is_by_current_user():
                self._cfTopicChanged(room, message.body)

        if live and alertIsDirectPing and self.getSetting("program", "away") and self._idle:
            self._getWorker().speak(room, unicode("{user}: {message}").format(
                user = message.user.name,
                message = self.getSetting("program", "away_message")
            ))

    def _displayUpload(self, view, message):
        image = None
        if message.upload['content_type'].startswith("image/"):
            try:
                request = urllib2.Request(message.upload['url'])
                auth_header = base64.encodestring('{}:{}'.format(self._worker.getApiToken(), 'X')).replace('\n', '')
                request.add_header("Authorization", "Basic {}".format(auth_header))
                image = urllib2.urlopen(request).read()
            except:
                pass

        if image:
            width = None
            try:
                imageFile = tempfile.NamedTemporaryFile('w+b')
                imageFile.write(image)
                (width, height) = Image.open(imageFile.name).size
                imageFile.close()

                maximumImageWidth = int(view.size().width() * 0.7) # 70% of viewport
                if width > maximumImageWidth:
                    width = maximumImageWidth
            except:
                pass

            html = self.MESSAGES["image"].format(
                type = message.upload['content_type'],
                data = base64.encodestring(image),
                url = message.upload['url'],
                name = message.upload['name'],
                attribs = "width=\"{width}\" ".format(width=width) if width else ""
            )
        else:
            html = self.MESSAGES["upload"].format(
                url = message.upload['url'],
                name = message.upload['name']
            )
        return html
        
    def _matchesAlert(self, message):
        matches = False
        regexes = []
        words = self.getSetting("alerts", "matches").split(";")

        for word in words:
            regexes.append("\\b{word}\\b".format(word=word))

        for regex in regexes:
            if QtCore.QString(message).contains(QtCore.QRegExp(regex, QtCore.Qt.CaseInsensitive)):
                matches = True
                break
        return matches

    def _cfConnected(self, user, rooms):
        self._connecting = False
        self._connected = True
        self._rooms = {}

        self._toolBar["rooms"].clear()
        for room in rooms:
            self._toolBar["rooms"].addItem(room["name"], room)

        self.statusBar().showMessage(self._("{user} connected to Campfire").format(user=user.name), 5000)
        self._updateLayout()

        if not self._pingTimer:
            self._pingTimer = QtCore.QTimer(self)
            self.connect(self._pingTimer, QtCore.SIGNAL("timeout()"), self.ping)
        self._pingTimer.start(60000) # Ping every minute

        if self.getSetting("program", "away"):
            self._setUpIdleTracker()

        if self.getSetting("connection", "join"):
            rooms = self.getSetting("connection", "rooms")
            if rooms:
                for roomId in rooms.split(","):
                    count = self._toolBar["rooms"].count()
                    if count:
                        roomIndex = None
                        for i in range(count):
                            data = self._toolBar["rooms"].itemData(i)
                            if not data.isNull():
                                data = data.toMap()
                                for key in data:
                                    if str(key) == "id" and str(data[key].toString()) == roomId:
                                        roomIndex = i
                                        break;
                                if roomIndex is not None:
                                    break
                        if roomIndex is not None:
                            self.joinRoom(roomIndex)

    def _cfDisconnected(self):
        if self._pingTimer:
            self._pingTimer.stop()
            self._pingTimer = None

        if self._idleTimer:
            self._setUpIdleTracker(False)

        self._connecting = False
        self._connected = False
        self._rooms = {}
        self._worker = None
        self.statusBar().clearMessage()

    def _cfRoomJoined(self, room, messages=[], rejoined=False):
        if room.id not in self._rooms:
            return

        if not rejoined:
            self._rooms[room.id].update(self._setupRoomUI(room))
            self._rooms[room.id]["room"] = room
        self._rooms[room.id]["stream"] = self._worker.getStream(room)
        self.updateRoomUsers(room.id)
        self.updateRoomUploads(room.id)
        if not rejoined:
            self.statusBar().showMessage(self._("Joined room {room}").format(room=room.name), 5000)
        self._updatedRoomsList()
        if not rejoined and messages:
            for message in messages:
                self._cfStreamMessage(room, message, live=False, updateRoom=False)

    def _cfSpoke(self, room, message):
        self._cfStreamMessage(room, message, live=False)
        self.statusBar().clearMessage()

    def _cfRoomLeft(self, room):
        if self._rooms[room.id]["stream"]:
            self._rooms[room.id]["stream"].stop().join()
        if self._rooms[room.id]["upload"]:
            self._rooms[room.id]["upload"].stop().join()

        self._tabs.removeTab(self._rooms[room.id]["tab"])
        del self._rooms[room.id]
        self.statusBar().showMessage(self._("Left room {room}").format(room=room.name), 5000)
        self._updatedRoomsList()

    def _cfRoomUsers(self, room, users, pinging=False):
        # We may be disconnecting while still processing the list
        if not room.id in self._rooms:
            return

        if not pinging:
            self.statusBar().clearMessage()
        self._rooms[room.id]["usersList"].clear()
        for user in users:
            item = QtGui.QListWidgetItem(user["name"])
            item.setData(QtCore.Qt.UserRole, user)
            self._rooms[room.id]["usersList"].addItem(item)

    def _cfRoomUploads(self, room, uploads):
        # We may be disconnecting while still processing the list
        if not room.id in self._rooms:
            return

        self.statusBar().clearMessage()
        label = self._rooms[room.id]["filesLabel"]
        if uploads:
            html = ""
            for upload in uploads:
                html += "{br}&bull; <a href=\"{url}\">{name}</a>".format(
                    br = "<br />" if html else "",
                    url = upload["full_url"],
                    name = upload["name"]
                )
            html = "{text}<br />{html}".format(
                text = self._("Latest uploads:"),
                html = html
            )

            label.setText(html)
            if not label.isVisible():
                label.show()
        elif label.isVisible():
            label.setText("")
            label.hide()

    def _cfUploadProgress(self, room, current, total):
        if not room.id in self._rooms:
            return
        
        progressBar = self._rooms[room.id]["uploadProgressBar"]
        if not self._rooms[room.id]["uploadWidget"].isVisible():
            self._rooms[room.id]["uploadWidget"].show()
            progressBar.setMaximum(total)

        progressBar.setValue(current)

    def _cfUploadFinished(self, room):
        if not room.id in self._rooms:
            return

        self._rooms[room.id]["upload"].join()
        self._rooms[room.id]["upload"] = None
        self._rooms[room.id]["uploadWidget"].hide()

    def _cfTopicChanged(self, room, topic):
        if not room.id in self._rooms:
            return
        
        self._rooms[room.id]["topicLabel"].setText(topic)
        self.statusBar().clearMessage()

    def _cfConnectError(self, error):
        self._cfDisconnected()
        self._updateLayout()
        self._cfError(error)

    def _cfError(self, error):
        self.statusBar().clearMessage()
        if not self._connected:
            QtGui.QMessageBox.critical(self, "Error", self._("Error while connecting: {error}".format(error = str(error))))
        else:
            QtGui.QMessageBox.critical(self, "Error", str(error))

    def _cfRoomError(self, error, room):
        self.statusBar().clearMessage()
        if isinstance(error, RuntimeError):
            (code, message) = error
            if code == 401:
                self.statusBar().showMessage(self._("Disconnected from room. Rejoining room {room}...").format(room=room.name), 5000)
                self._rooms[room.id]["stream"].stop().join()
                self._getWorker().join(room.id, True)
                return
        QtGui.QMessageBox.critical(self, "Error", str(error))

    def _roomSelected(self, index):
        self._updatedRoomsList(index)

    def _upload(self, room, path):
        self._rooms[room.id]["upload"] = self._worker.upload(room, path)
        self._updateRoomLayout()

    def _roomTabClose(self, tabIndex):
        for roomId in self._rooms:
            if self._rooms[roomId]["tab"] == tabIndex:
                self.leaveRoom(roomId)
                break

    def _roomTabFocused(self):
        tabIndex = self._tabs.currentIndex()
        if tabIndex < 0 or not self.isActiveWindow():
            return

        room = self._roomInTabIndex(tabIndex)
        if not room:
            return

        tabBar = self._tabs.tabBar()

        if self._rooms[room.id]["newMessages"] > 0:
            self._rooms[room.id]["newMessages"] = 0
            tabBar.setTabText(tabIndex, room.name)

        if tabBar.tabTextColor(tabIndex) != self.COLORS["normal"]:
            tabBar.setTabTextColor(tabIndex, self.COLORS["normal"])

        self._updateRoomLayout()

    def _roomInTabIndex(self, index):
        room = None
        for key in self._rooms:
            if self._rooms[key]["tab"] == index:
                room = self._rooms[key]["room"]
                break
        return room

    def _roomInIndex(self, index):
        room = {}
        data = self._toolBar["rooms"].itemData(index)
        if not data.isNull():
            data = data.toMap()
            for key in data:
                room[str(key)] = unicode(data[key].toString())
        return room

    def _connectWorkerSignals(self, worker):
        self.connect(worker, QtCore.SIGNAL("error(PyQt_PyObject)"), self._cfError)
        self.connect(worker, QtCore.SIGNAL("connected(PyQt_PyObject, PyQt_PyObject)"), self._cfConnected)
        self.connect(worker, QtCore.SIGNAL("connectError(PyQt_PyObject)"), self._cfConnectError)
        self.connect(worker, QtCore.SIGNAL("streamError(PyQt_PyObject, PyQt_PyObject)"), self._cfRoomError)
        self.connect(worker, QtCore.SIGNAL("uploadError(PyQt_PyObject, PyQt_PyObject)"), self._cfRoomError)
        self.connect(worker, QtCore.SIGNAL("joined(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"), self._cfRoomJoined)
        self.connect(worker, QtCore.SIGNAL("spoke(PyQt_PyObject, PyQt_PyObject)"), self._cfSpoke)
        self.connect(worker, QtCore.SIGNAL("streamMessage(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"), self._cfStreamMessage)
        self.connect(worker, QtCore.SIGNAL("left(PyQt_PyObject)"), self._cfRoomLeft)
        self.connect(worker, QtCore.SIGNAL("users(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"), self._cfRoomUsers)
        self.connect(worker, QtCore.SIGNAL("uploads(PyQt_PyObject, PyQt_PyObject)"), self._cfRoomUploads)
        self.connect(worker, QtCore.SIGNAL("uploadProgress(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)"), self._cfUploadProgress)
        self.connect(worker, QtCore.SIGNAL("uploadFinished(PyQt_PyObject)"), self._cfUploadFinished)
        self.connect(worker, QtCore.SIGNAL("topicChanged(PyQt_PyObject, PyQt_PyObject)"), self._cfTopicChanged)

    def _getWorker(self):
        if not hasattr(self, "_workers"):
            self._workers = []

        if self._workers:
            for worker in self._workers:
                if worker.isFinished():
                    return worker

        worker = copy.copy(self._worker)
        self._connectWorkerSignals(worker)
        self._workers.append(worker)
        return worker

    def _updatedRoomsList(self, index=None):
        if not index:
            index = self._toolBar["rooms"].currentIndex()

        room = self._roomInIndex(index)

        self._toolBar["join"].setEnabled(False)
        if not room or room["id"] not in self._rooms:
            self._toolBar["join"].setEnabled(True)

        centralWidget = self.centralWidget()
        if not self._tabs.count():
            centralWidget.hide()
        else:
            centralWidget.show()

    def _notify(self, room, message):
        raise NotImplementedError("_notify() must be implemented")

    def _updateRoomLayout(self):
        room = self.getCurrentRoom()
        if room:
            canUpload = not self._rooms[room.id]["upload"]
            uploadButton = self._rooms[room.id]["uploadButton"]
            if (
                (canUpload and not uploadButton.isEnabled()) or
                (not canUpload and uploadButton.isEnabled())
            ):
                uploadButton.setEnabled(canUpload)

    def _updateLayout(self):
        self._menus["file"]["connect"].setEnabled(not self._connected and self._canConnect and not self._connecting)
        self._menus["file"]["disconnect"].setEnabled(self._connected)

        roomsEmpty = self._toolBar["rooms"].count() == 1 and self._toolBar["rooms"].itemData(0).isNull()
        if not roomsEmpty and (not self._connected or not self._toolBar["rooms"].count()):
            self._toolBar["rooms"].clear()
            self._toolBar["rooms"].addItem(self._("No rooms available"))
            self._toolBar["rooms"].setEnabled(False)
        elif not roomsEmpty:
            self._toolBar["rooms"].setEnabled(True)

        self._toolBar["roomsLabel"].setEnabled(self._toolBar["rooms"].isEnabled())
        self._toolBar["join"].setEnabled(self._toolBar["rooms"].isEnabled())

    def _setupRoomUI(self, room):
        topic = room.topic if room.topic else ""
        topicLabel = ClickableQLabel(topic)
        topicLabel.setToolTip(self._("Click here to change room's topic"))
        topicLabel.setWordWrap(True)
        self.connect(topicLabel, QtCore.SIGNAL("clicked()"), self.changeTopic)

        view = SnakeFireWebView(self)
        view.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
        frame = view.page().mainFrame()

        #Send all link clicks to systems web browser
        view.page().setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks)
        def linkClicked(url): webbrowser.open(str(url.toString()))
        view.connect(view, QtCore.SIGNAL("linkClicked (const QUrl&)"), linkClicked)

        # Support auto scroll when needed
        def autoScroll(size): frame.scroll(0, size.height())
        frame.connect(frame, QtCore.SIGNAL("contentsSizeChanged (const QSize&)"), autoScroll)

        usersList = QtGui.QListWidget()

        filesLabel = QtGui.QLabel("")
        filesLabel.setOpenExternalLinks(True)
        filesLabel.setWordWrap(True)
        filesLabel.hide()

        uploadButton = QtGui.QPushButton(self._("&Upload new file"))
        self.connect(uploadButton, QtCore.SIGNAL("clicked()"), self.uploadFile)

        uploadProgressBar = QtGui.QProgressBar()
        uploadProgressLabel = QtGui.QLabel(self._("Uploading:"))

        uploadCancelButton = QtGui.QPushButton(self._("Cancel"))
        self.connect(uploadCancelButton, QtCore.SIGNAL("clicked()"), self.uploadCancel)

        uploadLayout = QtGui.QHBoxLayout()
        uploadLayout.addWidget(uploadProgressLabel)
        uploadLayout.addWidget(uploadProgressBar)
        uploadLayout.addWidget(uploadCancelButton)

        uploadWidget = QtGui.QWidget()
        uploadWidget.setLayout(uploadLayout)
        uploadWidget.hide()

        leftFrameLayout = QtGui.QVBoxLayout()
        leftFrameLayout.addWidget(topicLabel)
        leftFrameLayout.addWidget(view)
        leftFrameLayout.addWidget(uploadWidget)

        rightFrameLayout = QtGui.QVBoxLayout()
        rightFrameLayout.addWidget(QtGui.QLabel(self._("Users in room:")))
        rightFrameLayout.addWidget(usersList)
        rightFrameLayout.addWidget(filesLabel)
        rightFrameLayout.addWidget(uploadButton)
        rightFrameLayout.addStretch(1)

        leftFrame = QtGui.QWidget()
        leftFrame.setLayout(leftFrameLayout)

        rightFrame = QtGui.QWidget()
        rightFrame.setLayout(rightFrameLayout)

        splitter = QtGui.QSplitter()
        splitter.addWidget(leftFrame)
        splitter.addWidget(rightFrame)
        splitter.setSizes([splitter.size().width() * 0.75, splitter.size().width() * 0.25])

        index = self._tabs.addTab(splitter, room.name)
        self._tabs.setCurrentIndex(index)

        if not self.COLORS["normal"]:
            self.COLORS["normal"] = self._tabs.tabBar().tabTextColor(index)
        else:
            self._tabs.tabBar().setTabTextColor(index, self.COLORS["normal"])

        return {
            "tab": index,
            "view": view,
            "frame": frame,
            "usersList": usersList,
            "topicLabel": topicLabel,
            "filesLabel": filesLabel,
            "uploadButton": uploadButton,
            "uploadWidget": uploadWidget,
            "uploadProgressBar": uploadProgressBar,
            "uploadProgressLabel": uploadProgressLabel
        }

    def _setupUI(self):
        self.setWindowTitle(self.NAME)

        self._addMenu()
        self._addToolbar()

        self._tabs = QtGui.QTabWidget()
        self._tabs.setTabsClosable(True)
        self.connect(self._tabs, QtCore.SIGNAL("currentChanged(int)"), self._roomTabFocused)
        self.connect(self._tabs, QtCore.SIGNAL("tabCloseRequested(int)"), self._roomTabClose)

        self._editor = QtGui.QPlainTextEdit()
        self._editor.setFixedHeight(self._editor.fontMetrics().height() * 2)
        self._editor.installEventFilter(SuggesterKeyPressEventFilter(self, Suggester(self._editor)))

        speakButton = QtGui.QPushButton(self._("&Send"))
        self.connect(speakButton, QtCore.SIGNAL('clicked()'), self.speak)

        grid = QtGui.QGridLayout()
        grid.setRowStretch(0, 1)
        grid.addWidget(self._tabs, 0, 0, 1, -1)
        grid.addWidget(self._editor, 2, 0)
        grid.addWidget(speakButton, 2, 1)

        widget = QtGui.QWidget()
        widget.setLayout(grid)
        self.setCentralWidget(widget)

        tabWidgetFocusEventFilter = TabWidgetFocusEventFilter(self)
        self.connect(tabWidgetFocusEventFilter, QtCore.SIGNAL("tabFocused()"), self._roomTabFocused)
        widget.installEventFilter(tabWidgetFocusEventFilter)

        self.centralWidget().hide()

        size = self.getSetting("window", "size")

        if not size:
            size = QtCore.QSize(640, 480)

        self.resize(size)

        position = self.getSetting("window", "position")
        if not position:
            screen = QtGui.QDesktopWidget().screenGeometry()
            position = QtCore.QPoint((screen.width()-size.width())/2, (screen.height()-size.height())/2)

        self.move(position)

        self._updateLayout()

        menu = QtGui.QMenu(self)
        menu.addAction(self._menus["file"]["connect"])
        menu.addAction(self._menus["file"]["disconnect"])
        menu.addSeparator()
        menu.addAction(self._menus["file"]["exit"])

        self._trayIcon = Systray(self._icon, self)
        self._trayIcon.setContextMenu(menu)
        self._trayIcon.setToolTip(self.DESCRIPTION)

    def _addMenu(self):
        self._menus = {
            "file": {
                "connect": self._createAction(self._("&Connect"), self.connectNow, icon="connect.png"),
                "disconnect": self._createAction(self._("&Disconnect"), self.disconnectNow, icon="disconnect.png"),
                "exit": self._createAction(self._("E&xit"), self.exit)
            },
            "settings": {
                "alerts": self._createAction(self._("&Alerts..."), self.alerts, icon="alerts.png"),
                "options": self._createAction(self._("&Options..."), self.options)
            },
            "help": {
                "about": self._createAction(self._("A&bout"))
            }
        }

        menu = self.menuBar()

        file_menu = menu.addMenu(self._("&File"))
        file_menu.addAction(self._menus["file"]["connect"])
        file_menu.addAction(self._menus["file"]["disconnect"])
        file_menu.addSeparator()
        file_menu.addAction(self._menus["file"]["exit"])

        settings_menu = menu.addMenu(self._("S&ettings"))
        settings_menu.addAction(self._menus["settings"]["alerts"])
        settings_menu.addSeparator()
        settings_menu.addAction(self._menus["settings"]["options"])

        help_menu = menu.addMenu(self._("&Help"))
        help_menu.addAction(self._menus["help"]["about"])

    def _addToolbar(self):
        self._toolBar = {
            "connect": self._menus["file"]["connect"],
            "disconnect": self._menus["file"]["disconnect"],
            "roomsLabel": QtGui.QLabel(self._("Rooms:")),
            "rooms": QtGui.QComboBox(),
            "join": self._createAction(self._("Join room"), self.joinRoom, icon="join.png"),
            "alerts": self._menus["settings"]["alerts"]
        }

        self.connect(self._toolBar["rooms"], QtCore.SIGNAL("currentIndexChanged(int)"), self._roomSelected)

        toolBar = self.toolBar()
        toolBar.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
        toolBar.addAction(self._toolBar["connect"])
        toolBar.addAction(self._toolBar["disconnect"])
        toolBar.addSeparator();
        toolBar.addWidget(self._toolBar["roomsLabel"])
        toolBar.addWidget(self._toolBar["rooms"])
        toolBar.addAction(self._toolBar["join"])
        toolBar.addSeparator();
        toolBar.addAction(self._toolBar["alerts"])

    def _createAction(self, text, slot=None, shortcut=None, icon=None, 
        tip=None, checkable=False, signal="triggered()"):
        """ Create an action """
        action = QtGui.QAction(text, self) 
        if icon is not None:
            if not isinstance(icon, QtGui.QIcon):
                action.setIcon(QtGui.QIcon(":/icons/{icon}".format(icon=icon)))
            else:
                action.setIcon(icon)
        if shortcut is not None: 
            action.setShortcut(shortcut) 
        if tip is not None: 
            action.setToolTip(tip) 
            action.setStatusTip(tip) 
        if slot is not None: 
            self.connect(action, QtCore.SIGNAL(signal), slot) 
        if checkable: 
            action.setCheckable(True)
        return action

    def _plainTextToHTML(self, string):
        return string.replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br />")

    def _autoLink(self, string):
        urlre = re.compile("(\(?https?://[-A-Za-z0-9+&@#/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#/%=~_()|])(\">|</a>)?")
        urls = urlre.findall(string)
        cleanUrls = []
        for url in urls:
            if url[1]:
                continue

            currentUrl = url[0]
            if currentUrl[0] == '(' and currentUrl[-1] == ')':
                currentUrl = currentUrl[1:-1]

            if currentUrl in cleanUrls:
                continue

            cleanUrls.append(currentUrl)
            string = re.sub("(?<!(=\"|\">))" + re.escape(currentUrl),
                            "<a href=\"" + currentUrl + "\">" + currentUrl + "</a>",
                            string)
        return string

    def _(self, string, module=None):
        return str(QtCore.QCoreApplication.translate(module or Snakefire.NAME, string))