예제 #1
0
class Snakefire(object):
	DOMAIN = "snakefire.org"
	NAME = "Snakefire"
	DESCRIPTION = "Snakefire: Campfire Linux Client"
	VERSION = "1.0.1"
	ICON = "snakefire.png"
	COLORS = {
		"time": "c0c0c0",
		"alert": "ff0000",
		"join": "cb81cb",
		"leave": "cb81cb",
		"topic": "808080",
		"upload": "000000",
		"message": "000000",
		"nick": "808080",
		"nickAlert": "ff0000",
		"nickSelf": "000080",
		"tabs": {
			"normal": None,
			"new": QtGui.QColor(0, 0, 255),
			"alert": QtGui.QColor(255, 0, 0)
		}
	}

	def __init__(self):
		self.DESCRIPTION = self._(self.DESCRIPTION)
		self._worker = None
		self._settings = {}
		self._canConnect = False
		self._cfDisconnected()
		self._qsettings = QtCore.QSettings()
		self._icon = QtGui.QIcon(":/icons/%s" % (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 _(self, string, module=None):
		return str(QtCore.QCoreApplication.translate(module or Snakefire.NAME, string))

	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
			}
		}

		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 += ["minimize"]

			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()

	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 %s...") % room["name"])

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

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

		self.statusBar().showMessage(self._("Sending message to %s...") % room.name)
		self._getWorker().speak(room, unicode(message))
		self._editor.document().clear()

	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 %s...") % 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 %s") % room.name,
			QtGui.QLineEdit.Normal,
			room.topic
		)
		if ok:
			self.statusBar().showMessage(self._("Changing topic for room %s...") % room.name)
			self._getWorker().changeTopic(room, topic)

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

	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 in %s...") % 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 _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

		user = message.user.name
		notify = True
		alert = False

		if message.is_text() and not message.is_by_current_user():
			alert = self._matchesAlert(message.body)

		html = None
		if message.is_joining():
			html = "<font color=\"#%s\">" % self.COLORS["join"]
			html += "--&gt; %s joined %s" % (user, room.name)
			html += "</font>"
		elif message.is_leaving():
			html = "<font color=\"#%s\">" % self.COLORS["leave"]
			html += "&lt;-- %s has left %s" % (user, room.name)
			html += "</font>"
		elif message.is_text():
			body = self._plainTextToHTML(message.tweet["tweet"] if message.is_tweet() else message.body)
			if message.is_tweet():
				body = "<a href=\"%s\">%s</a> <a href=\"%s\">tweeted</a>: %s" % (
					"http://twitter.com/%s" % message.tweet["user"],
					message.tweet["user"], 
					message.tweet["url"],
					body
				)
			elif message.is_paste():
				body = "<br /><hr /><code>%s</code><hr />" % body
			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,  %s" % createdFormat

			html = "<font color=\"#%s\">[%s]</font> " % (self.COLORS["time"], created.toLocalTime().toString(createdFormat))

			if alert:
				html += "<font color=\"#%s\">" % self.COLORS["alert"]
			else:
				html += "<font color=\"#%s\">" % self.COLORS["message"]

			if message.is_by_current_user():
				html += "<font color=\"#%s\">" % self.COLORS["nickSelf"]
			elif alert:
			 	html += "<font color=\"#%s\">" % self.COLORS["nickAlert"]
			else:
				html += "<font color=\"#%s\">" % self.COLORS["nick"]

			html += "%s" % ("<strong>%s</strong>" % user if alert else user)
			html += "</font>: "
			html += body
			html += "</font>"
		elif message.is_upload():
			html = "<font color=\"#%s\">" % self.COLORS["upload"]
			html += "<strong>%s</strong> uploaded <a href=\"%s\">%s</a>" % (
				user,
				message.upload["url"],
				message.upload["name"]
			)
			html += "</font>"
		elif message.is_topic_change():
			html = "<font color=\"#%s\">" % self.COLORS["leave"]
			html += "%s changed topic to <strong>%s</strong>" % (user, message.body)
			html += "</font>"

		if html:
			html = "%s<br />" % html
			editor = self._rooms[room.id]["editor"]
			if not editor:
				return

			scrollbar = editor.verticalScrollBar()
			currentScrollbarValue = scrollbar.value()
			autoScroll = (currentScrollbarValue == scrollbar.maximum())
			editor.moveCursor(QtGui.QTextCursor.End)
			editor.textCursor().insertHtml(html)
			if autoScroll:
				scrollbar.setValue(scrollbar.maximum())
			else:
				scrollbar.setValue(currentScrollbarValue)

			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, "%s (%s)" % (room.name, self._rooms[room.id]["newMessages"]))

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

			if alert:
				if not isActiveTab:
					self._trayIcon.alert()
				if notify:
					self._notify(room, 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)

	def _matchesAlert(self, message):
		matches = False
		regexes = []
		words = [
			"Mariano Iglesias",
			"Mariano"
		]
		for word in words:
			regexes.append("\\b%s\\b" % 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._("%s connected to Campfire") % user.name, 5000)
		self._updateLayout()

		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):
		self._connecting = False
		self._connected = False
		self._rooms = {}
		self._worker = None
		self.statusBar().clearMessage()

	def _cfRoomJoined(self, room, messages=[]):
		if room.id not in self._rooms:
			return
		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)
		self.statusBar().showMessage(self._("Joined room %s") % room.name, 5000)
		self._updatedRoomsList()
		if 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 %s") % room.name, 5000)
		self._updatedRoomsList()

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

		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 += "%s&bull; <a href=\"%s\">%s</a>" % (
					"<br />" if html else "",
					upload["full_url"],
					upload["name"]
				)
			html = "%s<br />%s" % (
				self._("Latest uploads:"),
				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()
		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["tabs"]["normal"]:
			tabBar.setTabTextColor(tabIndex, self.COLORS["tabs"]["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)] = u"%s" % 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("joined(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)"), 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):
		topicLabel = ClickableQLabel(room.topic)
		topicLabel.setToolTip(self._("Click here to change room's topic"))
		topicLabel.setWordWrap(True)
		self.connect(topicLabel, QtCore.SIGNAL("clicked()"), self.changeTopic)

		editor = QtGui.QTextBrowser()
		editor.setOpenExternalLinks(True)

		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(editor)
		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["tabs"]["normal"]:
			self.COLORS["tabs"]["normal"] = self._tabs.tabBar().tabTextColor(index)
		else:
			self._tabs.tabBar().setTabTextColor(index, self.COLORS["tabs"]["normal"])

		return {
			"tab": index,
			"editor": editor,
			"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, 1, 0)
		grid.addWidget(speakButton, 1, 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/%s" % (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
예제 #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))
예제 #3
0
class Snakefire(object):
    DOMAIN = "snakefire.org"
    NAME = "Snakefire"
    DESCRIPTION = "Snakefire: Campfire Linux Client"
    VERSION = "1.0.1"
    ICON = "snakefire.png"
    COLORS = {
        "time": "c0c0c0",
        "alert": "ff0000",
        "join": "cb81cb",
        "leave": "cb81cb",
        "topic": "808080",
        "upload": "000000",
        "message": "000000",
        "nick": "808080",
        "nickAlert": "ff0000",
        "nickSelf": "000080",
        "tabs": {
            "normal": None,
            "new": QtGui.QColor(0, 0, 255),
            "alert": QtGui.QColor(255, 0, 0)
        }
    }

    def __init__(self):
        self.DESCRIPTION = self._(self.DESCRIPTION)
        self._worker = None
        self._settings = {}
        self._canConnect = False
        self._cfDisconnected()
        self._qsettings = QtCore.QSettings()
        self._icon = QtGui.QIcon(":/icons/%s" % (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 _(self, string, module=None):
        return str(
            QtCore.QCoreApplication.translate(module or Snakefire.NAME,
                                              string))

    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
            }
        }

        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 += ["minimize"]

            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()

    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 %s...") % room["name"])

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

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

        self.statusBar().showMessage(
            self._("Sending message to %s...") % room.name)
        self._getWorker().speak(room, unicode(message))
        self._editor.document().clear()

    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 %s...") %
                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 %s") % room.name,
            QtGui.QLineEdit.Normal, room.topic)
        if ok:
            self.statusBar().showMessage(
                self._("Changing topic for room %s...") % room.name)
            self._getWorker().changeTopic(room, topic)

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

    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 in %s...") %
                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 _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

        user = message.user.name
        notify = True
        alert = False

        if message.is_text() and not message.is_by_current_user():
            alert = self._matchesAlert(message.body)

        html = None
        if message.is_joining():
            html = "<font color=\"#%s\">" % self.COLORS["join"]
            html += "--&gt; %s joined %s" % (user, room.name)
            html += "</font>"
        elif message.is_leaving():
            html = "<font color=\"#%s\">" % self.COLORS["leave"]
            html += "&lt;-- %s has left %s" % (user, room.name)
            html += "</font>"
        elif message.is_text():
            body = self._plainTextToHTML(
                message.tweet["tweet"] if message.is_tweet() else message.body)
            if message.is_tweet():
                body = "<a href=\"%s\">%s</a> <a href=\"%s\">tweeted</a>: %s" % (
                    "http://twitter.com/%s" % message.tweet["user"],
                    message.tweet["user"], message.tweet["url"], body)
            elif message.is_paste():
                body = "<br /><hr /><code>%s</code><hr />" % body
            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,  %s" % createdFormat

            html = "<font color=\"#%s\">[%s]</font> " % (
                self.COLORS["time"],
                created.toLocalTime().toString(createdFormat))

            if alert:
                html += "<font color=\"#%s\">" % self.COLORS["alert"]
            else:
                html += "<font color=\"#%s\">" % self.COLORS["message"]

            if message.is_by_current_user():
                html += "<font color=\"#%s\">" % self.COLORS["nickSelf"]
            elif alert:
                html += "<font color=\"#%s\">" % self.COLORS["nickAlert"]
            else:
                html += "<font color=\"#%s\">" % self.COLORS["nick"]

            html += "%s" % ("<strong>%s</strong>" % user if alert else user)
            html += "</font>: "
            html += body
            html += "</font>"
        elif message.is_upload():
            html = "<font color=\"#%s\">" % self.COLORS["upload"]
            html += "<strong>%s</strong> uploaded <a href=\"%s\">%s</a>" % (
                user, message.upload["url"], message.upload["name"])
            html += "</font>"
        elif message.is_topic_change():
            html = "<font color=\"#%s\">" % self.COLORS["leave"]
            html += "%s changed topic to <strong>%s</strong>" % (user,
                                                                 message.body)
            html += "</font>"

        if html:
            html = "%s<br />" % html
            editor = self._rooms[room.id]["editor"]
            if not editor:
                return

            scrollbar = editor.verticalScrollBar()
            currentScrollbarValue = scrollbar.value()
            autoScroll = (currentScrollbarValue == scrollbar.maximum())
            editor.moveCursor(QtGui.QTextCursor.End)
            editor.textCursor().insertHtml(html)
            if autoScroll:
                scrollbar.setValue(scrollbar.maximum())
            else:
                scrollbar.setValue(currentScrollbarValue)

            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, "%s (%s)" %
                    (room.name, self._rooms[room.id]["newMessages"]))

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

            if alert:
                if not isActiveTab:
                    self._trayIcon.alert()
                if notify:
                    self._notify(room, 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)

    def _matchesAlert(self, message):
        matches = False
        regexes = []
        words = ["Mariano Iglesias", "Mariano"]
        for word in words:
            regexes.append("\\b%s\\b" % 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._("%s connected to Campfire") % user.name, 5000)
        self._updateLayout()

        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):
        self._connecting = False
        self._connected = False
        self._rooms = {}
        self._worker = None
        self.statusBar().clearMessage()

    def _cfRoomJoined(self, room, messages=[]):
        if room.id not in self._rooms:
            return
        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)
        self.statusBar().showMessage(
            self._("Joined room %s") % room.name, 5000)
        self._updatedRoomsList()
        if 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 %s") % room.name, 5000)
        self._updatedRoomsList()

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

        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 += "%s&bull; <a href=\"%s\">%s</a>" % (
                    "<br />" if html else "", upload["full_url"],
                    upload["name"])
            html = "%s<br />%s" % (self._("Latest uploads:"), 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()
        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["tabs"]["normal"]:
            tabBar.setTabTextColor(tabIndex, self.COLORS["tabs"]["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)] = str(unicode(data[key].toString(), "utf-8"))
        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("joined(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)"),
                     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):
        topicLabel = ClickableQLabel(room.topic)
        topicLabel.setToolTip(self._("Click here to change room's topic"))
        topicLabel.setWordWrap(True)
        self.connect(topicLabel, QtCore.SIGNAL("clicked()"), self.changeTopic)

        editor = QtGui.QTextBrowser()
        editor.setOpenExternalLinks(True)

        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(editor)
        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["tabs"]["normal"]:
            self.COLORS["tabs"]["normal"] = self._tabs.tabBar().tabTextColor(
                index)
        else:
            self._tabs.tabBar().setTabTextColor(index,
                                                self.COLORS["tabs"]["normal"])

        return {
            "tab": index,
            "editor": editor,
            "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, 1, 0)
        grid.addWidget(speakButton, 1, 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/%s" % (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
예제 #4
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))
예제 #5
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))