From 18c1e44ad0a60e887a5b1f0d9a38e4be6ad2d722 Mon Sep 17 00:00:00 2001
From: Vlad Voicu <vladvoic@gmail.com>
Date: Tue, 19 Apr 2011 11:49:45 +0300
Subject: Use a bar to show where the last read messages in a chat are

License: This patch is BSD-licensed, see  http://www.opensource.org/licenses/bsd-license.php

diff --git a/SwifTools/LastLineTracker.cpp b/SwifTools/LastLineTracker.cpp
new file mode 100644
index 0000000..a7360a8
--- /dev/null
+++ b/SwifTools/LastLineTracker.cpp
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include "LastLineTracker.h"
+
+using namespace Swift;
+
+LastLineTracker::LastLineTracker() {
+	lastFocus = true;
+	shouldMove = false;
+}
+
+void LastLineTracker::setHasFocus(bool focus) {
+	if (!focus && lastFocus) {
+			shouldMove = true;
+			lastFocus = focus;
+			return;
+	}
+	shouldMove = false;
+	lastFocus = focus;
+}
+
+bool LastLineTracker::getShouldMoveLastLine() {
+	bool ret = shouldMove;
+	shouldMove = false;
+	return ret;
+}
diff --git a/SwifTools/LastLineTracker.h b/SwifTools/LastLineTracker.h
new file mode 100644
index 0000000..b7c9a3b
--- /dev/null
+++ b/SwifTools/LastLineTracker.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+namespace Swift {
+	class LastLineTracker {
+		public:
+			LastLineTracker();
+			void setHasFocus(bool focus);
+			bool getShouldMoveLastLine();
+		private:
+			bool lastFocus;
+			bool shouldMove;
+	};
+}
diff --git a/SwifTools/SConscript b/SwifTools/SConscript
index 8d00418..e5085cc 100644
--- a/SwifTools/SConscript
+++ b/SwifTools/SConscript
@@ -27,6 +27,7 @@ if env["SCONS_STAGE"] == "build" :
 			"AutoUpdater/PlatformAutoUpdaterFactory.cpp",
 			"Linkify.cpp",
 			"TabComplete.cpp",
+			"LastLineTracker.cpp",
 		]
 
 	if swiftools_env.get("HAVE_SPARKLE", 0) :
diff --git a/SwifTools/UnitTest/LastLineTrackerTest.cpp b/SwifTools/UnitTest/LastLineTrackerTest.cpp
new file mode 100644
index 0000000..374f4de
--- /dev/null
+++ b/SwifTools/UnitTest/LastLineTrackerTest.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2011 Vlad Voicu
+ * Licensed under the Simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/extensions/TestFactoryRegistry.h>
+
+#include "SwifTools/LastLineTracker.h"
+
+using namespace Swift;
+
+class LastLineTrackerTest : public CppUnit::TestFixture {
+	CPPUNIT_TEST_SUITE(LastLineTrackerTest);
+	CPPUNIT_TEST(testFocusNormal);
+	CPPUNIT_TEST(testFocusOut);
+	CPPUNIT_TEST(testFocusOtherTab);
+	CPPUNIT_TEST(testRepeatedFocusOut);
+	CPPUNIT_TEST(testRepeatedFocusIn);
+	CPPUNIT_TEST_SUITE_END();
+	public:
+	LastLineTrackerTest () {
+	};
+	void testFocusNormal() {
+		LastLineTracker testling;
+		testling.setHasFocus(true);
+		CPPUNIT_ASSERT_EQUAL(false, testling.getShouldMoveLastLine());
+	}
+	void testFocusOut() {
+		LastLineTracker testling;
+		testling.setHasFocus(false);
+		CPPUNIT_ASSERT_EQUAL(true, testling.getShouldMoveLastLine());
+		CPPUNIT_ASSERT_EQUAL(false, testling.getShouldMoveLastLine());
+		CPPUNIT_ASSERT_EQUAL(false, testling.getShouldMoveLastLine());
+	}
+	void testFocusOtherTab() {
+		LastLineTracker testling;
+		testling.setHasFocus(true);
+		testling.setHasFocus(false);
+		CPPUNIT_ASSERT_EQUAL(true, testling.getShouldMoveLastLine());
+		CPPUNIT_ASSERT_EQUAL(false, testling.getShouldMoveLastLine());
+	}
+
+	void testRepeatedFocusOut() {
+		LastLineTracker testling;
+		testling.setHasFocus(true);
+		CPPUNIT_ASSERT_EQUAL(false, testling.getShouldMoveLastLine());
+		testling.setHasFocus(false);
+		CPPUNIT_ASSERT_EQUAL(true, testling.getShouldMoveLastLine());
+		testling.setHasFocus(false);
+		CPPUNIT_ASSERT_EQUAL(false, testling.getShouldMoveLastLine());
+	}
+	void testRepeatedFocusIn() {
+		LastLineTracker testling;
+		testling.setHasFocus(false);
+		CPPUNIT_ASSERT_EQUAL(true, testling.getShouldMoveLastLine());
+		testling.setHasFocus(true);
+		testling.setHasFocus(false);
+		CPPUNIT_ASSERT_EQUAL(true, testling.getShouldMoveLastLine());
+	}
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(LastLineTrackerTest);
diff --git a/SwifTools/UnitTest/SConscript b/SwifTools/UnitTest/SConscript
index 9fd1375..e41da64 100644
--- a/SwifTools/UnitTest/SConscript
+++ b/SwifTools/UnitTest/SConscript
@@ -3,4 +3,5 @@ Import("env")
 env.Append(UNITTEST_SOURCES = [
 		File("LinkifyTest.cpp"),
 		File("TabCompleteTest.cpp"),
+		File("LastLineTrackerTest.cpp")
 	])
diff --git a/Swift/QtUI/QtChatView.cpp b/Swift/QtUI/QtChatView.cpp
index 521b072..4846af6 100644
--- a/Swift/QtUI/QtChatView.cpp
+++ b/Swift/QtUI/QtChatView.cpp
@@ -106,6 +106,19 @@ void QtChatView::addToDOM(boost::shared_ptr<ChatSnippet> snippet) {
 	//qApp->processEvents();
 }
 
+void QtChatView::addLastSeenLine() {
+	if (lineSeparator_.isNull()) {
+		lineSeparator_ = newInsertPoint_.clone();
+		lineSeparator_.setInnerXml(QString("<hr/>"));
+		newInsertPoint_.prependOutside(lineSeparator_);
+	}
+	else {
+		QWebElement lineSeparatorC = lineSeparator_.clone();
+		lineSeparatorC.removeFromDocument();
+	}
+	newInsertPoint_.prependOutside(lineSeparator_);
+}
+
 void QtChatView::replaceLastMessage(const QString& newMessage) {
 	assert(viewReady_);
 	/* FIXME: must be queued? */
diff --git a/Swift/QtUI/QtChatView.h b/Swift/QtUI/QtChatView.h
index 58b33df..ce12ca8 100644
--- a/Swift/QtUI/QtChatView.h
+++ b/Swift/QtUI/QtChatView.h
@@ -26,8 +26,8 @@ namespace Swift {
 			Q_OBJECT
 		public:
 			QtChatView(QtChatTheme* theme, QWidget* parent);
-
 			void addMessage(boost::shared_ptr<ChatSnippet> snippet);
+			void addLastSeenLine();
 			void replaceLastMessage(const QString& newMessage);
 			void replaceLastMessage(const QString& newMessage, const QString& note);
 			void rememberScrolledToBottom();
@@ -63,6 +63,7 @@ namespace Swift {
 
 			QtChatTheme* theme_;
 			QWebElement newInsertPoint_;
+			QWebElement lineSeparator_;
 			QWebElement lastElement_;
 			QWebElement document_;
 	};
diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp
index 1a909fd..312ec65 100644
--- a/Swift/QtUI/QtChatWindow.cpp
+++ b/Swift/QtUI/QtChatWindow.cpp
@@ -40,7 +40,7 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt
 	QBoxLayout *layout = new QBoxLayout(QBoxLayout::TopToBottom, this);
 	layout->setContentsMargins(0,0,0,0);
 	layout->setSpacing(2);
-	
+
 
 	QSplitter *logRosterSplitter = new QSplitter(this);
 	logRosterSplitter->setAutoFillBackground(true);
@@ -72,7 +72,7 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt
 	input_ = new QtTextEdit(this);
 	input_->setAcceptRichText(false);
 	layout->addWidget(input_);
-	
+
 	inputClearing_ = false;
 	contactIsTyping_ = false;
 
@@ -150,7 +150,7 @@ void QtChatWindow::tabComplete() {
 }
 
 void QtChatWindow::setRosterModel(Roster* roster) {
-	treeWidget_->setRosterModel(roster);	
+	treeWidget_->setRosterModel(roster);
 }
 
 void QtChatWindow::setAvailableSecurityLabels(const std::vector<SecurityLabelsCatalog::Item>& labels) {
@@ -204,10 +204,13 @@ void QtChatWindow::qAppFocusChanged(QWidget *old, QWidget *now) {
 	Q_UNUSED(old);
 	Q_UNUSED(now);
 	if (isWidgetSelected()) {
+		lastLineTracker_.setHasFocus(true);
 		input_->setFocus();
 		onAllMessagesRead();
 	}
-	
+	else {
+		lastLineTracker_.setHasFocus(false);
+	}
 }
 
 void QtChatWindow::setInputEnabled(bool enabled) {
@@ -236,7 +239,7 @@ void QtChatWindow::setContactChatState(ChatState::ChatStateType state) {
 QtTabbable::AlertType QtChatWindow::getWidgetAlertState() {
 	if (contactIsTyping_) {
 		return ImpendingActivity;
-	} 
+	}
 	if (unreadCount_ > 0) {
 		return WaitingActivity;
 	}
@@ -265,7 +268,6 @@ std::string QtChatWindow::addMessage(const std::string &message, const std::stri
 	if (isWidgetSelected()) {
 		onAllMessagesRead();
 	}
-
 	QString scaledAvatarPath = QtScaledAvatarCache(32).getScaledAvatarPath(avatarPath.c_str());
 
 	QString htmlString;
@@ -281,6 +283,12 @@ std::string QtChatWindow::addMessage(const std::string &message, const std::stri
 	htmlString += styleSpanStart + messageHTML + styleSpanEnd;
 
 	bool appendToPrevious = !previousMessageWasSystem_ && !previousMessageWasPresence_ && ((senderIsSelf && previousMessageWasSelf_) || (!senderIsSelf && !previousMessageWasSelf_ && previousSenderName_ == P2QSTRING(senderName)));
+	if (lastLineTracker_.getShouldMoveLastLine()) {
+		/* should this be queued? */
+		messageLog_->addLastSeenLine();
+		/* if the line is added we should break the snippet */
+		appendToPrevious = false;
+	}
 	QString qAvatarPath =  scaledAvatarPath.isEmpty() ? "qrc:/icons/avatar.png" : QUrl::fromLocalFile(scaledAvatarPath).toEncoded();
 	std::string id = id_.generateID();
 	messageLog_->addMessage(boost::shared_ptr<ChatSnippet>(new MessageSnippet(htmlString, Qt::escape(P2QSTRING(senderName)), B2QDATE(time), qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id))));
@@ -399,7 +407,7 @@ void QtChatWindow::resizeEvent(QResizeEvent*) {
 }
 
 void QtChatWindow::moveEvent(QMoveEvent*) {
-	emit geometryChanged();	
+	emit geometryChanged();
 }
 
 void QtChatWindow::replaceLastMessage(const std::string& message) {
diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h
index 910019b..9e3aeb3 100644
--- a/Swift/QtUI/QtChatWindow.h
+++ b/Swift/QtUI/QtChatWindow.h
@@ -4,13 +4,14 @@
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
 
-#ifndef SWIFT_QtChatWindow_H
-#define SWIFT_QtChatWindow_H
+#pragma once
 
 #include "Swift/Controllers/UIInterfaces/ChatWindow.h"
 
 #include "QtTabbable.h"
 
+#include "SwifTools/LastLineTracker.h"
+
 #include "Swiften/Base/IDGenerator.h"
 
 class QTextEdit;
@@ -79,6 +80,7 @@ namespace Swift {
 
 			int unreadCount_;
 			bool contactIsTyping_;
+			LastLineTracker lastLineTracker_;
 			QString contact_;
 			QtChatView* messageLog_;
 			QtChatTheme* theme_;
@@ -97,5 +99,3 @@ namespace Swift {
 			IDGenerator id_;
 	};
 }
-
-#endif
-- 
cgit v0.10.2-6-g49f6