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 ![Screenshot](screen/osx.png) -``` -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 \