From 72f5284e493a2fb42d235c1c19800207c45f3ca1 Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Tue, 14 Jul 2015 12:38:59 -0400 Subject: [PATCH 1/7] Fix build --- programs/light_client/ClientDataModel.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/programs/light_client/ClientDataModel.cpp b/programs/light_client/ClientDataModel.cpp index 6b45b9e4..22a085df 100644 --- a/programs/light_client/ClientDataModel.cpp +++ b/programs/light_client/ClientDataModel.cpp @@ -49,7 +49,7 @@ Account* ChainDataModel::getAccount(qint64 id) ); } }); - } + } catch ( const fc::exception& e ) { Q_EMIT exceptionThrown( QString::fromStdString(e.to_string()) ); @@ -94,12 +94,12 @@ Account* ChainDataModel::getAccount(QString name) { by_name_idx.modify( itr, [=]( Account* a ){ - a->setProperty("id", result.front()->id.instance() ); + a->setProperty("id", qint64(result.front()->id.instance())); } ); } }); - } + } catch ( const fc::exception& e ) { Q_EMIT exceptionThrown( QString::fromStdString(e.to_string()) ); From f57205a2e696cb003f54fa3b8b8aac37d7bdc11d Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Tue, 14 Jul 2015 13:06:32 -0400 Subject: [PATCH 2/7] [GUI] Fix CMake error, add README.md --- programs/light_client/CMakeLists.txt | 4 +- programs/light_client/ClientDataModel.cpp | 4 +- programs/light_client/ClientDataModel.hpp | 46 ++++++++++++----------- programs/light_client/README.md | 11 ++++++ programs/light_client/main.cpp | 2 - programs/light_client/qml/main.qml | 3 +- 6 files changed, 42 insertions(+), 28 deletions(-) create mode 100644 programs/light_client/README.md diff --git a/programs/light_client/CMakeLists.txt b/programs/light_client/CMakeLists.txt index 7fc1bad1..54e8dfc9 100644 --- a/programs/light_client/CMakeLists.txt +++ b/programs/light_client/CMakeLists.txt @@ -16,6 +16,8 @@ file(GLOB QML qml/*) qt5_add_resources(QML_QRC qml/qml.qrc) add_executable(light_client ClientDataModel.cpp ClientDataModel.hpp main.cpp ${QML_QRC} ${QML}) -add_dependencies(light_client gen_qrc) +if (CMAKE_VERSION VERSION_LESS 3.0) + add_dependencies(light_client gen_qrc) +endif() target_link_libraries(light_client PRIVATE Qt5::Core Qt5::Widgets Qt5::Quick graphene_chain graphene_utilities fc graphene_app ) diff --git a/programs/light_client/ClientDataModel.cpp b/programs/light_client/ClientDataModel.cpp index 22a085df..3c48fee4 100644 --- a/programs/light_client/ClientDataModel.cpp +++ b/programs/light_client/ClientDataModel.cpp @@ -10,7 +10,7 @@ using namespace graphene::app; ChainDataModel::ChainDataModel( fc::thread& t, QObject* parent ) :QObject(parent),m_thread(&t){} -Account* ChainDataModel::getAccount(qint64 id) +Account* ChainDataModel::getAccount(ObjectId id) { auto& by_id_idx = m_accounts.get<::by_id>(); auto itr = by_id_idx.find(id); @@ -94,7 +94,7 @@ Account* ChainDataModel::getAccount(QString name) { by_name_idx.modify( itr, [=]( Account* a ){ - a->setProperty("id", qint64(result.front()->id.instance())); + a->setProperty("id", ObjectId(result.front()->id.instance())); } ); } diff --git a/programs/light_client/ClientDataModel.hpp b/programs/light_client/ClientDataModel.hpp index dd258e4c..d3ae8d1d 100644 --- a/programs/light_client/ClientDataModel.hpp +++ b/programs/light_client/ClientDataModel.hpp @@ -11,24 +11,36 @@ #include #include +#include #include #include using boost::multi_index_container; using namespace boost::multi_index; +using ObjectId = qint64; + Q_DECLARE_METATYPE(std::function) +class Crypto { + Q_GADGET + +public: + Q_INVOKABLE QString sha256(QByteArray data) { + return QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex(); + } +}; +QML_DECLARE_TYPE(Crypto) class Asset : public QObject { Q_OBJECT Q_PROPERTY(QString symbol MEMBER symbol) - Q_PROPERTY(qint64 id MEMBER id) + Q_PROPERTY(ObjectId id MEMBER id) Q_PROPERTY(quint8 precision MEMBER precision) QString symbol; - qint64 id; + ObjectId id; quint8 precision; }; @@ -37,33 +49,30 @@ class Balance : public QObject { Q_PROPERTY(Asset* type MEMBER type) Q_PROPERTY(qint64 amount MEMBER amount) - Q_PROPERTY(qint64 id MEMBER id) + Q_PROPERTY(ObjectId id MEMBER id) Asset* type; qint64 amount; - qint64 id; + ObjectId id; }; class Account : public QObject { Q_OBJECT Q_PROPERTY(QString name MEMBER name NOTIFY nameChanged) - Q_PROPERTY(qint64 id MEMBER id NOTIFY idChanged) + Q_PROPERTY(ObjectId id MEMBER id NOTIFY idChanged) Q_PROPERTY(QQmlListProperty balances READ balances) QList m_balances; public: - // Account(QObject* parent = nullptr) - // : QObject(parent){} - const QString& getName()const { return name; } - qint64 getId()const { return id; } + ObjectId getId()const { return id; } QQmlListProperty balances(); QString name; - qint64 id; + ObjectId id; signals: void nameChanged(); @@ -75,22 +84,19 @@ struct by_account_name; /** * @ingroup object_index */ -typedef multi_index_container< +using account_multi_index_type = multi_index_container< Account*, indexed_by< - hashed_unique< tag, const_mem_fun >, + hashed_unique< tag, const_mem_fun >, ordered_unique< tag, const_mem_fun > > -> account_multi_index_type; - - - +>; class ChainDataModel : public QObject { Q_OBJECT public: - Q_INVOKABLE Account* getAccount(qint64 id); + Q_INVOKABLE Account* getAccount(ObjectId id); Q_INVOKABLE Account* getAccount(QString name); ChainDataModel(){} @@ -107,14 +113,10 @@ private: std::string m_api_url; fc::api m_db_api; - qint64 m_account_query_num = -1; + ObjectId m_account_query_num = -1; account_multi_index_type m_accounts; }; - - - - class GrapheneApplication : public QObject { Q_OBJECT diff --git a/programs/light_client/README.md b/programs/light_client/README.md new file mode 100644 index 00000000..7eb2d2ab --- /dev/null +++ b/programs/light_client/README.md @@ -0,0 +1,11 @@ +== Graphene Client GUI == + +This is a Qt-based native GUI client for Graphene blockchains. + +To build this GUI, run cmake with -DBUILD_QT_GUI=ON + +This GUI depends on Qt 5.5 or later. If you do not have Qt 5.5 installed +in the canonical location on your OS (or if your OS does not have a +canonical location for libraries), you can specify the Qt path by running +cmake with -DCMAKE_PREFIX_PATH=/path/to/Qt/5.5/gcc_64 as appropriate for +your environment. diff --git a/programs/light_client/main.cpp b/programs/light_client/main.cpp index a761fbea..2c733daa 100644 --- a/programs/light_client/main.cpp +++ b/programs/light_client/main.cpp @@ -21,11 +21,9 @@ int main(int argc, char *argv[]) qmlRegisterType("Graphene.Client", 0, 1, "GrapheneApplication"); QQmlApplicationEngine engine; - /* QVariant crypto; crypto.setValue(Crypto()); engine.rootContext()->setContextProperty("Crypto", crypto); - */ #ifdef NDEBUG engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); #else diff --git a/programs/light_client/qml/main.qml b/programs/light_client/qml/main.qml index ef5f0c84..5ddf1bd0 100644 --- a/programs/light_client/qml/main.qml +++ b/programs/light_client/qml/main.qml @@ -28,6 +28,7 @@ ApplicationWindow { MenuItem { text: qsTr("Exit") onTriggered: Qt.quit(); + shortcut: "Ctrl+Q" } } } @@ -112,7 +113,7 @@ ApplicationWindow { } } } - + } From 5f5f376ed3996081d628b3b0649db1226620e68a Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Tue, 14 Jul 2015 15:06:00 -0400 Subject: [PATCH 3/7] [GUI] More work on transfer form --- programs/light_client/ClientDataModel.hpp | 1 + programs/light_client/main.cpp | 1 + programs/light_client/qml/AccountPicker.qml | 54 ++++++++++++++ programs/light_client/qml/FormBox.qml | 8 +- programs/light_client/qml/Identicon.qml | 39 ++++++++++ programs/light_client/qml/TransferForm.qml | 82 +++++++++++---------- programs/light_client/qml/main.qml | 4 +- programs/light_client/qml/qml.qrc | 4 +- 8 files changed, 150 insertions(+), 43 deletions(-) create mode 100644 programs/light_client/qml/AccountPicker.qml create mode 100644 programs/light_client/qml/Identicon.qml diff --git a/programs/light_client/ClientDataModel.hpp b/programs/light_client/ClientDataModel.hpp index d3ae8d1d..d7533ba4 100644 --- a/programs/light_client/ClientDataModel.hpp +++ b/programs/light_client/ClientDataModel.hpp @@ -19,6 +19,7 @@ using boost::multi_index_container; using namespace boost::multi_index; using ObjectId = qint64; +Q_DECLARE_METATYPE(ObjectId) Q_DECLARE_METATYPE(std::function) diff --git a/programs/light_client/main.cpp b/programs/light_client/main.cpp index 2c733daa..a2c12ac4 100644 --- a/programs/light_client/main.cpp +++ b/programs/light_client/main.cpp @@ -13,6 +13,7 @@ int main(int argc, char *argv[]) app.setOrganizationName("Cryptonomex, Inc."); qRegisterMetaType>(); + qRegisterMetaType(); qmlRegisterType("Graphene.Client", 0, 1, "Asset"); qmlRegisterType("Graphene.Client", 0, 1, "Balance"); diff --git a/programs/light_client/qml/AccountPicker.qml b/programs/light_client/qml/AccountPicker.qml new file mode 100644 index 00000000..b148f64f --- /dev/null +++ b/programs/light_client/qml/AccountPicker.qml @@ -0,0 +1,54 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.2 + +import Graphene.Client 0.1 + +import "." + +RowLayout { + property Account account + + property alias placeholderText: accountNameField.placeholderText + + function setFocus() { + accountNameField.forceActiveFocus() + } + + Identicon { + name: accountNameField.text + width: Scaling.cm(2) + height: Scaling.cm(2) + } + Column { + Layout.fillWidth: true + TextField { + id: accountNameField + + width: parent.width + onEditingFinished: accountDetails.update(text) + } + Label { + id: accountDetails + function update(name) { + if (!name) + { + text = "" + return + } + + account = app.model.getAccount(name) + if (account == null) + text = qsTr("Error fetching account.") + else + text = Qt.binding(function() { + if (account == null) + return qsTr("Account does not exist.") + return qsTr("Account ID: %1").arg(account.id < 0? qsTr("Loading...") + : account.id) + }) + } + } + } +} diff --git a/programs/light_client/qml/FormBox.qml b/programs/light_client/qml/FormBox.qml index 1df969ef..799069cf 100644 --- a/programs/light_client/qml/FormBox.qml +++ b/programs/light_client/qml/FormBox.qml @@ -22,8 +22,14 @@ Rectangle { function showForm(formType, params, closedCallback) { if (formType.status === Component.Error) console.log(formType.errorString()) + if (!params instanceof Object) + params = {app: app} + else + params.app = app - formContainer.data = [formType.createObject(formContainer, params)] + var form = formType.createObject(formContainer, params) + formContainer.data = [form] + form.finished.connect(function(){state = "HIDDEN"}) if (closedCallback instanceof Function) internal.callback = closedCallback state = "SHOWN" diff --git a/programs/light_client/qml/Identicon.qml b/programs/light_client/qml/Identicon.qml new file mode 100644 index 00000000..fb674e55 --- /dev/null +++ b/programs/light_client/qml/Identicon.qml @@ -0,0 +1,39 @@ +import QtQuick 2.5 + +import "jdenticon/jdenticon-1.0.1.min.js" as Jdenticon + +Canvas { + id: identicon + contextType: "2d" + + property var name + onNameChanged: requestPaint() + + onPaint: { + if (name) + Jdenticon.draw(identicon, name) + else { + var context = identicon.context + context.reset() + var draw_circle = function(context, x, y, radius) { + context.beginPath() + context.arc(x, y, radius, 0, 2 * Math.PI, false) + context.fillStyle = "rgba(0, 0, 0, 0.1)" + context.fill() + } + var size = Math.min(identicon.height, identicon.width) + var centerX = size / 2 + var centerY = size / 2 + var radius = size/15 + draw_circle(context, centerX, centerY, radius) + draw_circle(context, 2*radius, 2*radius, radius) + draw_circle(context, centerX, 2*radius, radius) + draw_circle(context, size - 2*radius, 2*radius, radius) + draw_circle(context, size - 2*radius, centerY, radius) + draw_circle(context, size - 2*radius, size - 2*radius, radius) + draw_circle(context, centerX, size - 2*radius, radius) + draw_circle(context, 2*radius, size - 2*radius, radius) + draw_circle(context, 2*radius, centerY, radius) + } + } +} diff --git a/programs/light_client/qml/TransferForm.qml b/programs/light_client/qml/TransferForm.qml index deab6255..8ff7beac 100644 --- a/programs/light_client/qml/TransferForm.qml +++ b/programs/light_client/qml/TransferForm.qml @@ -3,57 +3,61 @@ import QtQuick.Controls 1.4 import QtQuick.Dialogs 1.2 import QtQuick.Layouts 1.2 +import Graphene.Client 0.1 + import "." -import "jdenticon/jdenticon-1.0.1.min.js" as Jdenticon Rectangle { anchors.fill: parent + property alias senderAccount: senderPicker.account + property alias receiverAccount: recipientPicker.account + + property GrapheneApplication app + signal finished + Component.onCompleted: console.log("Made a transfer form") Component.onDestruction: console.log("Destroyed a transfer form") - Column { + ColumnLayout { anchors.centerIn: parent + width: parent.width - Scaling.cm(2) + spacing: Scaling.cm(1) + AccountPicker { + id: senderPicker + width: parent.width + Component.onCompleted: setFocus() + placeholderText: qsTr("Sender") + } + AccountPicker { + id: recipientPicker + width: parent.width + placeholderText: qsTr("Recipient") + layoutDirection: Qt.RightToLeft + } RowLayout { - Canvas { - id: identicon - width: Scaling.cm(2) - height: Scaling.cm(2) - contextType: "2d" - - onPaint: { - if (nameField.text) - Jdenticon.draw(identicon, nameField.text) - else { - var context = identicon.context - context.reset() - var draw_circle = function(context, x, y, radius) { - context.beginPath() - context.arc(x, y, radius, 0, 2 * Math.PI, false) - context.fillStyle = "rgba(0, 0, 0, 0.1)" - context.fill() - } - var size = Math.min(identicon.height, identicon.width) - var centerX = size / 2 - var centerY = size / 2 - var radius = size/15 - draw_circle(context, centerX, centerY, radius) - draw_circle(context, 2*radius, 2*radius, radius) - draw_circle(context, centerX, 2*radius, radius) - draw_circle(context, size - 2*radius, 2*radius, radius) - draw_circle(context, size - 2*radius, centerY, radius) - draw_circle(context, size - 2*radius, size - 2*radius, radius) - draw_circle(context, centerX, size - 2*radius, radius) - draw_circle(context, 2*radius, size - 2*radius, radius) - draw_circle(context, 2*radius, centerY, radius) - } - } + width: parent.width + SpinBox { + Layout.preferredWidth: Scaling.cm(4) + Layout.minimumWidth: Scaling.cm(1.5) + enabled: senderPicker.account + minimumValue: 0 + maximumValue: Number.POSITIVE_INFINITY } - TextField { - id: nameField - Layout.fillWidth: true - onTextChanged: identicon.requestPaint() + ComboBox { + Layout.minimumWidth: Scaling.cm(3) + enabled: senderPicker.account + model: ["CORE", "USD", "GOLD"] + } + Item { Layout.fillWidth: true } + Button { + text: qsTr("Cancel") + onClicked: finished() + } + Button { + text: qsTr("Transfer") + enabled: senderPicker.account } } } diff --git a/programs/light_client/qml/main.qml b/programs/light_client/qml/main.qml index 5ddf1bd0..9e6e1f96 100644 --- a/programs/light_client/qml/main.qml +++ b/programs/light_client/qml/main.qml @@ -76,9 +76,9 @@ ApplicationWindow { else { console.log("Waiting for result...") - acct.idChanged.connect(function(loadedAcct) { + acct.idChanged.connect(function() { console.log( "ID CHANGED" ); - console.log(JSON.stringify(loadedAcct)) + console.log(JSON.stringify(acct)) }) } } diff --git a/programs/light_client/qml/qml.qrc b/programs/light_client/qml/qml.qrc index c038aad0..ed16a610 100644 --- a/programs/light_client/qml/qml.qrc +++ b/programs/light_client/qml/qml.qrc @@ -3,8 +3,10 @@ main.qml TransferForm.qml FormBox.qml - jdenticon/jdenticon-1.0.1.min.js Scaling.qml + Identicon.qml + AccountPicker.qml + jdenticon/jdenticon-1.0.1.min.js From 04392d35989d2bccf721370e8ac9eec6945293a0 Mon Sep 17 00:00:00 2001 From: Vikram Rajkumar Date: Tue, 14 Jul 2015 15:21:04 -0400 Subject: [PATCH 4/7] Fix witness production with 1 second block intervals --- libraries/plugins/witness/witness.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/libraries/plugins/witness/witness.cpp b/libraries/plugins/witness/witness.cpp index c48184da..624ad59d 100644 --- a/libraries/plugins/witness/witness.cpp +++ b/libraries/plugins/witness/witness.cpp @@ -174,10 +174,10 @@ void witness_plugin::block_production_loop() _production_enabled = true; // is anyone scheduled to produce now or one second in the future? - uint32_t slot = db.get_slot_at_time( graphene::time::now() + fc::seconds(1) ); + const fc::time_point_sec now = graphene::time::now(); + uint32_t slot = db.get_slot_at_time( now ); graphene::chain::witness_id_type scheduled_witness = db.get_scheduled_witness( slot ).first; fc::time_point_sec scheduled_time = db.get_slot_time( slot ); - fc::time_point_sec now = graphene::time::now(); graphene::chain::public_key_type scheduled_key = scheduled_witness( db ).signing_key; auto is_scheduled = [&]() @@ -194,7 +194,7 @@ void witness_plugin::block_production_loop() uint32_t prate = db.witness_participation_rate(); if( prate < _required_witness_participation ) { - elog("Not producing block because node appers to be on a minority fork with only ${x}% witness participation", + elog("Not producing block because node appears to be on a minority fork with only ${x}% witness participation", ("x",uint32_t(100*uint64_t(prate) / GRAPHENE_1_PERCENT) ) ); return false; } @@ -212,21 +212,19 @@ void witness_plugin::block_production_loop() return false; } - // the local clock must be at least 1 second ahead of - // head_block_time. - if( (now - db.head_block_time()).to_seconds() <= 1 ) { + // the local clock must be at least 1 second ahead of head_block_time. + if( (now - db.head_block_time()).to_seconds() < GRAPHENE_MIN_BLOCK_INTERVAL ) { elog("Not producing block because head block is less than a second old."); return false; } // the local clock must be within 500 milliseconds of // the scheduled production time. - if( llabs((scheduled_time - now).count()) > fc::milliseconds(250).count() ) { + if( llabs((scheduled_time - now).count()) > fc::milliseconds( 500 ).count() ) { elog("Not producing block because network time is not within 250ms of scheduled block time."); return false; } - // we must know the private key corresponding to the witness's // published block production key. if( _private_keys.find( scheduled_key ) == _private_keys.end() ) { From 419ab4f9326d653df7a4d234ffb0b3c68a26ad34 Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Tue, 14 Jul 2015 15:33:52 -0400 Subject: [PATCH 5/7] [GUI] UX tweaks --- programs/light_client/qml/FormBox.qml | 45 +++++++++++++++++---------- programs/light_client/qml/main.qml | 2 -- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/programs/light_client/qml/FormBox.qml b/programs/light_client/qml/FormBox.qml index 799069cf..69539789 100644 --- a/programs/light_client/qml/FormBox.qml +++ b/programs/light_client/qml/FormBox.qml @@ -35,25 +35,36 @@ Rectangle { state = "SHOWN" } - MouseArea { - id: mouseTrap + FocusScope { + id: scope anchors.fill: parent - onClicked: { - mouse.accepted = true - greySheet.state = "HIDDEN" + + // Do not let focus leave this scope while form is open + onFocusChanged: if (enabled && !focus) forceActiveFocus() + + Keys.onEscapePressed: greySheet.state = "HIDDEN" + + MouseArea { + id: mouseTrap + anchors.fill: parent + onClicked: { + mouse.accepted = true + greySheet.state = "HIDDEN" + } + acceptedButtons: Qt.AllButtons + } + MouseArea { + // This mouse area blocks clicks inside the form from reaching the mouseTrap + anchors.fill: formContainer + acceptedButtons: Qt.AllButtons + onClicked: mouse.accepted = true + } + Item { + id: formContainer + anchors.centerIn: parent + width: parent.width / 2 + height: parent.height / 2 } - acceptedButtons: Qt.AllButtons - } - MouseArea { - anchors.fill: formContainer - acceptedButtons: Qt.AllButtons - onClicked: mouse.accepted = true - } - Item { - id: formContainer - anchors.centerIn: parent - width: parent.width / 2 - height: parent.height / 2 } states: [ diff --git a/programs/light_client/qml/main.qml b/programs/light_client/qml/main.qml index 9e6e1f96..196db8cb 100644 --- a/programs/light_client/qml/main.qml +++ b/programs/light_client/qml/main.qml @@ -113,8 +113,6 @@ ApplicationWindow { } } } - - } FormBox { From 1813e9f5f6b71cf6a9b5d2b348f688df247e25d0 Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Tue, 14 Jul 2015 16:08:54 -0400 Subject: [PATCH 6/7] [GUI] Fix crash from user-after-free The QML engine was taking ownership of Account objects, and garbage collecting them when it was done with them, thus causing a crash when the C++ accessed them. Fix by explicitly marking Account objects as being owned by the C++ so QML doesn't garbage collect them. --- programs/light_client/ClientDataModel.cpp | 2 ++ programs/light_client/qml/AccountPicker.qml | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/programs/light_client/ClientDataModel.cpp b/programs/light_client/ClientDataModel.cpp index 3c48fee4..f76267d2 100644 --- a/programs/light_client/ClientDataModel.cpp +++ b/programs/light_client/ClientDataModel.cpp @@ -17,6 +17,7 @@ Account* ChainDataModel::getAccount(ObjectId id) if( itr == by_id_idx.end() ) { auto tmp = new Account; + QQmlEngine::setObjectOwnership(tmp, QQmlEngine::CppOwnership); tmp->id = id; --m_account_query_num; tmp->name = QString::number( --m_account_query_num); auto result = m_accounts.insert( tmp ); @@ -67,6 +68,7 @@ Account* ChainDataModel::getAccount(QString name) if( itr == by_name_idx.end() ) { auto tmp = new Account; + QQmlEngine::setObjectOwnership(tmp, QQmlEngine::CppOwnership); tmp->id = --m_account_query_num; tmp->name = name; auto result = m_accounts.insert( tmp ); diff --git a/programs/light_client/qml/AccountPicker.qml b/programs/light_client/qml/AccountPicker.qml index b148f64f..d94549ad 100644 --- a/programs/light_client/qml/AccountPicker.qml +++ b/programs/light_client/qml/AccountPicker.qml @@ -49,6 +49,24 @@ RowLayout { : account.id) }) } + + Behavior on text { + SequentialAnimation { + PropertyAnimation { + target: accountDetails + property: "opacity" + from: 1; to: 0 + duration: 100 + } + PropertyAction { target: accountDetails; property: "text" } + PropertyAnimation { + target: accountDetails + property: "opacity" + from: 0; to: 1 + duration: 100 + } + } + } } } } From d176429dade07ab0db6d199a278625015e2ff4b2 Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Tue, 14 Jul 2015 16:49:17 -0400 Subject: [PATCH 7/7] [GUI] Add connection loss detection and reestablishment --- programs/light_client/ClientDataModel.cpp | 1 + programs/light_client/ClientDataModel.hpp | 2 ++ programs/light_client/qml/main.qml | 19 +++++++++++++++---- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/programs/light_client/ClientDataModel.cpp b/programs/light_client/ClientDataModel.cpp index f76267d2..4764a2c7 100644 --- a/programs/light_client/ClientDataModel.cpp +++ b/programs/light_client/ClientDataModel.cpp @@ -156,6 +156,7 @@ void GrapheneApplication::start( QString apiurl, QString user, QString pass ) m_client = std::make_shared(); ilog( "connecting...${s}", ("s",apiurl.toStdString()) ); auto con = m_client->connect( apiurl.toStdString() ); + m_connectionClosed = con->closed.connect([this]{queueExecute([this]{setIsConnected(false);});}); auto apic = std::make_shared(*con); auto remote_api = apic->get_remote_api< login_api >(1); auto db_api = apic->get_remote_api< database_api >(0); diff --git a/programs/light_client/ClientDataModel.hpp b/programs/light_client/ClientDataModel.hpp index d7533ba4..32344ba7 100644 --- a/programs/light_client/ClientDataModel.hpp +++ b/programs/light_client/ClientDataModel.hpp @@ -129,6 +129,8 @@ class GrapheneApplication : public QObject { ChainDataModel* m_model = nullptr; bool m_isConnected = false; + boost::signals2::scoped_connection m_connectionClosed; + std::shared_ptr m_client; fc::future m_done; diff --git a/programs/light_client/qml/main.qml b/programs/light_client/qml/main.qml index 196db8cb..e9c0a57f 100644 --- a/programs/light_client/qml/main.qml +++ b/programs/light_client/qml/main.qml @@ -14,10 +14,6 @@ ApplicationWindow { height: 480 title: qsTr("Hello World") - Component.onCompleted: { - app.start("ws://localhost:8090", "user", "pass") - } - menuBar: MenuBar { Menu { title: qsTr("File") @@ -32,9 +28,22 @@ ApplicationWindow { } } } + statusBar: StatusBar { + Label { + anchors.right: parent.right + text: app.isConnected? qsTr("Connected") : qsTr("Disconnected") + } + } GrapheneApplication { id: app + } + Timer { + running: !app.isConnected + interval: 5000 + repeat: true + onTriggered: app.start("ws://localhost:8090", "user", "pass") + triggeredOnStart: true } Settings { id: appSettings @@ -47,6 +56,8 @@ ApplicationWindow { Column { anchors.centerIn: parent + enabled: app.isConnected + Button { text: "Transfer" onClicked: formBox.showForm(Qt.createComponent("TransferForm.qml"), {},