diff --git a/README.md b/README.md
index cbda30a..fed0ff3 100644
--- a/README.md
+++ b/README.md
@@ -19,14 +19,14 @@ Tensor is an IM client for the [Matrix](https://matrix.org) protocol in developm
Just install things from "Pre-requisites" using your preferred package manager. If your Qt package base is fine-grained you might want to take a look at CMakeLists.txt to figure out which specific libraries Tensor uses.
### Building
-From the root directory of the project sources:
+From the root directory of the project sources, first update submodules:
+
```
-mkdir build
-cd build
-cmake ../
-make
+git submodule update --init # pull in qmatrixclient library
```
+Then open the project in QtCreator and build.
+
### Installation
From the root directory of the project sources:
```
@@ -46,39 +46,7 @@ Alternatively, [Download *Qt for Android*](http://www.qt.io/download-open-source

-```
-brew install qt5
-git submodule update --init # pull in qmatrixclient library
-cd lib
-git submodule update --init # pull in the KCoreAddons package
-cd ..
-mkdir build
-cd build
-cmake ../ -DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.5.1/ # or whatever version of qt5 brew installed
-make
-tensor &
-```
-
-### Troubleshooting
-
-If `cmake` fails with...
-```
-CMake Warning at CMakeLists.txt:11 (find_package):
- By not providing "FindQt5Widgets.cmake" in CMAKE_MODULE_PATH this project
- has asked CMake to find a package configuration file provided by
- "Qt5Widgets", but CMake did not find one.
-```
-...then you need to set the right -DCMAKE_PREFIX_PATH variable, see above.
-If `make` fails with...
-```
-Scanning dependencies of target tensor
-make[2]: *** No rule to make target `CMakeFiles/tensor.dir/build'. Stop.
-make[1]: *** [CMakeFiles/tensor.dir/all] Error 2
-```
-...then cmake failed to create a build target for tensor as it couldn't find
-an optional dependency - probably KCoreAddons. You probably forgot to do the
-`git submodule init && git submodule update` dance.
## iOS
diff --git a/client/qml/ChatRoom.qml b/client/qml/ChatRoom.qml
index 2e89130..f5f5677 100644
--- a/client/qml/ChatRoom.qml
+++ b/client/qml/ChatRoom.qml
@@ -1,6 +1,7 @@
import QtQuick 2.0
import QtQuick.Controls 1.0
import Matrix 1.0
+import 'jschat.js' as JsChat
Rectangle {
id: root
@@ -8,6 +9,7 @@ Rectangle {
property Connection currentConnection: null
property var currentRoom: null
+
function setRoom(room) {
currentRoom = room
messageModel.changeRoom(room)
@@ -41,17 +43,22 @@ Rectangle {
color: "grey"
}
Label {
- width: 64
+ id: authorlabel
+ width: 100
elide: Text.ElideRight
text: eventType == "message" ? author : "***"
- color: eventType == "message" ? "grey" : "lightgrey"
+ font.family: JsChat.Theme.nickFont()
+ color: eventType == "message" ? JsChat.NickColoring.get(author): "lightgrey"
horizontalAlignment: Text.AlignRight
}
Label {
+ id: contentlabel
text: content
wrapMode: Text.Wrap
width: parent.width - (x - parent.x) - spacing
color: eventType == "message" ? "black" : "lightgrey"
+ font.family: JsChat.Theme.textFont()
+ font.pointSize: JsChat.Theme.textSize()
}
}
diff --git a/client/qml/RoomList.qml b/client/qml/RoomList.qml
index ae0a875..2a9b6e2 100644
--- a/client/qml/RoomList.qml
+++ b/client/qml/RoomList.qml
@@ -1,6 +1,7 @@
import QtQuick 2.0
import QtQuick.Controls 1.0
import Matrix 1.0
+import 'jschat.js' as JsChat
Rectangle {
color: "#6a1b9a"
@@ -36,6 +37,11 @@ Rectangle {
roomListView.forceLayout()
}
+ function changeRoom(dir) {
+ roomListView.currentIndex = JsChat.posmod(roomListView.currentIndex + dir, roomListView.count);
+ enterRoom(rooms.roomAt(roomListView.currentIndex))
+ }
+
Column {
anchors.fill: parent
diff --git a/client/qml/RoomView.qml b/client/qml/RoomView.qml
index 1ac3b6b..f49c758 100644
--- a/client/qml/RoomView.qml
+++ b/client/qml/RoomView.qml
@@ -1,10 +1,18 @@
import QtQuick 2.0
import QtQuick.Controls 1.0
+import QtQuick.Controls.Styles 1.4
+import 'jschat.js' as JsChat
Item {
id: room
+ property var currentRoom
+ property var completion
+
+ signal changeRoom(int dir)
+
function setRoom(room) {
+ currentRoom = room
chat.setRoom(room)
}
@@ -17,6 +25,29 @@ Item {
textEntry.text = ''
}
+ function onKeyPressed(event) {
+ if ((event.key === Qt.Key_Tab) || (event.key === Qt.Key_Backtab)) {
+ if (completion === null) completion = new JsChat.NameCompletion(currentRoom.usernames(), textEntry.text);
+ event.accepted = true;
+ textEntry.text = completion.complete(event.key === Qt.Key_Tab);
+
+ } else if ((event.key !== Qt.Key_Shift) && (event.key !== Qt.Key_Alt) && (event.key !== Qt.Key_Control)) {
+ // reset
+ completion = null;
+ }
+
+ if ((event.modifiers & Qt.ControlModifier) === Qt.ControlModifier) {
+ if (event.key === Qt.Key_PageUp) {
+ event.accepted = true;
+ changeRoom(-1);
+ }
+ else if (event.key === Qt.Key_PageDown) {
+ event.accepted = true;
+ changeRoom(1);
+ }
+ }
+ }
+
ChatRoom {
id: chat
anchors.bottom: textEntry.top
@@ -31,8 +62,19 @@ Item {
anchors.left: parent.left
anchors.bottom: parent.bottom
focus: true
- textColor: "black"
+ /*
+ style: TextFieldStyle {
+ textColor: "black"
+ background: Rectangle {
+ color: "white"
+ }
+ }
+ */
+
placeholderText: qsTr("Say something...")
onAccepted: sendLine(text)
+
+ Keys.onBacktabPressed: onKeyPressed(event)
+ Keys.onPressed: onKeyPressed(event)
}
}
diff --git a/client/qml/Tensor.qml b/client/qml/Tensor.qml
index dedb49f..04de6ef 100644
--- a/client/qml/Tensor.qml
+++ b/client/qml/Tensor.qml
@@ -5,8 +5,8 @@ import Matrix 1.0
Rectangle {
id: window
visible: true
- width: 800
- height: 480
+ width: 960
+ height: 600
focus: true
color: "#eee"
@@ -79,6 +79,7 @@ Rectangle {
height: parent.height
Component.onCompleted: {
setConnection(connection)
+ roomView.changeRoom.connect(roomListItem.changeRoom)
}
}
}
diff --git a/client/qml/jschat.js b/client/qml/jschat.js
new file mode 100644
index 0000000..afa0e05
--- /dev/null
+++ b/client/qml/jschat.js
@@ -0,0 +1,102 @@
+function posmod(x, m) {
+ x = x % m;
+ if (x < 0) x += m;
+ return x;
+}
+
+function NameCompletion(userlist_, prefix_) {
+ this.userlist = [];
+ for (var i in userlist_) {
+ if (userlist_[i].startsWith(prefix_)) {
+ this.userlist.push(NameCompletion.stripIRC(userlist_[i]));
+ }
+ }
+ this.prefix = prefix_;
+ this.index = -1;
+ this.last_dir = 1;
+}
+
+NameCompletion.stripIRC = function(username) {
+ var suff = ' (IRC)';
+ if (username.endsWith(suff)) {
+ return username.substr(0, username.length - suff.length);
+ } else {
+ return username;
+ }
+};
+
+NameCompletion.prototype.get = function(forward) {
+ if (this.prefix.length === 0) throw new Error('no_prefix');
+ if (this.userlist.length === 0) throw new Error('no_completion');
+
+ var dir = forward ? 1 : -1;
+ this.index += dir;
+
+ return this.userlist[posmod(this.index, this.userlist.length)];
+};
+
+NameCompletion.prototype.complete = function(forward) {
+ return this.get(forward) + ', ';
+};
+
+var NickColoring = {
+ hashCode: function(str) { // java String#hashCode
+ var hash = 0;
+ for (var i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ return hash;
+ },
+
+ intToRGB: function(i){
+ var c = (i & 0x00FFFFFF)
+ .toString(16)
+ .toUpperCase();
+
+ return "#" + "00000".substring(0, 6 - c.length) + c;
+ },
+
+ get: function(nick) {
+ return NickColoring.intToRGB(NickColoring.hashCode(nick));
+ }
+};
+
+var Theme = {
+ textFont: function() { return Qt.platform.os == 'windows' ? 'Consolas' : 'Monaco' },
+ nickFont: function() { return Theme.textFont(); },
+ textSize: function() { return 11; }
+
+};
+
+// ---------
+
+function test() {
+ function assert_eq(act, exp, msg) {
+ if (act != exp) throw new Error('Assertion EQ actual == expected :\n' + act + ' == ' + exp + '\nfailed: ' + msg);
+ }
+
+ function assert_throws(fn) {
+ var t = true;
+ try { fn(); t = false; }
+ catch (e) {}
+ if (!t) throw new Error('throw expected');
+ }
+
+ var n = new NameCompletion(['Albert', 'Alaska', 'Bali', 'Czech'], 'A');
+ assert_eq(n.get(true), 'Albert');
+ assert_eq(n.get(true), 'Alaska');
+ assert_eq(n.get(true), 'Albert');
+
+ var n = new NameCompletion(['Albert', 'Alaska'], 'X');
+ assert_throws(function() { n.get(true); });
+ assert_throws(function() { n.get(false); });
+
+ var n = new NameCompletion(['Abb', 'Acc', 'Add', 'Aee'], 'A');
+ assert_eq(n.get(true), 'Abb');
+ assert_eq(n.get(true), 'Acc');
+ assert_eq(n.get(false), 'Abb');
+ assert_eq(n.get(false), 'Aee');
+
+}
+
+test();
diff --git a/client/resources.qrc b/client/resources.qrc
index e8b08f7..7e09658 100644
--- a/client/resources.qrc
+++ b/client/resources.qrc
@@ -6,5 +6,6 @@
qml/Tensor.qml
qml/ChatRoom.qml
qml/RoomView.qml
+ qml/jschat.js
diff --git a/tensor.icns b/tensor.icns
new file mode 100644
index 0000000..d12feff
Binary files /dev/null and b/tensor.icns differ
diff --git a/tensor.ico b/tensor.ico
new file mode 100644
index 0000000..a4e634d
Binary files /dev/null and b/tensor.ico differ
diff --git a/tensor.pro b/tensor.pro
index 239c1ed..370c1f2 100644
--- a/tensor.pro
+++ b/tensor.pro
@@ -1,7 +1,7 @@
TEMPLATE = app
QT += qml quick
-CONFIG += c++11
+CONFIG += c++11 qml_debug
include(lib/libqmatrixclient.pri)
@@ -23,6 +23,9 @@ QML_IMPORT_PATH =
# Default rules for deployment.
include(deployment.pri)
+ICON = tensor.icns
+RC_ICONS = tensor.ico
+
DISTFILES += \
android/AndroidManifest.xml \
android/gradle/wrapper/gradle-wrapper.jar \