mirror of
https://github.com/feather-wallet/feather.git
synced 2024-12-22 11:39:25 +00:00
History: rework
This commit is contained in:
parent
59586f4b6e
commit
cc5b3c3c27
34 changed files with 1504 additions and 632 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -13,3 +13,4 @@ feather.cbp
|
|||
src/tor/*
|
||||
!src/tor/.gitkeep
|
||||
src/config-feather.h
|
||||
src/assets/exec/*
|
||||
|
|
|
@ -19,7 +19,6 @@ Prices *AppContext::prices = nullptr;
|
|||
WalletKeysFilesModel *AppContext::wallets = nullptr;
|
||||
TxFiatHistory *AppContext::txFiatHistory = nullptr;
|
||||
double AppContext::balance = 0;
|
||||
QMap<QString, QString> AppContext::txDescriptionCache;
|
||||
QMap<QString, QString> AppContext::txCache;
|
||||
|
||||
AppContext::AppContext(QCommandLineParser *cmdargs) {
|
||||
|
@ -303,6 +302,13 @@ void AppContext::onPreferredFiatCurrencyChanged(const QString &symbol) {
|
|||
}
|
||||
}
|
||||
|
||||
void AppContext::onAmountPrecisionChanged(int precision) {
|
||||
if (!this->currentWallet) return;
|
||||
auto *model = this->currentWallet->transactionHistoryModel();
|
||||
if (!model) return;
|
||||
model->amountPrecision = precision;
|
||||
}
|
||||
|
||||
void AppContext::onWalletOpened(Wallet *wallet) {
|
||||
auto state = wallet->status();
|
||||
if (state != Wallet::Status_Ok) {
|
||||
|
@ -770,21 +776,28 @@ void AppContext::onTransactionCreated(PendingTransaction *tx, const QVector<QStr
|
|||
}
|
||||
|
||||
void AppContext::onTransactionCommitted(bool status, PendingTransaction *tx, const QStringList& txid){
|
||||
this->currentWallet->history()->refresh(this->currentWallet->currentSubaddressAccount());
|
||||
this->currentWallet->coins()->refresh(this->currentWallet->currentSubaddressAccount());
|
||||
if (status) {
|
||||
for (const auto &entry: txid) {
|
||||
this->currentWallet->setUserNote(entry, this->tmpTxDescription);
|
||||
}
|
||||
this->tmpTxDescription = "";
|
||||
}
|
||||
|
||||
// Store wallet immediately so we don't risk losing tx key if wallet crashes
|
||||
this->currentWallet->store();
|
||||
|
||||
this->updateBalance();
|
||||
this->currentWallet->history()->refresh(this->currentWallet->currentSubaddressAccount());
|
||||
this->currentWallet->coins()->refresh(this->currentWallet->currentSubaddressAccount());
|
||||
|
||||
emit transactionCommitted(status, tx, txid);
|
||||
this->updateBalance();
|
||||
|
||||
// this tx was a donation to Feather, stop our nagging
|
||||
if(this->donationSending) {
|
||||
this->donationSending = false;
|
||||
config()->set(Config::donateBeg, -1);
|
||||
}
|
||||
|
||||
emit transactionCommitted(status, tx, txid);
|
||||
}
|
||||
|
||||
void AppContext::storeWallet() {
|
||||
|
|
|
@ -85,7 +85,6 @@ public:
|
|||
static Prices *prices;
|
||||
static WalletKeysFilesModel *wallets;
|
||||
static double balance;
|
||||
static QMap<QString, QString> txDescriptionCache;
|
||||
static QMap<QString, QString> txCache;
|
||||
static TxFiatHistory *txFiatHistory;
|
||||
|
||||
|
@ -119,6 +118,7 @@ public slots:
|
|||
void onOpenAliasResolve(const QString &openAlias);
|
||||
void onSetRestoreHeight(quint64 height);
|
||||
void onPreferredFiatCurrencyChanged(const QString &symbol);
|
||||
void onAmountPrecisionChanged(int precision);
|
||||
|
||||
private slots:
|
||||
void onWSNodes(const QJsonArray &nodes);
|
||||
|
|
226
src/dialog/TxProofDialog.cpp
Normal file
226
src/dialog/TxProofDialog.cpp
Normal file
|
@ -0,0 +1,226 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// Copyright (c) 2020-2021, The Monero Project.
|
||||
|
||||
#include "TxProofDialog.h"
|
||||
#include "ui_TxProofDialog.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "libwalletqt/Transfer.h"
|
||||
#include "utils/utils.h"
|
||||
|
||||
TxProofDialog::TxProofDialog(QWidget *parent, Wallet *wallet, TransactionInfo *txInfo)
|
||||
: QDialog(parent)
|
||||
, ui(new Ui::TxProofDialog)
|
||||
, m_wallet(wallet)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
m_txid = txInfo->hash();
|
||||
m_txKey = m_wallet->getTxKey(m_txid);
|
||||
m_direction = txInfo->direction();
|
||||
|
||||
for (auto const &t: txInfo->transfers()) {
|
||||
m_OutDestinations.push_back(t->address());
|
||||
}
|
||||
|
||||
for (auto const &s: txInfo->subaddrIndex()) {
|
||||
m_InDestinations.push_back(wallet->address(txInfo->subaddrAccount(), s));
|
||||
}
|
||||
|
||||
// Due to some logic in core we can't create OutProofs
|
||||
// for churn transactions that sweep from and send to the same address
|
||||
for (auto const &address : m_InDestinations) {
|
||||
m_OutDestinations.removeAll(address);
|
||||
}
|
||||
|
||||
connect(ui->radio_SpendProof, &QRadioButton::toggled, this, &TxProofDialog::selectSpendProof);
|
||||
connect(ui->radio_OutProof, &QRadioButton::toggled, this, &TxProofDialog::selectOutProof);
|
||||
connect(ui->radio_InProof, &QRadioButton::toggled, this, &TxProofDialog::selectInProof);
|
||||
|
||||
connect(ui->btn_getFormattedProof, &QPushButton::pressed, this, &TxProofDialog::getFormattedProof);
|
||||
connect(ui->btn_getSignature, &QPushButton::pressed, this, &TxProofDialog::getSignature);
|
||||
|
||||
ui->radio_SpendProof->setChecked(true);
|
||||
ui->label_txid->setText(m_txid);
|
||||
|
||||
ui->group_summary->hide(); // todo
|
||||
|
||||
this->adjustSize();
|
||||
}
|
||||
|
||||
void TxProofDialog::setTxId(const QString &txid) {
|
||||
ui->label_txid->setText(txid);
|
||||
}
|
||||
|
||||
void TxProofDialog::selectSpendProof() {
|
||||
m_mode = Mode::SpendProof;
|
||||
this->resetFrames();
|
||||
|
||||
if (m_direction == TransactionInfo::Direction_In) {
|
||||
this->showWarning("Your wallet did not construct this transaction. Creating a SpendProof is not possible.");
|
||||
return;
|
||||
}
|
||||
|
||||
ui->frame_message->show();
|
||||
ui->label_summary->setText("This proof shows you created a transaction with the txid shown above.");
|
||||
}
|
||||
|
||||
void TxProofDialog::selectOutProof() {
|
||||
m_mode = Mode::OutProof;
|
||||
this->resetFrames();
|
||||
|
||||
if (m_OutDestinations.empty()) {
|
||||
this->showWarning("This transaction did not spend any outputs owned by this wallet. Creating an OutProof is not possible.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_txKey.isEmpty()) {
|
||||
this->showWarning("No transaction key stored for this transaction. Creating an OutProof is not possible.");
|
||||
return;
|
||||
}
|
||||
|
||||
this->selectTxProof();
|
||||
ui->combo_address->addItems(m_OutDestinations);
|
||||
ui->label_summary->setText("This proof shows you paid x XMR to the address selected above.");
|
||||
}
|
||||
|
||||
void TxProofDialog::selectInProof() {
|
||||
m_mode = Mode::InProof;
|
||||
this->resetFrames();
|
||||
|
||||
if (m_InDestinations.empty()) {
|
||||
this->showWarning("Your wallet did not receive any outputs in this transaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
this->selectTxProof();
|
||||
ui->combo_address->addItems(m_InDestinations);
|
||||
ui->label_summary->setText("This proof shows you received x XMR to the address selected above.");
|
||||
}
|
||||
|
||||
void TxProofDialog::selectTxProof() {
|
||||
ui->frame_txKeyWarning->hide();
|
||||
ui->frame_message->show();
|
||||
ui->frame_address->show();
|
||||
ui->combo_address->clear();
|
||||
}
|
||||
|
||||
void TxProofDialog::resetFrames() {
|
||||
ui->frame_txKeyWarning->hide();
|
||||
ui->frame_message->hide();
|
||||
ui->frame_address->hide();
|
||||
this->toggleButtons(true);
|
||||
}
|
||||
|
||||
void TxProofDialog::toggleButtons(bool enabled) {
|
||||
ui->btn_getFormattedProof->setEnabled(enabled);
|
||||
ui->btn_getSignature->setEnabled(enabled);
|
||||
}
|
||||
|
||||
void TxProofDialog::showWarning(const QString &message) {
|
||||
this->toggleButtons(false);
|
||||
ui->frame_txKeyWarning->show();
|
||||
ui->label_txKeyWarning->setText(message);
|
||||
}
|
||||
|
||||
void TxProofDialog::getFormattedProof() {
|
||||
QString message = ui->message->toPlainText();
|
||||
QString address = ui->combo_address->currentText();
|
||||
QString nettype = Utils::QtEnumToString(m_wallet->nettype()).toLower();
|
||||
nettype = nettype.replace(0, 1, nettype[0].toUpper()); // Capitalize first letter
|
||||
|
||||
TxProof proof = this->getProof();
|
||||
|
||||
if (!proof.error.isEmpty()) {
|
||||
QMessageBox::warning(this, "Get formatted proof", QString("Failed to get proof signature: %1").arg(proof.error));
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList signatureSplit;
|
||||
for (int i = 0; i < proof.proof.length(); i += 64) {
|
||||
signatureSplit.append(proof.proof.mid(i, 64));
|
||||
}
|
||||
QString signature = signatureSplit.join('\n');
|
||||
|
||||
QString formattedProof = [this, nettype, message, address, signature]{
|
||||
switch (m_mode) {
|
||||
case Mode::SpendProof: {
|
||||
return QString("-----BEGIN SPENDPROOF-----\n"
|
||||
"Network: Monero %1\n"
|
||||
"Txid: %2\n"
|
||||
"\n"
|
||||
"%3\n"
|
||||
"-----BEGIN SPENDPROOF SIGNATURE-----\n"
|
||||
"\n"
|
||||
"%4\n"
|
||||
"-----END SPENDPROOF SIGNATURE-----").arg(nettype, m_txid, message, signature);
|
||||
}
|
||||
case Mode::OutProof: {
|
||||
return QString("-----BEGIN OUTPROOF-----\n"
|
||||
"Network: Monero %1\n"
|
||||
"Txid: %2\n"
|
||||
"Address: %3\n"
|
||||
"\n"
|
||||
"%4\n"
|
||||
"-----BEGIN OUTPROOF SIGNATURE-----\n"
|
||||
"\n"
|
||||
"%5\n"
|
||||
"-----END OUTPROOF SIGNATURE-----").arg(nettype, m_txid, address, message, signature);
|
||||
}
|
||||
case Mode::InProof: {
|
||||
return QString("-----BEGIN INPROOF-----\n"
|
||||
"Network: Monero %1\n"
|
||||
"Txid: %2\n"
|
||||
"Address: %3\n"
|
||||
"\n"
|
||||
"%4\n"
|
||||
"-----BEGIN INPROOF SIGNATURE-----\n"
|
||||
"\n"
|
||||
"%5\n"
|
||||
"-----END INPROOF SIGNATURE-----").arg(nettype, m_txid, address, message, signature);
|
||||
}
|
||||
default:
|
||||
return QString("");
|
||||
}
|
||||
}();
|
||||
|
||||
Utils::copyToClipboard(formattedProof);
|
||||
QMessageBox::information(this, "Get formatted proof", "Formatted proof copied to clipboard");
|
||||
}
|
||||
|
||||
void TxProofDialog::getSignature() {
|
||||
TxProof proof = this->getProof();
|
||||
|
||||
if (!proof.error.isEmpty()) {
|
||||
QMessageBox::warning(this, "Get proof signature", QString("Failed to get proof signature: %1").arg(proof.error));
|
||||
return;
|
||||
}
|
||||
|
||||
Utils::copyToClipboard(proof.proof);
|
||||
QMessageBox::information(this, "Get proof singature", "Proof signature copied to clipboard");
|
||||
}
|
||||
|
||||
TxProof TxProofDialog::getProof() {
|
||||
QString message = ui->message->toPlainText();
|
||||
QString address = ui->combo_address->currentText();
|
||||
|
||||
TxProof proof = [this, message, address]{
|
||||
switch (m_mode) {
|
||||
case Mode::SpendProof: {
|
||||
return m_wallet->getSpendProof(m_txid, message);
|
||||
}
|
||||
case Mode::OutProof:
|
||||
case Mode::InProof: { // Todo: split this into separate functions
|
||||
return m_wallet->getTxProof(m_txid, address, message);
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
return proof;
|
||||
}
|
||||
|
||||
TxProofDialog::~TxProofDialog() {
|
||||
delete ui;
|
||||
}
|
||||
|
56
src/dialog/TxProofDialog.h
Normal file
56
src/dialog/TxProofDialog.h
Normal file
|
@ -0,0 +1,56 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// Copyright (c) 2020-2021, The Monero Project.
|
||||
|
||||
#ifndef FEATHER_TXPROOFDIALOG_H
|
||||
#define FEATHER_TXPROOFDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "libwalletqt/Wallet.h"
|
||||
#include "libwalletqt/TransactionInfo.h"
|
||||
|
||||
namespace Ui {
|
||||
class TxProofDialog;
|
||||
}
|
||||
|
||||
class TxProofDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TxProofDialog(QWidget *parent, Wallet *wallet, TransactionInfo *txid);
|
||||
~TxProofDialog() override;
|
||||
void setTxId(const QString &txid);
|
||||
|
||||
private slots:
|
||||
void selectSpendProof();
|
||||
void selectOutProof();
|
||||
void selectInProof();
|
||||
void selectTxProof();
|
||||
|
||||
private:
|
||||
enum Mode {
|
||||
SpendProof = 0,
|
||||
OutProof,
|
||||
InProof
|
||||
};
|
||||
|
||||
void getFormattedProof();
|
||||
void getSignature();
|
||||
TxProof getProof();
|
||||
void resetFrames();
|
||||
void toggleButtons(bool enabled);
|
||||
void showWarning(const QString &message);
|
||||
|
||||
QStringList m_OutDestinations;
|
||||
QStringList m_InDestinations;
|
||||
QString m_txid;
|
||||
QString m_txKey;
|
||||
Mode m_mode;
|
||||
TransactionInfo::Direction m_direction;
|
||||
|
||||
Ui::TxProofDialog *ui;
|
||||
Wallet *m_wallet;
|
||||
};
|
||||
|
||||
#endif //FEATHER_TXPROOFDIALOG_H
|
301
src/dialog/TxProofDialog.ui
Normal file
301
src/dialog/TxProofDialog.ui
Normal file
|
@ -0,0 +1,301 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TxProofDialog</class>
|
||||
<widget class="QDialog" name="TxProofDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>542</width>
|
||||
<height>827</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Create Tx Proof</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="group_proofType">
|
||||
<property name="title">
|
||||
<string>Select proof type:</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="radio_SpendProof">
|
||||
<property name="text">
|
||||
<string>Prove authorship of a transaction (SpendProof)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="radio_OutProof">
|
||||
<property name="text">
|
||||
<string>Prove a payment to an address (OutProof)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="radio_InProof">
|
||||
<property name="text">
|
||||
<string>Prove ownership of an output (InProof)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Txid:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_txid">
|
||||
<property name="text">
|
||||
<string>txid</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>5</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_txKeyWarning">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_txKeyWarning">
|
||||
<property name="text">
|
||||
<string>No transaction key stored for this transaction. Creating an OutProof is not possible.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_message">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Message: (optional)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="message"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_address">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>5</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Address:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="combo_address"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::MinimumExpanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="group_summary">
|
||||
<property name="title">
|
||||
<string>Summary:</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_summary">
|
||||
<property name="text">
|
||||
<string>This proof shows I paid x XMR to ADDR.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btn_getFormattedProof">
|
||||
<property name="text">
|
||||
<string>Get Formatted Proof</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btn_getSignature">
|
||||
<property name="text">
|
||||
<string>Get Signature</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -20,21 +20,18 @@ TransactionInfoDialog::TransactionInfoDialog(Wallet *wallet, TransactionInfo *tx
|
|||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
m_txProofWidget = new TxProofWidget(this, wallet, txInfo);
|
||||
m_txid = txInfo->hash();
|
||||
ui->label_txid->setText(m_txid);
|
||||
|
||||
ui->label_txid->setText(QString(txInfo->hash()));
|
||||
|
||||
if (txInfo->direction() == TransactionInfo::Direction_In) {
|
||||
ui->frameTxKey->hide();
|
||||
} else {
|
||||
QString txKey = m_wallet->getTxKey(txInfo->hash());
|
||||
if (txKey.isEmpty()) {
|
||||
ui->btn_CopyTxKey->setEnabled(false);
|
||||
ui->btn_CopyTxKey->setToolTip("Transaction key unknown");
|
||||
}
|
||||
m_txKey = txKey;
|
||||
}
|
||||
|
||||
connect(ui->btn_CopyTxKey, &QPushButton::pressed, this, &TransactionInfoDialog::copyTxKey);
|
||||
connect(ui->btn_createTxProof, &QPushButton::pressed, this, &TransactionInfoDialog::createTxProof);
|
||||
|
||||
QString blockHeight = QString::number(txInfo->blockHeight());
|
||||
if (blockHeight == "0")
|
||||
|
@ -66,7 +63,7 @@ TransactionInfoDialog::TransactionInfoDialog(Wallet *wallet, TransactionInfo *tx
|
|||
ui->frameDestinations->hide();
|
||||
}
|
||||
|
||||
ui->txProofWidget->addWidget(m_txProofWidget);
|
||||
m_txProofDialog = new TxProofDialog(this, m_wallet, txInfo);
|
||||
|
||||
this->adjustSize();
|
||||
}
|
||||
|
@ -75,6 +72,10 @@ void TransactionInfoDialog::copyTxKey() {
|
|||
Utils::copyToClipboard(m_txKey);
|
||||
}
|
||||
|
||||
void TransactionInfoDialog::createTxProof() {
|
||||
m_txProofDialog->show();
|
||||
}
|
||||
|
||||
TransactionInfoDialog::~TransactionInfoDialog() {
|
||||
delete ui;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
#include "libwalletqt/Coins.h"
|
||||
#include "libwalletqt/TransactionInfo.h"
|
||||
#include "libwalletqt/Wallet.h"
|
||||
#include "widgets/txproofwidget.h"
|
||||
#include "dialog/TxProofDialog.h"
|
||||
|
||||
namespace Ui {
|
||||
class TransactionInfoDialog;
|
||||
|
@ -26,13 +26,15 @@ public:
|
|||
|
||||
private:
|
||||
void copyTxKey();
|
||||
void createTxProof();
|
||||
|
||||
Ui::TransactionInfoDialog *ui;
|
||||
|
||||
TxProofDialog *m_txProofDialog;
|
||||
TransactionInfo *m_txInfo;
|
||||
Wallet *m_wallet;
|
||||
TxProofWidget *m_txProofWidget;
|
||||
QString m_txKey;
|
||||
QString m_txid;
|
||||
};
|
||||
|
||||
#endif //FEATHER_TRANSACTIONINFODIALOG_H
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>829</width>
|
||||
<height>570</height>
|
||||
<height>493</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -187,16 +187,47 @@
|
|||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Maximum</enum>
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>15</height>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QPushButton" name="btn_CopyTxKey">
|
||||
<property name="text">
|
||||
<string>Copy Transaction Key</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btn_createTxProof">
|
||||
<property name="text">
|
||||
<string>Create Transaction Proof</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frameTxKey">
|
||||
<property name="frameShape">
|
||||
|
@ -218,39 +249,9 @@
|
|||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btn_CopyTxKey">
|
||||
<property name="text">
|
||||
<string>Copy</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Transaction Key</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="txProofWidget"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include "ui_verifyproofdialog.h"
|
||||
|
||||
#include "libwalletqt/WalletManager.h"
|
||||
#include "utils/utils.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
|
||||
|
@ -15,25 +16,34 @@ VerifyProofDialog::VerifyProofDialog(Wallet *wallet, QWidget *parent)
|
|||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
m_success = QPixmap(":/assets/images/confirmed.png").scaled(32, 32, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
m_failure = QPixmap(":/assets/images/expired.png").scaled(32, 32, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
|
||||
ui->frame_status->hide();
|
||||
connect(ui->input_formattedProof, &QPlainTextEdit::textChanged, [this]{
|
||||
ui->frame_status->hide();
|
||||
});
|
||||
|
||||
connect(ui->btn_verify, &QPushButton::clicked, this, &VerifyProofDialog::checkProof);
|
||||
connect(ui->btn_verifyFormattedProof, &QPushButton::clicked, this, &VerifyProofDialog::checkFormattedProof);
|
||||
|
||||
connect(ui->btn_clear, &QPushButton::clicked, [this]{
|
||||
switch (ui->tabWidget->currentIndex()) {
|
||||
case 0:
|
||||
ui->lineEdit_spendTxID->clear();
|
||||
ui->lineEdit_spendMessage->clear();
|
||||
ui->input_SpendMessage->clear();
|
||||
ui->input_SpendProof->clear();
|
||||
break;
|
||||
case 1:
|
||||
ui->lineEdit_outTxID->clear();
|
||||
ui->lineEdit_outAddress->clear();
|
||||
ui->lineEdit_outMessage->clear();
|
||||
ui->input_OutMessage->clear();
|
||||
ui->input_OutProof->clear();
|
||||
break;
|
||||
case 2:
|
||||
ui->lineEdit_inTxID->clear();
|
||||
ui->lineEdit_inAddress->clear();
|
||||
ui->lineEdit_inMessage->clear();
|
||||
ui->input_inMessage->clear();
|
||||
ui->input_InProof->clear();
|
||||
break;
|
||||
}
|
||||
|
@ -48,7 +58,7 @@ VerifyProofDialog::~VerifyProofDialog()
|
|||
void VerifyProofDialog::checkProof() {
|
||||
switch (ui->tabWidget->currentIndex()) {
|
||||
case 0:
|
||||
this->checkSpendProof();
|
||||
this->checkSpendProof(ui->lineEdit_spendTxID->text(), ui->input_SpendMessage->toPlainText(), ui->input_SpendProof->toPlainText());
|
||||
break;
|
||||
case 1:
|
||||
this->checkOutProof();
|
||||
|
@ -59,40 +69,105 @@ void VerifyProofDialog::checkProof() {
|
|||
}
|
||||
}
|
||||
|
||||
void VerifyProofDialog::checkSpendProof() {
|
||||
auto r = m_wallet->checkSpendProof(ui->lineEdit_spendTxID->text(), ui->lineEdit_spendMessage->text(), ui->input_SpendProof->toPlainText());
|
||||
|
||||
if (!r.first) {
|
||||
QMessageBox::information(this, "Information", m_wallet->errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
r.second ? QMessageBox::information(this, "Information", "Proof is valid")
|
||||
: QMessageBox::warning(this, "Warning", "Proof is invalid");
|
||||
}
|
||||
|
||||
void VerifyProofDialog::checkOutProof() {
|
||||
this->checkTxProof(ui->lineEdit_outTxID->text(), ui->lineEdit_outAddress->text(), ui->lineEdit_outMessage->text(), ui->input_OutProof->toPlainText());
|
||||
}
|
||||
|
||||
void VerifyProofDialog::checkInProof() {
|
||||
this->checkTxProof(ui->lineEdit_inTxID->text(), ui->lineEdit_inAddress->text(), ui->lineEdit_inMessage->text(), ui->input_InProof->toPlainText());
|
||||
}
|
||||
|
||||
void VerifyProofDialog::checkTxProof(const QString &txId, const QString &address, const QString &message,
|
||||
const QString &signature) {
|
||||
TxProofResult r = m_wallet->checkTxProof(txId, address, message, signature);
|
||||
|
||||
if (!r.success) {
|
||||
QMessageBox::information(this, "Information", m_wallet->errorString());
|
||||
this->proofStatus(false, m_wallet->errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!r.good) {
|
||||
QMessageBox::warning(this, "Warning", "Proof is invalid");
|
||||
this->proofStatus(false, "Proof is invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
QString msg = QString("This address received %1 monero, with %2 confirmation(s)").arg(WalletManager::displayAmount(r.received), QString::number(r.confirmations));
|
||||
QMessageBox::information(this, "Information", QString("Proof is valid.\n\n%1").arg(msg));
|
||||
this->proofStatus(true, QString("Proof is valid.\n\nThis address received %1 XMR, with %2 confirmation(s)").arg(WalletManager::displayAmount(r.received), QString::number(r.confirmations)));
|
||||
}
|
||||
|
||||
void VerifyProofDialog::checkSpendProof(const QString &txId, const QString &message, const QString &signature) {
|
||||
auto r = m_wallet->checkSpendProof(txId, message, signature);
|
||||
|
||||
if (!r.first) {
|
||||
this->proofStatus(false, m_wallet->errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
r.second ? this->proofStatus(true, "Proof is valid")
|
||||
: this->proofStatus(false, "Proof is invalid");
|
||||
}
|
||||
|
||||
void VerifyProofDialog::checkOutProof() {
|
||||
this->checkTxProof(ui->lineEdit_outTxID->text(), ui->lineEdit_outAddress->text(), ui->input_OutMessage->toPlainText(), ui->input_OutProof->toPlainText());
|
||||
}
|
||||
|
||||
void VerifyProofDialog::checkInProof() {
|
||||
this->checkTxProof(ui->lineEdit_inTxID->text(), ui->lineEdit_inAddress->text(), ui->input_inMessage->toPlainText(), ui->input_InProof->toPlainText());
|
||||
}
|
||||
|
||||
void VerifyProofDialog::checkFormattedProof() {
|
||||
QRegularExpression proof("-----BEGIN (?<type>\\w+)-----\\n"
|
||||
"Network: (?<coin>\\w+) (?<network>\\w+)\\n"
|
||||
"Txid: (?<txid>[0-9a-f]{64})\\n"
|
||||
"(Address: (?<address>\\w+)\\n)?"
|
||||
"\\n?"
|
||||
"(?<message>.*?)\\n"
|
||||
"-----BEGIN \\1 SIGNATURE-----\\n"
|
||||
"\\n?"
|
||||
"(?<signature>.*?)\\n"
|
||||
"-----END \\1 SIGNATURE-----",
|
||||
QRegularExpression::CaseInsensitiveOption | QRegularExpression::DotMatchesEverythingOption);
|
||||
|
||||
QString formattedProof = ui->input_formattedProof->toPlainText();
|
||||
QRegularExpressionMatch match = proof.match(formattedProof);
|
||||
|
||||
if (!match.hasMatch()) {
|
||||
this->proofStatus(false, "Unable to parse proof");
|
||||
return;
|
||||
}
|
||||
|
||||
QString type = match.captured("type").toLower();
|
||||
QString coin = match.captured("coin").toLower();
|
||||
QString network = match.captured("network").toLower();
|
||||
QString txid = match.captured("txid");
|
||||
QString address = match.captured("address");
|
||||
QString message = match.captured("message");
|
||||
QString signature = match.captured("signature").remove('\n');
|
||||
|
||||
QStringList validTypes = {"inproof", "spendproof", "outproof"};
|
||||
if (!validTypes.contains(type)) {
|
||||
this->proofStatus(false, QString("Unknown proof type: %1").arg(type));
|
||||
return;
|
||||
}
|
||||
|
||||
if (coin != "monero") {
|
||||
this->proofStatus(false, QString("Can't verify proof for coin: %1").arg(coin));
|
||||
return;
|
||||
}
|
||||
|
||||
QString walletNetwork = Utils::QtEnumToString(m_wallet->nettype()).toLower();
|
||||
if (network != walletNetwork) {
|
||||
this->proofStatus(false, QString("Can't verify proof for %1 network when %2 wallet is opened").arg(network, walletNetwork));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "outproof" || type == "inproof") {
|
||||
this->checkTxProof(txid, address, message, signature);
|
||||
}
|
||||
if (type == "spendproof") {
|
||||
this->checkSpendProof(txid, message, signature);
|
||||
}
|
||||
}
|
||||
|
||||
void VerifyProofDialog::proofStatus(bool success, const QString &message) {
|
||||
if (ui->tabWidget_proofFormat->currentIndex() == 0) {
|
||||
ui->frame_status->show();
|
||||
ui->label_icon->setPixmap(success ? m_success : m_failure);
|
||||
ui->label_status->setText(message);
|
||||
}
|
||||
else {
|
||||
success ? QMessageBox::information(this, "Information", message)
|
||||
: QMessageBox::warning(this, "Warning", message);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
#define FEATHER_VERIFYPROOFDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QIcon>
|
||||
#include "libwalletqt/Wallet.h"
|
||||
|
||||
namespace Ui {
|
||||
|
@ -24,9 +25,14 @@ private slots:
|
|||
|
||||
private:
|
||||
void checkTxProof(const QString &txId, const QString &address, const QString &message, const QString &signature);
|
||||
void checkSpendProof();
|
||||
void checkSpendProof(const QString &txId, const QString &message, const QString &signature);
|
||||
void checkOutProof();
|
||||
void checkInProof();
|
||||
void checkFormattedProof();
|
||||
void proofStatus(bool success, const QString &message);
|
||||
|
||||
QPixmap m_success;
|
||||
QPixmap m_failure;
|
||||
|
||||
Ui::VerifyProofDialog *ui;
|
||||
Wallet *m_wallet;
|
||||
|
|
|
@ -6,14 +6,123 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1123</width>
|
||||
<height>413</height>
|
||||
<width>770</width>
|
||||
<height>587</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Verify transaction proof</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget_proofFormat">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab_formatted">
|
||||
<attribute name="title">
|
||||
<string>Formatted</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_16">
|
||||
<property name="text">
|
||||
<string>Paste formatted proof here:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="input_formattedProof"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_status">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_icon">
|
||||
<property name="text">
|
||||
<string>icon</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_status">
|
||||
<property name="text">
|
||||
<string>Proof is valid</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btn_verifyFormattedProof">
|
||||
<property name="text">
|
||||
<string>Verify</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_manual">
|
||||
<attribute name="title">
|
||||
<string>Manual</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
|
@ -24,7 +133,7 @@
|
|||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>2</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="SpendProof">
|
||||
<attribute name="title">
|
||||
|
@ -57,13 +166,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit_spendMessage">
|
||||
<property name="placeholderText">
|
||||
<string>Optional message against which the signature is signed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
|
@ -81,6 +183,13 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPlainTextEdit" name="input_SpendMessage">
|
||||
<property name="placeholderText">
|
||||
<string>Optional message against which the signature is signed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -153,13 +262,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit_outMessage">
|
||||
<property name="placeholderText">
|
||||
<string>Optional message against which the signature is signed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QPlainTextEdit" name="input_OutProof">
|
||||
<property name="placeholderText">
|
||||
|
@ -167,6 +269,13 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPlainTextEdit" name="input_OutMessage">
|
||||
<property name="placeholderText">
|
||||
<string>Optional message against which the signature is signed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -212,13 +321,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_13">
|
||||
<property name="text">
|
||||
<string>Address:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit_inAddress">
|
||||
<property name="placeholderText">
|
||||
|
@ -226,6 +328,13 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPlainTextEdit" name="input_InProof">
|
||||
<property name="placeholderText">
|
||||
<string>InProof..</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit_inTxID">
|
||||
<property name="placeholderText">
|
||||
|
@ -233,6 +342,13 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QPlainTextEdit" name="input_inMessage">
|
||||
<property name="placeholderText">
|
||||
<string>Optional message against which the signature is signed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_15">
|
||||
<property name="text">
|
||||
|
@ -240,17 +356,10 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit_inMessage">
|
||||
<property name="placeholderText">
|
||||
<string>Optional message against which the signature is signed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPlainTextEdit" name="input_InProof">
|
||||
<property name="placeholderText">
|
||||
<string>InProof..</string>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_13">
|
||||
<property name="text">
|
||||
<string>Address:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -309,6 +418,10 @@
|
|||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "historywidget.h"
|
||||
#include "ui_historywidget.h"
|
||||
#include "dialog/transactioninfodialog.h"
|
||||
#include "dialog/TxProofDialog.h"
|
||||
#include <QMessageBox>
|
||||
|
||||
HistoryWidget::HistoryWidget(QWidget *parent)
|
||||
|
@ -20,6 +21,7 @@ HistoryWidget::HistoryWidget(QWidget *parent)
|
|||
// copy menu
|
||||
m_copyMenu->setIcon(QIcon(":/assets/images/copy.png"));
|
||||
m_copyMenu->addAction("Transaction ID", this, [this]{copy(copyField::TxID);});
|
||||
m_copyMenu->addAction("Description", this, [this]{copy(copyField::Description);});
|
||||
m_copyMenu->addAction("Date", this, [this]{copy(copyField::Date);});
|
||||
m_copyMenu->addAction("Amount", this, [this]{copy(copyField::Amount);});
|
||||
|
||||
|
@ -48,74 +50,65 @@ void HistoryWidget::showContextMenu(const QPoint &point) {
|
|||
}
|
||||
|
||||
QMenu menu(this);
|
||||
TransactionInfo::Direction direction;
|
||||
QString txid;
|
||||
bool unconfirmed;
|
||||
m_txHistory->transaction(m_model->mapToSource(index).row(), [&direction, &txid, &unconfirmed](TransactionInfo &tInfo) {
|
||||
direction = tInfo.direction();
|
||||
txid = tInfo.hash();
|
||||
unconfirmed = tInfo.isFailed() || tInfo.isPending();
|
||||
});
|
||||
|
||||
if (AppContext::txCache.contains(txid) && unconfirmed && direction != TransactionInfo::Direction_In) {
|
||||
auto *tx = ui->history->currentEntry();
|
||||
if (!tx) return;
|
||||
|
||||
bool unconfirmed = tx->isFailed() || tx->isPending();
|
||||
if (AppContext::txCache.contains(tx->hash()) && unconfirmed && tx->direction() != TransactionInfo::Direction_In) {
|
||||
menu.addAction(QIcon(":/assets/images/info.png"), "Resend transaction", this, &HistoryWidget::onResendTransaction);
|
||||
}
|
||||
|
||||
menu.addMenu(m_copyMenu);
|
||||
menu.addAction(QIcon(":/assets/images/info.png"), "Show details", this, &HistoryWidget::showTxDetails);
|
||||
menu.addAction(QIcon(":/assets/images/network.png"), "View on block explorer", this, &HistoryWidget::onViewOnBlockExplorer);
|
||||
menu.addAction("Create tx proof", this, &HistoryWidget::createTxProof);
|
||||
|
||||
menu.exec(ui->history->viewport()->mapToGlobal(point));
|
||||
}
|
||||
|
||||
void HistoryWidget::onResendTransaction() {
|
||||
QModelIndex index = ui->history->currentIndex();
|
||||
QString txid;
|
||||
m_txHistory->transaction(m_model->mapToSource(index).row(), [&txid](TransactionInfo &tInfo) {
|
||||
txid = tInfo.hash();
|
||||
});
|
||||
|
||||
auto *tx = ui->history->currentEntry();
|
||||
if (tx) {
|
||||
QString txid = tx->hash();
|
||||
emit resendTransaction(txid);
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryWidget::setModel(TransactionHistoryProxyModel *model, Wallet *wallet)
|
||||
{
|
||||
m_model = model;
|
||||
m_wallet = wallet;
|
||||
m_txHistory = m_wallet->history();
|
||||
ui->history->setModel(m_model);
|
||||
ui->history->setHistoryModel(m_model);
|
||||
m_wallet->transactionHistoryModel()->amountPrecision = config()->get(Config::amountPrecision).toInt();
|
||||
|
||||
ui->history->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
ui->history->header()->setSectionResizeMode(TransactionHistoryModel::Description, QHeaderView::Stretch);
|
||||
ui->history->hideColumn(TransactionHistoryModel::TxID);
|
||||
// Load view state
|
||||
ui->history->setViewState(QByteArray::fromBase64(config()->get(Config::GUI_HistoryViewState).toByteArray()));
|
||||
}
|
||||
|
||||
void HistoryWidget::resetModel()
|
||||
{
|
||||
// Save view state
|
||||
config()->set(Config::GUI_HistoryViewState, ui->history->viewState().toBase64());
|
||||
config()->sync();
|
||||
|
||||
ui->history->setModel(nullptr);
|
||||
}
|
||||
|
||||
void HistoryWidget::showTxDetails() {
|
||||
QModelIndex index = ui->history->currentIndex();
|
||||
auto *tx = ui->history->currentEntry();
|
||||
if (!tx) return;
|
||||
|
||||
TransactionInfo *i = nullptr;
|
||||
m_txHistory->transaction(m_model->mapToSource(index).row(), [&i](TransactionInfo &tInfo) {
|
||||
i = &tInfo;
|
||||
});
|
||||
|
||||
if (i != nullptr) {
|
||||
auto * dialog = new TransactionInfoDialog(m_wallet, i, this);
|
||||
dialog->exec();
|
||||
}
|
||||
auto *dialog = new TransactionInfoDialog(m_wallet, tx, this);
|
||||
dialog->show();
|
||||
}
|
||||
|
||||
void HistoryWidget::onViewOnBlockExplorer() {
|
||||
QModelIndex index = ui->history->currentIndex();
|
||||
auto *tx = ui->history->currentEntry();
|
||||
if (!tx) return;
|
||||
|
||||
QString txid;
|
||||
m_txHistory->transaction(m_model->mapToSource(index).row(), [&txid](TransactionInfo &tInfo) {
|
||||
txid = tInfo.hash();
|
||||
});
|
||||
QString txid = tx->hash();
|
||||
emit viewOnBlockExplorer(txid);
|
||||
}
|
||||
|
||||
|
@ -126,25 +119,34 @@ void HistoryWidget::setSearchText(const QString &text) {
|
|||
void HistoryWidget::setSearchFilter(const QString &filter) {
|
||||
if (!m_model) return;
|
||||
m_model->setSearchFilter(filter);
|
||||
ui->history->setSearchMode(!filter.isEmpty());
|
||||
}
|
||||
|
||||
void HistoryWidget::createTxProof() {
|
||||
auto *tx = ui->history->currentEntry();
|
||||
if (!tx) return;
|
||||
|
||||
auto *dialog = new TxProofDialog(this, m_wallet, tx);
|
||||
dialog->exec();
|
||||
dialog->deleteLater();
|
||||
}
|
||||
|
||||
void HistoryWidget::copy(copyField field) {
|
||||
QModelIndex index = ui->history->currentIndex();
|
||||
auto *tx = ui->history->currentEntry();
|
||||
if (!tx) return;
|
||||
|
||||
QString data;
|
||||
m_txHistory->transaction(m_model->mapToSource(index).row(), [field, &data](TransactionInfo &tInfo) {
|
||||
QString data = [field, tx]{
|
||||
switch(field) {
|
||||
case copyField::TxID:
|
||||
data = tInfo.hash();
|
||||
break;
|
||||
return tx->hash();
|
||||
case copyField::Date:
|
||||
data = tInfo.timestamp().toString("yyyy-MM-dd HH:mm");
|
||||
break;
|
||||
return tx->timestamp().toString("yyyy-MM-dd HH:mm");
|
||||
case copyField::Amount:
|
||||
data = tInfo.displayAmount();
|
||||
break;
|
||||
return tx->displayAmount();
|
||||
default:
|
||||
return QString("");
|
||||
}
|
||||
});
|
||||
}();
|
||||
|
||||
Utils::copyToClipboard(data);
|
||||
}
|
||||
|
|
|
@ -40,10 +40,12 @@ private slots:
|
|||
void onViewOnBlockExplorer();
|
||||
void setSearchFilter(const QString &filter);
|
||||
void onResendTransaction();
|
||||
void createTxProof();
|
||||
|
||||
private:
|
||||
enum copyField {
|
||||
TxID = 0,
|
||||
Description,
|
||||
Date,
|
||||
Amount
|
||||
};
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeView" name="history">
|
||||
<widget class="HistoryView" name="history">
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
|
@ -115,6 +115,13 @@
|
|||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>HistoryView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>model/HistoryView.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
|
|
@ -32,6 +32,15 @@ TransactionInfo* TransactionHistory::transaction(const QString &id)
|
|||
return itr != m_tinfo.end() ? *itr : nullptr;
|
||||
}
|
||||
|
||||
TransactionInfo* TransactionHistory::transaction(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_tinfo.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return m_tinfo[index];
|
||||
}
|
||||
|
||||
void TransactionHistory::refresh(quint32 accountIndex)
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
|
|
|
@ -29,6 +29,7 @@ class TransactionHistory : public QObject
|
|||
public:
|
||||
Q_INVOKABLE bool transaction(int index, std::function<void (TransactionInfo &)> callback);
|
||||
Q_INVOKABLE TransactionInfo * transaction(const QString &id);
|
||||
TransactionInfo* transaction(int index);
|
||||
Q_INVOKABLE void refresh(quint32 accountIndex);
|
||||
Q_INVOKABLE void setTxNote(const QString &txid, const QString ¬e);
|
||||
Q_INVOKABLE bool writeCSV(const QString &path);
|
||||
|
|
|
@ -255,6 +255,7 @@ MainWindow::MainWindow(AppContext *ctx, QWidget *parent) :
|
|||
connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, m_balanceWidget, &TickerWidget::init);
|
||||
connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, m_ctx, &AppContext::onPreferredFiatCurrencyChanged);
|
||||
connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, ui->sendWidget, QOverload<>::of(&SendWidget::onPreferredFiatCurrencyChanged));
|
||||
connect(m_windowSettings, &Settings::amountPrecisionChanged, m_ctx, &AppContext::onAmountPrecisionChanged);
|
||||
|
||||
// Skin
|
||||
connect(m_windowSettings, &Settings::skinChanged, this, &MainWindow::skinChanged);
|
||||
|
@ -584,9 +585,7 @@ void MainWindow::onWalletOpened() {
|
|||
m_wizard->hide();
|
||||
}
|
||||
|
||||
this->raise();
|
||||
this->show();
|
||||
this->activateWindow();
|
||||
this->bringToFront();
|
||||
this->setEnabled(true);
|
||||
if(!m_ctx->tor->torConnected)
|
||||
this->setStatusText("Wallet opened - Starting Tor (may take a while)");
|
||||
|
@ -770,13 +769,6 @@ void MainWindow::onTransactionCommitted(bool status, PendingTransaction *tx, con
|
|||
QString body = QString("Successfully sent %1 transaction(s).").arg(txid.count());
|
||||
QMessageBox::information(this, "Transactions sent", body);
|
||||
ui->sendWidget->clearFields();
|
||||
|
||||
for(const auto &entry: txid) {
|
||||
m_ctx->currentWallet->setUserNote(entry, m_ctx->tmpTxDescription);
|
||||
AppContext::txDescriptionCache[entry] = m_ctx->tmpTxDescription;
|
||||
}
|
||||
|
||||
m_ctx->tmpTxDescription = "";
|
||||
} else {
|
||||
auto err = tx->errorString();
|
||||
QString body = QString("Error committing transaction: %1").arg(err);
|
||||
|
@ -1341,6 +1333,14 @@ QString MainWindow::statusDots() {
|
|||
return QString(".").repeated(m_statusDots);
|
||||
}
|
||||
|
||||
void MainWindow::bringToFront() {
|
||||
ensurePolished();
|
||||
setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive);
|
||||
show();
|
||||
raise();
|
||||
activateWindow();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() {
|
||||
delete ui;
|
||||
}
|
||||
|
|
|
@ -172,6 +172,7 @@ private:
|
|||
void setStatusText(const QString &text, bool override = false, int timeout = 1000);
|
||||
void showBalanceDialog();
|
||||
QString statusDots();
|
||||
void bringToFront();
|
||||
|
||||
WalletWizard *createWizard(WalletWizard::Page startPage);
|
||||
|
||||
|
|
188
src/model/HistoryView.cpp
Normal file
188
src/model/HistoryView.cpp
Normal file
|
@ -0,0 +1,188 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// Copyright (c) 2020-2021, The Monero Project.
|
||||
|
||||
#include "HistoryView.h"
|
||||
|
||||
#include "TransactionHistoryProxyModel.h"
|
||||
|
||||
#include <QHeaderView>
|
||||
#include <QMenu>
|
||||
|
||||
HistoryView::HistoryView(QWidget *parent)
|
||||
: QTreeView(parent)
|
||||
, m_model(nullptr)
|
||||
, m_headerMenu(new QMenu(this))
|
||||
{
|
||||
setUniformRowHeights(true);
|
||||
setRootIsDecorated(false);
|
||||
setAlternatingRowColors(true);
|
||||
setDragEnabled(true);
|
||||
setSortingEnabled(true);
|
||||
|
||||
header()->setStretchLastSection(false);
|
||||
header()->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
connect(header(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(showHeaderMenu(QPoint)));
|
||||
}
|
||||
|
||||
void HistoryView::setHistoryModel(TransactionHistoryProxyModel *model) {
|
||||
m_model = model;
|
||||
m_model->setSortCaseSensitivity(Qt::CaseInsensitive);
|
||||
m_model->setSortRole(Qt::UserRole);
|
||||
m_model->setDynamicSortFilter(true);
|
||||
m_model->setSortLocaleAware(true);
|
||||
|
||||
QTreeView::setModel(m_model);
|
||||
resetViewToDefaults();
|
||||
|
||||
m_headerMenu->clear();
|
||||
|
||||
// Actions to toggle column visibility, each carrying the corresponding
|
||||
// column index as data
|
||||
m_columnActions = new QActionGroup(this);
|
||||
m_columnActions->setExclusive(false);
|
||||
for (int visualIndex = 1; visualIndex < header()->count(); ++visualIndex) {
|
||||
int logicalIndex = header()->logicalIndex(visualIndex);
|
||||
QString caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString();
|
||||
if (caption.isEmpty()) {
|
||||
caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::ToolTipRole).toString();
|
||||
}
|
||||
|
||||
auto action = m_headerMenu->addAction(caption);
|
||||
action->setCheckable(true);
|
||||
action->setData(logicalIndex);
|
||||
m_columnActions->addAction(action);
|
||||
}
|
||||
connect(m_columnActions, &QActionGroup::triggered, this, &HistoryView::toggleColumnVisibility);
|
||||
|
||||
m_headerMenu->addSeparator();
|
||||
m_headerMenu->addAction(tr("Fit to window"), this, &HistoryView::fitColumnsToWindow);
|
||||
m_headerMenu->addAction(tr("Fit to contents"), this, &HistoryView::fitColumnsToContents);
|
||||
m_headerMenu->addSeparator();
|
||||
m_headerMenu->addAction(tr("Reset to defaults"), this, &HistoryView::resetViewToDefaults);
|
||||
|
||||
fitColumnsToWindow();
|
||||
}
|
||||
|
||||
TransactionHistoryModel* HistoryView::sourceModel()
|
||||
{
|
||||
return dynamic_cast<TransactionHistoryModel *>(m_model->sourceModel());
|
||||
}
|
||||
|
||||
TransactionInfo* HistoryView::currentEntry()
|
||||
{
|
||||
QModelIndexList list = selectionModel()->selectedRows();
|
||||
if (list.size() == 1) {
|
||||
return this->sourceModel()->entryFromIndex(m_model->mapToSource(list.first()));
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryView::setSearchMode(bool mode) {
|
||||
m_inSearchMode = mode;
|
||||
|
||||
if (mode) {
|
||||
header()->showSection(TransactionHistoryModel::TxID);
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray HistoryView::viewState() const
|
||||
{
|
||||
return header()->saveState();
|
||||
}
|
||||
|
||||
bool HistoryView::setViewState(const QByteArray& state)
|
||||
{
|
||||
// Reset to unsorted first (https://bugreports.qt.io/browse/QTBUG-86694)
|
||||
header()->setSortIndicator(-1, Qt::AscendingOrder);
|
||||
bool status = header()->restoreState(state);
|
||||
m_columnsNeedRelayout = state.isEmpty();
|
||||
return status;
|
||||
}
|
||||
|
||||
void HistoryView::showHeaderMenu(const QPoint& position)
|
||||
{
|
||||
const QList<QAction*> actions = m_columnActions->actions();
|
||||
for (auto& action : actions) {
|
||||
Q_ASSERT(static_cast<QMetaType::Type>(action->data().type()) == QMetaType::Int);
|
||||
if (static_cast<QMetaType::Type>(action->data().type()) != QMetaType::Int) {
|
||||
continue;
|
||||
}
|
||||
int columnIndex = action->data().toInt();
|
||||
action->setChecked(!isColumnHidden(columnIndex));
|
||||
}
|
||||
|
||||
m_headerMenu->popup(mapToGlobal(position));
|
||||
}
|
||||
|
||||
void HistoryView::toggleColumnVisibility(QAction* action)
|
||||
{
|
||||
// Verify action carries a column index as data. Since QVariant.toInt()
|
||||
// below will accept anything that's interpretable as int, perform a type
|
||||
// check here to make sure data actually IS int
|
||||
Q_ASSERT(static_cast<QMetaType::Type>(action->data().type()) == QMetaType::Int);
|
||||
if (static_cast<QMetaType::Type>(action->data().type()) != QMetaType::Int) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle column visibility. Visible columns will only be hidden if at
|
||||
// least one visible column remains, as the table header will disappear
|
||||
// entirely when all columns are hidden
|
||||
int columnIndex = action->data().toInt();
|
||||
if (action->isChecked()) {
|
||||
header()->showSection(columnIndex);
|
||||
if (header()->sectionSize(columnIndex) == 0) {
|
||||
header()->resizeSection(columnIndex, header()->defaultSectionSize());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((header()->count() - header()->hiddenSectionCount()) > 1) {
|
||||
header()->hideSection(columnIndex);
|
||||
return;
|
||||
}
|
||||
action->setChecked(true);
|
||||
}
|
||||
|
||||
void HistoryView::fitColumnsToWindow()
|
||||
{
|
||||
header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
header()->setSectionResizeMode(TransactionHistoryModel::Description, QHeaderView::Stretch);
|
||||
header()->setStretchLastSection(false);
|
||||
QCoreApplication::processEvents();
|
||||
}
|
||||
|
||||
void HistoryView::fitColumnsToContents()
|
||||
{
|
||||
header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
QCoreApplication::processEvents();
|
||||
header()->setSectionResizeMode(QHeaderView::Interactive);
|
||||
}
|
||||
|
||||
void HistoryView::resetViewToDefaults()
|
||||
{
|
||||
if (m_inSearchMode) {
|
||||
header()->showSection(TransactionHistoryModel::TxID);
|
||||
} else {
|
||||
header()->hideSection(TransactionHistoryModel::TxID);
|
||||
}
|
||||
header()->showSection(TransactionHistoryModel::Date);
|
||||
header()->showSection(TransactionHistoryModel::Description);
|
||||
header()->showSection(TransactionHistoryModel::Amount);
|
||||
header()->showSection(TransactionHistoryModel::FiatAmount);
|
||||
|
||||
// Reset column order to logical indices
|
||||
for (int i = 0; i < header()->count(); ++i) {
|
||||
header()->moveSection(header()->visualIndex(i), i);
|
||||
}
|
||||
|
||||
m_model->sort(TransactionHistoryModel::Date, Qt::DescendingOrder);
|
||||
sortByColumn(TransactionHistoryModel::Date, Qt::DescendingOrder);
|
||||
|
||||
// The following call only relayouts reliably if the widget has been shown
|
||||
// already, so only do it if the widget is visible and let showEvent() handle
|
||||
// the initial default layout.
|
||||
if (isVisible()) {
|
||||
fitColumnsToWindow();
|
||||
}
|
||||
}
|
44
src/model/HistoryView.h
Normal file
44
src/model/HistoryView.h
Normal file
|
@ -0,0 +1,44 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// Copyright (c) 2020-2021, The Monero Project.
|
||||
|
||||
#ifndef FEATHER_HISTORYVIEW_H
|
||||
#define FEATHER_HISTORYVIEW_H
|
||||
|
||||
#include <QTreeView>
|
||||
#include <QActionGroup>
|
||||
|
||||
#include "TransactionHistoryModel.h"
|
||||
|
||||
class HistoryView : public QTreeView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit HistoryView(QWidget* parent = nullptr);
|
||||
void setHistoryModel(TransactionHistoryProxyModel *model);
|
||||
TransactionInfo* currentEntry();
|
||||
|
||||
void setSearchMode(bool mode);
|
||||
QByteArray viewState() const;
|
||||
bool setViewState(const QByteArray& state);
|
||||
|
||||
private slots:
|
||||
void showHeaderMenu(const QPoint& position);
|
||||
void toggleColumnVisibility(QAction* action);
|
||||
void fitColumnsToWindow();
|
||||
void fitColumnsToContents();
|
||||
void resetViewToDefaults();
|
||||
|
||||
private:
|
||||
TransactionHistoryModel* sourceModel();
|
||||
|
||||
TransactionHistoryProxyModel* m_model;
|
||||
bool m_inSearchMode = false;
|
||||
bool m_columnsNeedRelayout = true;
|
||||
|
||||
QMenu* m_headerMenu;
|
||||
QActionGroup* m_columnActions;
|
||||
};
|
||||
|
||||
|
||||
#endif //FEATHER_HISTORYVIEW_H
|
|
@ -16,7 +16,7 @@ QString ModelUtils::displayAddress(const QString& address, int sections, const Q
|
|||
for (int i = 0; i < sections; i += 1) {
|
||||
list << address.mid(i*5, 5);
|
||||
}
|
||||
list << "...";
|
||||
list << "…"; // utf-8 Horizontal Ellipsis
|
||||
for (int i = sections; i > 0; i -= 1) {
|
||||
list << address.mid(address.length() - i * 5, 5);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include "TransactionInfo.h"
|
||||
#include "globals.h"
|
||||
#include "utils/ColorScheme.h"
|
||||
#include "ModelUtils.h"
|
||||
|
||||
TransactionHistoryModel::TransactionHistoryModel(QObject *parent)
|
||||
: QAbstractTableModel(parent),
|
||||
|
@ -38,6 +39,11 @@ TransactionHistory *TransactionHistoryModel::transactionHistory() const {
|
|||
return m_transactionHistory;
|
||||
}
|
||||
|
||||
TransactionInfo* TransactionHistoryModel::entryFromIndex(const QModelIndex &index) const {
|
||||
Q_ASSERT(index.isValid() && index.row() < m_transactionHistory->count());
|
||||
return m_transactionHistory->transaction(index.row());
|
||||
}
|
||||
|
||||
int TransactionHistoryModel::rowCount(const QModelIndex &parent) const {
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
|
@ -65,8 +71,8 @@ QVariant TransactionHistoryModel::data(const QModelIndex &index, int role) const
|
|||
QVariant result;
|
||||
|
||||
bool found = m_transactionHistory->transaction(index.row(), [this, &index, &result, &role](const TransactionInfo &tInfo) {
|
||||
if(role == Qt::DisplayRole || role == Qt::EditRole) {
|
||||
result = parseTransactionInfo(tInfo, index.column());
|
||||
if(role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole) {
|
||||
result = parseTransactionInfo(tInfo, index.column(), role);
|
||||
}
|
||||
else if (role == Qt::TextAlignmentRole) {
|
||||
switch (index.column()) {
|
||||
|
@ -130,31 +136,26 @@ QVariant TransactionHistoryModel::data(const QModelIndex &index, int role) const
|
|||
return result;
|
||||
}
|
||||
|
||||
QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionInfo &tInfo, int column) const
|
||||
QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionInfo &tInfo, int column, int role) const
|
||||
{
|
||||
switch (column)
|
||||
{
|
||||
case Column::Date:
|
||||
return tInfo.timestamp().toString("yyyy-MM-dd HH:mm ");
|
||||
case Column::Description: {
|
||||
// if this tx is still in the pool, then we wont get the
|
||||
// description. We've cached it inside `AppContext::txDescriptionCache`
|
||||
// for the time being.
|
||||
if(tInfo.isPending()) {
|
||||
auto hash = tInfo.hash();
|
||||
if (AppContext::txDescriptionCache.contains(hash))
|
||||
return AppContext::txDescriptionCache[hash];
|
||||
}
|
||||
case Column::Description:
|
||||
return tInfo.description();
|
||||
}
|
||||
case Column::Amount:
|
||||
{
|
||||
QString amount = QString::number(tInfo.balanceDelta() / globals::cdiv, 'f', 4);
|
||||
if (role == Qt::UserRole) {
|
||||
return tInfo.balanceDelta();
|
||||
}
|
||||
QString amount = QString::number(tInfo.balanceDelta() / globals::cdiv, 'f', this->amountPrecision);
|
||||
amount = (tInfo.direction() == TransactionInfo::Direction_Out) ? "-" + amount : "+" + amount;
|
||||
return amount;
|
||||
}
|
||||
case Column::TxID:
|
||||
return tInfo.hash();
|
||||
case Column::TxID: {
|
||||
return ModelUtils::displayAddress(tInfo.hash(), 1);
|
||||
}
|
||||
case Column::FiatAmount:
|
||||
{
|
||||
double usd_price = AppContext::txFiatHistory->get(tInfo.timestamp().toString("yyyyMMdd"));
|
||||
|
@ -164,8 +165,11 @@ QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionInfo &tI
|
|||
double usd_amount = usd_price * (tInfo.balanceDelta() / globals::cdiv);
|
||||
if(this->preferredFiatSymbol != "USD")
|
||||
usd_amount = AppContext::prices->convert("USD", this->preferredFiatSymbol, usd_amount);
|
||||
double fiat_rounded = ceil(Utils::roundSignificant(usd_amount, 3) * 100.0) / 100.0;
|
||||
if (role == Qt::UserRole) {
|
||||
return usd_amount;
|
||||
}
|
||||
|
||||
double fiat_rounded = ceil(Utils::roundSignificant(usd_amount, 3) * 100.0) / 100.0;
|
||||
return QString("%1").arg(Utils::amountToCurrencyString(fiat_rounded, this->preferredFiatSymbol));
|
||||
}
|
||||
default:
|
||||
|
@ -224,15 +228,10 @@ bool TransactionHistoryModel::setData(const QModelIndex &index, const QVariant &
|
|||
}
|
||||
|
||||
Qt::ItemFlags TransactionHistoryModel::flags(const QModelIndex &index) const {
|
||||
bool isPending;
|
||||
m_transactionHistory->transaction(index.row(), [this, &isPending](const TransactionInfo &tInfo){
|
||||
isPending = tInfo.isPending();
|
||||
});
|
||||
|
||||
if (!index.isValid())
|
||||
return Qt::ItemIsEnabled;
|
||||
|
||||
if (index.column() == Description && !isPending)
|
||||
if (index.column() == Description)
|
||||
return QAbstractTableModel::flags(index) | Qt::ItemIsEditable;
|
||||
|
||||
return QAbstractTableModel::flags(index);
|
||||
|
|
|
@ -24,9 +24,9 @@ public:
|
|||
enum Column
|
||||
{
|
||||
Date = 0,
|
||||
TxID,
|
||||
Description,
|
||||
Amount,
|
||||
TxID,
|
||||
FiatAmount,
|
||||
COUNT
|
||||
};
|
||||
|
@ -34,8 +34,11 @@ public:
|
|||
explicit TransactionHistoryModel(QObject * parent = nullptr);
|
||||
void setTransactionHistory(TransactionHistory * th);
|
||||
TransactionHistory * transactionHistory() const;
|
||||
TransactionInfo* entryFromIndex(const QModelIndex& index) const;
|
||||
|
||||
QString preferredFiatSymbol = "USD";
|
||||
int amountPrecision = 4;
|
||||
|
||||
int rowCount(const QModelIndex & parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override;
|
||||
|
@ -48,7 +51,7 @@ signals:
|
|||
void transactionHistoryChanged();
|
||||
|
||||
private:
|
||||
QVariant parseTransactionInfo(const TransactionInfo &tInfo, int column) const;
|
||||
QVariant parseTransactionInfo(const TransactionInfo &tInfo, int column, int role) const;
|
||||
|
||||
TransactionHistory * m_transactionHistory;
|
||||
QIcon m_unconfirmedTx;
|
||||
|
|
|
@ -16,6 +16,10 @@ TransactionHistoryProxyModel::TransactionHistoryProxyModel(Wallet *wallet, QObje
|
|||
m_history = m_wallet->history();
|
||||
}
|
||||
|
||||
TransactionHistory* TransactionHistoryProxyModel::history() {
|
||||
return m_history;
|
||||
}
|
||||
|
||||
bool TransactionHistoryProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
QString description, txid, subaddrlabel;
|
||||
|
|
|
@ -16,6 +16,7 @@ Q_OBJECT
|
|||
public:
|
||||
explicit TransactionHistoryProxyModel(Wallet *wallet, QObject* parent = nullptr);
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const;
|
||||
TransactionHistory* history();
|
||||
|
||||
public slots:
|
||||
void setSearchFilter(const QString& searchString){
|
||||
|
|
|
@ -43,9 +43,15 @@ Settings::Settings(QWidget *parent) :
|
|||
if (m_skins.contains(settingsSkin))
|
||||
ui->comboBox_skin->setCurrentIndex(m_skins.indexOf(settingsSkin));
|
||||
|
||||
for (int i = 0; i <= 12; i++) {
|
||||
ui->comboBox_amountPrecision->addItem(QString::number(i));
|
||||
}
|
||||
ui->comboBox_amountPrecision->setCurrentIndex(config()->get(Config::amountPrecision).toInt());
|
||||
|
||||
connect(ui->comboBox_skin, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_skinChanged);
|
||||
connect(ui->comboBox_blockExplorer, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_blockExplorerChanged);
|
||||
connect(ui->comboBox_redditFrontend, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_redditFrontendChanged);
|
||||
connect(ui->comboBox_amountPrecision, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_amountPrecisionChanged);
|
||||
|
||||
// setup preferred fiat currency combobox
|
||||
QStringList fiatCurrencies;
|
||||
|
@ -100,6 +106,11 @@ void Settings::comboBox_redditFrontendChanged(int pos) {
|
|||
config()->set(Config::redditFrontend, redditFrontend);
|
||||
}
|
||||
|
||||
void Settings::comboBox_amountPrecisionChanged(int pos) {
|
||||
config()->set(Config::amountPrecision, pos);
|
||||
emit amountPrecisionChanged(pos);
|
||||
}
|
||||
|
||||
void Settings::copyToClipboard() {
|
||||
ui->textLogs->copy();
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ signals:
|
|||
void skinChanged(QString skinName);
|
||||
void showHomeCCS(bool);
|
||||
void blockExplorerChanged(QString blockExplorer);
|
||||
void amountPrecisionChanged(int precision);
|
||||
|
||||
public slots:
|
||||
void updatePaths();
|
||||
|
@ -38,6 +39,7 @@ public slots:
|
|||
void comboBox_skinChanged(int pos);
|
||||
void comboBox_blockExplorerChanged(int pos);
|
||||
void comboBox_redditFrontendChanged(int pos);
|
||||
void comboBox_amountPrecisionChanged(int pos);
|
||||
|
||||
private:
|
||||
QStringList m_skins{"Native", "QDarkStyle", "Breeze/Dark", "Breeze/Light"};
|
||||
|
|
|
@ -161,14 +161,14 @@
|
|||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="checkBox_externalLink">
|
||||
<property name="text">
|
||||
<string>Warn before opening external link</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="checkBox_hideBalance">
|
||||
<property name="text">
|
||||
<string>Hide balance</string>
|
||||
|
@ -201,6 +201,16 @@
|
|||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Amount precision:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="comboBox_amountPrecision"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_node">
|
||||
|
|
|
@ -46,7 +46,9 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
|
|||
{Config::firstRun,{QS("firstRun"), false}},
|
||||
{Config::hideBalance, {QS("hideBalance"), false}},
|
||||
{Config::redditFrontend, {QS("redditFrontend"), "old.reddit.com"}},
|
||||
{Config::showHistorySyncNotice, {QS("showHistorySyncNotice"), true}}
|
||||
{Config::showHistorySyncNotice, {QS("showHistorySyncNotice"), true}},
|
||||
{Config::GUI_HistoryViewState, {QS("GUI_HistoryViewState"), {}}},
|
||||
{Config::amountPrecision, {QS("amountPrecision"), 4}}
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -48,7 +48,9 @@ public:
|
|||
firstRun,
|
||||
hideBalance,
|
||||
redditFrontend,
|
||||
showHistorySyncNotice
|
||||
showHistorySyncNotice,
|
||||
GUI_HistoryViewState,
|
||||
amountPrecision
|
||||
};
|
||||
|
||||
~Config() override;
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// Copyright (c) 2020-2021, The Monero Project.
|
||||
|
||||
#include "txproofwidget.h"
|
||||
#include "ui_txproofwidget.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "utils/utils.h"
|
||||
|
||||
TxProofWidget::TxProofWidget(QWidget *parent, Wallet *wallet, TransactionInfo *txInfo)
|
||||
: QWidget(parent)
|
||||
, ui(new Ui::TxProofWidget)
|
||||
, m_wallet(wallet)
|
||||
, m_txid(txInfo->hash())
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
if (txInfo->direction() == TransactionInfo::Direction_Out) {
|
||||
for (auto const &d: txInfo->destinations()) {
|
||||
ui->comboBox_TxProofAddresses->addItem(d);
|
||||
}
|
||||
} else {
|
||||
ui->btn_copySpendProof->setEnabled(false);
|
||||
|
||||
for (auto const &s: txInfo->subaddrIndex()) {
|
||||
ui->comboBox_TxProofAddresses->addItem(wallet->address(txInfo->subaddrAccount(), s));
|
||||
}
|
||||
}
|
||||
|
||||
if (ui->comboBox_TxProofAddresses->count() == 0) {
|
||||
ui->btn_copyTxProof->setEnabled(false);
|
||||
}
|
||||
|
||||
connect(ui->btn_copySpendProof, &QPushButton::clicked, this, &TxProofWidget::copySpendProof);
|
||||
connect(ui->btn_copyTxProof, &QPushButton::clicked, this, &TxProofWidget::copyTxProof);
|
||||
|
||||
ui->label_SpendProof->setHelpText("A SpendProof proves authorship of a transaction.\n\n"
|
||||
"SpendProofs do not prove that a particular amount was sent to an address, for that use OutProofs.");
|
||||
ui->label_InOutProof->setHelpText("An InProof proves ownership of an output.\n"
|
||||
"An OutProof shows the prover sent an output to an address.");
|
||||
}
|
||||
|
||||
void TxProofWidget::copySpendProof() {
|
||||
auto txproof = m_wallet->getSpendProof(m_txid, "");
|
||||
if (!txproof.error.isEmpty()) {
|
||||
QMessageBox::warning(this, "Copy SpendProof", QString("Failed to copy SpendProof").arg(txproof.error));
|
||||
return;
|
||||
}
|
||||
|
||||
Utils::copyToClipboard(txproof.proof);
|
||||
QMessageBox::information(this, "Copy SpendProof", "SpendProof copied to clipboard");
|
||||
}
|
||||
|
||||
void TxProofWidget::copyTxProof() {
|
||||
auto txproof = m_wallet->getTxProof(m_txid, ui->comboBox_TxProofAddresses->currentText(), "");
|
||||
if (!txproof.error.isEmpty()) {
|
||||
QMessageBox::warning(this, "Copy Transaction Proof", QString("Failed to copy transaction proof: %1").arg(txproof.error));
|
||||
return;
|
||||
}
|
||||
|
||||
Utils::copyToClipboard(txproof.proof);
|
||||
QMessageBox::information(this, "Copy Transaction Proof", "Transaction proof copied to clipboard");
|
||||
}
|
||||
|
||||
TxProofWidget::~TxProofWidget() {
|
||||
delete ui;
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// Copyright (c) 2020-2021, The Monero Project.
|
||||
|
||||
#ifndef FEATHER_TXPROOFWIDGET_H
|
||||
#define FEATHER_TXPROOFWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "libwalletqt/Wallet.h"
|
||||
#include "libwalletqt/TransactionInfo.h"
|
||||
|
||||
namespace Ui {
|
||||
class TxProofWidget;
|
||||
}
|
||||
|
||||
class TxProofWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TxProofWidget(QWidget *parent, Wallet *wallet, TransactionInfo *txid);
|
||||
~TxProofWidget() override;
|
||||
|
||||
private:
|
||||
void copySpendProof();
|
||||
void copyTxProof();
|
||||
|
||||
Ui::TxProofWidget *ui;
|
||||
QString m_txid;
|
||||
Wallet *m_wallet;
|
||||
};
|
||||
|
||||
#endif //FEATHER_TXPROOFWIDGET_H
|
|
@ -1,110 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TxProofWidget</class>
|
||||
<widget class="QWidget" name="TxProofWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>647</width>
|
||||
<height>79</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="btn_copySpendProof">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Copy</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="HelpLabel" name="label_SpendProof">
|
||||
<property name="text">
|
||||
<string>SpendProof</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="btn_copyTxProof">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Copy</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="HelpLabel" name="label_InOutProof">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>In/OutProof</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_TxProofAddresses">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>HelpLabel</class>
|
||||
<extends>QLabel</extends>
|
||||
<header>components.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
Loading…
Reference in a new issue