diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 000000000..cb0f9724d --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,137 @@ +# Validity Wallet - Agent Handoff Document + +**Last Updated:** 2026-02-26 (All phases complete) +**Author:** Claude Code (Opus 4.6) +**Project:** Validity PoS Cryptocurrency Wallet (Qt5/C++11, forked from Bitcoin Core 0.13.2) +**Repo:** https://github.com/kirklandsig/Validity (fork of RadiumCore/Validity) +**PR:** https://github.com/RadiumCore/Validity/pull/11 (OPEN) +**Branch:** `feature/dark-theme-dashboard` +**Hard Constraint:** NO consensus/blockchain changes. UI/client-side ONLY. Zero fork risk. + +--- + +## Current Status: All Phases Complete + +**Build status:** PASSING (all changes compiled successfully) +**Binary:** `~/Validity-build/src/qt/validity-qt.exe` + +### Git History (most recent first) +1. (pending) — Light theme + drag-drop card reordering + branding completion +2. `054096399` — Complete Bitcoin -> Validity branding in all UI form files +3. `ad748a8c9` — Complete Bitcoin -> Validity branding across all UI strings +4. `ad42fc5e9` — Research reports for JJ (codebase upgrade feasibility) +5. `e1d02be82` — Comprehensive dark theme polish (526 lines QSS) + branding fixes +6. `5a1cadf4e` — System tray enhancements + notification improvements +7. `7fa0c0308` — Backup Wizard + Network Peer Map visualization +8. `d56cba331` — Phase 3 dashboard layout + UI polish +9. `e58fb9734` — Phase 2 visual overhaul, sidebar nav, security fixes +10. `33f8cb1c0` — Phase 1 dark theme system + overview dashboard + +All pushed to `origin/feature/dark-theme-dashboard`. + +--- + +## What Was Done + +### Phase 1 (Committed: 33f8cb1c0) +- Dark theme system (ThemeManager) with Dark/Light toggle +- Card-based dashboard redesign (Balance, Staking Chart, Network, Transactions) +- 30-day staking rewards chart (custom QPainter bar chart) +- QSS theme files (dark.qss, light.qss) +- Build system updates (Makefile.qt.include, bitcoin.qrc) + +### Phase 2 (Committed: e58fb9734) +- Full visual overhaul: sidebar navigation, splash screen, modal overlay +- Deep navy/purple gradients matching validitytech.com +- Security: CExtKey buffer overflow fix, AskPassphraseDialog fall-through fix +- 18 files modified total + +### Phase 3 (Committed: d56cba331) +- Staking chart summary: 8-column layout with pipe separators +- Layout swap: Transactions full-width middle, Network compact bottom +- NUM_ITEMS 5->7, division-by-zero guards, pwalletMain null check +- Removed ~860 lines of hardcoded palette XML from sendcoinsentry.ui +- Fee warning color fix for dark theme readability + +### Phase 4 Features (Committed: 7fa0c0308) +- **Backup Wizard**: QDialog + QStackedWidget, 4 pages, File > Backup Wizard + - New files: `backupwizard.h`, `backupwizard.cpp` +- **Network Peer Map**: Custom QPainter world map, peer dots by latency, hover tooltips + - New files: `peermapwidget.h`, `peermapwidget.cpp`, `geoip.h`, `geoip.cpp` + - New tab "Peer Map" in RPC Console + +### Tray + Notifications (Committed: 5a1cadf4e) +- Expanded tray menu: Overview, Transactions, Backup Wizard, Lock/Unlock +- Staking reward notifications with "Staking Reward!" title +- Richer tray tooltip: weight, network weight, connections +- Branding: "Bitcoin network" -> "Validity network" + +### Comprehensive UI Polish (Committed: e1d02be82) +- 526 new lines of QSS covering all remaining pages +- Send/Receive: fee section, coin control, buttons, validation states +- Transaction history: filter bar, table hover/select, context menu +- Address book: button styling, table hover effects +- Coin control dialog: tree widget, checkboxes, select all button +- Options dialog: tab pane, theme combo, status label +- Global: disabled states, focus indicators, tooltip styling, slider +- Fixed "Invalid Bitcoin address" -> "Invalid Validity address" +- Fixed Windows startup shortcut names: Bitcoin -> Validity + +### Complete Branding Cleanup (Committed: ad748a8c9, 054096399) +- All user-facing "Bitcoin" references replaced with "Validity" across: + - 6 .cpp files: addressbookpage, bitcoin, bitcoingui, editaddressdialog, guiutil, paymentserver + - 6 .ui files: optionsdialog, overviewpage, intro, receivecoinsdialog, sendcoinsentry, signverifymessagedialog +- IPC server name: "BitcoinQt" -> "ValidityQt" +- Windows startup shortcuts: "Bitcoin.lnk" -> "Validity.lnk" + +### Light Theme + Drag-Drop Card Reordering (Latest) +- **Light theme** complete rewrite: 51 lines -> 700+ lines matching dark theme coverage + - Palette: white (#ffffff), surface (#f0f2f5), green (#2d8f5e), danger (#d04040) + - Covers all pages: cards, sidebar, tabs, tables, dialogs, scrollbars, inputs +- **Drag-and-drop card reordering**: Event filter approach for dashboard cards + - New files: `carddragdrop.h`, `carddragdrop.cpp` + - Registered cards: transactionsCard, networkCard (top row fixed at top) + - Drop indicator, QSettings persistence ("OverviewCardOrder"), order restore on startup + +--- + +## Architecture Notes + +### Key Files +- `src/qt/bitcoingui.cpp` — Main window, toolbar, menu. ThemeManager lives here. +- `src/qt/overviewpage.cpp` — Dashboard. StakingChartWidget + CardDragDropManager live here. +- `src/qt/thememanager.cpp` — Loads QSS from resources, applies via `qApp->setStyleSheet()`. +- `src/qt/stakingchartwidget.cpp` — Custom QPainter widget. 30 bars with hover. +- `src/qt/carddragdrop.cpp` — Event filter drag-drop reordering for dashboard cards. +- `src/qt/backupwizard.cpp` — Backup wizard dialog (QDialog + QStackedWidget). +- `src/qt/peermapwidget.cpp` — Peer map world visualization. +- `src/qt/geoip.cpp` — IP-to-coordinate lookup via first-octet heuristic. +- `src/qt/res/themes/dark.qss` — 1500+ lines of comprehensive dark theme styling. +- `src/qt/res/themes/light.qss` — 700+ lines comprehensive light theme. + +### Build Workflow +1. Edit files on Windows: `C:\Users\yanal\OneDrive\Desktop\Claude\Projects\Validity\` +2. Sync: `cp ... && dos2unix ...` (or use sync_to_wsl.sh for batch) +3. Build: `cd ~/Validity-build && PATH=/usr/local/sbin:...:/bin make -j$(nproc)` +4. Binary: `src/qt/validity-qt.exe` + +### WSL Build Environment +- WSL2 Ubuntu on Windows 11 +- Build directory: `~/Validity-build/` (native Linux filesystem) +- Windows source: `/mnt/c/Users/yanal/OneDrive/Desktop/Claude/Projects/Validity/` + +--- + +## Remaining Work + +All major features and polish items are complete. Potential future work: +- Test on live network with real wallet +- Additional icon/graphic updates if validitytech.com branding changes +- Accessibility improvements (screen reader, high contrast) +- Translations update (qt/locale/ files still reference Bitcoin in some strings) + +### JJ's Research Request: Codebase Upgrade +Research completed via 3 agents. Full reports saved in `docs/plans/`: +- `2026-02-26-bitcoin-core-port-feasibility.md` — Bitcoin Core 30.0 direct port (12-24 months, extreme risk) +- `2026-02-26-blackcoin-rebase-feasibility.md` — **RECOMMENDED:** BlackCoin More 26.x rebase (1-3 months) +- `2026-02-26-pos-consensus-inventory.md` — Complete PoS consensus code inventory (~1,670 lines across 12 files) diff --git a/docs/plans/2026-02-25-dark-theme-dashboard-design.md b/docs/plans/2026-02-25-dark-theme-dashboard-design.md new file mode 100644 index 000000000..9874c0b10 --- /dev/null +++ b/docs/plans/2026-02-25-dark-theme-dashboard-design.md @@ -0,0 +1,46 @@ +# Validity Wallet: Dark Theme + Dashboard Modernization + +**Date:** 2026-02-25 +**Status:** Approved +**Constraint:** No consensus/fork changes — UI/UX only + +## Theme System + +ThemeManager class loads QSS stylesheets at runtime. Two themes: Light (preserves current) and Dark (new). Preference persisted via QSettings. Toggle in Options dialog. + +### Dark Palette (Validity-branded) +- Background: #1e1e2e +- Card/Surface: #2a2a3d +- Card Border: #3a3a4d +- Primary Accent: #43b581 (existing Validity green) +- Text Primary: #e0e0e0 +- Text Secondary: #a0a0b0 +- Negative: #e05555 +- Warning: #f0b060 + +## Dashboard Cards (Overview Page) + +Replace flat grid with 4 cards: +1. **Balance Card** (top-left) — Available, Pending, Immature, Stake, Total +2. **Staking Rewards Card** (top-right) — 30-day bar chart + summary stats +3. **Network Stats Card** (bottom-left) — Supply, network weight, your weight %, daily reward +4. **Recent Transactions Card** (bottom-right) — Last 5 transactions + +## Staking Chart Widget + +Custom QPainter widget: 30 vertical bars (one per day), x-axis dates, y-axis amounts. Uses PrepareRangeForStakeReport() data. Validity green bars, hover tooltips. + +## New Files +- src/qt/res/themes/dark.qss +- src/qt/res/themes/light.qss +- src/qt/thememanager.h/.cpp +- src/qt/stakingchartwidget.h/.cpp + +## Modified Files +- src/qt/forms/overviewpage.ui +- src/qt/overviewpage.cpp/.h +- src/qt/bitcoingui.cpp/.h +- src/qt/optionsdialog.cpp/.ui +- src/qt/guiconstants.h +- src/qt/bitcoin.qrc +- src/qt/Makefile.qt.include diff --git a/docs/plans/2026-02-26-bitcoin-core-port-feasibility.md b/docs/plans/2026-02-26-bitcoin-core-port-feasibility.md new file mode 100644 index 000000000..93eab29e2 --- /dev/null +++ b/docs/plans/2026-02-26-bitcoin-core-port-feasibility.md @@ -0,0 +1,151 @@ +# Feasibility Study: Porting Validity (Bitcoin Core 0.13.2 Fork) to Latest Bitcoin Core + +> **Research conducted by:** Claude Code (Opus 4.6) — February 26, 2026 +> **Conclusion:** Technically feasible but represents 12-24 months of work with 2-3 experienced C++ developers. Not recommended as primary path. + +--- + +## 1. Current State of Bitcoin Core + +**Latest version: Bitcoin Core 30.0** (released late October 2025, with patches 30.1 and 30.2). This represents approximately **9 years and 18+ major releases** since Bitcoin Core 0.13.2 (January 2017). + +Version numbering changed: 0.13 -> 0.14 -> ... -> 0.21 -> 22.0 (dropped leading zero) -> 23 -> ... -> 30. + +--- + +## 2. MAJOR Architectural Changes Between 0.13.2 and 30.0 + +### 2.1 The main.cpp Dissolution (CRITICAL) +Validity currently has `src/main.cpp` and `src/main.h` as its monolithic validation/networking file — this is where the vast majority of PoS consensus hooks live. In modern Bitcoin Core: + +- **`main.cpp` no longer exists.** Split into: + - `src/validation.cpp` / `src/validation.h` — block and transaction validation + - `src/net_processing.cpp` / `src/net_processing.h` — P2P message handling + - `src/node/blockstorage.cpp` — block storage management + - `src/txmempool.cpp` was significantly refactored + +This is the single biggest obstacle — Validity's PoS logic is scattered across ~30+ locations in `main.cpp`. + +### 2.2 ChainstateManager and CChainState (CRITICAL) +- **`ChainstateManager`** introduced (PR #17737) to manage one or two chainstates (for AssumeUTXO) +- Global `mapBlockIndex` is gone; replaced by `BlockManager` through `ChainstateManager` +- All Validity PoS code accessing `mapBlockIndex` as a global would need rewriting + +### 2.3 libbitcoinkernel Consensus Extraction (CRITICAL) +Bitcoin Core is extracting its consensus engine into `src/kernel/`. Adding PoS to this would mean extending a library specifically designed around PoW. + +### 2.4 Process Separation / Multiprocess Architecture +- `src/interfaces/` defines abstract interfaces: `interfaces::Chain`, `interfaces::Node`, `interfaces::Wallet` +- Wallet code can no longer freely call into validation code +- The staking miner (`ThreadStakeMiner`) directly accesses wallet and chain state — this tight coupling would need decomposition + +### 2.5 Build System: Autotools to CMake (CRITICAL) +- Bitcoin Core 29+ uses **CMake** exclusively (Autotools deleted entirely) +- Validity's entire build system would need rewriting + +### 2.6 C++ Standard: C++11 to C++20 (CRITICAL) +- Validity uses **C++11**; Bitcoin Core 27+ requires **C++20** +- Modern features used extensively: `std::optional`, `std::variant`, `std::filesystem`, concepts, ranges +- OpenSSL entirely removed; replaced with native crypto + +### 2.7 SegWit +- Completely changes transaction serialization (witness data) +- `CTransaction` now has witness fields; `GetHash()` vs `GetWitnessHash()` distinction +- Validity's `CTransaction` has a **`nTime` field** (PoS) and `IsCoinStake()` — must coexist with SegWit + +### 2.8 Taproot/Schnorr (BIP 340/341/342) +- SegWit v1 with Schnorr signatures +- Major script verification engine changes +- Validity uses older `secp256k1` with custom Schnorr module + +### 2.9 Descriptor Wallets +- Legacy wallets (Berkeley DB) deprecated and being removed +- New wallets use **SQLite** with output descriptors +- BDB dependency being phased out + +### 2.10 Serialization Framework Rewrite +- Old `ADD_SERIALIZE_METHODS` / `SerializationOp` with `nType`/`nVersion` replaced by `SERIALIZE_METHODS` macro +- Every serializable class would need updating + +### 2.11 P2P Protocol Changes +- BIP155 (addrv2): Tor v3 and other address types +- BIP324 (v2 P2P transport): Encrypted connections +- Compact Block Relay improvements +- Package Relay + +### 2.12 CCoins vs Coin +- 0.13.2 uses `CCoins` (all outputs of a transaction) +- Modern uses `Coin` (individual UTXO entries) +- Validity's PoS code directly creates `CCoins` objects — all UTXO access patterns change + +--- + +## 3. Validity's PoS Code That Must Be Ported + +| File | Purpose | Porting Complexity | +|------|---------|-------------------| +| `src/pos.cpp` (~257 lines) | Stake kernel hash, proof verification, stake cache | Very High | +| `src/pos.h` (~45 lines) | PoS interface declarations | High | +| `src/pow.cpp` / `src/pow.h` | Modified for dual PoW/PoS difficulty | Very High | +| `src/miner.cpp` | `ThreadStakeMiner`, PoS block assembly | Very High | +| `src/primitives/block.h` | `vchBlockSig`, `IsProofOfStake()` | Very High | +| `src/primitives/transaction.h` | `nTime` field, `IsCoinStake()` | Very High | +| `src/chain.h` | `nStakeModifier`, `BLOCK_PROOF_OF_STAKE` | Very High | +| `src/consensus/params.h` | PoS parameters, protocol versions | High | +| `src/main.cpp` | ~30+ PoS integration points | EXTREME | +| `src/chainparams.cpp` | PoS chain parameters | High | + +--- + +## 4. Estimated Scope + +### Phase 1: Foundation (3-6 months, 1-2 devs) +1. Fork Bitcoin Core 30.0 +2. Add `nTime` to `CTransaction`, `vchBlockSig` to `CBlock` +3. Add `nStakeModifier` to `CBlockIndex` +4. Extend `Consensus::Params` with PoS parameters +5. Port Scrypt hashing, set up CMake, update to C++20 + +### Phase 2: Consensus Engine (4-8 months) +1. Port `pos.cpp` to use `Coin` and `ChainstateManager` +2. Modify `validation.cpp` with all ~30+ PoS integration points +3. Modify `node/miner.cpp` for PoS block assembly +4. Integrate with `libbitcoinkernel` + +### Phase 3: Wallet and RPC (2-4 months) +1. Staking wallet integration with descriptor wallets +2. Port staking RPCs +3. Update `interfaces::Chain` for PoS + +### Phase 4: Testing (3-6 months) +1. Regression testing against existing blockchain +2. Network protocol compatibility or migration plan + +**Total estimated files: ~55-90 | New PoS code: 2,000-4,000 lines | Total delta: 10,000-20,000 lines** + +--- + +## 5. Biggest Risks + +1. **Consensus Divergence** — Every historical block must validate identically under new code +2. **Serialization Compatibility** — Custom fields must remain backward-compatible +3. **SegWit/Taproot + PoS Interaction** — Uncharted territory for coinstake transactions +4. **Global State Elimination** — All PoS functions need context objects instead of globals +5. **Maintainability** — Future Bitcoin Core releases would require careful merging + +--- + +## 6. Alternative Approaches + +| Option | Effort | Risk | Benefit | +|--------|--------|------|---------| +| **A: Full Rebase to 30.0** | 12-24 months | Extreme | Modern everything | +| **B: Incremental (0.13→0.15→...→30)** | 18-36 months | High per step | Lower risk per step | +| **C: Cherry-pick security fixes** | 2-4 months | Low | Fast, minimal risk | +| **D: Rebase onto BlackCoin More 26.x** | 1-3 months | Medium | Best effort/reward ratio | + +--- + +## 7. Bottom Line + +**Porting to Bitcoin Core 30.0 directly is roughly equivalent to building a new PoS cryptocurrency from scratch on a modern base.** The recommended path is **Option D: Rebase onto BlackCoin More 26.x** (see separate report). diff --git a/docs/plans/2026-02-26-blackcoin-rebase-feasibility.md b/docs/plans/2026-02-26-blackcoin-rebase-feasibility.md new file mode 100644 index 000000000..2d74dfbe4 --- /dev/null +++ b/docs/plans/2026-02-26-blackcoin-rebase-feasibility.md @@ -0,0 +1,193 @@ +# Feasibility Study: Upgrading Validity to BlackCoin More 26.x + +> **Research conducted by:** Claude Code (Opus 4.6) — February 26, 2026 +> **Conclusion:** RECOMMENDED PATH. BlackCoin More 26.x is the clear best choice. Estimated 1-3 months for JJ. + +--- + +## Executive Summary + +Validity is running on Bitcoin Core 0.13.2 — nearly a decade old. The most natural upgrade target is **BlackCoin More** (blackcoin-more), which shares direct lineage with Validity and has already completed the exact upgrade path from 0.13.2 to Bitcoin Core 26.2. + +--- + +## 1. Current State of Validity + +- **Base:** Bitcoin Core 0.13.2 with minor patches +- **PoS Protocol:** BlackCoin PoS v3.0 with Qtum stake cache +- **Client Version:** 13.2.1 (protocol version 99008) +- **Custom Features:** + - Dev Fund mechanism (5-of-12 multisig, block 1,655,000) + - Spread Fees Protocol (fees distributed over 1440 subsequent blocks) + - Custom stake maturity schedule (changes at ProtocolV4) + - Target block spacing 57 seconds (post-ProtocolV4; was 60) + +**Developer relationship:** JJ (Justin, Validity's main dev) has directly contributed to BlackCoin More. His commits appear in BlackCoin changelog (staking memory leak fix in v2.13.2.6, dust mitigation in v2.13.2.7). + +--- + +## 2. Candidate Upgrade Bases + +### Option A: BlackCoin More 26.x — RECOMMENDED + +**Repository:** [CoinBlack/blackcoin-more](https://github.com/CoinBlack/blackcoin-more) +**Latest Version:** v26.2.0 (released 2024-12-18) +**Bitcoin Core Base:** Bitcoin Core 26.2 +**Branches:** 2.13.2.x, 13.2, 22.x, 25.x, **26.x (stable)**, 28.x (experimental) + +**Why this is the best option:** +1. **Direct lineage.** Validity is literally a fork of BlackCoin. The PoS kernel code is nearly identical. +2. **JJ already knows the codebase.** He has contributed patches to it. +3. **Proven upgrade path.** BlackCoin More has completed 0.13.2 → 22.1 → 25.1 → 26.0 → 26.2. +4. **Wallet compatibility.** Wallets from 13.2 work in 26.2. + +**Features gained:** +- SegWit support (activating on BLK mainnet June 2025) +- Descriptor wallets with full staking support +- Multiple wallet staking simultaneously +- V2 P2P transport (encrypted connections, BIP324) +- Compact block relay (BIP152) +- Modern UTXO model (per-output instead of per-transaction) +- `optimizeutxoset` RPC for efficient staking +- Removed OpenSSL dependency +- All Bitcoin Core security fixes from 0.14 through 26.2 + +### Option B: PIVX 5.6.x — Poor Fit +- Different PoS protocol (not BlackCoin PoS v3) +- Dash lineage, not Bitcoin Core +- Includes masternodes, SHIELD privacy — unnecessary complexity + +### Option C: Particl Core 23.x — Poor Fit +- Completely different PoS protocol (PPoS) +- Privacy-focused (CT, RingCT) — unnecessary complexity + +### Option D: Qtum 25.x — Poor Fit +- Different PoS protocol (MPoS) +- EVM layer adds enormous unnecessary complexity + +--- + +## 3. Recommended Upgrade Path + +### Strategy +**Rebase Validity onto BlackCoin More 26.x**, then re-apply Validity's custom consensus rules on top. + +### What Must Be Preserved (Consensus-Critical) + +1. **Block reward schedule** — 30+ height-based reward phases in main.cpp +2. **Dev Fund mechanism** — DEV_FUND_BLOCK_HEIGHT at 1,655,000, 5-of-12 multisig +3. **ProtocolV4 rules** — 57-second block spacing, changed stake maturity +4. **Spread Fees Protocol** — Fee distribution over 1440 blocks +5. **Chain parameters** — Genesis block, network ports, magic bytes, checkpoints, DNS seeds +6. **Network protocol version** — 99008 +7. **All historical consensus transitions** — V1/V2/V3/V4 activation times/heights + +### What Changes Automatically + +| Area | Validity (0.13.2) | BlackCoin More (26.x) | +|------|------|------| +| Source layout | `main.cpp` monolith | `validation.cpp` + `net_processing.cpp` | +| UTXO model | Per-transaction (`CCoins`) | Per-output (`Coin`) | +| PoS verification | Raw `CCoins*` pointers | `CCoinsViewCache` + `Coin` objects | +| Wallet | Legacy only | Legacy + Descriptor + HD | +| P2P | V1 unencrypted | V2 encrypted (BIP324) | +| Qt | Qt5 only | Qt5 (Qt6 in 28.x) | +| OpenSSL | Required | Removed | +| SegWit | Removed | Supported | +| Compact blocks | Disabled | Enabled (BIP152) | + +### Files Requiring Validity-Specific Modifications + +| File (in 26.x layout) | Work Required | Difficulty | +|------|------|------| +| `src/consensus/params.h` | Port ProtocolV4, Dev Fund, reward params, Spread Fees | Medium | +| `src/validation.cpp` | Port reward logic, Dev Fund output, Spread Fees, maturity | High | +| `src/chainparams.cpp` | All Validity chain params | Medium | +| `src/pos.cpp` / `src/pos.h` | Minimal — same PoS kernel. Port V4 maturity | Low | +| `src/pow.cpp` | Port ProtocolV4 difficulty changes | Low-Medium | +| `src/version.h` | Validity protocol version | Trivial | +| `src/qt/*` | Validity branding + UI theme (reuse dark theme work) | Low | + +### Estimated Effort + +- **JJ (knows both codebases):** 2-4 weeks +- **Experienced C++ blockchain developer:** 4-8 weeks +- **Testing and QA:** Additional 2-4 weeks +- **Total: 1-3 months** + +### Step-by-Step Migration Plan + +1. **Fork BlackCoin More 26.x** as new Validity base +2. **Rename/rebrand** all BlackCoin references to Validity +3. **Port chainparams.cpp** — genesis, magic, seeds, checkpoints, ports +4. **Port consensus rules** from old `main.cpp` to new `validation.cpp`: + - Block reward schedule (30+ phases) + - Dev Fund output logic + - Spread Fees Protocol + - ProtocolV4 activation +5. **Port PoS customizations** to `pos.cpp` (stake maturity at V4) +6. **Port difficulty adjustments** to `pow.cpp` +7. **Port version/protocol identifiers** +8. **Test on regtest** — verify genesis, block creation, staking, rewards +9. **Test on testnet** — extended staking and chain validation +10. **Full chain sync test** — download and validate entire Validity blockchain +11. **Port UI customizations** (dark theme, dashboard — reuse current work) +12. **Release candidate testing** with community + +--- + +## 4. Risks and Challenges + +### 4.1 The main.cpp Split +Every Validity consensus change in `main.cpp` must be correctly placed in the new `validation.cpp` + `net_processing.cpp` architecture. BlackCoin More has already done this for their consensus rules, which provides a template. + +### 4.2 UTXO Model Change +Old `CCoins` pointers replaced by `Coin` objects. BlackCoin More 26.x has already solved this. + +### 4.3 SegWit Activation +BlackCoin More 26.x adds SegWit. Validity can keep it present but never activated if desired, or plan a hard fork to enable it. + +### 4.4 Transaction Timestamp +BlackCoin More's 13.2.0 removed transaction timestamps. The new code uses `coinPrev.nTime` with fallback to `blockFrom->nTime`. Must be handled carefully. + +### 4.5 Backward Compatibility +Users will need to resync from scratch (database incompatibilities). Wallet files should be compatible. + +### 4.6 SmartChain Integration +Any changes to RPC interfaces or transaction formats could break SmartChain. Needs thorough testing. + +--- + +## 5. What Validity Gains + +### Security +- 8+ years of Bitcoin Core security patches +- Removal of OpenSSL +- Modern memory management (no raw pointer leaks) +- Header spam / fake stake vulnerability fixes +- Rolling checkpoints + +### Performance +- ~30-40% faster initial block sync +- 10-20% less memory usage +- Compact block relay (BIP152) + +### Features +- Descriptor wallets with HD key derivation +- SegWit (optional activation) +- Encrypted P2P +- Multiple wallet staking +- `optimizeutxoset` RPC +- Modern RPC interface + +--- + +## 6. Conclusion + +**BlackCoin More 26.x is the overwhelmingly best choice.** The alternatives all use fundamentally different PoS protocols. The BlackCoin More path works because: +1. Identical PoS kernel math +2. JJ has existing relationship with BlackCoin devs +3. BlackCoin More has already completed the exact migration +4. Validity's custom features are isolated to ~5 files + +**Decision for JJ:** Target 26.x (stable, proven) now. Upgrade to 28.x later. diff --git a/docs/plans/2026-02-26-phase2-implementation-plan.md b/docs/plans/2026-02-26-phase2-implementation-plan.md new file mode 100644 index 000000000..7aa286902 --- /dev/null +++ b/docs/plans/2026-02-26-phase2-implementation-plan.md @@ -0,0 +1,109 @@ +# Phase 2: Full Visual Overhaul + Remaining Features + +**Date:** 2026-02-26 +**Constraint:** NO consensus/fork changes. UI/client-side ONLY. +**Goal:** Match validitytech.com aesthetic, implement all remaining recommendations, security audit. + +## Priority 1: Visual Overhaul (Match validitytech.com) + +### 1A. Updated Dark Theme (dark.qss rewrite) +Redesign the dark theme to match the website's premium, cosmic aesthetic: +- **Background gradient:** Deep navy #0a0a1a to dark purple #1a1030 +- **Card surfaces:** Semi-transparent dark with subtle border glow +- **Accent color:** Keep #43b581 (Validity green) — matches website exactly +- **Typography:** Larger headings, more letter-spacing, bolder weights +- **Buttons:** Pill-shaped with green accent, hover glow effects +- **Progress bars:** Green gradient with glow +- **Inputs:** Dark with subtle border, green focus ring +- **Overall vibe:** Premium, futuristic, cryptocurrency-native + +### 1B. Custom Sidebar Navigation +Replace the default toolbar/tab bar with a modern vertical sidebar: +- Validity logo at top +- Icon + text navigation items (Overview, Send, Receive, Transactions, Addresses) +- Active state with green accent bar +- Collapsible to icon-only mode +- Staking status indicator in sidebar footer + +### 1C. Updated Splash Screen +- Dark background with Validity logo +- Animated loading progress +- Step descriptions ("Loading block index...", "Verifying blocks...") + +### 1D. Improved Window Chrome +- Custom title bar styling (within QSS limits) +- Status bar with staking indicator, connection count, block height +- Unified dark appearance + +## Priority 2: Security Audit + +### 2A. Client-Side Code Review +- RPC command input validation +- Wallet encryption implementation review +- Key storage and memory handling +- Network message parsing safety +- Serialization buffer overflow checks +- User input sanitization (addresses, amounts, labels) +- QSS/theme loading safety +- File permission handling +- Memory wiping for sensitive data +- Review `#include "rpc/blockchain.cpp"` pattern + +## Priority 3: UI Page Improvements + +### 3A. Send Page Modernization +- Modern form layout with card styling +- Amount input with unit selector +- Fee estimation display +- Transaction preview before send +- Success/failure feedback + +### 3B. Receive Page Modernization +- QR code display with dark theme support +- Copy address button with visual feedback +- Request amount field +- Generated URI display + +### 3C. Transaction History +- Date range filter +- Amount filter +- Type filter (sent, received, staked) +- Search by address/label +- Better transaction detail panel +- Export to CSV button + +### 3D. Address Book +- Search functionality +- Category/label system +- Quick copy with feedback +- Contact card layout + +## Priority 4: Medium Features + +### 4A. Options Dialog Modernization +- Categorized settings with sidebar +- Theme picker section +- Network settings with visual status +- Wallet settings (backup path, etc.) + +### 4B. System Tray Improvements +- Balance in tooltip +- Staking status icon +- Quick actions menu + +### 4C. Notification System +- Desktop notifications for staking rewards +- Transaction received alerts +- Notification preferences in settings + +## Implementation Order + +1. dark.qss rewrite (visual overhaul foundation) +2. Sidebar navigation widget +3. Send/Receive page styling +4. Transaction history improvements +5. Address book improvements +6. Options dialog +7. Splash screen +8. Security audit report +9. System tray + notifications diff --git a/docs/plans/2026-02-26-pos-consensus-inventory.md b/docs/plans/2026-02-26-pos-consensus-inventory.md new file mode 100644 index 000000000..6f4180e87 --- /dev/null +++ b/docs/plans/2026-02-26-pos-consensus-inventory.md @@ -0,0 +1,216 @@ +# Validity Wallet: Proof of Stake Consensus Code Inventory + +> **Research conducted by:** Claude Code (Opus 4.6) — February 26, 2026 +> **Purpose:** Complete inventory of all consensus-touching PoS code for porting reference + +--- + +## Executive Summary + +Validity contains **~1,670 lines of custom PoS consensus code** across **12 core files**. The implementation is BlackCoin PoS v3 with Qtum stake cache, plus Validity-specific reward schedule and Dev Fund. **Zero blockchain-incompatible changes** in our UI work — all modifications are PoS-specific and don't touch Bitcoin's base consensus. + +--- + +## 1. Core PoS Kernel Files + +### 1.1 src/pos.h (46 lines) +- `ComputeStakeModifier()` — Hash modifier computation +- `CheckCoinStakeTimestamp()` — Protocol-specific timestamp validation +- `CheckStakeKernelHash()` — Core kernel hash against target +- `CheckProofOfStake()` — Main PoS proof validation +- `CheckKernel()` — Two overloads with stake cache support (Qtum) +- `CacheKernel()` — Stake cache implementation +- `VerifySignature()` — Coinstake signature verification + +### 1.2 src/pos.cpp (258 lines) + +**ComputeStakeModifier()** (lines 28-36) +```cpp +hash(kernel || pindexPrev->nStakeModifier) +``` + +**CheckCoinStakeTimestamp()** (lines 39-46) +- V2: block time == tx time, with alignment mask `(nTime & nStakeTimestampMask) == 0` +- V1: simple equality check + +**CheckStakeKernelHash()** (lines 73-119) — BlackCoin Kernel Protocol v3 +``` +hash(nStakeModifier || txPrev.nTime || prevout.hash || prevout.n || nTime) < bnTarget * nWeight +``` + +**CheckProofOfStake()** (lines 122-163) +- Protocol-specific maturity: V4: 120 blocks, V3: 60 blocks, V1-V2: 6 hours + +**CheckKernel()** (lines 179-231) — Direct + cache-enabled overloads + +**CacheKernel()** (lines 233-257) — Populates `CStakeCache` map + +--- + +## 2. Consensus Parameters + +### 2.1 src/consensus/params.h (138 lines) + +**Protocol Version Checks:** +```cpp +IsProtocolV1RetargetingFixed(nTime) // Always true +IsProtocolV2(nTime) // nTime > nGenesisBlockTime +IsProtocolV3(nTime) // nTime > 1461851161 +IsAvgFeeProtocol(nTime) // nTime > 1470919889 +IsProtocolV4(nHeight) // nHeight >= 1655000 (DEV_FUND_BLOCK_HEIGHT) +IsBlockDevFund(nHeight) // nHeight % 10080 == 0 && IsProtocolV4 +``` + +**PoS Parameters:** +```cpp +nStakeTimestampMask = 0xf // Alignment mask +nStakeMaturity: V4=120, V3=60 // Variable block maturity +nStakeMinAge = 6 * 60 * 60 // Pre-V3: 6 hours +GetTargetSpacing: V4=57s, else=60s +nLastPOWBlock = 20160 // Last PoW-allowed block +``` + +### 2.2 src/chainparams.cpp (Mainnet) +```cpp +posLimit = 0x0000ffffffffffff... +posLimitV2 = 0x00000fffffffffff... +nTargetTimespan = 60s +nTargetTimespanNEW = 15 * 60s +nProtocolV3Time = 1461851161 +nStakeTimestampMask = 0xf +nCoinbaseMaturity = 60 +nStakeMinAge = 21600s +``` +Genesis Block: 1431857735 (May 17, 2015) + +--- + +## 3. Block Validation & Difficulty + +### 3.1 src/pow.cpp (119 lines) + +**GetNextTargetRequired()** — Entry point for difficulty +- Finds last block of same type (PoS/PoW) +- V3+: Caps actual spacing at `nTargetSpacing * 10` +- Dynamic interval: Before block 48: 1 interval, After: 15 intervals + +### 3.2 src/main.cpp — Block Validation + +**Key integration points (~45 lines):** +- Stake modifier computation (line 2728) +- Difficulty check (line 2731) +- Coinstake kernel validation (line 2749) +- Kernel maturity check (line 2744) +- Coinstake maturity enforcement (lines 2331-2339) + +--- + +## 4. Transaction Types + +### src/primitives/transaction.h (8 lines) +```cpp +bool IsCoinStake() const { + return (vin.size() > 0 && (!vin[0].prevout.IsNull()) && + vout.size() >= 2 && vout[0].IsEmpty()); +} +``` + +### src/primitives/block.h (8 lines) +```cpp +bool IsProofOfStake() const { + return (vtx.size() > 1 && vtx[1].IsCoinStake()); +} +``` + +--- + +## 5. Staking Miner & Block Assembly + +### src/wallet/wallet.cpp — CreateCoinStake() (lines 688-888, ~200 lines) +1. Lock wallet, gather unspent coins +2. Iterate with descending value preference +3. Call `CheckKernel()` with stake cache +4. Extract script pubkey, get private key +5. Build coinstake inputs/outputs +6. Sign all inputs with `SIGHASH_ALL` +7. Validate size < MAX_STANDARD_TX_SIZE + +### src/miner.cpp — Block Template (~150 lines) +- `GetProofOfWorkReward()` — Fixed 10,000 VAL per PoW block +- `CreateNewBlock()` — Handles both PoW and PoS assembly +- `ThreadStakeMiner()` — Main staking thread + +--- + +## 6. Reward Functions + +### getFixedStakeSubsidy() (~100 lines) +Complex schedule with 30+ phases: +- Blocks 0-2779: 0 VAL +- Blocks 2880-30240: 25 VAL +- Blocks 30241-337999: 5 VAL +- Blocks 338000+: Graduated decline (4.5, 4, 3.5, ..., 0.22 VAL) +- Blocks 1655000+ (V4): Dev fund tiers + +### GetProofOfStakeSubsidy() (~25 lines) +```cpp +nSubsidy = getFixedStakeSubsidy(nHeight); +// Add running average fees (protocol version dependent) +if (nHeight >= AVG_FEE_START_BLOCK_V2) + return nSubsidy + GetRunningFee(pindexPrev, nFees); +``` + +### GetDevSubsidy() (~70 lines) +- V4+ only (block 1,655,000+) +- Weekly distribution (every 10,080 blocks) +- 25+ yearly declining tiers from 264 VAL to 0 + +### GetRunningFee() (~40 lines) +- Rolling 10-block average of transaction fees +- Cache to avoid repeated disk lookups + +--- + +## 7. Summary Table + +| File | Lines | Component | +|------|-------|-----------| +| pos.h | 46 | Headers & declarations | +| pos.cpp | 258 | Kernel validation, coinstake proof | +| consensus/params.h | 30 | Protocol versions, PoS params | +| chainparams.cpp | 20 | Mainnet PoS config | +| pow.cpp | 85 | Difficulty retargeting (PoS-specific) | +| main.cpp (validation) | 100 | Block validation, stake modifier | +| primitives/transaction.h | 8 | IsCoinStake() | +| primitives/block.h | 8 | IsProofOfStake() | +| wallet/wallet.cpp | 200 | CreateCoinStake() | +| miner.cpp | 150 | Block template, rewards | +| main.cpp (rewards) | 270 | Subsidy, fees, dev fund | +| chain.h | 2 | nStakeModifier field | +| **TOTAL** | **~1,177** | **Custom PoS consensus** | + +--- + +## 8. Portability Assessment + +### Must Be Ported for PoS: +1. pos.h/pos.cpp (100%) — Core kernel validation +2. Consensus params (100%) — All PoS difficulty parameters +3. Block/Transaction extensions (100%) — IsProofOfStake(), IsCoinStake(), nStakeModifier +4. Difficulty retargeting (100%) — GetNextTargetRequired() with V2/V3/V4 logic +5. Reward schedules (90%) — Custom to Validity +6. CreateCoinStake() (100%) — Staking miner logic +7. Block validation hooks (100%) — ConnectBlock() PoS checks + +### Reusable from Bitcoin Core: +- Standard script validation +- Transaction serialization +- Network protocol +- Mempool management +- All PoW components (unchanged) + +### Key Dependencies: +- All PoS functions depend on `Params().GetConsensus()` for config +- Stake validation needs `mapBlockIndex` and transaction lookup +- Block assembly needs `CBlockIndex` with nStakeModifier +- Difficulty needs access to previous block timestamps/targets diff --git a/docs/plans/2026-02-26-security-audit-report.md b/docs/plans/2026-02-26-security-audit-report.md new file mode 100644 index 000000000..ccc61f317 --- /dev/null +++ b/docs/plans/2026-02-26-security-audit-report.md @@ -0,0 +1,84 @@ +# Validity Wallet Security Audit Report + +**Date:** 2026-02-26 +**Scope:** Full codebase — wallet encryption, key management, serialization, network, RPC, Qt UI +**Constraint:** No consensus/blockchain changes (UI and client-side only) + +--- + +## Summary + +15 findings across the codebase. 2 critical/medium issues were fixed immediately. The remaining findings are documented below for future work. + +| Severity | Count | Fixed | +|----------|-------|-------| +| CRITICAL | 1 | 1 | +| HIGH | 2 | 0 | +| MEDIUM | 4 | 1 | +| LOW | 2 | 0 | +| INFO | 6 | — | + +--- + +## Fixed Issues + +### 1. CRITICAL — CExtKey::Unserialize Buffer Overflow +**File:** `src/key.h` (CExtKey::Unserialize) +**Issue:** `ReadCompactSize(s)` returns an attacker-controlled length that was used directly in `s.read()` without validation. A crafted serialized extended key could cause a stack buffer overflow writing into a fixed `BIP32_EXTKEY_SIZE` (74-byte) array. +**Fix:** Added length validation: `if (len != BIP32_EXTKEY_SIZE) throw std::runtime_error("Invalid extended key size\n");` +**Note:** CExtPubKey::Unserialize already had this check. CExtKey was missing it. + +### 2. MEDIUM — UnlockStaking Case Fall-Through +**File:** `src/qt/askpassphrasedialog.cpp` +**Issue:** The `UnlockStaking` case in the constructor's switch statement fell through into the `Unlock` case, causing the staking checkbox to be immediately unchecked by the Unlock case's `setChecked(false)`. This made the "Unlock for staking only" feature unreliable. +**Fix:** Added `break;` after UnlockStaking case and duplicated the necessary UI setup for the Unlock case independently. + +--- + +## Open Issues (Not Fixed — Require Deeper Changes) + +### 3. HIGH — #include of .cpp File +**File:** `src/rpc/misc.cpp` +**Issue:** Contains `#include "rpc/blockchain.cpp"` which includes an entire implementation file. This is fragile — any change to blockchain.cpp could break misc.cpp in unexpected ways. +**Recommendation:** Extract shared functions into a proper header file (`rpc/blockchain.h`) and include that instead. + +### 4. HIGH — MSVC memory_cleanse May Be No-Op +**File:** `src/support/cleanse.cpp` +**Issue:** `memory_cleanse()` uses `OPENSSL_cleanse()` which may be optimized away by MSVC in release builds. On Windows, `SecureZeroMemory()` should be used instead to guarantee the memory is actually zeroed. +**Recommendation:** Add `#ifdef _WIN32` / `SecureZeroMemory()` path, or use a volatile function pointer pattern to prevent optimization. + +### 5. MEDIUM — Passphrase in Non-Secure Memory +**File:** `src/qt/askpassphrasedialog.cpp` (accept method) +**Issue:** `ui->passEdit1->text().toStdString()` creates temporary `std::string` objects in non-mlock'd heap memory. The passphrase briefly exists outside secure allocator protection before being assigned to `SecureString`. +**Recommendation:** Consider using `QLineEdit::text().toUtf8().constData()` to reduce copies, or implement a custom QLineEdit that stores text in secure memory. + +### 6. MEDIUM — Division by Zero in Staking Calculations +**File:** `src/qt/overviewpage.cpp` +**Issue:** Staking reward time estimation divides by network weight without checking for zero. If `nNetworkWeight` is 0 (e.g., during initial sync), this causes undefined behavior. +**Recommendation:** Add `if (nNetworkWeight > 0)` guard before division. + +### 7. MEDIUM — pwalletMain Null Dereference Risk +**File:** `src/qt/overviewpage.cpp` +**Issue:** `pwalletMain` is accessed without null checks in several staking-related functions. During early startup or if wallet is disabled, this could cause a crash. +**Recommendation:** Add null checks before all `pwalletMain` accesses. + +### 8. LOW — ReadVarInt Unbounded Loop +**File:** `src/serialize.h` +**Issue:** `ReadVarInt()` loops reading bytes until it finds one without the continuation bit. A malformed stream could cause excessive looping. +**Recommendation:** Add a maximum iteration count (e.g., 10 for uint64_t). + +### 9. LOW — RPC Cookie Not Wiped on Shutdown +**File:** `src/rpc/protocol.cpp` +**Issue:** The `.cookie` authentication file is deleted on shutdown but its contents are not overwritten first. The data may remain on disk in unallocated sectors. +**Recommendation:** Overwrite cookie file contents before deletion. + +--- + +## Informational (No Action Required) + +1. **Theme loading is safe** — QSS loaded from Qt resource system (compiled into binary), not from external files. No injection risk. +2. **Serialization limits work** — `ReadCompactSize()` enforces `MAX_SIZE` limit (0x2000000), preventing memory exhaustion from oversized allocations. +3. **Network peer validation** — Peer addresses are validated before use; no IP spoofing risk in the address management layer. +4. **File permissions** — Wallet file permissions are set correctly on creation (0600 on Unix). +5. **Single KDF method** — Only one key derivation function is supported for wallet encryption. Adding scrypt or Argon2 would be a future improvement but is not a vulnerability. +6. **Qt resource paths** — All resource paths use `:/` prefix (compiled resources), not filesystem paths. Safe from path traversal. diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 7591ef43e..61ade5db8 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -158,7 +158,12 @@ QT_MOC_CPP = \ qt/moc_utilitydialog.cpp \ qt/moc_walletframe.cpp \ qt/moc_walletmodel.cpp \ - qt/moc_walletview.cpp + qt/moc_walletview.cpp \ + qt/moc_thememanager.cpp \ + qt/moc_stakingchartwidget.cpp \ + qt/moc_backupwizard.cpp \ + qt/moc_peermapwidget.cpp \ + qt/moc_carddragdrop.cpp BITCOIN_MM = \ qt/macdockiconhandler.mm \ @@ -220,6 +225,12 @@ BITCOIN_QT_H = \ qt/sendcoinsentry.h \ qt/signverifymessagedialog.h \ qt/splashscreen.h \ + qt/stakingchartwidget.h \ + qt/thememanager.h \ + qt/backupwizard.h \ + qt/peermapwidget.h \ + qt/carddragdrop.h \ + qt/geoip.h \ qt/trafficgraphwidget.h \ qt/transactiondesc.h \ qt/transactiondescdialog.h \ @@ -310,11 +321,14 @@ BITCOIN_QT_BASE_CPP = \ qt/peertablemodel.cpp \ qt/platformstyle.cpp \ qt/qvalidatedlineedit.cpp \ + qt/thememanager.cpp \ qt/qvaluecombobox.cpp \ qt/rpcconsole.cpp \ qt/splashscreen.cpp \ qt/trafficgraphwidget.cpp \ - qt/utilitydialog.cpp + qt/utilitydialog.cpp \ + qt/peermapwidget.cpp \ + qt/geoip.cpp BITCOIN_QT_WINDOWS_CPP = qt/winshutdownmonitor.cpp @@ -327,6 +341,9 @@ BITCOIN_QT_WALLET_CPP = \ qt/editaddressdialog.cpp \ qt/openuridialog.cpp \ qt/overviewpage.cpp \ + qt/stakingchartwidget.cpp \ + qt/backupwizard.cpp \ + qt/carddragdrop.cpp \ qt/paymentrequestplus.cpp \ qt/paymentserver.cpp \ qt/receivecoinsdialog.cpp \ @@ -354,7 +371,11 @@ if ENABLE_WALLET BITCOIN_QT_CPP += $(BITCOIN_QT_WALLET_CPP) endif -RES_IMAGES = +RES_THEMES = \ + qt/res/themes/dark.qss \ + qt/res/themes/light.qss + +RES_IMAGES = RES_MOVIES = $(wildcard $(srcdir)/qt/res/movies/spinner-*.png) @@ -369,7 +390,7 @@ qt_libbitcoinqt_a_CXXFLAGS = $(AM_CXXFLAGS) $(QT_PIE_FLAGS) qt_libbitcoinqt_a_OBJCXXFLAGS = $(AM_OBJCXXFLAGS) $(QT_PIE_FLAGS) qt_libbitcoinqt_a_SOURCES = $(BITCOIN_QT_CPP) $(BITCOIN_QT_H) $(QT_FORMS_UI) \ - $(QT_QRC) $(QT_QRC_LOCALE) $(QT_TS) $(PROTOBUF_PROTO) $(RES_ICONS) $(RES_IMAGES) $(RES_MOVIES) + $(QT_QRC) $(QT_QRC_LOCALE) $(QT_TS) $(PROTOBUF_PROTO) $(RES_ICONS) $(RES_IMAGES) $(RES_MOVIES) $(RES_THEMES) nodist_qt_libbitcoinqt_a_SOURCES = $(QT_MOC_CPP) $(QT_MOC) $(PROTOBUF_CC) \ $(PROTOBUF_H) $(QT_QRC_CPP) $(QT_QRC_LOCALE_CPP) diff --git a/src/key.h b/src/key.h index b4f48d59f..9f5efb3a0 100644 --- a/src/key.h +++ b/src/key.h @@ -182,6 +182,8 @@ struct CExtKey { void Unserialize(Stream& s, int nType, int nVersion) { unsigned int len = ::ReadCompactSize(s); + if (len != BIP32_EXTKEY_SIZE) + throw std::runtime_error("Invalid extended key size\n"); unsigned char code[BIP32_EXTKEY_SIZE]; s.read((char *)&code[0], len); Decode(code); diff --git a/src/qt/addressbookpage.cpp b/src/qt/addressbookpage.cpp index 727956c88..1e6ab0025 100644 --- a/src/qt/addressbookpage.cpp +++ b/src/qt/addressbookpage.cpp @@ -67,11 +67,11 @@ AddressBookPage::AddressBookPage(const PlatformStyle *platformStyle, Mode mode, switch(tab) { case SendingTab: - ui->labelExplanation->setText(tr("These are your Bitcoin addresses for sending payments. Always check the amount and the receiving address before sending coins.")); + ui->labelExplanation->setText(tr("These are your Validity addresses for sending payments. Always check the amount and the receiving address before sending coins.")); ui->deleteAddress->setVisible(true); break; case ReceivingTab: - ui->labelExplanation->setText(tr("These are your Bitcoin addresses for receiving payments. It is recommended to use a new receiving address for each transaction.")); + ui->labelExplanation->setText(tr("These are your Validity addresses for receiving payments. It is recommended to use a new receiving address for each transaction.")); ui->deleteAddress->setVisible(false); break; } diff --git a/src/qt/askpassphrasedialog.cpp b/src/qt/askpassphrasedialog.cpp index e08155560..c435e472d 100644 --- a/src/qt/askpassphrasedialog.cpp +++ b/src/qt/askpassphrasedialog.cpp @@ -55,9 +55,16 @@ AskPassphraseDialog::AskPassphraseDialog(Mode mode, QWidget *parent) : case UnlockStaking: ui->stakingCheckBox->setChecked(true); ui->stakingCheckBox->show(); + ui->warningLabel->setText(tr("This operation needs your wallet passphrase to unlock the wallet.")); + ui->passLabel2->hide(); + ui->passEdit2->hide(); + ui->passLabel3->hide(); + ui->passEdit3->hide(); + setWindowTitle(tr("Unlock wallet for staking")); + break; case Unlock: // Ask passphrase - ui->stakingCheckBox->setChecked(false); - ui->stakingCheckBox->show(); + ui->stakingCheckBox->setChecked(false); + ui->stakingCheckBox->show(); ui->warningLabel->setText(tr("This operation needs your wallet passphrase to unlock the wallet.")); ui->passLabel2->hide(); ui->passEdit2->hide(); @@ -105,11 +112,19 @@ void AskPassphraseDialog::accept() oldpass.reserve(MAX_PASSPHRASE_SIZE); newpass1.reserve(MAX_PASSPHRASE_SIZE); newpass2.reserve(MAX_PASSPHRASE_SIZE); - // TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string) - // Alternately, find a way to make this input mlock()'d to begin with. - oldpass.assign(ui->passEdit1->text().toStdString().c_str()); - newpass1.assign(ui->passEdit2->text().toStdString().c_str()); - newpass2.assign(ui->passEdit3->text().toStdString().c_str()); + // Convert passphrase via QByteArray and zero immediately to avoid + // leaving plaintext on the ordinary (non-mlock'd) heap. + QByteArray raw1 = ui->passEdit1->text().toUtf8(); + oldpass.assign(raw1.constData(), raw1.size()); + raw1.fill(0); + + QByteArray raw2 = ui->passEdit2->text().toUtf8(); + newpass1.assign(raw2.constData(), raw2.size()); + raw2.fill(0); + + QByteArray raw3 = ui->passEdit3->text().toUtf8(); + newpass2.assign(raw3.constData(), raw3.size()); + raw3.fill(0); secureClearPassFields(); diff --git a/src/qt/backupwizard.cpp b/src/qt/backupwizard.cpp new file mode 100644 index 000000000..6a9324371 --- /dev/null +++ b/src/qt/backupwizard.cpp @@ -0,0 +1,295 @@ +// Copyright (c) 2026 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "backupwizard.h" +#include "walletmodel.h" +#include "guiutil.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BackupWizard::BackupWizard(WalletModel *_walletModel, QWidget *parent) + : QDialog(parent), walletModel(_walletModel), currentPage(0), backupSuccess(false) +{ + setWindowTitle(tr("Backup Wallet")); + setMinimumSize(520, 420); + + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + // Stacked widget for pages + stack = new QStackedWidget; + mainLayout->addWidget(stack, 1); + + // Navigation buttons + QHBoxLayout *buttonLayout = new QHBoxLayout; + backButton = new QPushButton(tr("< Back")); + nextButton = new QPushButton(tr("Next >")); + cancelButton = new QPushButton(tr("Cancel")); + + buttonLayout->addWidget(backButton); + buttonLayout->addStretch(); + buttonLayout->addWidget(cancelButton); + buttonLayout->addWidget(nextButton); + mainLayout->addLayout(buttonLayout); + + connect(backButton, SIGNAL(clicked()), this, SLOT(prevPage())); + connect(nextButton, SIGNAL(clicked()), this, SLOT(nextPage())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); + + // Create pages + setupIntroPage(); + setupLocationPage(); + setupProgressPage(); + setupCompletePage(); + + updateButtons(); +} + +void BackupWizard::setupIntroPage() +{ + QWidget *page = new QWidget; + QVBoxLayout *layout = new QVBoxLayout(page); + + QLabel *title = new QLabel(tr("Backup Your Wallet")); + title->setStyleSheet("font-size: 18px; font-weight: bold; color: #43b581;"); + + QLabel *subtitle = new QLabel(tr("Create a secure copy of your wallet data.")); + subtitle->setStyleSheet("color: #8888a8; font-size: 13px; margin-bottom: 16px;"); + + QLabel *body = new QLabel(tr( + "This wizard will help you create a backup of your wallet.\n\n" + "Your wallet file contains your private keys, transaction history, " + "and address labels. If this file is lost or corrupted, you will " + "lose access to your funds.\n\n" + "It is recommended to:\n" + " \342\200\242 Back up to an external drive or cloud storage\n" + " \342\200\242 Keep multiple copies in different locations\n" + " \342\200\242 Back up after every 100 transactions\n\n" + "Click Next to choose a backup location." + )); + body->setWordWrap(true); + body->setStyleSheet("font-size: 13px; line-height: 1.5;"); + + layout->addWidget(title); + layout->addWidget(subtitle); + layout->addWidget(body); + layout->addStretch(); + + stack->addWidget(page); +} + +void BackupWizard::setupLocationPage() +{ + QWidget *page = new QWidget; + QVBoxLayout *layout = new QVBoxLayout(page); + + QLabel *title = new QLabel(tr("Choose Backup Location")); + title->setStyleSheet("font-size: 18px; font-weight: bold; color: #43b581;"); + + QLabel *subtitle = new QLabel(tr("Select where to save the backup file.")); + subtitle->setStyleSheet("color: #8888a8; font-size: 13px; margin-bottom: 16px;"); + + pathEdit = new QLineEdit; + pathEdit->setPlaceholderText(tr("Click Browse to choose a location...")); + pathEdit->setReadOnly(true); + + QPushButton *browseButton = new QPushButton(tr("Browse...")); + connect(browseButton, SIGNAL(clicked()), this, SLOT(browseClicked())); + + QHBoxLayout *pathLayout = new QHBoxLayout; + pathLayout->addWidget(pathEdit); + pathLayout->addWidget(browseButton); + + QLabel *hint = new QLabel(tr( + "Tip: Choose an external drive, USB stick, or cloud-synced folder " + "for maximum safety." + )); + hint->setWordWrap(true); + hint->setStyleSheet("color: #8888a8; font-size: 11px; margin-top: 16px;"); + + // Default path suggestion + QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + if (!defaultPath.isEmpty()) { + pathEdit->setText(defaultPath + "/validity-wallet-backup.dat"); + } + + layout->addWidget(title); + layout->addWidget(subtitle); + layout->addLayout(pathLayout); + layout->addWidget(hint); + layout->addStretch(); + + stack->addWidget(page); +} + +void BackupWizard::setupProgressPage() +{ + QWidget *page = new QWidget; + QVBoxLayout *layout = new QVBoxLayout(page); + + QLabel *title = new QLabel(tr("Creating Backup")); + title->setStyleSheet("font-size: 18px; font-weight: bold; color: #43b581;"); + + statusLabel = new QLabel(tr("Preparing backup...")); + statusLabel->setAlignment(Qt::AlignCenter); + statusLabel->setWordWrap(true); + statusLabel->setStyleSheet("font-size: 14px;"); + + progressBar = new QProgressBar; + progressBar->setRange(0, 0); // indeterminate + + layout->addWidget(title); + layout->addStretch(); + layout->addWidget(statusLabel); + layout->addSpacing(12); + layout->addWidget(progressBar); + layout->addStretch(); + + stack->addWidget(page); +} + +void BackupWizard::setupCompletePage() +{ + QWidget *page = new QWidget; + QVBoxLayout *layout = new QVBoxLayout(page); + + QLabel *title = new QLabel(tr("Backup Complete")); + title->setStyleSheet("font-size: 18px; font-weight: bold; color: #43b581;"); + + resultLabel = new QLabel; + resultLabel->setWordWrap(true); + resultLabel->setStyleSheet("font-size: 13px;"); + + pathResultLabel = new QLabel; + pathResultLabel->setWordWrap(true); + pathResultLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + + layout->addWidget(title); + layout->addSpacing(16); + layout->addWidget(resultLabel); + layout->addSpacing(12); + layout->addWidget(pathResultLabel); + layout->addStretch(); + + stack->addWidget(page); +} + +void BackupWizard::updateButtons() +{ + backButton->setEnabled(currentPage > 0 && currentPage < 3); + backButton->setVisible(currentPage > 0 && currentPage < 3); + + if (currentPage == 3) { + nextButton->setText(tr("Done")); + } else { + nextButton->setText(tr("Next >")); + } + + cancelButton->setVisible(currentPage < 2); +} + +void BackupWizard::nextPage() +{ + if (currentPage == 1 && pathEdit->text().isEmpty()) { + return; // Don't proceed without a path + } + + if (currentPage == 1) { + // Moving to progress page - perform backup + currentPage = 2; + stack->setCurrentIndex(currentPage); + updateButtons(); + QApplication::processEvents(); + performBackup(); + return; + } + + if (currentPage == 3) { + accept(); // Done + return; + } + + currentPage++; + stack->setCurrentIndex(currentPage); + updateButtons(); +} + +void BackupWizard::prevPage() +{ + if (currentPage > 0) { + currentPage--; + stack->setCurrentIndex(currentPage); + updateButtons(); + } +} + +void BackupWizard::browseClicked() +{ + QString filename = QFileDialog::getSaveFileName(this, + tr("Backup Wallet"), pathEdit->text(), + tr("Wallet Data (*.dat);;All Files (*)")); + + if (!filename.isEmpty()) { + pathEdit->setText(filename); + } +} + +void BackupWizard::performBackup() +{ + QString backupPath = pathEdit->text(); + statusLabel->setText(tr("Backing up wallet to:\n%1").arg(backupPath)); + progressBar->setRange(0, 0); + QApplication::processEvents(); + + // Perform backup (synchronous - wallet.dat is small) + backupSuccess = walletModel->backupWallet(backupPath); + + if (backupSuccess) { + QFileInfo fi(backupPath); + statusLabel->setText(tr("Backup successful!")); + statusLabel->setStyleSheet("color: #43b581; font-weight: bold; font-size: 16px;"); + progressBar->setRange(0, 100); + progressBar->setValue(100); + } else { + statusLabel->setText(tr("Backup failed!\n\nCould not write to:\n%1").arg(backupPath)); + statusLabel->setStyleSheet("color: #e05555; font-weight: bold; font-size: 14px;"); + progressBar->setRange(0, 100); + progressBar->setValue(0); + } + + // Auto-advance to complete page + currentPage = 3; + stack->setCurrentIndex(currentPage); + + if (backupSuccess) { + resultLabel->setText(tr( + "Your wallet has been successfully backed up!\n\n" + "Remember to:\n" + " \342\200\242 Store this backup in a safe location\n" + " \342\200\242 Create backups regularly\n" + " \342\200\242 Never share your wallet file with anyone" + )); + resultLabel->setStyleSheet("font-size: 13px;"); + pathResultLabel->setText(tr("Backup saved to:\n%1").arg(backupPath)); + pathResultLabel->setStyleSheet("color: #43b581;"); + } else { + resultLabel->setText(tr( + "The backup could not be completed.\n\n" + "Please try again with a different location." + )); + resultLabel->setStyleSheet("color: #e05555;"); + pathResultLabel->clear(); + } + + updateButtons(); +} diff --git a/src/qt/backupwizard.h b/src/qt/backupwizard.h new file mode 100644 index 000000000..a30b3cad5 --- /dev/null +++ b/src/qt/backupwizard.h @@ -0,0 +1,63 @@ +// Copyright (c) 2026 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_BACKUPWIZARD_H +#define BITCOIN_QT_BACKUPWIZARD_H + +#include + +class WalletModel; + +QT_BEGIN_NAMESPACE +class QLabel; +class QLineEdit; +class QPushButton; +class QProgressBar; +class QStackedWidget; +QT_END_NAMESPACE + +class BackupWizard : public QDialog +{ + Q_OBJECT + +public: + explicit BackupWizard(WalletModel *walletModel, QWidget *parent = 0); + +private Q_SLOTS: + void nextPage(); + void prevPage(); + void browseClicked(); + void performBackup(); + +private: + void setupIntroPage(); + void setupLocationPage(); + void setupProgressPage(); + void setupCompletePage(); + void updateButtons(); + + WalletModel *walletModel; + QStackedWidget *stack; + + // Navigation buttons + QPushButton *backButton; + QPushButton *nextButton; + QPushButton *cancelButton; + + // Location page widgets + QLineEdit *pathEdit; + + // Progress page widgets + QProgressBar *progressBar; + QLabel *statusLabel; + + // Complete page widgets + QLabel *resultLabel; + QLabel *pathResultLabel; + + int currentPage; + bool backupSuccess; +}; + +#endif // BITCOIN_QT_BACKUPWIZARD_H diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index b709d197d..6157e4f9e 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -498,7 +498,7 @@ void BitcoinApplication::shutdownResult(int retval) void BitcoinApplication::handleRunawayException(const QString &message) { - QMessageBox::critical(0, "Runaway exception", BitcoinGUI::tr("A fatal error occurred. Bitcoin can no longer continue safely and will quit.") + QString("\n\n") + message); + QMessageBox::critical(0, "Runaway exception", BitcoinGUI::tr("A fatal error occurred. Validity can no longer continue safely and will quit.") + QString("\n\n") + message); ::exit(EXIT_FAILURE); } diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index 81e750a6e..21a1c1c42 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -55,6 +55,10 @@ res/icons/staking_on.png res/icons/staking_off.png + + res/themes/dark.qss + res/themes/light.qss + res/movies/spinner-000.png res/movies/spinner-001.png diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 2e4b67231..e3df35493 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -20,6 +20,7 @@ #include "optionsmodel.h" #include "platformstyle.h" #include "rpcconsole.h" +#include "thememanager.h" #include "utilitydialog.h" #ifdef ENABLE_WALLET @@ -56,6 +57,7 @@ #include #include #include +#include #include #include @@ -119,8 +121,13 @@ BitcoinGUI::BitcoinGUI(const Config *cfg, const PlatformStyle *platformStyle, co prevBlocks(0), spinnerFrame(0), platformStyle(platformStyle), - cfg(cfg) + cfg(cfg), + themeManager(new ThemeManager(this)), + navToolbar(0) { + // Apply saved theme (dark by default) + themeManager->loadSavedTheme(); + GUIUtil::restoreWindowGeometry("nWindow", QSize(850, 550), this); QString windowTitle = tr(PACKAGE_NAME) + " - "; @@ -226,14 +233,7 @@ BitcoinGUI::BitcoinGUI(const Config *cfg, const PlatformStyle *platformStyle, co progressBar->setAlignment(Qt::AlignCenter); progressBar->setVisible(false); - // Override style sheet for progress bar for styles that have a segmented progress bar, - // as they make the text unreadable (workaround for issue #1071) - // See https://doc.qt.io/qt-5/gallery.html - QString curStyle = QApplication::style()->metaObject()->className(); - if(curStyle == "QWindowsStyle" || curStyle == "QWindowsXPStyle") - { - progressBar->setStyleSheet("QProgressBar { background-color: #e8e8e8; border: 1px solid grey; border-radius: 7px; padding: 1px; text-align: center; } QProgressBar::chunk { background: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #FF8000, stop: 1 orange); border-radius: 7px; margin: 0px; }"); - } + // Progress bar styling is now handled by the theme system (QSS) statusBar()->addWidget(progressBarLabel); statusBar()->addWidget(progressBar); @@ -283,7 +283,7 @@ void BitcoinGUI::createActions() tabGroup->addAction(overviewAction); sendCoinsAction = new QAction(platformStyle->SingleColorIcon(":/icons/send"), tr("&Send"), this); - sendCoinsAction->setStatusTip(tr("Send coins to a Bitcoin address")); + sendCoinsAction->setStatusTip(tr("Send coins to a Validity address")); sendCoinsAction->setToolTip(sendCoinsAction->statusTip()); sendCoinsAction->setCheckable(true); sendCoinsAction->setShortcut(QKeySequence(Qt::ALT + Qt::Key_2)); @@ -351,6 +351,8 @@ void BitcoinGUI::createActions() encryptWalletAction->setCheckable(true); backupWalletAction = new QAction(platformStyle->TextColorIcon(":/icons/filesave"), tr("&Backup Wallet..."), this); backupWalletAction->setStatusTip(tr("Backup wallet to another location")); + backupWizardAction = new QAction(platformStyle->TextColorIcon(":/icons/filesave"), tr("Backup &Wizard..."), this); + backupWizardAction->setStatusTip(tr("Guided backup of your wallet")); changePassphraseAction = new QAction(platformStyle->TextColorIcon(":/icons/key"), tr("&Change Passphrase..."), this); changePassphraseAction->setStatusTip(tr("Change the passphrase used for wallet encryption")); unlockWalletAction = new QAction(platformStyle->TextColorIcon(":/icons/lock_open"), tr("&Unlock Wallet..."), this); @@ -358,9 +360,9 @@ void BitcoinGUI::createActions() lockWalletAction = new QAction(platformStyle->TextColorIcon(":/icons/lock_closed"), tr("&Lock Wallet"), this); lockWalletAction->setToolTip(tr("Lock wallet")); signMessageAction = new QAction(platformStyle->TextColorIcon(":/icons/edit"), tr("Sign &message..."), this); - signMessageAction->setStatusTip(tr("Sign messages with your Bitcoin addresses to prove you own them")); + signMessageAction->setStatusTip(tr("Sign messages with your Validity addresses to prove you own them")); verifyMessageAction = new QAction(platformStyle->TextColorIcon(":/icons/verify"), tr("&Verify message..."), this); - verifyMessageAction->setStatusTip(tr("Verify messages to ensure they were signed with specified Bitcoin addresses")); + verifyMessageAction->setStatusTip(tr("Verify messages to ensure they were signed with specified Validity addresses")); openRPCConsoleAction = new QAction(platformStyle->TextColorIcon(":/icons/debugwindow"), tr("&Debug window"), this); openRPCConsoleAction->setStatusTip(tr("Open debugging and diagnostic console")); @@ -377,7 +379,7 @@ void BitcoinGUI::createActions() showHelpMessageAction = new QAction(platformStyle->TextColorIcon(":/icons/info"), tr("&Command-line options"), this); showHelpMessageAction->setMenuRole(QAction::NoRole); - showHelpMessageAction->setStatusTip(tr("Show the %1 help message to get a list with possible Bitcoin command-line options").arg(tr(PACKAGE_NAME))); + showHelpMessageAction->setStatusTip(tr("Show the %1 help message to get a list with possible command-line options").arg(tr(PACKAGE_NAME))); connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit())); connect(aboutAction, SIGNAL(triggered()), this, SLOT(aboutClicked())); @@ -394,6 +396,7 @@ void BitcoinGUI::createActions() { connect(encryptWalletAction, SIGNAL(triggered(bool)), walletFrame, SLOT(encryptWallet(bool))); connect(backupWalletAction, SIGNAL(triggered()), walletFrame, SLOT(backupWallet())); + connect(backupWizardAction, SIGNAL(triggered()), walletFrame, SLOT(backupWizard())); connect(changePassphraseAction, SIGNAL(triggered()), walletFrame, SLOT(changePassphrase())); connect(unlockWalletAction, SIGNAL(triggered()), walletFrame, SLOT(unlockWallet())); connect(lockWalletAction, SIGNAL(triggered()), walletFrame, SLOT(lockWallet())); @@ -425,6 +428,7 @@ void BitcoinGUI::createMenuBar() { file->addAction(openAction); file->addAction(backupWalletAction); + file->addAction(backupWizardAction); file->addAction(signMessageAction); file->addAction(verifyMessageAction); file->addSeparator(); @@ -460,14 +464,75 @@ void BitcoinGUI::createToolBars() { if(walletFrame) { - QToolBar *toolbar = addToolBar(tr("Tabs toolbar")); - toolbar->setMovable(false); - toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - toolbar->addAction(overviewAction); - toolbar->addAction(sendCoinsAction); - toolbar->addAction(receiveCoinsAction); - toolbar->addAction(historyAction); + navToolbar = new QToolBar(tr("Navigation")); + navToolbar->setObjectName("navSidebar"); + navToolbar->setMovable(false); + navToolbar->setFloatable(false); + navToolbar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + navToolbar->setIconSize(QSize(24, 24)); + + // Add spacing at top for branding area + QWidget *topSpacer = new QWidget(); + topSpacer->setFixedHeight(12); + navToolbar->addWidget(topSpacer); + + navToolbar->addAction(overviewAction); + navToolbar->addAction(sendCoinsAction); + navToolbar->addAction(receiveCoinsAction); + navToolbar->addAction(historyAction); + overviewAction->setChecked(true); + + // Add stretch to push content to top + QWidget *spacer = new QWidget(); + spacer->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + navToolbar->addWidget(spacer); + + addToolBar(Qt::LeftToolBarArea, navToolbar); + + // Defer icon colorization until after window is visible + QTimer::singleShot(0, this, SLOT(recolorToolbarIcons())); + + // Re-colorize when theme changes + connect(themeManager, SIGNAL(themeChanged(int)), this, SLOT(recolorToolbarIcons())); + } +} + +static QIcon colorizeIcon(const QIcon &oldIcon, const QColor &color) +{ + QIcon newIcon; + QList sizes = oldIcon.availableSizes(); + if (sizes.isEmpty()) + sizes << QSize(24, 24); + Q_FOREACH(QSize sz, sizes) { + QImage img(oldIcon.pixmap(sz).toImage()); + img = img.convertToFormat(QImage::Format_ARGB32); + for (int x = img.width(); x--; ) + for (int y = img.height(); y--; ) { + QRgb rgb = img.pixel(x, y); + img.setPixel(x, y, qRgba(color.red(), color.green(), color.blue(), qAlpha(rgb))); + } + newIcon.addPixmap(QPixmap::fromImage(img)); + } + return newIcon; +} + +void BitcoinGUI::recolorToolbarIcons() +{ + QColor iconColor = QApplication::palette().color(QPalette::WindowText); + + // On first call, snapshot original icons before colorization + if (originalIcons.isEmpty()) { + QList allActions = findChildren(); + Q_FOREACH(QAction *action, allActions) { + if (!action->icon().isNull()) + originalIcons[action] = action->icon(); + } + } + + // Always colorize from the stored originals to avoid degradation + for (auto it = originalIcons.constBegin(); it != originalIcons.constEnd(); ++it) { + it.key()->setIcon(colorizeIcon(it.value(), iconColor)); } } @@ -568,6 +633,7 @@ void BitcoinGUI::setWalletActionsEnabled(bool enabled) historyAction->setEnabled(enabled); encryptWalletAction->setEnabled(enabled); backupWalletAction->setEnabled(enabled); + backupWizardAction->setEnabled(enabled); changePassphraseAction->setEnabled(enabled); unlockWalletAction->setEnabled(enabled); signMessageAction->setEnabled(enabled); @@ -614,11 +680,15 @@ void BitcoinGUI::createTrayIconMenu() // Configuration of the tray icon (or dock icon) icon menu trayIconMenu->addAction(toggleHideAction); trayIconMenu->addSeparator(); + trayIconMenu->addAction(overviewAction); trayIconMenu->addAction(sendCoinsMenuAction); trayIconMenu->addAction(receiveCoinsMenuAction); + trayIconMenu->addAction(historyAction); trayIconMenu->addSeparator(); - trayIconMenu->addAction(signMessageAction); - trayIconMenu->addAction(verifyMessageAction); + trayIconMenu->addAction(backupWizardAction); + trayIconMenu->addAction(encryptWalletAction); + trayIconMenu->addAction(unlockWalletAction); + trayIconMenu->addAction(lockWalletAction); trayIconMenu->addSeparator(); trayIconMenu->addAction(optionsAction); trayIconMenu->addAction(openRPCConsoleAction); @@ -740,7 +810,7 @@ void BitcoinGUI::setNumConnections(int count) default: icon = ":/icons/connect_4"; break; } labelConnectionsIcon->setPixmap(platformStyle->SingleColorIcon(icon).pixmap(STATUSBAR_ICONSIZE,STATUSBAR_ICONSIZE)); - labelConnectionsIcon->setToolTip(tr("%n active connection(s) to Bitcoin network", "", count)); + labelConnectionsIcon->setToolTip(tr("%n active connection(s) to Validity network", "", count)); } void BitcoinGUI::updateHeadersSyncProgressLabel() @@ -864,7 +934,7 @@ void BitcoinGUI::setNumBlocks(int count, const QDateTime& blockDate, double nVer void BitcoinGUI::message(const QString &title, const QString &message, unsigned int style, bool *ret) { - QString strTitle = tr("Bitcoin"); // default title + QString strTitle = tr(PACKAGE_NAME); // default title // Default to information icon int nMBoxIcon = QMessageBox::Information; int nNotifyIcon = Notificator::Information; @@ -890,7 +960,7 @@ void BitcoinGUI::message(const QString &title, const QString &message, unsigned break; } } - // Append title to "Bitcoin - " + // Append title to "Validity - " if (!msgType.isEmpty()) strTitle += " - " + msgType; @@ -974,16 +1044,33 @@ void BitcoinGUI::showEvent(QShowEvent *event) #ifdef ENABLE_WALLET void BitcoinGUI::incomingTransaction(const QString& date, int unit, const CAmount& amount, const QString& type, const QString& address, const QString& label) { - // On new transaction, make an info balloon - QString msg = tr("Date: %1\n").arg(date) + - tr("Amount: %1\n").arg(BitcoinUnits::formatWithUnit(unit, amount, true)) + - tr("Type: %1\n").arg(type); - if (!label.isEmpty()) - msg += tr("Label: %1\n").arg(label); - else if (!address.isEmpty()) - msg += tr("Address: %1\n").arg(address); - message((amount)<0 ? tr("Sent transaction") : tr("Incoming transaction"), - msg, CClientUIInterface::MSG_INFORMATION); + // Determine transaction category for better notifications + bool isStakingReward = (type == "Mined" || type == "mined"); + QString title; + + if (isStakingReward) { + title = tr("Staking Reward!"); + } else if (amount < 0) { + title = tr("Sent transaction"); + } else { + title = tr("Incoming transaction"); + } + + QString msg; + if (isStakingReward) { + msg = tr("You earned %1 from staking!\n").arg(BitcoinUnits::formatWithUnit(unit, amount, true)) + + tr("Date: %1").arg(date); + } else { + msg = tr("Date: %1\n").arg(date) + + tr("Amount: %1\n").arg(BitcoinUnits::formatWithUnit(unit, amount, true)) + + tr("Type: %1\n").arg(type); + if (!label.isEmpty()) + msg += tr("Label: %1\n").arg(label); + else if (!address.isEmpty()) + msg += tr("Address: %1\n").arg(address); + } + + message(title, msg, CClientUIInterface::MSG_INFORMATION); } #endif // ENABLE_WALLET @@ -1158,20 +1245,36 @@ void BitcoinGUI::updateStakingIcon() nNetworkWeight /= COIN; labelStakingIcon->setPixmap(platformStyle->SingleColorIcon(":/icons/staking_on").pixmap(STATUSBAR_ICONSIZE,STATUSBAR_ICONSIZE)); labelStakingIcon->setToolTip(tr("Staking.
Your weight is %1
Network weight is %2
Expected time to earn reward is %3").arg(nWeight).arg(nNetworkWeight).arg(text)); +#ifndef Q_OS_MAC + if (trayIcon) { + int nConns = clientModel ? clientModel->getNumConnections() : 0; + trayIcon->setToolTip(tr("%1\nStaking - reward in ~%2\nWeight: %3 | Network: %4\nConnections: %5") + .arg(tr(PACKAGE_NAME)).arg(text).arg(nWeight).arg(nNetworkWeight).arg(nConns)); + } +#endif } else { labelStakingIcon->setPixmap(platformStyle->SingleColorIcon(":/icons/staking_off").pixmap(STATUSBAR_ICONSIZE,STATUSBAR_ICONSIZE)); + QString stakingMsg; if (vNodes.empty()) - labelStakingIcon->setToolTip(tr("Not staking because wallet is offline")); + stakingMsg = tr("Not staking - wallet is offline"); else if (IsInitialBlockDownload()) - labelStakingIcon->setToolTip(tr("Not staking because wallet is syncing")); + stakingMsg = tr("Not staking - wallet is syncing"); else if (!nWeight) - labelStakingIcon->setToolTip(tr("Not staking because you don't have mature coins")); - else if (pwalletMain && pwalletMain->IsLocked()) - labelStakingIcon->setToolTip(tr("Not staking because wallet is locked")); + stakingMsg = tr("Not staking - no mature coins"); + else if (pwalletMain && pwalletMain->IsLocked()) + stakingMsg = tr("Not staking - wallet is locked"); else - labelStakingIcon->setToolTip(tr("Not staking")); + stakingMsg = tr("Not staking"); + labelStakingIcon->setToolTip(stakingMsg); +#ifndef Q_OS_MAC + if (trayIcon) { + int nConns = clientModel ? clientModel->getNumConnections() : 0; + trayIcon->setToolTip(tr("%1\n%2\nConnections: %3") + .arg(tr(PACKAGE_NAME)).arg(stakingMsg).arg(nConns)); + } +#endif } } @@ -1268,7 +1371,8 @@ UnitDisplayStatusBarControl::UnitDisplayStatusBarControl(const PlatformStyle *pl } setMinimumSize(max_width, 0); setAlignment(Qt::AlignRight | Qt::AlignVCenter); - setStyleSheet(QString("QLabel { color : %1 }").arg(platformStyle->SingleColor().name())); + // Let QSS theme handle the color instead of hardcoding from platformStyle + setStyleSheet(""); } /** So that it responds to button clicks */ diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 16909715d..fd2bf928b 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -17,6 +17,7 @@ #include #include #include +#include #include @@ -27,6 +28,7 @@ class OptionsModel; class PlatformStyle; class RPCConsole; class SendCoinsRecipient; +class ThemeManager; class UnitDisplayStatusBarControl; class WalletFrame; class WalletModel; @@ -116,6 +118,7 @@ class BitcoinGUI : public QMainWindow QAction *toggleHideAction; QAction *encryptWalletAction; QAction *backupWalletAction; + QAction *backupWizardAction; QAction *changePassphraseAction; QAction *unlockWalletAction; QAction *lockWalletAction; @@ -139,6 +142,9 @@ class BitcoinGUI : public QMainWindow const PlatformStyle *platformStyle; const Config *cfg; + ThemeManager *themeManager; + QToolBar *navToolbar; + QMap originalIcons; /** Create the main UI actions. */ void createActions(); @@ -255,6 +261,9 @@ private Q_SLOTS: void setTrayIconVisible(bool); void showModalOverlay(); + + /** Recolor toolbar icons to match current theme palette */ + void recolorToolbarIcons(); }; class UnitDisplayStatusBarControl : public QLabel diff --git a/src/qt/carddragdrop.cpp b/src/qt/carddragdrop.cpp new file mode 100644 index 000000000..193d568c3 --- /dev/null +++ b/src/qt/carddragdrop.cpp @@ -0,0 +1,441 @@ +// Copyright (c) 2011-2015 The Bitcoin Core developers +// Copyright (c) 2025-2026 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "carddragdrop.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DashboardGridManager::DashboardGridManager(QWidget *parent) + : QObject(parent), + dashboard(parent), + hSplitter(0), leftColumn(0), rightColumn(0), + isDragging(false), dragCard(0), + dragPreview(0), dropLine(0), + targetCol(0), targetIdx(-1) +{ +} + +DashboardGridManager::~DashboardGridManager() +{ + saveLayout(); + if (isDragging) { + qApp->removeEventFilter(this); + isDragging = false; + if (dragPreview) { + delete dragPreview; + dragPreview = 0; + } + if (dropLine) dropLine->hide(); + } +} + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- + +void DashboardGridManager::installFilters(QWidget *w) +{ + w->installEventFilter(this); + Q_FOREACH(QObject *child, w->children()) { + QWidget *cw = qobject_cast(child); + if (cw) installFilters(cw); + } +} + +QWidget* DashboardGridManager::cardForWidget(QWidget *w) +{ + while (w) { + if (allCards.contains(w)) return w; + w = w->parentWidget(); + } + return 0; +} + +bool DashboardGridManager::isInteractive(QWidget *w) +{ + // Walk from the event source up to the card; if any ancestor (before + // the card itself) is an interactive control, suppress drag initiation. + while (w && !allCards.contains(w)) { + if (qobject_cast(w) || + qobject_cast(w) || + qobject_cast(w) || + qobject_cast(w) || + qobject_cast(w) || + qobject_cast(w)) + return true; + + // Viewports of scroll areas are plain QWidgets — catch them too + QWidget *par = w->parentWidget(); + if (par) { + QAbstractScrollArea *sa = qobject_cast(par); + if (sa && sa->viewport() == w) + return true; + } + w = w->parentWidget(); + } + return false; +} + +QSplitter* DashboardGridManager::setupGrid(QList cards) +{ + allCards = cards; + + leftColumn = new QSplitter(Qt::Vertical, dashboard); + leftColumn->setChildrenCollapsible(false); + leftColumn->setMinimumWidth(180); + + rightColumn = new QSplitter(Qt::Vertical, dashboard); + rightColumn->setChildrenCollapsible(false); + rightColumn->setMinimumWidth(180); + + hSplitter = new QSplitter(Qt::Horizontal, dashboard); + hSplitter->setChildrenCollapsible(false); + hSplitter->addWidget(leftColumn); + hSplitter->addWidget(rightColumn); + hSplitter->setStretchFactor(0, 1); + hSplitter->setStretchFactor(1, 1); + + // Try to restore a saved arrangement; fall back to defaults. + if (!restoreFromSettings()) { + // Default: first 3 cards left, last card (transactions) right + for (int i = 0; i < cards.size(); i++) { + if (i < cards.size() - 1) + leftColumn->addWidget(cards[i]); + else + rightColumn->addWidget(cards[i]); + } + } + + // Install event filters on every widget inside every card + Q_FOREACH(QWidget *card, cards) { + installFilters(card); + } + + // Green drop indicator (hidden until a drag is active) + dropLine = new QWidget(dashboard); + dropLine->setFixedHeight(4); + dropLine->setStyleSheet("background-color: #43b581; border-radius: 2px;"); + dropLine->hide(); + + // Persist splitter sizes when the user resizes + connect(hSplitter, SIGNAL(splitterMoved(int,int)), this, SLOT(saveLayout())); + connect(leftColumn, SIGNAL(splitterMoved(int,int)), this, SLOT(saveLayout())); + connect(rightColumn, SIGNAL(splitterMoved(int,int)), this, SLOT(saveLayout())); + + return hSplitter; +} + +// --------------------------------------------------------------------------- +// Event filter +// --------------------------------------------------------------------------- + +bool DashboardGridManager::eventFilter(QObject *obj, QEvent *event) +{ + // ---- Global capture while dragging (installed on qApp) ---- + if (isDragging) { + switch (event->type()) { + case QEvent::MouseMove: { + QMouseEvent *me = static_cast(event); + moveDrag(me->globalPos()); + return true; + } + case QEvent::MouseButtonRelease: + endDrag(); + return true; + case QEvent::KeyPress: { + QKeyEvent *ke = static_cast(event); + if (ke->key() == Qt::Key_Escape) { + abortDrag(); + return true; + } + break; + } + default: + break; + } + return false; + } + + // ---- Normal mode: detect drag start on card children ---- + QWidget *w = qobject_cast(obj); + if (!w) return false; + + switch (event->type()) { + case QEvent::MouseButtonPress: { + QMouseEvent *me = static_cast(event); + if (me->button() == Qt::LeftButton && !isInteractive(w)) { + QWidget *card = cardForWidget(w); + if (card) { + dragCard = card; + dragStartPos = me->globalPos(); + } + } + break; + } + case QEvent::MouseMove: { + if (dragCard) { + QMouseEvent *me = static_cast(event); + if ((me->globalPos() - dragStartPos).manhattanLength() >= DRAG_DIST) { + beginDrag(dragCard, me->globalPos()); + return true; + } + } + break; + } + case QEvent::MouseButtonRelease: + dragCard = 0; + break; + default: + break; + } + + return false; +} + +// --------------------------------------------------------------------------- +// Drag lifecycle +// --------------------------------------------------------------------------- + +void DashboardGridManager::beginDrag(QWidget *card, const QPoint &globalPos) +{ + isDragging = true; + + // Capture all mouse events globally + qApp->installEventFilter(this); + + // Floating thumbnail preview + QPixmap pix = card->grab(); + dragPreview = new QLabel(0); + dragPreview->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + dragPreview->setAttribute(Qt::WA_TransparentForMouseEvents); + dragPreview->setPixmap(pix.scaled(pix.width() * 6 / 10, + pix.height() * 6 / 10, + Qt::KeepAspectRatio, + Qt::SmoothTransformation)); + dragPreview->setWindowOpacity(0.65); + dragPreview->adjustSize(); + dragPreview->move(globalPos - QPoint(dragPreview->width() / 2, 20)); + dragPreview->show(); + + moveDrag(globalPos); +} + +void DashboardGridManager::moveDrag(const QPoint &globalPos) +{ + if (dragPreview) + dragPreview->move(globalPos - QPoint(dragPreview->width() / 2, 20)); + + QPair target = findTarget(globalPos); + targetCol = target.first; + targetIdx = target.second; + + if (targetCol) + positionDropLine(targetCol, targetIdx); + else + hideDropLine(); +} + +void DashboardGridManager::endDrag() +{ + qApp->removeEventFilter(this); + isDragging = false; + + if (dragPreview) { + dragPreview->hide(); + dragPreview->deleteLater(); + dragPreview = 0; + } + hideDropLine(); + + if (dragCard && targetCol && targetIdx >= 0) { + QSplitter *fromCol = qobject_cast(dragCard->parentWidget()); + if (fromCol) { + int fromIdx = fromCol->indexOf(dragCard); + int adj = targetIdx; + if (fromCol == targetCol && fromIdx < adj) + adj--; + + if (fromCol != targetCol || fromIdx != adj) { + targetCol->insertWidget(adj, dragCard); + saveLayout(); + } + } + } + + dragCard = 0; + targetCol = 0; + targetIdx = -1; +} + +void DashboardGridManager::abortDrag() +{ + qApp->removeEventFilter(this); + isDragging = false; + + if (dragPreview) { + dragPreview->hide(); + dragPreview->deleteLater(); + dragPreview = 0; + } + hideDropLine(); + + dragCard = 0; + targetCol = 0; + targetIdx = -1; +} + +// --------------------------------------------------------------------------- +// Hit testing & visual feedback +// --------------------------------------------------------------------------- + +QPair DashboardGridManager::findTarget(const QPoint &globalPos) +{ + if (!leftColumn || !rightColumn) + return qMakePair((QSplitter*)0, -1); + + // Determine which column the cursor is over + QRect leftGeo = QRect(leftColumn->mapToGlobal(QPoint(0, 0)), leftColumn->size()); + QRect rightGeo = QRect(rightColumn->mapToGlobal(QPoint(0, 0)), rightColumn->size()); + + QSplitter *col = 0; + if (leftGeo.contains(globalPos)) + col = leftColumn; + else if (rightGeo.contains(globalPos)) + col = rightColumn; + else { + int dL = qAbs(globalPos.x() - leftGeo.center().x()); + int dR = qAbs(globalPos.x() - rightGeo.center().x()); + col = (dL <= dR) ? leftColumn : rightColumn; + } + + // Find the insertion index closest to the cursor Y + int count = col->count(); + if (count == 0) + return qMakePair(col, 0); + + int bestIdx = count; + int bestDist = INT_MAX; + + for (int i = 0; i <= count; i++) { + int y; + if (i == 0) { + y = col->widget(0)->mapToGlobal(QPoint(0, 0)).y(); + } else { + QWidget *prev = col->widget(i - 1); + y = prev->mapToGlobal(QPoint(0, prev->height())).y(); + } + + int dist = qAbs(globalPos.y() - y); + if (dist < bestDist) { + bestDist = dist; + bestIdx = i; + } + } + + return qMakePair(col, bestIdx); +} + +void DashboardGridManager::positionDropLine(QSplitter *col, int idx) +{ + if (!dropLine || !col) return; + + int y; + if (col->count() == 0) { + y = col->mapTo(dashboard, QPoint(0, col->height() / 2)).y(); + } else if (idx <= 0) { + QWidget *first = col->widget(0); + y = first->mapTo(dashboard, QPoint(0, 0)).y(); + } else if (idx >= col->count()) { + QWidget *last = col->widget(col->count() - 1); + y = last->mapTo(dashboard, QPoint(0, last->height())).y(); + } else { + QWidget *above = col->widget(idx - 1); + y = above->mapTo(dashboard, QPoint(0, above->height())).y(); + } + + QPoint colOrigin = col->mapTo(dashboard, QPoint(0, 0)); + dropLine->setGeometry(colOrigin.x() + 4, y - 2, col->width() - 8, 4); + dropLine->raise(); + dropLine->show(); +} + +void DashboardGridManager::hideDropLine() +{ + if (dropLine) dropLine->hide(); +} + +// --------------------------------------------------------------------------- +// Persistence +// --------------------------------------------------------------------------- + +void DashboardGridManager::saveLayout() +{ + if (!leftColumn || !rightColumn || !hSplitter) return; + + QSettings settings; + QStringList leftNames, rightNames; + + for (int i = 0; i < leftColumn->count(); i++) + leftNames << leftColumn->widget(i)->objectName(); + for (int i = 0; i < rightColumn->count(); i++) + rightNames << rightColumn->widget(i)->objectName(); + + settings.setValue("DashGridLeft", leftNames); + settings.setValue("DashGridRight", rightNames); + settings.setValue("DashGridH", hSplitter->saveState()); + settings.setValue("DashGridLV", leftColumn->saveState()); + settings.setValue("DashGridRV", rightColumn->saveState()); +} + +bool DashboardGridManager::restoreFromSettings() +{ + QSettings settings; + QStringList leftNames = settings.value("DashGridLeft").toStringList(); + QStringList rightNames = settings.value("DashGridRight").toStringList(); + + if (leftNames.isEmpty() && rightNames.isEmpty()) + return false; + + // Build name → widget map + QMap cardMap; + Q_FOREACH(QWidget *card, allCards) + cardMap[card->objectName()] = card; + + Q_FOREACH(const QString &name, leftNames) { + if (cardMap.contains(name)) + leftColumn->addWidget(cardMap.take(name)); + } + Q_FOREACH(const QString &name, rightNames) { + if (cardMap.contains(name)) + rightColumn->addWidget(cardMap.take(name)); + } + + // Any cards not in the saved layout go to left column + Q_FOREACH(QWidget *card, cardMap.values()) + leftColumn->addWidget(card); + + // Restore splitter geometry + QByteArray hState = settings.value("DashGridH").toByteArray(); + QByteArray lvState = settings.value("DashGridLV").toByteArray(); + QByteArray rvState = settings.value("DashGridRV").toByteArray(); + + if (!hState.isEmpty()) hSplitter->restoreState(hState); + if (!lvState.isEmpty()) leftColumn->restoreState(lvState); + if (!rvState.isEmpty()) rightColumn->restoreState(rvState); + + return true; +} diff --git a/src/qt/carddragdrop.h b/src/qt/carddragdrop.h new file mode 100644 index 000000000..214549f28 --- /dev/null +++ b/src/qt/carddragdrop.h @@ -0,0 +1,81 @@ +// Copyright (c) 2011-2015 The Bitcoin Core developers +// Copyright (c) 2025-2026 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_CARDDRAGDROP_H +#define BITCOIN_QT_CARDDRAGDROP_H + +#include +#include +#include +#include +#include + +class QLabel; + +/** + * Manages a 2-column resizable dashboard grid with drag-and-drop card + * rearrangement. Cards can be dragged between columns and reordered + * within a column. Both column widths and individual card heights are + * user-resizable via QSplitter handles. Layout is persisted to QSettings. + */ +class DashboardGridManager : public QObject +{ + Q_OBJECT + +public: + explicit DashboardGridManager(QWidget *parent = 0); + ~DashboardGridManager(); + + /** Build the grid and return the top-level splitter to add to a layout. */ + QSplitter* setupGrid(QList cards); + +public Q_SLOTS: + /** Persist current card arrangement and splitter sizes to QSettings. */ + void saveLayout(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) Q_DECL_OVERRIDE; + +private: + QWidget *dashboard; + QSplitter *hSplitter; // horizontal: left column | right column + QSplitter *leftColumn; // vertical splitter for left cards + QSplitter *rightColumn; // vertical splitter for right cards + QList allCards; + + // Drag state + bool isDragging; + QWidget *dragCard; + QPoint dragStartPos; + QLabel *dragPreview; // floating thumbnail following the cursor + QWidget *dropLine; // green bar showing where the card will land + + // Current drop target + QSplitter *targetCol; + int targetIdx; + + static const int DRAG_DIST = 20; + + // Helpers + QWidget* cardForWidget(QWidget *w); + bool isInteractive(QWidget *w); + void installFilters(QWidget *w); + + // Drag lifecycle + void beginDrag(QWidget *card, const QPoint &globalPos); + void moveDrag(const QPoint &globalPos); + void endDrag(); + void abortDrag(); + + // Hit testing and visual feedback + QPair findTarget(const QPoint &globalPos); + void positionDropLine(QSplitter *col, int idx); + void hideDropLine(); + + // Persistence + bool restoreFromSettings(); +}; + +#endif // BITCOIN_QT_CARDDRAGDROP_H diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index 6e5b56d4f..17be5a754 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -588,7 +588,7 @@ void CoinControlDialog::updateLabels(WalletModel *model, QDialog* dialog) } // turn label red when dust - l7->setStyleSheet((fDust) ? "color:red;" : ""); + l7->setStyleSheet((fDust) ? "color:#e05555;" : ""); // tool tips QString toolTipDust = tr("This label turns red if any recipient receives an amount smaller than the current dust threshold."); diff --git a/src/qt/editaddressdialog.cpp b/src/qt/editaddressdialog.cpp index 5f45031e9..d9d6ed4fb 100644 --- a/src/qt/editaddressdialog.cpp +++ b/src/qt/editaddressdialog.cpp @@ -107,7 +107,7 @@ void EditAddressDialog::accept() break; case AddressTableModel::INVALID_ADDRESS: QMessageBox::warning(this, windowTitle(), - tr("The entered address \"%1\" is not a valid Bitcoin address.").arg(ui->addressEdit->text()), + tr("The entered address \"%1\" is not a valid Validity address.").arg(ui->addressEdit->text()), QMessageBox::Ok, QMessageBox::Ok); break; case AddressTableModel::DUPLICATE_ADDRESS: diff --git a/src/qt/forms/intro.ui b/src/qt/forms/intro.ui index e4ff3da1a..33168ce52 100644 --- a/src/qt/forms/intro.ui +++ b/src/qt/forms/intro.ui @@ -56,7 +56,7 @@ - %1 will download and store a copy of the Bitcoin block chain. At least %2GB of data will be stored in this directory, and it will grow over time. The wallet will also be stored in this directory. + %1 will download and store a copy of the Validity block chain. At least %2GB of data will be stored in this directory, and it will grow over time. The wallet will also be stored in this directory. true diff --git a/src/qt/forms/modaloverlay.ui b/src/qt/forms/modaloverlay.ui index 65a7a6c77..f1e7833c8 100644 --- a/src/qt/forms/modaloverlay.ui +++ b/src/qt/forms/modaloverlay.ui @@ -32,7 +32,7 @@ - #bgWidget { background: rgba(0,0,0,220); } + #bgWidget { background: rgba(10,10,26,220); } @@ -50,9 +50,9 @@ - #contentWidget { background: rgba(255,255,255,240); border-radius: 6px; } + #contentWidget { background: rgba(21,21,48,245); border: 1px solid #2a2a4d; border-radius: 12px; } -QLabel { color: rgb(40,40,40); } +QLabel { color: #e8e8f0; } diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index 0f1b3f4a7..31fae83fe 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -189,7 +189,7 @@ - Automatically open the Bitcoin client port on the router. This only works when your router supports UPnP and it is enabled. + Automatically open the Validity client port on the router. This only works when your router supports UPnP and it is enabled. Map port using &UPnP @@ -209,7 +209,7 @@ - Connect to the Bitcoin network through a SOCKS5 proxy. + Connect to the Validity network through a SOCKS5 proxy. &Connect through SOCKS5 proxy (default proxy): @@ -396,7 +396,7 @@ - Connect to the Bitcoin network through a separate SOCKS5 proxy for Tor hidden services. + Connect to the Validity network through a separate SOCKS5 proxy for Tor hidden services. Use separate SOCKS5 proxy to reach peers via Tor hidden services: @@ -554,6 +554,30 @@ &Display + + + + + + &Theme: + + + Qt::PlainText + + + themeCombo + + + + + + + Choose between Dark and Light theme for the wallet interface. + + + + + diff --git a/src/qt/forms/overviewpage.ui b/src/qt/forms/overviewpage.ui index 1156be8db..5ab5e9f1c 100644 --- a/src/qt/forms/overviewpage.ui +++ b/src/qt/forms/overviewpage.ui @@ -6,21 +6,36 @@ 0 0 - 741 - 668 + 850 + 700 Form + + 12 + + + 16 + + + 16 + + + 16 + + + 16 + false - QLabel { background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #F0D0A0, stop:1 #F8D488); color:#000000; } + true @@ -33,965 +48,938 @@ + - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 8 + + + - - - - - - 75 - true - - - - Balances - - - - - - - true - - - - 30 - 16777215 - - - - The displayed information may be out of date. Your wallet automatically synchronizes with the Bitcoin network after a connection is established, but this process has not completed yet. - - - - - - - :/icons/warning - :/icons/warning:/icons/warning - - - - 24 - 24 - - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + + 75 + true + 11 + + + + Balances + + - - - 12 - - - - - - 75 - true - - - - IBeamCursor - - - Unconfirmed transactions to watch-only addresses - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 75 - true - - - - IBeamCursor - - - Total of transactions that have yet to be confirmed, and do not yet count toward the spendable balance - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 75 - true - - - - IBeamCursor - - - Mined balance in watch-only addresses that has not yet matured - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Qt::Horizontal - - - - - - - - 0 - 0 - - - - - 140 - 0 - - - - Qt::Horizontal - - - - - - - Total: - - - - - - - - 75 - true - - - - IBeamCursor - - - Mined balance that has not yet matured - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Immature: - - - - - - - - 75 - true - - - - IBeamCursor - - - Your current total balance - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 75 - true - - - - IBeamCursor - - - Current total balance in watch-only addresses - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Watch-only: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Available: - - - - - - - - 75 - true - - - - IBeamCursor - - - Your current spendable balance - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 75 - true - - - - IBeamCursor - - - Your current balance in watch-only addresses - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Pending: - - - - - - - Spendable: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Stake: - - - - - - - - 75 - true - - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 75 - true - - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + true + + + + 30 + 16777215 + + + + The displayed information may be out of date. Your wallet automatically synchronizes with the Validity network after a connection is established, but this process has not completed yet. + + + + + + + :/icons/warning + :/icons/warning + + + + + 24 + 24 + + + + true + + - + - Qt::Vertical - - - QSizePolicy::Fixed + Qt::Horizontal - 20 - 75 + 40 + 20 - - - - - - - 75 - true - - - - Staking Report - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + + + + 10 + + + + + Spendable: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + - - - - 12 - - - - - Qt::Horizontal - - - - - - - Last Year: - - - - - - - - 75 - true - - - - IBeamCursor - - - Total VAL staked in the past Week - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 75 - true - - - - IBeamCursor - - - Total VAL staked in the history of this wallet - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Last 24 Hours: - - - - - - - - 75 - true - - - - IBeamCursor - - - Total VAL staked in the past 24 Hours - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 75 - true - - - - IBeamCursor - - - Total VAL staked in the past Year - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Total staked: - - - - - - - - 75 - true - - - - IBeamCursor - - - Total VAL staked in the past 30 days - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Last 30 Days: - - - - - - - Last Week: - - - - - - - - 75 - true - - - - IBeamCursor - - - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 75 - true - - - - IBeamCursor - - - Estimated rewards for 24 hours of staking - - - 0.000 000 00 BTC - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Estimated Daily Rewards - - - - + + + + Watch-only: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + - - + + + + Available: + + - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - - 75 - true - - - - Recent transactions - - - - - - - true - - - - 30 - 16777215 - - - - The displayed information may be out of date. Your wallet automatically synchronizes with the Bitcoin network after a connection is established, but this process has not completed yet. - - - - - - - :/icons/warning - :/icons/warning:/icons/warning - - - - 24 - 24 - - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + + 75true + + + IBeamCursor + + + Your current spendable balance + + + 0.000 000 00 BTC + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + - - - - QListView { background: transparent; } + + + + 75true - - QFrame::NoFrame + + IBeamCursor - - Qt::ScrollBarAlwaysOff + + Your current balance in watch-only addresses - - Qt::ScrollBarAlwaysOff + + 0.000 000 00 BTC - - QAbstractItemView::NoSelection + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - + + + + Pending: + + + + + + + 75true + + + IBeamCursor + + + Total of transactions that have yet to be confirmed, and do not yet count toward the spendable balance + + + 0.000 000 00 BTC + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + 75true + + + IBeamCursor + + + Unconfirmed transactions to watch-only addresses + + + 0.000 000 00 BTC + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Immature: + + + + + + + 75true + + + IBeamCursor + + + Mined balance that has not yet matured + + + 0.000 000 00 BTC + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + 75true + + + IBeamCursor + + + Mined balance in watch-only addresses that has not yet matured + + + 0.000 000 00 BTC + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Stake: + + + + + + + 75true + + + 0.000 000 00 BTC + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + 75true + + + 0.000 000 00 BTC + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + - Qt::Vertical + Qt::Horizontal - - QSizePolicy::Expanding + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + Qt::Horizontal + + + + + + + 75true + + + Total: + + + + + + + + 75 + true + 12 + + + + IBeamCursor + + + Your current total balance + + + 0.000 000 00 BTC + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 75 + true + 12 + + + + IBeamCursor + + + Current total balance in watch-only addresses + + + 0.000 000 00 BTC + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal - 20 + 40 20 - - - - - - 12 - - - - - Qt::Horizontal - - - - - - - Circulating / Max Supply - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - IBeamCursor - - - Total VAL staked in the past 24 Hours - - - Total Staking Coins - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - 100 - - - -1 - - - - - - - 900000 - - - -1 - - - - - - - Annual Generation - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - IBeamCursor - - - Total VAL staked in the past 30 days - - - Your Network Stake - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - 0 - - - -1 - - - - - - - 0 - - - -1 - - - - - - - - - - - - - - + + + + + Qt::Vertical + + + + 20 + 10 + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 8 + + + + + + + + 75 + true + 11 + + + + Staking Rewards (30 Days) + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + 2 + + + 4 + + + + + + 24h: + + + Qt::AlignRight|Qt::AlignVCenter + + + + + + + 75true + + + IBeamCursor + + + Total VAL staked in the past 24 Hours + + + 0.00 VAL + + + Qt::AlignLeft|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + | + + + Qt::AlignCenter + + + color: #5a5a7a; + + + + + + + 7d: + + + Qt::AlignRight|Qt::AlignVCenter + + + + + + + 75true + + + IBeamCursor + + + Total VAL staked in the past Week + + + 0.00 VAL + + + Qt::AlignLeft|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + | + + + Qt::AlignCenter + + + color: #5a5a7a; + + + + + + + 30d: + + + Qt::AlignRight|Qt::AlignVCenter + + + + + + + 75true + + + IBeamCursor + + + Total VAL staked in the past 30 days + + + 0.00 VAL + + + Qt::AlignLeft|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 1y: + + + Qt::AlignRight|Qt::AlignVCenter + + + + + + + 75true + + + IBeamCursor + + + Total VAL staked in the past Year + + + 0.00 VAL + + + Qt::AlignLeft|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + | + + + Qt::AlignCenter + + + color: #5a5a7a; + + + + + + + All time: + + + Qt::AlignRight|Qt::AlignVCenter + + + + + + + 75true + + + IBeamCursor + + + Total VAL staked in the history of this wallet + + + 0.00 VAL + + + Qt::AlignLeft|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + | + + + Qt::AlignCenter + + + color: #5a5a7a; + + + + + + + Est. daily: + + + Qt::AlignRight|Qt::AlignVCenter + + + + + + + 75true + + + IBeamCursor + + + Estimated rewards for 24 hours of staking + + + 0.00 VAL + + + Qt::AlignLeft|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 8 + + + + + + + + 75 + true + 11 + + + + Recent Transactions + + + + + + + true + + + + 30 + 16777215 + + + + The displayed information may be out of date. Your wallet automatically synchronizes with the Validity network after a connection is established, but this process has not completed yet. + + + + + + + :/icons/warning + :/icons/warning + + + + + 24 + 24 + + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + QListView { background: transparent; } + + + QFrame::NoFrame + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::NoSelection + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 6 + + + + + + 75 + true + 11 + + + + Network + + + + + + + 16 + + + 4 + + + + + + Circulating / Max Supply + + + + + + + 100 + + + -1 + + + + + + + Total Staking Coins + + + + + + + 100 + + + -1 + + + + + + + Your Network Stake + + + + + + + 100 + + + -1 + + + + + + + Annual Generation + + + + + + + 100 + + + -1 + + + + + + + diff --git a/src/qt/forms/receivecoinsdialog.ui b/src/qt/forms/receivecoinsdialog.ui index 03fcb2fb5..a6379c069 100644 --- a/src/qt/forms/receivecoinsdialog.ui +++ b/src/qt/forms/receivecoinsdialog.ui @@ -48,7 +48,7 @@ - An optional message to attach to the payment request, which will be displayed when the request is opened. Note: The message will not be sent with the payment over the Bitcoin network. + An optional message to attach to the payment request, which will be displayed when the request is opened. Note: The message will not be sent with the payment over the Validity network. &Message: @@ -71,7 +71,7 @@ - An optional message to attach to the payment request, which will be displayed when the request is opened. Note: The message will not be sent with the payment over the Bitcoin network. + An optional message to attach to the payment request, which will be displayed when the request is opened. Note: The message will not be sent with the payment over the Validity network. diff --git a/src/qt/forms/sendcoinsdialog.ui b/src/qt/forms/sendcoinsdialog.ui index d2cfc90be..c1e589a0b 100644 --- a/src/qt/forms/sendcoinsdialog.ui +++ b/src/qt/forms/sendcoinsdialog.ui @@ -133,7 +133,7 @@ - color:red;font-weight:bold; + color:#e05555;font-weight:bold; Insufficient funds! diff --git a/src/qt/forms/sendcoinsentry.ui b/src/qt/forms/sendcoinsentry.ui index d0748c2ed..d7699d5cf 100644 --- a/src/qt/forms/sendcoinsentry.ui +++ b/src/qt/forms/sendcoinsentry.ui @@ -57,7 +57,7 @@ - The Bitcoin address to send the payment to + The Validity address to send the payment to @@ -199,7 +199,7 @@ - A message that was attached to the bitcoin: URI which will be stored with the transaction for your reference. Note: This message will not be sent over the Bitcoin network. + A message that was attached to the validity: URI which will be stored with the transaction for your reference. Note: This message will not be sent over the Validity network. Qt::PlainText @@ -216,426 +216,11 @@ - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 191 - - - - - - - 127 - 127 - 63 - - - - - - - 170 - 170 - 84 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 127 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 191 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 191 - - - - - - - 127 - 127 - 63 - - - - - - - 170 - 170 - 84 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 127 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 191 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 127 - 127 - 63 - - - - - - - 255 - 255 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 191 - - - - - - - 127 - 127 - 63 - - - - - - - 170 - 170 - 84 - - - - - - - 127 - 127 - 63 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 63 - - - - - - - 255 - 255 - 127 - - - - - - - 255 - 255 - 127 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 127 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - This is an unauthenticated payment request. - true + false QFrame::NoFrame @@ -718,453 +303,11 @@ - - - - - - - 0 - 0 - 0 - - - - - - - 140 - 232 - 119 - - - - - - - 230 - 255 - 224 - - - - - - - 185 - 243 - 171 - - - - - - - 70 - 116 - 59 - - - - - - - 93 - 155 - 79 - - - - - - - 0 - 0 - 0 - - - - - - - 155 - 255 - 147 - - - - - - - 0 - 0 - 0 - - - - - - - 119 - 255 - 233 - - - - - - - 140 - 232 - 119 - - - - - - - 0 - 0 - 0 - - - - - - - 197 - 243 - 187 - - - - - - - 125 - 194 - 122 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 0 - 0 - 0 - - - - - - - 140 - 232 - 119 - - - - - - - 230 - 255 - 224 - - - - - - - 185 - 243 - 171 - - - - - - - 70 - 116 - 59 - - - - - - - 93 - 155 - 79 - - - - - - - 0 - 0 - 0 - - - - - - - 155 - 255 - 147 - - - - - - - 0 - 0 - 0 - - - - - - - 119 - 255 - 233 - - - - - - - 140 - 232 - 119 - - - - - - - 0 - 0 - 0 - - - - - - - 197 - 243 - 187 - - - - - - - 125 - 194 - 122 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 70 - 116 - 59 - - - - - - - 140 - 232 - 119 - - - - - - - 230 - 255 - 224 - - - - - - - 185 - 243 - 171 - - - - - - - 70 - 116 - 59 - - - - - - - 93 - 155 - 79 - - - - - - - 70 - 116 - 59 - - - - - - - 155 - 255 - 147 - - - - - - - 70 - 116 - 59 - - - - - - - 140 - 232 - 119 - - - - - - - 140 - 232 - 119 - - - - - - - 0 - 0 - 0 - - - - - - - 140 - 232 - 119 - - - - - - - 125 - 194 - 122 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - This is an authenticated payment request. - true + false QFrame::NoFrame diff --git a/src/qt/forms/signverifymessagedialog.ui b/src/qt/forms/signverifymessagedialog.ui index 92f6430c5..be99dc28e 100644 --- a/src/qt/forms/signverifymessagedialog.ui +++ b/src/qt/forms/signverifymessagedialog.ui @@ -48,7 +48,7 @@ - The Bitcoin address to sign the message with + The Validity address to sign the message with @@ -152,7 +152,7 @@ - Sign the message to prove you own this Bitcoin address + Sign the message to prove you own this Validity address Sign &Message @@ -258,7 +258,7 @@ - The Bitcoin address the message was signed with + The Validity address the message was signed with @@ -295,7 +295,7 @@ - Verify the message to ensure it was signed with the specified Bitcoin address + Verify the message to ensure it was signed with the specified Validity address Verify &Message diff --git a/src/qt/geoip.cpp b/src/qt/geoip.cpp new file mode 100644 index 000000000..9185e3311 --- /dev/null +++ b/src/qt/geoip.cpp @@ -0,0 +1,206 @@ +// Copyright (c) 2025-2026 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "geoip.h" +#include + +namespace GeoIP { + +struct GeoRegion { + double lat; + double lon; + const char *name; +}; + +// Approximate continent/region centroids +static const GeoRegion regions[] = { + { 38.0, -97.0, "North America" }, // 0 + { -14.0, -51.0, "South America" }, // 1 + { 50.0, 10.0, "Europe" }, // 2 + { 25.0, 45.0, "Middle East" }, // 3 + { 10.0, 25.0, "Africa" }, // 4 + { 35.0, 105.0, "East Asia" }, // 5 + { 20.0, 78.0, "South Asia" }, // 6 + { -25.0, 134.0, "Oceania" }, // 7 + { 60.0, 90.0, "Russia/CIS" }, // 8 + { 5.0, 115.0, "Southeast Asia" }, // 9 +}; + +static const int NUM_REGIONS = 10; + +// First-octet to region index mapping (rough approximation based on IANA allocation) +static int octetToRegion(int firstOctet) +{ + // Major North America blocks + if ((firstOctet >= 3 && firstOctet <= 9) || + (firstOctet >= 12 && firstOctet <= 19) || + (firstOctet >= 23 && firstOctet <= 24) || + (firstOctet >= 32 && firstOctet <= 35) || + (firstOctet >= 44 && firstOctet <= 48) || + (firstOctet >= 52 && firstOctet <= 56) || + (firstOctet >= 63 && firstOctet <= 76) || + (firstOctet >= 96 && firstOctet <= 99) || + (firstOctet >= 104 && firstOctet <= 108) || + (firstOctet >= 128 && firstOctet <= 135) || + (firstOctet >= 142 && firstOctet <= 148) || + (firstOctet >= 152 && firstOctet <= 155) || + (firstOctet >= 160 && firstOctet <= 168) || + (firstOctet >= 198 && firstOctet <= 199) || + (firstOctet >= 204 && firstOctet <= 209) || + (firstOctet >= 216 && firstOctet <= 223)) + return 0; + + // Major Europe blocks + if ((firstOctet == 2) || + (firstOctet >= 25 && firstOctet <= 31) || + (firstOctet >= 36 && firstOctet <= 37) || + (firstOctet == 46) || + (firstOctet == 51) || + (firstOctet >= 57 && firstOctet <= 62) || + (firstOctet >= 77 && firstOctet <= 95) || + (firstOctet >= 109 && firstOctet <= 127) || + (firstOctet >= 136 && firstOctet <= 141) || + (firstOctet >= 149 && firstOctet <= 151) || + (firstOctet >= 176 && firstOctet <= 195) || + (firstOctet >= 212 && firstOctet <= 215)) + return 2; + + // East Asia + if ((firstOctet == 1) || + (firstOctet == 14) || + (firstOctet == 27) || + (firstOctet == 39) || + (firstOctet == 42) || + (firstOctet == 49) || + (firstOctet == 58) || + (firstOctet >= 60 && firstOctet <= 61) || + (firstOctet >= 101 && firstOctet <= 103) || + (firstOctet == 106) || + (firstOctet >= 110 && firstOctet <= 126) || + (firstOctet == 133) || + (firstOctet == 150) || + (firstOctet == 153) || + (firstOctet == 163) || + (firstOctet == 175) || + (firstOctet >= 202 && firstOctet <= 203) || + (firstOctet >= 210 && firstOctet <= 211) || + (firstOctet >= 218 && firstOctet <= 222)) + return 5; + + // South America + if ((firstOctet == 177) || + (firstOctet == 179) || + (firstOctet == 181) || + (firstOctet >= 186 && firstOctet <= 191) || + (firstOctet >= 200 && firstOctet <= 201)) + return 1; + + // Oceania + if ((firstOctet == 116) || + (firstOctet >= 120 && firstOctet <= 122) || + (firstOctet == 124) || + (firstOctet == 144)) + return 7; + + // Africa + if ((firstOctet == 41) || + (firstOctet == 102) || + (firstOctet == 105) || + (firstOctet == 154) || + (firstOctet == 156) || + (firstOctet >= 196 && firstOctet <= 197)) + return 4; + + // South Asia (India, etc.) + if ((firstOctet == 43) || + (firstOctet == 59) || + (firstOctet == 111) || + (firstOctet == 115) || + (firstOctet == 117) || + (firstOctet == 164) || + (firstOctet == 182)) + return 6; + + // Default: Europe (most crypto nodes tend to be NA/EU) + return 2; +} + +QPair lookup(const QString &ipAddress) +{ + // Strip port and handle IPv6 notation + QString ip = ipAddress; + if (ip.startsWith('[')) { + // Bracketed IPv6 - extract content between brackets + int closeBracket = ip.indexOf(']'); + if (closeBracket > 0) + ip = ip.mid(1, closeBracket - 1); + else + ip = ip.mid(1); + + // Check for IPv4-mapped IPv6 (::ffff:1.2.3.4) + if (ip.contains("::ffff:")) { + ip = ip.section("::ffff:", 1, 1); + } else { + // Pure IPv6 - use a hash to spread around a visible location + uint hash = qHash(ipAddress); + double latJitter = ((hash & 0xFF) % 30 - 15) * 0.5; + double lonJitter = (((hash >> 8) & 0xFF) % 40 - 20) * 0.5; + return QPair(48.0 + latJitter, 8.0 + lonJitter); + } + } + if (ip.contains(':')) { + ip = ip.section(':', 0, 0); + } + + QStringList parts = ip.split('.'); + if (parts.size() != 4) { + // Fallback for unparseable addresses - spread them visibly + uint hash = qHash(ipAddress); + double latJitter = ((hash & 0xFF) % 30 - 15) * 0.5; + double lonJitter = (((hash >> 8) & 0xFF) % 40 - 20) * 0.5; + return QPair(48.0 + latJitter, 8.0 + lonJitter); + } + + int firstOctet = parts[0].toInt(); + int regionIdx = octetToRegion(firstOctet); + if (regionIdx < 0 || regionIdx >= NUM_REGIONS) + regionIdx = 2; // fallback Europe + + // Add visual spread based on remaining octets so peers don't stack + double latJitter = (parts[1].toInt() % 20 - 10) * 0.5; + double lonJitter = (parts[2].toInt() % 20 - 10) * 0.5; + + return QPair( + regions[regionIdx].lat + latJitter, + regions[regionIdx].lon + lonJitter + ); +} + +QString regionName(const QString &ipAddress) +{ + QString ip = ipAddress; + if (ip.startsWith('[')) { + int closeBracket = ip.indexOf(']'); + if (closeBracket > 0) + ip = ip.mid(1, closeBracket - 1); + if (ip.contains("::ffff:")) + ip = ip.section("::ffff:", 1, 1); + else + return "Unknown"; + } + if (ip.contains(':')) + ip = ip.section(':', 0, 0); + + QStringList parts = ip.split('.'); + if (parts.size() != 4) + return "Unknown"; + + int regionIdx = octetToRegion(parts[0].toInt()); + if (regionIdx < 0 || regionIdx >= NUM_REGIONS) + return "Unknown"; + + return regions[regionIdx].name; +} + +} // namespace GeoIP diff --git a/src/qt/geoip.h b/src/qt/geoip.h new file mode 100644 index 000000000..560e6ca0d --- /dev/null +++ b/src/qt/geoip.h @@ -0,0 +1,22 @@ +// Copyright (c) 2026 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_GEOIP_H +#define BITCOIN_QT_GEOIP_H + +#include +#include + +// Lightweight IP geolocation - maps IP addresses to approximate lat/lon +// Uses hardcoded IP range heuristics (no external dependencies) +namespace GeoIP { + // Returns (latitude, longitude) for an IP address + // Returns (0, 0) if lookup fails + QPair lookup(const QString &ipAddress); + + // Returns region name for display + QString regionName(const QString &ipAddress); +} + +#endif // BITCOIN_QT_GEOIP_H diff --git a/src/qt/guiconstants.h b/src/qt/guiconstants.h index afafed64c..bb5a337c1 100644 --- a/src/qt/guiconstants.h +++ b/src/qt/guiconstants.h @@ -16,23 +16,23 @@ static const int STATUSBAR_ICONSIZE = 16; static const bool DEFAULT_SPLASHSCREEN = true; -/* Invalid field background style */ -#define STYLE_INVALID "background:#FF8080" +/* Invalid field background style (dark-theme compatible) */ +#define STYLE_INVALID "background:#5d2020" -/* Transaction list -- unconfirmed transaction */ -#define COLOR_UNCONFIRMED QColor(128, 128, 128) -/* Transaction list -- negative amount */ -#define COLOR_NEGATIVE QColor(255, 0, 0) +/* Transaction list -- unconfirmed transaction (muted on dark bg) */ +#define COLOR_UNCONFIRMED QColor(136, 136, 168) +/* Transaction list -- negative amount (soft red for dark theme) */ +#define COLOR_NEGATIVE QColor(224, 85, 85) /* Transaction list -- bare address (without label) */ -#define COLOR_BAREADDRESS QColor(140, 140, 140) +#define COLOR_BAREADDRESS QColor(136, 136, 168) /* Transaction list -- TX status decoration - open until date */ -#define COLOR_TX_STATUS_OPENUNTILDATE QColor(64, 64, 255) +#define COLOR_TX_STATUS_OPENUNTILDATE QColor(100, 130, 255) /* Transaction list -- TX status decoration - offline */ -#define COLOR_TX_STATUS_OFFLINE QColor(192, 192, 192) +#define COLOR_TX_STATUS_OFFLINE QColor(90, 90, 122) /* Transaction list -- TX status decoration - danger, tx needs attention */ -#define COLOR_TX_STATUS_DANGER QColor(200, 100, 100) -/* Transaction list -- TX status decoration - default color */ -#define COLOR_BLACK QColor(0, 0, 0) +#define COLOR_TX_STATUS_DANGER QColor(224, 85, 85) +/* Transaction list -- TX status decoration - default color (light for dark bg) */ +#define COLOR_BLACK QColor(232, 232, 240) /* Tooltips longer than this (in characters) are converted into rich text, so that they can be word-wrapped. diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 5126f88cb..9be30bd8d 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -150,7 +150,7 @@ void setupAddressWidget(QValidatedLineEdit *widget, QWidget *parent) #if QT_VERSION >= 0x040700 // We don't want translators to use own addresses in translations // and this is the only place, where this address is supplied. - widget->setPlaceholderText(QObject::tr("Enter a Bitcoin address (e.g. %1)") + widget->setPlaceholderText(QObject::tr("Enter a Validity address (e.g. %1)") .arg(QString::fromStdString(DummyAddress(params, GetConfig())))); #endif widget->setValidator(new BitcoinAddressEntryValidator(params.CashAddrPrefix(), parent)); @@ -681,15 +681,15 @@ boost::filesystem::path static StartupShortcutPath() { std::string chain = ChainNameFromCommandLine(); if (chain == CBaseChainParams::MAIN) - return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin.lnk"; + return GetSpecialFolderPath(CSIDL_STARTUP) / "Validity.lnk"; if (chain == CBaseChainParams::TESTNET) // Remove this special case when CBaseChainParams::TESTNET = "testnet4" - return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin (testnet).lnk"; - return GetSpecialFolderPath(CSIDL_STARTUP) / strprintf("Bitcoin (%s).lnk", chain); + return GetSpecialFolderPath(CSIDL_STARTUP) / "Validity (testnet).lnk"; + return GetSpecialFolderPath(CSIDL_STARTUP) / strprintf("Validity (%s).lnk", chain); } bool GetStartOnSystemStartup() { - // check for Bitcoin*.lnk + // check for Validity*.lnk return boost::filesystem::exists(StartupShortcutPath()); } diff --git a/src/qt/intro.cpp b/src/qt/intro.cpp index b628c4926..12ba9d8ac 100644 --- a/src/qt/intro.cpp +++ b/src/qt/intro.cpp @@ -225,7 +225,7 @@ void Intro::setStatus(int status, const QString &message, quint64 bytesAvailable break; case FreespaceChecker::ST_ERROR: ui->errorMessage->setText(tr("Error") + ": " + message); - ui->errorMessage->setStyleSheet("QLabel { color: #800000 }"); + ui->errorMessage->setStyleSheet("QLabel { color: #e05555 }"); break; } /* Indicate number of bytes available */ @@ -237,7 +237,7 @@ void Intro::setStatus(int status, const QString &message, quint64 bytesAvailable if(bytesAvailable < requiredSpace * GB_BYTES) { freeString += " " + tr("(of %n GB needed)", "", requiredSpace); - ui->freeSpace->setStyleSheet("QLabel { color: #800000 }"); + ui->freeSpace->setStyleSheet("QLabel { color: #e05555 }"); } else { ui->freeSpace->setStyleSheet(""); } diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index a25a1eb13..4528194cb 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -23,11 +23,15 @@ #include +#include "thememanager.h" + +#include #include #include #include #include #include +#include #include OptionsDialog::OptionsDialog(QWidget *parent, bool enableWallet) : @@ -38,6 +42,18 @@ OptionsDialog::OptionsDialog(QWidget *parent, bool enableWallet) : { ui->setupUi(this); + /* Theme picker init */ + ui->themeCombo->addItem(tr("Dark"), ThemeManager::Dark); + ui->themeCombo->addItem(tr("Light"), ThemeManager::Light); + { + QSettings settings; + int savedTheme = settings.value("nTheme", ThemeManager::Dark).toInt(); + int idx = ui->themeCombo->findData(savedTheme); + if (idx >= 0) + ui->themeCombo->setCurrentIndex(idx); + } + connect(ui->themeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onThemeChanged(int))); + /* Main elements init */ ui->databaseCache->setMinimum(nMinDbCache); ui->databaseCache->setMaximum(nMaxDbCache); @@ -271,7 +287,7 @@ void OptionsDialog::on_hideTrayIcon_stateChanged(int fState) void OptionsDialog::showRestartWarning(bool fPersistent) { - ui->statusLabel->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel->setStyleSheet("QLabel { color: #e05555; }"); if(fPersistent) { @@ -303,7 +319,7 @@ void OptionsDialog::updateProxyValidationState() else { setOkButtonState(false); - ui->statusLabel->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel->setStyleSheet("QLabel { color: #e05555; }"); ui->statusLabel->setText(tr("The supplied proxy address is invalid.")); } } @@ -335,6 +351,21 @@ QValidator(parent) { } +void OptionsDialog::onThemeChanged(int index) +{ + int themeId = ui->themeCombo->itemData(index).toInt(); + // Find the ThemeManager in the parent chain + QObject *p = parent(); + while (p) { + ThemeManager *tm = p->findChild(); + if (tm) { + tm->setTheme(static_cast(themeId)); + break; + } + p = p->parent(); + } +} + QValidator::State ProxyAddressValidator::validate(QString &input, int &pos) const { Q_UNUSED(pos); diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index 23c7c9828..a3b27f63b 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -56,6 +56,7 @@ private Q_SLOTS: void showRestartWarning(bool fPersistent = false); void clearStatusLabel(); void updateProxyValidationState(); + void onThemeChanged(int index); /* query the networks, for which the default proxy is used */ void updateDefaultProxyNets(); diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 27ce0be92..aa98dc665 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -1,10 +1,10 @@ // Copyright (c) 2011-2015 The Bitcoin Core developers +// Copyright (c) 2025-2026 The Validity developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "overviewpage.h" #include "ui_overviewpage.h" -#include "rpc/blockchain.cpp" #include "bitcoinunits.h" #include "clientmodel.h" @@ -12,6 +12,7 @@ #include "guiutil.h" #include "optionsmodel.h" #include "platformstyle.h" +#include "stakingchartwidget.h" #include "transactionfilterproxy.h" #include "transactiontablemodel.h" #include "walletmodel.h" @@ -20,11 +21,19 @@ #include "wallet/wallet.h" #include "walletframe.h" +#include "carddragdrop.h" + +#include "rpc/server.h" + #include +#include #include +#include +#include +#include #define DECORATION_SIZE 54 -#define NUM_ITEMS 5 +#define NUM_ITEMS 7 class TxViewDelegate : public QAbstractItemDelegate { @@ -126,7 +135,15 @@ OverviewPage::OverviewPage(const PlatformStyle *platformStyle, QWidget *parent) currentWatchUnconfBalance(-1), currentWatchImmatureBalance(-1), currentWatchOnlyStake(-1), - txdelegate(new TxViewDelegate(platformStyle, this)) + txdelegate(new TxViewDelegate(platformStyle, this)), + stakingChart(0), + gridManager(0), + lastStaking(false), + initialStatsLoaded(false), + cachedWalletTxCount(0), + cachedBlockHeight(0), + cachedSupply(0), + cachedNetworkWeight(0) { ui->setupUi(this); @@ -141,56 +158,61 @@ OverviewPage::OverviewPage(const PlatformStyle *platformStyle, QWidget *parent) ui->listTransactions->setIconSize(QSize(DECORATION_SIZE, DECORATION_SIZE)); ui->listTransactions->setMinimumHeight(NUM_ITEMS * (DECORATION_SIZE + 2)); ui->listTransactions->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->listTransactions->setUniformItemSizes(true); connect(ui->listTransactions, SIGNAL(clicked(QModelIndex)), this, SLOT(handleTransactionClicked(QModelIndex))); // start with displaying the "out of sync" warnings showOutOfSyncWarning(true); - //NewBlock(false); connect(ui->labelWalletStatus, SIGNAL(clicked()), this, SLOT(handleOutOfSyncWarningClicks())); connect(ui->labelTransactionsStatus, SIGNAL(clicked()), this, SLOT(handleOutOfSyncWarningClicks())); + // Initially hide staking-specific widgets ui->progressBar_AnnualGeneration->setVisible(false); - ui->labelAnualGenerationText->setVisible(false); + ui->labelAnualGenerationText->setVisible(false); ui->progressBar_MyWeight->setVisible(false); ui->labelMyWeightText->setVisible(false); ui->labelExpectedStakingStats->setVisible(false); ui->labelExpectedStakingStatsText->setVisible(false); - - - - - - - - - ui->progressBar_AnnualGeneration->setStyleSheet("QProgressBar { background-color: white; border: 0px solid grey; border-radius: 0px; padding: 1px; text-align: center; } QProgressBar::chunk { background: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #43b581, stop: 1 #43b581); border-radius: 0px; margin: 0px; }"); - ui->progressBar_MyWeight->setStyleSheet("QProgressBar { background-color: white; border: 0px solid grey; border-radius: 0px; padding: 1px; text-align: center; } QProgressBar::chunk { background: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #43b581, stop: 1 #43b581); border-radius: 0px; margin: 0px; }"); - ui->progressBar_Supply->setStyleSheet("QProgressBar { background-color: white; border: 0px solid grey; border-radius: 0px; padding: 1px; text-align: center; } QProgressBar::chunk { background: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #43b581, stop: 1 #43b581); border-radius: 0px; margin: 0px; }"); - ui->progressBar_TotalStaking->setStyleSheet("QProgressBar { background-color: white; border: 0px solid grey; border-radius: 0px; padding: 1px; text-align: center; } QProgressBar::chunk { background: QLinearGradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #43b581, stop: 1 #43b581); border-radius: 0px; margin: 0px; }"); - - ui->labelSupplyText->setVisible(true); - ui->progressBar_Supply->setVisible(true); - ui->labelTotalStakingText->setVisible(true); - ui->progressBar_TotalStaking->setVisible(true); - + // Progress bar styling is handled by the theme QSS now, + // but set alignment and max values ui->progressBar_MyWeight->setAlignment(Qt::AlignCenter); ui->progressBar_Supply->setAlignment(Qt::AlignCenter); ui->progressBar_TotalStaking->setAlignment(Qt::AlignCenter); ui->progressBar_AnnualGeneration->setAlignment(Qt::AlignCenter); - ui->progressBar_Supply->setMaximum(100); ui->progressBar_TotalStaking->setMaximum(100); ui->progressBar_MyWeight->setMaximum(100); ui->progressBar_AnnualGeneration->setMaximum(100); - - - - - + ui->labelSupplyText->setVisible(true); + ui->progressBar_Supply->setVisible(true); + ui->labelTotalStakingText->setVisible(true); + ui->progressBar_TotalStaking->setVisible(true); + + // Create and insert the staking chart widget + stakingChart = new StakingChartWidget(this); + ui->chartPlaceholder->addWidget(stakingChart); + stakingChart->setMinimumHeight(140); + + // Rearrange cards into a 2-column drag-and-drop grid + QVBoxLayout *topLayout = qobject_cast(layout()); + if (topLayout) { + topLayout->removeWidget(ui->frame); + topLayout->removeWidget(ui->frame_2); + topLayout->removeWidget(ui->transactionsCard); + topLayout->removeWidget(ui->networkCard); + + // Card order: balance, staking, network on left; transactions (tall) on right + QList cards; + cards << ui->frame << ui->frame_2 << ui->networkCard << ui->transactionsCard; + + gridManager = new DashboardGridManager(this); + QSplitter *grid = gridManager->setupGrid(cards); + topLayout->addWidget(grid); + } } void OverviewPage::handleTransactionClicked(const QModelIndex &index) @@ -209,9 +231,6 @@ OverviewPage::~OverviewPage() delete ui; } - - - void OverviewPage::setBalance(const CAmount& balance, const CAmount& unconfirmedBalance, const CAmount& immatureBalance, const CAmount& stake, const CAmount& watchOnlyBalance, const CAmount& watchUnconfBalance, const CAmount& watchImmatureBalance, const CAmount& watchOnlyStake) { int unit = walletModel->getOptionsModel()->getDisplayUnit(); @@ -248,7 +267,6 @@ void OverviewPage::setBalance(const CAmount& balance, const CAmount& unconfirmed ui->labelStake->setVisible(showStake || showWatchOnlyStake); ui->labelStakeText->setVisible(showStake || showWatchOnlyStake); ui->labelWatchStake->setVisible(showWatchOnlyStake); // show watch-only stake balance - } // show/hide watch-only labels @@ -275,7 +293,7 @@ void OverviewPage::setClientModel(ClientModel *model) { // Show warning if this is a prerelease version connect(model, SIGNAL(alertsChanged(QString)), this, SLOT(updateAlerts(QString))); - + connect(model, SIGNAL(numBlocksChanged(int, QDateTime, double, bool)), this, SLOT(BlockCountChanged(int, QDateTime, double, bool))); updateAlerts(model->getStatusBarWarnings()); } @@ -286,19 +304,7 @@ void OverviewPage::setWalletModel(WalletModel *model) this->walletModel = model; if(model && model->getOptionsModel()) { - // Set up transaction list - filter.reset(new TransactionFilterProxy()); - filter->setSourceModel(model->getTransactionTableModel()); - filter->setLimit(NUM_ITEMS); - filter->setDynamicSortFilter(true); - filter->setSortRole(Qt::EditRole); - filter->setShowInactive(false); - filter->sort(TransactionTableModel::Date, Qt::DescendingOrder); - - ui->listTransactions->setModel(filter.get()); - ui->listTransactions->setModelColumn(TransactionTableModel::ToAddress); - - // Keep up to date with wallet + // Show balances immediately (lightweight) setBalance(model->getBalance(), model->getUnconfirmedBalance(), model->getImmatureBalance(), model->getStake(), model->getWatchBalance(), model->getWatchUnconfirmedBalance(), model->getWatchImmatureBalance(), model->getWatchStake()); connect(model, SIGNAL(balanceChanged(CAmount,CAmount,CAmount,CAmount,CAmount,CAmount,CAmount,CAmount)), this, SLOT(setBalance(CAmount,CAmount,CAmount,CAmount,CAmount,CAmount,CAmount,CAmount))); @@ -306,19 +312,39 @@ void OverviewPage::setWalletModel(WalletModel *model) updateWatchOnlyLabels(model->haveWatchOnly()); connect(model, SIGNAL(notifyWatchonlyChanged(bool)), this, SLOT(updateWatchOnlyLabels(bool))); + + // Defer the heavy transaction list setup — it triggers full wallet scan + QTimer::singleShot(500, this, SLOT(setupTransactionList())); } // update the display unit, to not use the default ("BTC") updateDisplayUnit(); } +void OverviewPage::setupTransactionList() +{ + if (!walletModel || !walletModel->getOptionsModel()) + return; + + filter.reset(new TransactionFilterProxy()); + filter->setSourceModel(walletModel->getTransactionTableModel()); + filter->setLimit(NUM_ITEMS); + filter->setDynamicSortFilter(true); + filter->setSortRole(Qt::EditRole); + filter->setShowInactive(false); + filter->sort(TransactionTableModel::Date, Qt::DescendingOrder); + + ui->listTransactions->setModel(filter.get()); + ui->listTransactions->setModelColumn(TransactionTableModel::ToAddress); +} + void OverviewPage::updateDisplayUnit() { if(walletModel && walletModel->getOptionsModel()) { if(currentBalance != -1) - setBalance(currentBalance, currentUnconfirmedBalance, currentImmatureBalance, currentStake, - currentWatchOnlyBalance, currentWatchUnconfBalance, currentWatchImmatureBalance, currentWatchOnlyStake); + setBalance(currentBalance, currentUnconfirmedBalance, currentImmatureBalance, currentStake, + currentWatchOnlyBalance, currentWatchUnconfBalance, currentWatchImmatureBalance, currentWatchOnlyStake); // Update txdelegate->unit with the current unit txdelegate->unit = walletModel->getOptionsModel()->getDisplayUnit(); @@ -338,97 +364,179 @@ void OverviewPage::showOutOfSyncWarning(bool fShow) ui->labelWalletStatus->setVisible(fShow); ui->labelTransactionsStatus->setVisible(fShow); } -using namespace boost; - - -using namespace std; - - struct StakePeriodRange_T { int64_t Start; int64_t End; int64_t Total; int Count; - string Name; + std::string Name; }; -typedef vector vStakePeriodRange_T; +typedef std::vector vStakePeriodRange_T; extern vStakePeriodRange_T PrepareRangeForStakeReport(); extern int GetsStakeSubTotal(vStakePeriodRange_T& aRange); +extern double GetSupply(); -double round(double value){ - int64_t pre_round = value * 100;; - return ((double)pre_round) / 100; +static double roundTo2(double value){ + return (double)((int64_t)(value * 100)) / 100.0; } void OverviewPage::BlockCountChanged(int count, const QDateTime& blockDate, double nVerificationProgress, bool header){ - //if flast update time was less than 5 seconds ago, do nothing - if ((GetTime() - nLastReportUpdate) < 5) + //if last update time was less than 5 seconds ago, do nothing + if ((GetTime() - nLastReportUpdate) < 5) return; - -// if initial block download, do nothing + + // if initial block download, do nothing if(IsInitialBlockDownload()) return; - // if walletmodel is not avalible, do nothing + // if walletmodel is not available, do nothing if (!walletModel || !walletModel->getOptionsModel()) return; - + if (!pwalletMain) + return; + + // Don't block UI waiting for locks held by background tx loader. + // TRY_LOCK must stay in scope — nested LOCK() calls succeed via recursive_mutex. + TRY_LOCK(cs_main, lockMain); + if (!lockMain) + return; + TRY_LOCK(pwalletMain->cs_wallet, lockWallet); + if (!lockWallet) + return; + bool staking = pwalletMain->IsStaking(); - - -// if staking status has changed, force update + // if staking status has changed, force update if(lastStaking != staking) nLastReportUpdate = 0; lastStaking = staking; - + // Defer the very first stats load so the UI renders immediately + if (!initialStatsLoaded) { + initialStatsLoaded = true; + QPointer guard(this); + QTimer::singleShot(2000, [guard]() { + if (guard) guard->deferredStatsLoad(); + }); + return; + } if ((GetTime() - nLastReportUpdate) > 300) { - int64_t nMyWeight = pwalletMain ? pwalletMain->GetStakeWeight() : 0; + int64_t nMyWeight; + { + LOCK(pwalletMain->cs_wallet); + nMyWeight = pwalletMain->GetStakeWeight(); + } int64_t nNetworkWeight; - int64_t nCoinSupply ; + int64_t nCoinSupply; { LOCK(cs_main); - nNetworkWeight = GetPoSKernelPS(); - nCoinSupply = GetSupply(); - + nNetworkWeight = GetPoSKernelPS(); + + // Cache validation: rescan UTXO if block height changed, + // network weight drifted significantly, or cache is empty + bool cacheValid = (cachedSupply > 0) && + (count == cachedBlockHeight) && + (cachedNetworkWeight > 0) && + (qAbs(nNetworkWeight - cachedNetworkWeight) < + cachedNetworkWeight / 5); // <20% drift + + if (!cacheValid) { + nCoinSupply = GetSupply(); + cachedSupply = nCoinSupply; + cachedNetworkWeight = nNetworkWeight; + cachedBlockHeight = count; + } else { + nCoinSupply = cachedSupply; + } } - - int unit = walletModel->getOptionsModel()->getDisplayUnit(); - - UpdateHistoricalStakingStats(unit); + + // Cache validation: rescan wallet txs if count changed or + // staking status changed (new stakes may have matured) + size_t currentTxCount; + { + LOCK(pwalletMain->cs_wallet); + currentTxCount = pwalletMain->mapWallet.size(); + } + if (currentTxCount != cachedWalletTxCount) { + UpdateHistoricalStakingStats(unit); + cachedWalletTxCount = currentTxCount; + } + UpdateNetworkStats(nCoinSupply, nNetworkWeight, unit); - - UpdateCurrentStakingStats(staking, nMyWeight,nNetworkWeight, unit, count); - + UpdateCurrentStakingStats(staking, nMyWeight, nNetworkWeight, unit, count); // Save the last update nLastReportUpdate = GetTime(); } } +void OverviewPage::deferredStatsLoad() +{ + if (!walletModel || !walletModel->getOptionsModel() || !pwalletMain) + return; + + // Acquire both locks non-blocking. If the background transaction + // loader holds them, reschedule instead of freezing the UI. + // TRY_LOCK stays in scope — nested LOCK() calls succeed via recursive_mutex. + TRY_LOCK(cs_main, lockMain); + if (!lockMain) { + QPointer guard(this); + QTimer::singleShot(2000, [guard]() { + if (guard) guard->deferredStatsLoad(); + }); + return; + } + TRY_LOCK(pwalletMain->cs_wallet, lockWallet); + if (!lockWallet) { + QPointer guard(this); + QTimer::singleShot(2000, [guard]() { + if (guard) guard->deferredStatsLoad(); + }); + return; + } + + bool staking = pwalletMain->IsStaking(); + int64_t nMyWeight = pwalletMain->GetStakeWeight(); + + int64_t nNetworkWeight = GetPoSKernelPS(); + int64_t nCoinSupply = GetSupply(); + cachedSupply = nCoinSupply; + cachedNetworkWeight = nNetworkWeight; + cachedBlockHeight = chainActive.Height(); + + int unit = walletModel->getOptionsModel()->getDisplayUnit(); + + UpdateHistoricalStakingStats(unit); + cachedWalletTxCount = pwalletMain->mapWallet.size(); + + UpdateNetworkStats(nCoinSupply, nNetworkWeight, unit); + UpdateCurrentStakingStats(staking, nMyWeight, nNetworkWeight, unit, chainActive.Height()); + + nLastReportUpdate = GetTime(); +} + void OverviewPage::UpdateHistoricalStakingStats(int unit){ // Get data for staking report vStakePeriodRange_T aRange = PrepareRangeForStakeReport(); GetsStakeSubTotal(aRange); - + if (aRange.size() < 35) + return; - - // Prepair the subtotals - CAmount amount24h = round(aRange[30].Total); - CAmount amount7d = round(aRange[31].Total); - CAmount amount30d = round(aRange[32].Total); - CAmount amount1y = round(aRange[33].Total); - CAmount amountAll = round(aRange[34].Total); + // Prepare the subtotals (indices 30-34 are the summary periods) + CAmount amount24h = aRange[30].Total; + CAmount amount7d = aRange[31].Total; + CAmount amount30d = aRange[32].Total; + CAmount amount1y = aRange[33].Total; + CAmount amountAll = aRange[34].Total; // Display staking history ui->label24hStakingStats->setText(BitcoinUnits::formatWithUnit(unit, amount24h, false, BitcoinUnits::separatorAlways, 2)); @@ -439,13 +547,28 @@ void OverviewPage::UpdateHistoricalStakingStats(int unit){ uiInterface.SetStaked(amountAll, amount24h, amount7d); - + // Update the staking chart with daily data (first 30 entries are individual days) + QVector chartData; + for (int i = 0; i < 30 && i < (int)aRange.size(); i++) { + StakeDayData day; + // Convert timestamp to short date label + QDateTime dt; +#if QT_VERSION >= 0x050800 + dt = QDateTime::fromSecsSinceEpoch(aRange[i].Start); +#else + dt = QDateTime::fromTime_t(aRange[i].Start); +#endif + day.label = dt.toString("MMM d"); + day.amount = aRange[i].Total; + chartData.append(day); + } + stakingChart->setUnit(unit); + stakingChart->setData(chartData); } void OverviewPage::UpdateCurrentStakingStats(bool staking, int64_t nMyWeight, int64_t nNetworkWeight, int unit, int nHeight){ - - // set visability + // set visability ui->labelMyWeightText->setVisible(staking); ui->progressBar_MyWeight->setVisible(staking); ui->labelAnualGenerationText->setVisible(staking); @@ -455,34 +578,36 @@ void OverviewPage::UpdateCurrentStakingStats(bool staking, int64_t nMyWeight, in if(!staking) return; - + //Set user stake weight progress bar - double pMyWeight = ((double)nMyWeight/(double)nNetworkWeight); + double pMyWeight = (nNetworkWeight > 0) ? ((double)nMyWeight/(double)nNetworkWeight) : 0; ui->progressBar_MyWeight->setValue(pMyWeight*100); - ui->progressBar_MyWeight->setFormat(tr("%1%").arg(round(pMyWeight*100))); + ui->progressBar_MyWeight->setFormat(tr("%1%").arg(roundTo2(pMyWeight*100))); - double nStakeSubsidy = getFixedStakeSubsidy(nHeight); - double nAnnualCoins = ((nStakeSubsidy * 60 * 25 * 365) * pMyWeight); - double nTotalBalance = currentBalance + currentUnconfirmedBalance + currentImmatureBalance + currentStake; - double pAnualPercent = round(nAnnualCoins/nTotalBalance); + double nStakeSubsidy = (double)getFixedStakeSubsidy(nHeight); + double nAnnualCoins = (nStakeSubsidy * 60.0 * 25.0 * 365.0) * pMyWeight; + double nTotalBalance = (double)(currentBalance + currentUnconfirmedBalance + currentImmatureBalance + currentStake); + double pAnualPercent = (nTotalBalance > 0) ? roundTo2(nAnnualCoins / nTotalBalance) : 0; //set stake generation bar ui->progressBar_AnnualGeneration->setValue(pAnualPercent*100); - ui->progressBar_AnnualGeneration->setFormat(tr("%1% ").arg(round(pAnualPercent*100))); + ui->progressBar_AnnualGeneration->setFormat(tr("%1% ").arg(roundTo2(pAnualPercent*100))); - CAmount nExpectedDailyReward = (1440 * nStakeSubsidy) * pMyWeight ; - ui->labelExpectedStakingStats->setText(BitcoinUnits::formatWithUnit(unit, nExpectedDailyReward, false, BitcoinUnits::separatorAlways, 2)); + double rawDailyReward = (1440.0 * nStakeSubsidy) * pMyWeight; + CAmount nExpectedDailyReward = (rawDailyReward > 0 && rawDailyReward < (double)MAX_MONEY) + ? (CAmount)rawDailyReward : 0; + ui->labelExpectedStakingStats->setText(BitcoinUnits::formatWithUnit(unit, nExpectedDailyReward, false, BitcoinUnits::separatorAlways, 2)); } void OverviewPage::UpdateNetworkStats(int64_t nCoinSupply, int64_t nNetworkWeight, int unit){ double pCoinSupply = (((double)nCoinSupply/100000000)/(double)9000000) *100 ; - double pStakingCoins = ((double)nNetworkWeight/(double)nCoinSupply) *100; + double pStakingCoins = (nCoinSupply > 0) ? (((double)nNetworkWeight/(double)nCoinSupply) *100) : 0; //update total staking coins bar ui->progressBar_TotalStaking->setValue(pStakingCoins); - ui->progressBar_TotalStaking->setFormat(tr("%1% (%2)").arg(round(pStakingCoins)).arg(BitcoinUnits::format(unit, nNetworkWeight, false, BitcoinUnits::separatorNever, 0))); + ui->progressBar_TotalStaking->setFormat(tr("%1% (%2)").arg(roundTo2(pStakingCoins)).arg(BitcoinUnits::format(unit, nNetworkWeight, false, BitcoinUnits::separatorNever, 0))); ui->progressBar_TotalStaking->setAlignment(Qt::AlignCenter); - + // update coin supply bar ui->progressBar_Supply->setValue(pCoinSupply); ui->progressBar_Supply->setFormat(tr("%1 / %2").arg(BitcoinUnits::format(unit, nCoinSupply, false, BitcoinUnits::separatorNever, 0)).arg(9000000)); @@ -493,50 +618,4 @@ void OverviewPage::UpdateNetworkStats(int64_t nCoinSupply, int64_t nNetworkWeigh void OverviewPage::NewBlock(bool fImmediate, int nHeight) { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } - - - - - diff --git a/src/qt/overviewpage.h b/src/qt/overviewpage.h index 41cbe2e8e..b1aee68e6 100644 --- a/src/qt/overviewpage.h +++ b/src/qt/overviewpage.h @@ -1,4 +1,5 @@ // Copyright (c) 2011-2015 The Bitcoin Core developers +// Copyright (c) 2025-2026 The Validity developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -8,12 +9,16 @@ #include "amount.h" #include +#include +#include #include class ClientModel; class TransactionFilterProxy; class TxViewDelegate; class PlatformStyle; +class DashboardGridManager; +class StakingChartWidget; class WalletModel; namespace Ui { @@ -42,16 +47,12 @@ class OverviewPage : public QWidget void UpdateNetworkStats(int64_t nCoinSupply, int64_t nNetworkWeight, int unit); public Q_SLOTS: - void setBalance(const CAmount& balance, const CAmount& unconfirmedBalance, const CAmount& immatureBalance, const CAmount& stake, + void setBalance(const CAmount& balance, const CAmount& unconfirmedBalance, const CAmount& immatureBalance, const CAmount& stake, const CAmount& watchOnlyBalance, const CAmount& watchUnconfBalance, const CAmount& watchImmatureBalance, const CAmount& watchOnlyStake); - - - Q_SIGNALS: void transactionClicked(const QModelIndex &index); - void outOfSyncWarningClicked(); - + void outOfSyncWarningClicked(); private: Ui::OverviewPage *ui; @@ -70,11 +71,20 @@ public Q_SLOTS: std::unique_ptr filter; qint64 nLastReportUpdate = 0; bool lastStaking; - - + bool initialStatsLoaded; + + // Caches to avoid rescanning wallet/UTXO on every update + size_t cachedWalletTxCount; + int cachedBlockHeight; + int64_t cachedSupply; + int64_t cachedNetworkWeight; + StakingChartWidget *stakingChart; + DashboardGridManager *gridManager; private Q_SLOTS: + void deferredStatsLoad(); + void setupTransactionList(); void updateDisplayUnit(); void handleTransactionClicked(const QModelIndex &index); void updateAlerts(const QString &warnings); diff --git a/src/qt/paymentserver.cpp b/src/qt/paymentserver.cpp index 9a92063f7..80fb658fb 100644 --- a/src/qt/paymentserver.cpp +++ b/src/qt/paymentserver.cpp @@ -81,7 +81,7 @@ namespace // Anon namespace // static QString ipcServerName() { - QString name("BitcoinQt"); + QString name("ValidityQt"); // Append a simple hash of the datadir // Note that GetDataDir(true) returns a different path @@ -502,7 +502,7 @@ bool PaymentServer::handleURI(const QString &scheme, const QString &s) else { Q_EMIT message(tr("URI handling"), tr("URI cannot be parsed! This can be caused by an invalid " - "Bitcoin address or malformed URI parameters."), + "Validity address or malformed URI parameters."), CClientUIInterface::ICON_WARNING); } diff --git a/src/qt/peermapwidget.cpp b/src/qt/peermapwidget.cpp new file mode 100644 index 000000000..9a3154107 --- /dev/null +++ b/src/qt/peermapwidget.cpp @@ -0,0 +1,360 @@ +// Copyright (c) 2026 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "peermapwidget.h" + +#include +#include +#include +#include +#include + +PeerMapWidget::PeerMapWidget(QWidget *parent) + : QWidget(parent), hoveredPeer(-1) +{ + setMouseTracking(true); + setMinimumSize(400, 250); +} + +void PeerMapWidget::setPeers(const QVector &peers) +{ + peerNodes = peers; + hoveredPeer = -1; + update(); +} + +void PeerMapWidget::clear() +{ + peerNodes.clear(); + hoveredPeer = -1; + update(); +} + +QSize PeerMapWidget::minimumSizeHint() const { return QSize(400, 250); } +QSize PeerMapWidget::sizeHint() const { return QSize(600, 350); } + +QPointF PeerMapWidget::geoToScreen(double lat, double lon, const QRectF &mapRect) const +{ + // Simple equirectangular projection + double x = (lon + 180.0) / 360.0 * mapRect.width() + mapRect.left(); + double y = (90.0 - lat) / 180.0 * mapRect.height() + mapRect.top(); + return QPointF(x, y); +} + +QColor PeerMapWidget::pingColor(int pingMs) const +{ + if (pingMs < 0) return QColor("#5a5a7a"); // unknown + if (pingMs < 100) return QColor("#43b581"); // green - good + if (pingMs < 300) return QColor("#faa61a"); // yellow - ok + return QColor("#e05555"); // red - poor +} + +void PeerMapWidget::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + // Background + painter.fillRect(rect(), QColor("#0a0a1a")); + + // Map area with margin + QRectF mapRect = QRectF(rect()).adjusted(20, 20, -20, -40); + + // Draw grid lines (subtle) + painter.setPen(QPen(QColor(30, 30, 60), 0.5)); + for (int lon = -180; lon <= 180; lon += 60) { + QPointF top = geoToScreen(85, lon, mapRect); + QPointF bot = geoToScreen(-85, lon, mapRect); + painter.drawLine(top, bot); + } + for (int lat = -60; lat <= 80; lat += 30) { + QPointF left = geoToScreen(lat, -180, mapRect); + QPointF right = geoToScreen(lat, 180, mapRect); + painter.drawLine(left, right); + } + + // Draw continent outlines + drawWorldOutline(painter, mapRect); + + // Our position (center of map) + QPointF center = mapRect.center(); + + // Draw connection lines first (below dots) + for (int i = 0; i < peerNodes.size(); i++) { + const PeerMapNode &peer = peerNodes[i]; + QPointF pos = geoToScreen(peer.latitude, peer.longitude, mapRect); + + QColor lineColor = pingColor(peer.pingMs); + lineColor.setAlpha(40); + painter.setPen(QPen(lineColor, 1.0, Qt::DashLine)); + painter.drawLine(center, pos); + } + + // Draw center node (us) + painter.setPen(Qt::NoPen); + painter.setBrush(QColor("#43b581")); + painter.drawEllipse(center, 6, 6); + painter.setPen(QPen(QColor("#43b581"), 2)); + painter.setBrush(Qt::NoBrush); + painter.drawEllipse(center, 10, 10); + + // Draw peer dots + for (int i = 0; i < peerNodes.size(); i++) { + const PeerMapNode &peer = peerNodes[i]; + QPointF pos = geoToScreen(peer.latitude, peer.longitude, mapRect); + drawPeerDot(painter, peer, pos, i == hoveredPeer); + } + + // Draw tooltip for hovered peer + if (hoveredPeer >= 0 && hoveredPeer < peerNodes.size()) { + QPointF pos = geoToScreen(peerNodes[hoveredPeer].latitude, + peerNodes[hoveredPeer].longitude, mapRect); + drawTooltip(painter, peerNodes[hoveredPeer], pos); + } + + // Legend + painter.setFont(QFont(painter.font().family(), 9)); + int ly = rect().bottom() - 18; + int lx = 25; + + painter.setBrush(QColor("#43b581")); + painter.setPen(Qt::NoPen); + painter.drawEllipse(QPointF(lx, ly), 4, 4); + painter.setPen(QColor("#8888a8")); + painter.drawText(lx + 8, ly + 4, "<100ms"); + + lx += 70; + painter.setBrush(QColor("#faa61a")); + painter.setPen(Qt::NoPen); + painter.drawEllipse(QPointF(lx, ly), 4, 4); + painter.setPen(QColor("#8888a8")); + painter.drawText(lx + 8, ly + 4, "<300ms"); + + lx += 70; + painter.setBrush(QColor("#e05555")); + painter.setPen(Qt::NoPen); + painter.drawEllipse(QPointF(lx, ly), 4, 4); + painter.setPen(QColor("#8888a8")); + painter.drawText(lx + 8, ly + 4, ">300ms"); + + // Peer count + painter.setPen(QColor("#e8e8f0")); + painter.drawText(rect().right() - 150, ly + 4, + QString("%1 peers connected").arg(peerNodes.size())); +} + +void PeerMapWidget::drawPeerDot(QPainter &painter, const PeerMapNode &peer, + const QPointF &pos, bool hovered) const +{ + QColor color = pingColor(peer.pingMs); + double radius = hovered ? 7 : 5; + + // Glow effect for hovered + if (hovered) { + QColor glow = color; + glow.setAlpha(60); + painter.setPen(Qt::NoPen); + painter.setBrush(glow); + painter.drawEllipse(pos, 12, 12); + } + + // Dot + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawEllipse(pos, radius, radius); + + // Inbound indicator (small ring) + if (peer.isInbound) { + painter.setPen(QPen(QColor("#ffffff"), 1.5)); + painter.setBrush(Qt::NoBrush); + painter.drawEllipse(pos, radius + 2, radius + 2); + } +} + +void PeerMapWidget::drawTooltip(QPainter &painter, const PeerMapNode &peer, + const QPointF &pos) const +{ + QString text = QString("%1\n%2\nPing: %3ms\nIn: %4 KB / Out: %5 KB%6") + .arg(peer.address) + .arg(peer.subversion) + .arg(peer.pingMs >= 0 ? QString::number(peer.pingMs) : "?") + .arg(peer.bytesIn / 1024) + .arg(peer.bytesOut / 1024) + .arg(peer.isInbound ? "\n(inbound)" : ""); + + QFont font = painter.font(); + font.setPointSize(10); + QFontMetrics fm(font); + QStringList lines = text.split('\n'); + + int maxWidth = 0; + for (int i = 0; i < lines.size(); i++) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) + maxWidth = qMax(maxWidth, fm.horizontalAdvance(lines[i])); +#else + maxWidth = qMax(maxWidth, fm.width(lines[i])); +#endif + } + + int padding = 8; + int lineHeight = fm.height(); + QRectF bgRect(pos.x() + 12, pos.y() - 10, + maxWidth + padding * 2, lines.size() * lineHeight + padding * 2); + + // Keep on screen + if (bgRect.right() > rect().right() - 10) + bgRect.moveRight(pos.x() - 12); + if (bgRect.bottom() > rect().bottom() - 30) + bgRect.moveBottom(pos.y() - 10); + + painter.setPen(QPen(QColor("#2a2a4d"), 1)); + painter.setBrush(QColor(15, 15, 36, 240)); + painter.drawRoundedRect(bgRect, 6, 6); + + painter.setPen(QColor("#e8e8f0")); + painter.setFont(font); + for (int i = 0; i < lines.size(); i++) { + painter.drawText(bgRect.left() + padding, + bgRect.top() + padding + (i + 1) * lineHeight - fm.descent(), + lines[i]); + } +} + +void PeerMapWidget::mouseMoveEvent(QMouseEvent *event) +{ + QRectF mapRect = QRectF(rect()).adjusted(20, 20, -20, -40); + int closest = -1; + double minDist = 15.0; // pixel threshold + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QPointF mousePos = event->position(); +#else + QPointF mousePos = event->pos(); +#endif + + for (int i = 0; i < peerNodes.size(); i++) { + QPointF pos = geoToScreen(peerNodes[i].latitude, peerNodes[i].longitude, mapRect); + double dx = mousePos.x() - pos.x(); + double dy = mousePos.y() - pos.y(); + double dist = qSqrt(dx * dx + dy * dy); + if (dist < minDist) { + minDist = dist; + closest = i; + } + } + + if (closest != hoveredPeer) { + hoveredPeer = closest; + update(); + } +} + +void PeerMapWidget::leaveEvent(QEvent *) +{ + if (hoveredPeer >= 0) { + hoveredPeer = -1; + update(); + } +} + +void PeerMapWidget::drawWorldOutline(QPainter &painter, const QRectF &mapRect) const +{ + painter.setPen(QPen(QColor(40, 40, 80), 1.2)); + painter.setBrush(QColor(20, 20, 45)); + + // Simplified continent outlines (lat, lon pairs) + // North America + { + QPainterPath path; + double pts[][2] = { + {50,-130},{55,-125},{60,-140},{65,-170},{72,-155},{72,-95},{60,-65}, + {47,-55},{43,-65},{40,-74},{30,-82},{25,-80},{25,-97},{30,-105}, + {32,-117},{38,-122},{48,-124},{50,-130} + }; + path.moveTo(geoToScreen(pts[0][0], pts[0][1], mapRect)); + for (int i = 1; i < 18; i++) + path.lineTo(geoToScreen(pts[i][0], pts[i][1], mapRect)); + path.closeSubpath(); + painter.drawPath(path); + } + + // South America + { + QPainterPath path; + double pts[][2] = { + {12,-72},{7,-77},{0,-80},{-5,-81},{-15,-76},{-22,-70},{-35,-57}, + {-52,-68},{-55,-70},{-55,-65},{-40,-62},{-35,-55}, + {-22,-40},{-10,-37},{-3,-50},{5,-60},{10,-72},{12,-72} + }; + path.moveTo(geoToScreen(pts[0][0], pts[0][1], mapRect)); + for (int i = 1; i < 18; i++) + path.lineTo(geoToScreen(pts[i][0], pts[i][1], mapRect)); + path.closeSubpath(); + painter.drawPath(path); + } + + // Europe + { + QPainterPath path; + double pts[][2] = { + {36,-9},{37,0},{43,5},{46,14},{42,18},{40,20},{38,24}, + {41,29},{45,30},{50,30},{55,20},{57,24},{60,25},{68,28}, + {71,25},{70,20},{64,14},{58,12},{55,8},{54,10},{53,5}, + {51,4},{49,-1},{48,-5},{43,-9},{36,-9} + }; + path.moveTo(geoToScreen(pts[0][0], pts[0][1], mapRect)); + for (int i = 1; i < 26; i++) + path.lineTo(geoToScreen(pts[i][0], pts[i][1], mapRect)); + path.closeSubpath(); + painter.drawPath(path); + } + + // Africa + { + QPainterPath path; + double pts[][2] = { + {37,10},{32,13},{30,32},{22,37},{12,44},{2,42},{-11,40}, + {-15,42},{-26,33},{-34,18},{-30,17},{-17,12},{-5,12}, + {5,1},{5,-5},{7,-8},{5,-2},{6,2},{4,10},{0,10}, + {5,-10},{15,-17},{22,-17},{30,-10},{35,-1},{37,10} + }; + path.moveTo(geoToScreen(pts[0][0], pts[0][1], mapRect)); + for (int i = 1; i < 26; i++) + path.lineTo(geoToScreen(pts[i][0], pts[i][1], mapRect)); + path.closeSubpath(); + painter.drawPath(path); + } + + // Asia + { + QPainterPath path; + double pts[][2] = { + {42,32},{40,44},{38,58},{25,57},{23,68},{28,77},{22,88}, + {21,92},{28,97},{23,104},{22,114},{30,122},{35,129}, + {40,132},{45,142},{50,143},{55,137},{60,130},{65,140}, + {68,170},{72,180},{72,120},{70,90},{68,60},{60,60}, + {55,70},{50,55},{45,40},{42,32} + }; + path.moveTo(geoToScreen(pts[0][0], pts[0][1], mapRect)); + for (int i = 1; i < 28; i++) + path.lineTo(geoToScreen(pts[i][0], pts[i][1], mapRect)); + path.closeSubpath(); + painter.drawPath(path); + } + + // Australia + { + QPainterPath path; + double pts[][2] = { + {-12,132},{-14,127},{-22,114},{-32,115},{-35,117},{-35,138}, + {-38,146},{-37,150},{-33,152},{-28,153},{-24,150},{-18,146}, + {-14,144},{-12,142},{-11,136},{-12,132} + }; + path.moveTo(geoToScreen(pts[0][0], pts[0][1], mapRect)); + for (int i = 1; i < 16; i++) + path.lineTo(geoToScreen(pts[i][0], pts[i][1], mapRect)); + path.closeSubpath(); + painter.drawPath(path); + } +} diff --git a/src/qt/peermapwidget.h b/src/qt/peermapwidget.h new file mode 100644 index 000000000..2df205f00 --- /dev/null +++ b/src/qt/peermapwidget.h @@ -0,0 +1,57 @@ +// Copyright (c) 2026 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_PEERMAPWIDGET_H +#define BITCOIN_QT_PEERMAPWIDGET_H + +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QPaintEvent; +class QMouseEvent; +QT_END_NAMESPACE + +struct PeerMapNode { + QString address; + QString subversion; + double latitude; + double longitude; + int pingMs; + qint64 bytesIn; + qint64 bytesOut; + bool isInbound; +}; + +class PeerMapWidget : public QWidget +{ + Q_OBJECT + +public: + explicit PeerMapWidget(QWidget *parent = 0); + + void setPeers(const QVector &peers); + void clear(); + + QSize minimumSizeHint() const override; + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void leaveEvent(QEvent *event) override; + +private: + QVector peerNodes; + int hoveredPeer; + + QPointF geoToScreen(double lat, double lon, const QRectF &mapRect) const; + void drawWorldOutline(QPainter &painter, const QRectF &mapRect) const; + void drawPeerDot(QPainter &painter, const PeerMapNode &peer, const QPointF &pos, bool hovered) const; + void drawTooltip(QPainter &painter, const PeerMapNode &peer, const QPointF &pos) const; + QColor pingColor(int pingMs) const; +}; + +#endif // BITCOIN_QT_PEERMAPWIDGET_H diff --git a/src/qt/res/themes/dark.qss b/src/qt/res/themes/dark.qss new file mode 100644 index 000000000..bc02661cf --- /dev/null +++ b/src/qt/res/themes/dark.qss @@ -0,0 +1,1423 @@ +/* ============================================================ + Validity Dark Theme — Premium Cosmic Aesthetic + Matches validitytech.com design language + + Palette: + --bg-deep: #0a0a1a (deepest navy) + --bg-dark: #0f0f24 (dark navy) + --bg-surface: #151530 (card surface) + --bg-elevated: #1a1a3a (elevated surface) + --bg-hover: #222245 (hover state) + --border: #2a2a4d (subtle border) + --border-glow: #3a3a6d (brighter border) + --text-primary: #e8e8f0 (main text) + --text-secondary:#8888a8 (muted text) + --text-dim: #5a5a7a (disabled/dim) + --accent: #43b581 (Validity green) + --accent-hover: #4ecc92 (green hover) + --accent-dim: rgba(67, 181, 129, 0.15) + --accent-glow: rgba(67, 181, 129, 0.25) + --danger: #e05555 (error/warning red) + ============================================================ */ + +/* === Global Base === */ +QWidget { + background-color: #0f0f24; + color: #e8e8f0; + font-family: "Segoe UI", "Inter", "Helvetica Neue", Arial, sans-serif; + font-size: 13px; +} + +QMainWindow { + background-color: #0a0a1a; +} + +/* === Overview Page Cards === */ +QFrame#balanceCard, QFrame#stakingChartCard, +QFrame#networkCard, QFrame#transactionsCard { + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #181838, stop:1 #141430); + border: 1px solid #2a2a4d; + border-radius: 12px; + padding: 16px; +} + +QFrame#balanceCard:hover, QFrame#stakingChartCard:hover, +QFrame#networkCard:hover, QFrame#transactionsCard:hover { + border-color: rgba(67, 181, 129, 0.3); +} + +/* === Labels === */ +QLabel { + background: transparent; + color: #e8e8f0; + border: none; +} + +QLabel[class="card-header"] { + color: #e8e8f0; + font-weight: bold; + font-size: 14px; + letter-spacing: 1px; +} + +QLabel#labelTotal, QLabel#labelWatchTotal { + color: #43b581; + font-size: 16px; + font-weight: bold; +} + +/* Muted labels for secondary info */ +QLabel#labelBalanceText, QLabel#labelPendingText, +QLabel#labelImmatureText, QLabel#labelStakeText { + color: #8888a8; + font-size: 12px; +} + +/* === Menu Bar === */ +QMenuBar { + background-color: #0a0a1a; + color: #8888a8; + border-bottom: 1px solid #1a1a3a; + padding: 2px 0; + font-size: 12px; +} + +QMenuBar::item { + background: transparent; + padding: 6px 14px; + border-radius: 6px; + margin: 2px 2px; +} + +QMenuBar::item:selected { + background-color: #1a1a3a; + color: #e8e8f0; +} + +QMenuBar::item:pressed { + background-color: #222245; +} + +/* === Dropdown Menus === */ +QMenu { + background-color: #151530; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 8px; + padding: 6px 0; +} + +QMenu::item { + padding: 8px 28px 8px 20px; + border-radius: 4px; + margin: 2px 6px; +} + +QMenu::item:selected { + background-color: rgba(67, 181, 129, 0.15); + color: #43b581; +} + +QMenu::separator { + height: 1px; + background: #2a2a4d; + margin: 6px 12px; +} + +QMenu::icon { + padding-left: 8px; +} + +/* === Sidebar Toolbar (Vertical Navigation) === */ +QToolBar { + background-color: #0a0a1a; + border: none; + border-right: 1px solid #1a1a3a; + spacing: 4px; + padding: 8px 4px; +} + +QToolBar::separator { + height: 1px; + background: #1a1a3a; + margin: 8px 12px; +} + +QToolButton { + background: transparent; + color: #8888a8; + border: none; + border-radius: 10px; + padding: 12px 16px; + font-size: 13px; + font-weight: 500; + min-width: 100px; + text-align: left; +} + +QToolButton:hover { + background-color: #1a1a3a; + color: #e8e8f0; +} + +QToolButton:checked { + background-color: rgba(67, 181, 129, 0.12); + color: #43b581; + border-left: 3px solid #43b581; + padding-left: 13px; +} + +QToolButton:pressed { + background-color: rgba(67, 181, 129, 0.2); +} + +/* === Status Bar === */ +QStatusBar { + background-color: #0a0a1a; + color: #8888a8; + border-top: 1px solid #1a1a3a; + font-size: 11px; + min-height: 24px; +} + +QStatusBar::item { + border: none; +} + +QStatusBar QLabel { + color: #8888a8; + padding: 2px 6px; + font-size: 11px; +} + +/* === Push Buttons (Pill-Shaped) === */ +QPushButton { + background-color: #1a1a3a; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 18px; + padding: 8px 24px; + min-height: 22px; + font-size: 13px; + font-weight: 500; +} + +QPushButton:hover { + background-color: #222245; + border-color: #3a3a6d; + color: #ffffff; +} + +QPushButton:pressed { + background-color: #43b581; + color: #ffffff; + border-color: #43b581; +} + +QPushButton:disabled { + background-color: #111128; + color: #5a5a7a; + border-color: #1a1a3a; +} + +/* Primary action buttons */ +QPushButton#sendButton, QPushButton#clearButton_s, +QPushButton#requestPaymentButton { + background-color: #43b581; + color: #ffffff; + border: none; + font-weight: bold; +} + +QPushButton#sendButton:hover, QPushButton#clearButton_s:hover, +QPushButton#requestPaymentButton:hover { + background-color: #4ecc92; +} + +QPushButton#sendButton:pressed, QPushButton#clearButton_s:pressed, +QPushButton#requestPaymentButton:pressed { + background-color: #38a070; +} + +QPushButton:flat { + background: transparent; + border: none; + color: #8888a8; +} + +QPushButton:flat:hover { + color: #43b581; + background-color: rgba(67, 181, 129, 0.08); +} + +/* === Line Edit (Text Inputs) === */ +QLineEdit { + background-color: #0a0a1a; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 10px; + padding: 8px 14px; + selection-background-color: #43b581; + selection-color: #ffffff; + font-size: 13px; +} + +QLineEdit:focus { + border-color: #43b581; + background-color: #0d0d20; +} + +QLineEdit:hover { + border-color: #3a3a6d; +} + +QLineEdit:disabled { + background-color: #0a0a18; + color: #5a5a7a; + border-color: #1a1a3a; +} + +/* === Combo Box === */ +QComboBox { + background-color: #0a0a1a; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 10px; + padding: 8px 14px; + min-width: 80px; + font-size: 13px; +} + +QComboBox:hover { + border-color: #3a3a6d; +} + +QComboBox:focus { + border-color: #43b581; +} + +QComboBox::drop-down { + border: none; + width: 28px; +} + +QComboBox::down-arrow { + image: none; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid #8888a8; + margin-right: 10px; +} + +QComboBox QAbstractItemView { + background-color: #151530; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 8px; + selection-background-color: rgba(67, 181, 129, 0.15); + selection-color: #43b581; + padding: 4px; +} + +/* === Spin Box === */ +QSpinBox, QDoubleSpinBox { + background-color: #0a0a1a; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 10px; + padding: 6px 10px; + font-size: 13px; +} + +QSpinBox:focus, QDoubleSpinBox:focus { + border-color: #43b581; +} + +QSpinBox:hover, QDoubleSpinBox:hover { + border-color: #3a3a6d; +} + +QSpinBox::up-button, QDoubleSpinBox::up-button, +QSpinBox::down-button, QDoubleSpinBox::down-button { + background: transparent; + border: none; + width: 20px; +} + +/* === Progress Bar (Green Glow) === */ +QProgressBar { + background-color: #0a0a1a; + border: 1px solid #2a2a4d; + border-radius: 8px; + padding: 1px; + text-align: center; + color: #e8e8f0; + font-size: 11px; + min-height: 16px; +} + +QProgressBar::chunk { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #2d8a5e, stop:0.5 #43b581, stop:1 #4ecc92); + border-radius: 7px; +} + +/* === Tab Widget === */ +QTabWidget::pane { + background-color: #0f0f24; + border: 1px solid #2a2a4d; + border-radius: 8px; + top: -1px; +} + +QTabBar::tab { + background-color: #0a0a1a; + color: #8888a8; + border: 1px solid #1a1a3a; + border-bottom: none; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + padding: 10px 20px; + margin-right: 2px; + font-size: 12px; +} + +QTabBar::tab:selected { + background-color: #0f0f24; + color: #43b581; + border-color: #2a2a4d; + border-bottom: 2px solid #43b581; + font-weight: bold; +} + +QTabBar::tab:hover:!selected { + background-color: #151530; + color: #e8e8f0; +} + +/* === Table / Tree / List Views === */ +QTableView, QTreeView, QListView { + background-color: #0f0f24; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 8px; + gridline-color: #1a1a3a; + selection-background-color: rgba(67, 181, 129, 0.15); + selection-color: #e8e8f0; + alternate-background-color: #111128; + font-size: 12px; +} + +QTableView::item, QTreeView::item, QListView::item { + padding: 6px 4px; + border-bottom: 1px solid #1a1a3a; +} + +QTableView::item:hover, QTreeView::item:hover, QListView::item:hover { + background-color: rgba(67, 181, 129, 0.08); +} + +QTableView::item:selected, QTreeView::item:selected, QListView::item:selected { + background-color: rgba(67, 181, 129, 0.15); +} + +QHeaderView::section { + background-color: #0a0a1a; + color: #8888a8; + border: none; + border-bottom: 1px solid #2a2a4d; + border-right: 1px solid #1a1a3a; + padding: 8px 10px; + font-weight: bold; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1px; +} + +QHeaderView::section:hover { + color: #e8e8f0; +} + +/* === Scroll Bars (Thin, Minimal) === */ +QScrollBar:vertical { + background: transparent; + width: 8px; + margin: 0; + border: none; +} + +QScrollBar::handle:vertical { + background: #2a2a4d; + border-radius: 4px; + min-height: 30px; +} + +QScrollBar::handle:vertical:hover { + background: #3a3a6d; +} + +QScrollBar::handle:vertical:pressed { + background: #43b581; +} + +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + height: 0; +} + +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: transparent; +} + +QScrollBar:horizontal { + background: transparent; + height: 8px; + margin: 0; + border: none; +} + +QScrollBar::handle:horizontal { + background: #2a2a4d; + border-radius: 4px; + min-width: 30px; +} + +QScrollBar::handle:horizontal:hover { + background: #3a3a6d; +} + +QScrollBar::handle:horizontal:pressed { + background: #43b581; +} + +QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { + width: 0; +} + +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { + background: transparent; +} + +/* === Check Box / Radio Button === */ +QCheckBox, QRadioButton { + color: #e8e8f0; + spacing: 10px; + font-size: 13px; +} + +QCheckBox::indicator, QRadioButton::indicator { + width: 18px; + height: 18px; + border: 2px solid #3a3a6d; + background: #0a0a1a; +} + +QCheckBox::indicator { + border-radius: 5px; +} + +QRadioButton::indicator { + border-radius: 11px; +} + +QCheckBox::indicator:hover, QRadioButton::indicator:hover { + border-color: #43b581; +} + +QCheckBox::indicator:checked, QRadioButton::indicator:checked { + background: #43b581; + border-color: #43b581; +} + +/* === Group Box === */ +QGroupBox { + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 10px; + margin-top: 16px; + padding-top: 20px; + font-weight: bold; +} + +QGroupBox::title { + subcontrol-origin: margin; + left: 16px; + padding: 0 8px; + color: #8888a8; + font-size: 12px; + letter-spacing: 1px; +} + +/* === Dialog === */ +QDialog { + background-color: #0f0f24; +} + +/* === Line (separator) === */ +QFrame[frameShape="4"], +QFrame[frameShape="5"] { + color: #1a1a3a; +} + +/* === Text Edit / Plain Text Edit === */ +QTextEdit, QPlainTextEdit { + background-color: #0a0a1a; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 8px; + padding: 8px; + selection-background-color: #43b581; + selection-color: #ffffff; + font-size: 13px; +} + +QTextEdit:focus, QPlainTextEdit:focus { + border-color: #43b581; +} + +/* === Overview Page Specific === */ +QFrame#frame, QFrame#frame_2 { + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #181838, stop:1 #141430); + border: 1px solid #2a2a4d; + border-radius: 12px; +} + +/* Alert banner */ +QLabel#labelAlerts { + background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #3d2020, stop:1 #4d2525); + color: #f08080; + border: 1px solid #5d3030; + border-radius: 8px; + padding: 8px 12px; + font-size: 12px; +} + +/* Staking stats period labels */ +QLabel#label24hStakingStatsText, QLabel#label7dStakingStatsText, +QLabel#label30dStakingStatsText, QLabel#label1yStakingStatsText, +QLabel#labelallStakingStatsText, QLabel#labelExpectedStakingStatsText { + color: #8888a8; + font-size: 11px; + padding-right: 2px; +} + +/* Staking stats value labels */ +QLabel#label24hStakingStats, QLabel#label7dStakingStats, +QLabel#label30dStakingStats, QLabel#label1yStakingStats, +QLabel#labelallStakingStats, QLabel#labelExpectedStakingStats { + font-size: 12px; + padding-left: 2px; +} + +/* Recent transactions list */ +QListView#listTransactions { + background: transparent; + border: none; +} + +/* === Send Coins Page === */ +QFrame#SendCoins { + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #181838, stop:1 #141430); + border: 1px solid #2a2a4d; + border-radius: 12px; + padding: 16px; + margin: 4px 0; +} + +/* Payment request frames — subtle accent borders */ +QFrame#SendCoins_UnauthenticatedPaymentRequest { + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #1a1a30, stop:1 #141430); + border: 1px solid #4a4a2d; + border-left: 3px solid #c4a32f; + border-radius: 8px; + padding: 16px; + margin: 4px 0; +} + +QFrame#SendCoins_AuthenticatedPaymentRequest { + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #1a2a1a, stop:1 #142014); + border: 1px solid #2d4a2d; + border-left: 3px solid #43b581; + border-radius: 8px; + padding: 16px; + margin: 4px 0; +} + +/* Coin control frame */ +QFrame#frameCoinControl { + background-color: #151530; + border: 1px solid #2a2a4d; + border-radius: 10px; + padding: 12px; +} + +/* Fee section frame */ +QFrame#frameFee { + background-color: #111128; + border: 1px solid #1a1a3a; + border-radius: 10px; + padding: 10px; +} + +/* === Modal Overlay === */ +QWidget#bgWidget { + background-color: rgba(10, 10, 26, 0.92); +} + +QWidget#contentWidget { + background-color: #151530; + border: 1px solid #2a2a4d; + border-radius: 12px; +} + +/* === RPC Console === */ +QWidget#RPCConsole QTextEdit { + background-color: #0a0a1a; + color: #43b581; + font-family: "Consolas", "Fira Code", "Monaco", monospace; + font-size: 12px; +} + +/* === Specific Button Overrides === */ +QPushButton#deleteButton, QPushButton#deleteButton_is, QPushButton#deleteButton_s { + background-color: transparent; + color: #e05555; + border: 1px solid #5d3030; + border-radius: 18px; +} + +QPushButton#deleteButton:hover, QPushButton#deleteButton_is:hover, QPushButton#deleteButton_s:hover { + background-color: rgba(224, 85, 85, 0.1); + border-color: #e05555; +} + +/* Copy/paste buttons */ +QPushButton#addressBookButton, QPushButton#pasteButton, +QPushButton#copyAddressButton { + background: transparent; + border: 1px solid #2a2a4d; + border-radius: 18px; + padding: 6px 12px; + min-width: 30px; +} + +QPushButton#addressBookButton:hover, QPushButton#pasteButton:hover, +QPushButton#copyAddressButton:hover { + border-color: #43b581; + color: #43b581; +} + +/* === Amount Field === */ +QWidget#BitcoinAmountField QLineEdit { + font-size: 15px; + font-weight: bold; +} + +/* === Wallet Encryption/Passphrase === */ +QDialog#AskPassphraseDialog QLineEdit { + font-size: 14px; + padding: 10px 14px; +} + +/* === Intro/Welcome Dialog === */ +QDialog#Intro { + background-color: #0f0f24; +} + +QDialog#Intro QRadioButton { + padding: 8px; +} + +/* === Receive Coins Page === */ +QFrame#frame2 { + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #181838, stop:1 #141430); + border: 1px solid #2a2a4d; + border-radius: 12px; + padding: 16px; +} + +/* QR Code dialog */ +QDialog#ReceiveRequestDialog { + background-color: #0f0f24; +} + +QDialog#ReceiveRequestDialog QTextEdit { + background-color: #0a0a1a; + border: 1px solid #2a2a4d; + border-radius: 8px; + padding: 12px; +} + +/* === RPC Console Enhanced === */ +QWidget#RPCConsole { + background-color: #0f0f24; +} + +QWidget#RPCConsole QPlainTextEdit { + background-color: #050510; + color: #43b581; + font-family: "Consolas", "Fira Code", "Monaco", monospace; + font-size: 12px; + border: 1px solid #1a1a3a; + border-radius: 8px; + padding: 8px; +} + +QWidget#RPCConsole QLineEdit#lineEdit { + background-color: #0a0a1a; + color: #43b581; + font-family: "Consolas", "Fira Code", "Monaco", monospace; + font-size: 13px; + border: 1px solid #2a2a4d; + border-radius: 8px; + padding: 8px 12px; +} + +QWidget#RPCConsole QLineEdit#lineEdit:focus { + border-color: #43b581; +} + +/* Info panel labels */ +QWidget#RPCConsole QLabel { + font-size: 12px; +} + +/* === Sign/Verify Message Dialog === */ +QDialog#SignVerifyMessageDialog QTextEdit { + background-color: #0a0a1a; + border: 1px solid #2a2a4d; + border-radius: 8px; + font-family: "Consolas", "Fira Code", monospace; + font-size: 12px; +} + +/* === Edit Address Dialog === */ +QDialog#EditAddressDialog { + background-color: #0f0f24; +} + +/* === Transaction Description Dialog === */ +QDialog#TransactionDescDialog { + background-color: #0f0f24; +} + +QDialog#TransactionDescDialog QTextEdit { + background-color: #0a0a1a; + border: 1px solid #2a2a4d; + border-radius: 8px; +} + +/* === Date/Time Pickers === */ +QDateTimeEdit { + background-color: #0a0a1a; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 8px; + padding: 6px 10px; + font-size: 13px; +} + +QDateTimeEdit:focus { + border-color: #43b581; +} + +QDateTimeEdit::drop-down { + border: none; + width: 24px; +} + +QCalendarWidget { + background-color: #151530; + color: #e8e8f0; +} + +QCalendarWidget QAbstractItemView { + background-color: #0f0f24; + color: #e8e8f0; + selection-background-color: #43b581; + selection-color: #ffffff; + alternate-background-color: #111128; +} + +QCalendarWidget QToolButton { + background-color: #1a1a3a; + color: #e8e8f0; + border: none; + border-radius: 6px; + padding: 4px 8px; +} + +QCalendarWidget QToolButton:hover { + background-color: #222245; +} + +/* === Scroll Area (used in Send page) === */ +QScrollArea { + background: transparent; + border: none; +} + +QScrollArea > QWidget > QWidget { + background: transparent; +} + +/* === Backup Wizard Dialog === */ +QDialog#BackupWizard { + background-color: #0f0f24; +} + +/* === Peer Map Widget === */ +PeerMapWidget { + background-color: #0a0a1a; + border: 1px solid #2a2a4d; + border-radius: 8px; +} + +/* ================================================================ + SEND / RECEIVE / ADDRESS BOOK POLISH + ================================================================ */ + +/* --- Fee Section --- */ +QLabel#labelSmartFee { + font-weight: bold; + font-size: 13px; + color: #43b581; +} + +QLabel#labelSmartFee2 { + color: #8888a8; + font-size: 11px; +} + +QLabel#labelFeeEstimation { + color: #8888a8; + font-size: 11px; +} + +QLabel#labelBalance { + font-weight: bold; + font-size: 14px; + color: #43b581; +} + +QRadioButton#radioSmartFee, +QRadioButton#radioCustomFee { + spacing: 6px; + padding: 4px 8px; + border-radius: 6px; +} + +QRadioButton#radioSmartFee:checked, +QRadioButton#radioCustomFee:checked { + background-color: rgba(67, 181, 129, 0.08); +} + +QCheckBox#checkBoxMinimumFee { + color: #8888a8; +} + +/* --- Coin Control Stats --- */ +QLabel#labelCoinControlQuantityText, +QLabel#labelCoinControlBytesText, +QLabel#labelCoinControlAmountText, +QLabel#labelCoinControlFeeText, +QLabel#labelCoinControlAfterFeeText, +QLabel#labelCoinControlChangeText, +QLabel#labelCoinControlLowOutputText, +QLabel#labelCoinControlAutomaticConfirmation { + color: #8888a8; + font-size: 11px; +} + +QLabel#labelCoinControlQuantity, +QLabel#labelCoinControlBytes, +QLabel#labelCoinControlAmount, +QLabel#labelCoinControlFee, +QLabel#labelCoinControlAfterFee, +QLabel#labelCoinControlChange { + font-family: "Consolas", "Courier New", monospace; + color: #e8e8f0; + font-size: 12px; + font-weight: bold; +} + +QLabel#labelCoinControlLowOutput { + font-weight: bold; +} + +QLabel#labelCoinControlChangeLabel { + font-size: 12px; +} + +/* --- Send Coins Entry Icon Buttons --- */ +QPushButton#addressBookButton, +QPushButton#pasteButton { + background-color: transparent; + border: 1px solid #2a2a4d; + border-radius: 6px; + padding: 4px; + min-width: 28px; + min-height: 28px; +} + +QPushButton#addressBookButton:hover, +QPushButton#pasteButton:hover { + background-color: rgba(67, 181, 129, 0.12); + border-color: #43b581; +} + +QPushButton#deleteButton { + background-color: transparent; + border: 1px solid rgba(224, 85, 85, 0.3); + border-radius: 6px; + padding: 4px; + min-width: 28px; + min-height: 28px; +} + +QPushButton#deleteButton:hover { + background-color: rgba(224, 85, 85, 0.15); + border-color: #e05555; +} + +/* --- Input Validation States --- */ +QValidatedLineEdit { + border: 1px solid #2a2a4d; + border-radius: 6px; + padding: 6px 10px; + background-color: #111128; + color: #e8e8f0; +} + +QValidatedLineEdit:focus { + border-color: #43b581; +} + +/* --- Receive Page Form --- */ +QLabel#label_4, +QLabel#label_3, +QLabel#label_5 { + font-weight: bold; + color: #b8b8d0; + font-size: 13px; +} + +QLabel#label_6 { + color: #8888a8; + font-size: 12px; + padding: 8px 0; +} + +QPushButton#receiveButton { + background-color: #43b581; + color: #ffffff; + font-weight: bold; + font-size: 14px; + border: none; + border-radius: 8px; + padding: 10px 24px; +} + +QPushButton#receiveButton:hover { + background-color: #3da374; +} + +QPushButton#receiveButton:pressed { + background-color: #35916a; +} + +/* --- Recent Requests Table --- */ +QTableView#recentRequestsView { + gridline-color: #1a1a3a; + selection-background-color: rgba(67, 181, 129, 0.2); + selection-color: #e8e8f0; +} + +QTableView#recentRequestsView::item:hover { + background-color: rgba(67, 181, 129, 0.08); +} + +QPushButton#showRequestButton { + background-color: #1a1a3a; + color: #43b581; + border: 1px solid #43b581; + border-radius: 6px; + padding: 6px 16px; + font-weight: bold; +} + +QPushButton#showRequestButton:hover { + background-color: rgba(67, 181, 129, 0.15); +} + +QPushButton#removeRequestButton { + background-color: #1a1a3a; + color: #e05555; + border: 1px solid rgba(224, 85, 85, 0.4); + border-radius: 6px; + padding: 6px 16px; +} + +QPushButton#removeRequestButton:hover { + background-color: rgba(224, 85, 85, 0.12); + border-color: #e05555; +} + +/* --- Receive Request Dialog (QR Code) --- */ +QTextEdit#outUri { + background-color: #111128; + border: 1px solid #2a2a4d; + border-radius: 6px; + color: #43b581; + font-family: "Consolas", "Courier New", monospace; + font-size: 12px; + padding: 8px; + selection-background-color: rgba(67, 181, 129, 0.3); +} + +/* --- Address Book Page --- */ +QTableView#tableView { + gridline-color: #1a1a3a; + selection-background-color: rgba(67, 181, 129, 0.2); + selection-color: #e8e8f0; +} + +QTableView#tableView::item:hover { + background-color: rgba(67, 181, 129, 0.08); +} + +QPushButton#newAddress { + background-color: #43b581; + color: #ffffff; + font-weight: bold; + border: none; + border-radius: 6px; + padding: 8px 16px; +} + +QPushButton#newAddress:hover { + background-color: #3da374; +} + +QPushButton#copyAddress { + background-color: #1a1a3a; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 6px; + padding: 8px 16px; +} + +QPushButton#copyAddress:hover { + background-color: #222245; + border-color: #43b581; +} + +QPushButton#deleteAddress { + background-color: #1a1a3a; + color: #e05555; + border: 1px solid rgba(224, 85, 85, 0.3); + border-radius: 6px; + padding: 8px 16px; +} + +QPushButton#deleteAddress:hover { + background-color: rgba(224, 85, 85, 0.12); + border-color: #e05555; +} + +QPushButton#exportButton { + background-color: #1a1a3a; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 6px; + padding: 8px 16px; +} + +QPushButton#exportButton:hover { + background-color: #222245; +} + +/* --- Options Dialog Enhancement --- */ +QDialog#OptionsDialog QTabWidget::pane { + border: 1px solid #2a2a4d; + border-radius: 0 0 8px 8px; + background-color: #0f0f24; + padding: 12px; +} + +QDialog#OptionsDialog QTabBar::tab { + padding: 8px 20px; + font-size: 13px; +} + +QLabel#themeLabel { + font-weight: bold; + font-size: 13px; + color: #b8b8d0; +} + +QComboBox#themeCombo { + min-width: 160px; + font-size: 13px; +} + +/* --- Options Dialog Status Bar (restart warning) --- */ +QLabel#statusLabel { + color: #e05555; + font-weight: bold; + font-size: 12px; +} + +/* --- Send Confirmation Dialog --- */ +SendConfirmationDialog QPushButton#sendButton { + background-color: #43b581; + color: #ffffff; + font-weight: bold; +} + +/* --- Clear / Add Recipient Buttons --- */ +QPushButton#addButton { + background-color: #1a1a3a; + color: #43b581; + border: 1px solid #43b581; + border-radius: 6px; + padding: 8px 16px; + font-weight: bold; +} + +QPushButton#addButton:hover { + background-color: rgba(67, 181, 129, 0.12); +} + +QPushButton#clearButton { + background-color: #1a1a3a; + color: #8888a8; + border: 1px solid #2a2a4d; + border-radius: 6px; + padding: 8px 16px; +} + +QPushButton#clearButton:hover { + background-color: #222245; + color: #e8e8f0; +} + +/* --- Slider Styling (Fee Slider) --- */ +QSlider::groove:horizontal { + border: none; + height: 6px; + background-color: #1a1a3a; + border-radius: 3px; +} + +QSlider::handle:horizontal { + background-color: #43b581; + border: 2px solid #35916a; + width: 16px; + height: 16px; + margin: -6px 0; + border-radius: 9px; +} + +QSlider::handle:horizontal:hover { + background-color: #4cc48f; + border-color: #43b581; +} + +QSlider::sub-page:horizontal { + background-color: #43b581; + border-radius: 3px; +} + +/* ================================================================ + TRANSACTION HISTORY VIEW POLISH + ================================================================ */ + +/* --- Transaction Filter Bar --- */ +TransactionView QComboBox { + min-height: 28px; + padding: 4px 8px; +} + +TransactionView QLineEdit { + min-height: 28px; + padding: 4px 8px; + border: 1px solid #2a2a4d; + border-radius: 6px; + background-color: #111128; +} + +TransactionView QLineEdit:focus { + border-color: #43b581; +} + +/* --- Transaction Table --- */ +TransactionView QTableView { + gridline-color: #1a1a3a; + selection-background-color: rgba(67, 181, 129, 0.2); + selection-color: #e8e8f0; + alternate-background-color: #111128; + font-size: 13px; +} + +TransactionView QTableView::item { + padding: 6px 8px; + border-bottom: 1px solid #111128; +} + +TransactionView QTableView::item:hover { + background-color: rgba(67, 181, 129, 0.06); +} + +TransactionView QTableView::item:selected { + background-color: rgba(67, 181, 129, 0.18); +} + +/* --- Date Range Widget --- */ +TransactionView QDateTimeEdit { + min-height: 28px; +} + +/* --- Context Menu --- */ +TransactionView QMenu { + background-color: #151530; + border: 1px solid #2a2a4d; + border-radius: 8px; + padding: 4px; +} + +TransactionView QMenu::item { + padding: 8px 20px; + border-radius: 4px; +} + +TransactionView QMenu::item:selected { + background-color: rgba(67, 181, 129, 0.15); + color: #43b581; +} + +/* ================================================================ + COIN CONTROL DIALOG POLISH + ================================================================ */ + +CoinControlDialog QTreeWidget { + background-color: #0a0a1a; + alternate-background-color: #0d0d20; + border: 1px solid #2a2a4d; + border-radius: 8px; + gridline-color: #1a1a3a; +} + +CoinControlDialog QTreeWidget::item { + padding: 4px 8px; + border-bottom: 1px solid #1a1a3a; +} + +CoinControlDialog QTreeWidget::item:hover { + background-color: rgba(67, 181, 129, 0.08); +} + +CoinControlDialog QTreeWidget::item:selected { + background-color: rgba(67, 181, 129, 0.15); +} + +CoinControlDialog QTreeWidget::indicator:unchecked { + border: 2px solid #3a3a6d; + border-radius: 3px; + background-color: #111128; + width: 14px; + height: 14px; +} + +CoinControlDialog QTreeWidget::indicator:checked { + border: 2px solid #43b581; + border-radius: 3px; + background-color: #43b581; + width: 14px; + height: 14px; +} + +CoinControlDialog QPushButton#pushButtonSelectAll { + background-color: #1a1a3a; + color: #43b581; + border: 1px solid #43b581; + border-radius: 6px; + padding: 6px 14px; + font-weight: bold; +} + +CoinControlDialog QPushButton#pushButtonSelectAll:hover { + background-color: rgba(67, 181, 129, 0.12); +} + +CoinControlDialog QRadioButton { + spacing: 6px; +} + +/* ================================================================ + GLOBAL FOCUS / ACCESSIBILITY POLISH + ================================================================ */ + +/* Focus ring for all interactive elements */ +QLineEdit:focus, +QTextEdit:focus, +QPlainTextEdit:focus, +QSpinBox:focus, +QDoubleSpinBox:focus { + border-color: #43b581; +} + +/* Disabled state for all elements */ +QPushButton:disabled { + background-color: #111128; + color: #3a3a6d; + border-color: #1a1a3a; +} + +QLineEdit:disabled, +QComboBox:disabled, +QSpinBox:disabled { + background-color: #0a0a1a; + color: #3a3a6d; + border-color: #151530; +} + +/* ================================================================ + TOOLTIPS + ================================================================ */ +QToolTip { + background-color: #1a1a3a; + color: #e8e8f0; + border: 1px solid #2a2a4d; + border-radius: 6px; + padding: 6px 10px; + font-size: 12px; +} + +/* ================================================================ + QSplitter — Resizable grid handles + ================================================================ */ +QSplitter::handle { + background-color: transparent; +} +QSplitter::handle:horizontal { + width: 6px; + background-color: transparent; +} +QSplitter::handle:vertical { + height: 6px; + background-color: transparent; +} +QSplitter::handle:hover { + background-color: rgba(67, 181, 129, 0.3); + border-radius: 2px; +} +QSplitter::handle:pressed { + background-color: rgba(67, 181, 129, 0.5); +} diff --git a/src/qt/res/themes/light.qss b/src/qt/res/themes/light.qss new file mode 100644 index 000000000..71f7cff20 --- /dev/null +++ b/src/qt/res/themes/light.qss @@ -0,0 +1,929 @@ +/* ============================================================ + Validity Light Theme — Clean Professional Aesthetic + Matches validitytech.com with a light, airy feel + + Palette: + --bg-white: #ffffff (pure white) + --bg-light: #f8f9fa (off-white) + --bg-surface: #f0f2f5 (card surface) + --bg-elevated: #e8eaed (elevated surface) + --bg-hover: #e2e5e9 (hover state) + --border: #d0d4da (subtle border) + --border-focus: #a0a8b4 (focused border) + --text-primary: #1a1a2e (dark navy text) + --text-secondary:#5a6270 (muted text) + --text-dim: #8a9199 (disabled/dim) + --accent: #2d8f5e (Validity green) + --accent-hover: #248a54 (green hover) + --accent-light: rgba(45, 143, 94, 0.1) + --danger: #d04040 (error red) + ============================================================ */ + +/* === Global Base === */ +QWidget { + background-color: #f8f9fa; + color: #1a1a2e; + font-family: "Segoe UI", "Inter", "Helvetica Neue", Arial, sans-serif; + font-size: 13px; +} + +QMainWindow { + background-color: #f0f2f5; +} + +/* === Overview Page Cards === */ +QFrame#balanceCard, QFrame#stakingChartCard, +QFrame#networkCard, QFrame#transactionsCard { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 12px; + padding: 16px; +} + +QFrame#balanceCard:hover, QFrame#stakingChartCard:hover, +QFrame#networkCard:hover, QFrame#transactionsCard:hover { + border-color: rgba(45, 143, 94, 0.4); +} + +QFrame#frame, QFrame#frame_2 { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 8px; +} + +/* === Labels === */ +QLabel { + background: transparent; + color: #1a1a2e; +} + +QLabel[class="card-header"] { + font-size: 16px; + font-weight: bold; + color: #1a1a2e; +} + +QLabel#labelTotal, QLabel#labelWatchTotal { + color: #2d8f5e; + font-weight: bold; +} + +QLabel#labelBalanceText, QLabel#labelPendingText, +QLabel#labelImmatureText, QLabel#labelStakeText, +QLabel#labelWatchPending, QLabel#labelWatchImmature, +QLabel#labelWatchonly, QLabel#labelWatchAvailable { + color: #5a6270; + font-size: 12px; +} + +QLabel#labelAlerts { + background-color: #fff3cd; + color: #856404; + border: 1px solid #ffc107; + border-radius: 6px; + padding: 8px 12px; + font-weight: bold; +} + +/* === Staking Stats Labels === */ +QLabel#label24hStakingStatsText, QLabel#label7dStakingStatsText, +QLabel#label30dStakingStatsText, QLabel#labelExpectedStakingStatsText { + color: #5a6270; + font-size: 11px; +} + +QLabel#label24hStakingStats, QLabel#label7dStakingStats, +QLabel#label30dStakingStats, QLabel#labelExpectedStakingStats { + font-weight: bold; + color: #2d8f5e; + font-size: 12px; +} + +QLabel#labelSep1, QLabel#labelSep2, QLabel#labelSep3 { + color: #d0d4da; + font-size: 14px; +} + +/* === Menu Bar === */ +QMenuBar { + background-color: #ffffff; + color: #1a1a2e; + border-bottom: 1px solid #d0d4da; + padding: 2px 0; +} + +QMenuBar::item { + padding: 6px 12px; + border-radius: 4px; +} + +QMenuBar::item:selected { + background-color: rgba(45, 143, 94, 0.1); + color: #2d8f5e; +} + +QMenu { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 8px; + padding: 4px; +} + +QMenu::item { + padding: 8px 24px; + border-radius: 4px; +} + +QMenu::item:selected { + background-color: rgba(45, 143, 94, 0.1); + color: #2d8f5e; +} + +QMenu::separator { + height: 1px; + background-color: #e8eaed; + margin: 4px 12px; +} + +/* === Sidebar / Toolbar === */ +QToolBar { + background-color: #ffffff; + border-right: 1px solid #d0d4da; + spacing: 2px; + padding: 8px 4px; +} + +QToolBar::separator { + height: 1px; + background-color: #e8eaed; + margin: 8px 12px; +} + +QToolButton { + background-color: transparent; + color: #5a6270; + border: none; + border-radius: 8px; + padding: 10px 16px; + font-size: 13px; + text-align: left; + min-width: 140px; +} + +QToolButton:hover { + background-color: rgba(45, 143, 94, 0.08); + color: #2d8f5e; +} + +QToolButton:checked { + background-color: rgba(45, 143, 94, 0.12); + color: #2d8f5e; + font-weight: bold; +} + +QToolButton:pressed { + background-color: rgba(45, 143, 94, 0.18); +} + +/* === Status Bar === */ +QStatusBar { + background-color: #ffffff; + border-top: 1px solid #d0d4da; + color: #5a6270; + font-size: 12px; +} + +/* === Buttons === */ +QPushButton { + background-color: #ffffff; + color: #1a1a2e; + border: 1px solid #d0d4da; + border-radius: 6px; + padding: 8px 16px; + font-size: 13px; +} + +QPushButton:hover { + background-color: #f0f2f5; + border-color: #a0a8b4; +} + +QPushButton:pressed { + background-color: #e8eaed; +} + +QPushButton:disabled { + background-color: #f0f2f5; + color: #a0a8b4; + border-color: #e8eaed; +} + +/* Primary action buttons */ +QPushButton#sendButton, QPushButton#clearButton_s, +QPushButton#requestPaymentButton { + background-color: #2d8f5e; + color: #ffffff; + border: none; + font-weight: bold; + font-size: 14px; + padding: 10px 24px; + border-radius: 8px; +} + +QPushButton#sendButton:hover, QPushButton#clearButton_s:hover, +QPushButton#requestPaymentButton:hover { + background-color: #248a54; +} + +QPushButton#sendButton:pressed, QPushButton#clearButton_s:pressed, +QPushButton#requestPaymentButton:pressed { + background-color: #1e7548; +} + +QPushButton#receiveButton { + background-color: #2d8f5e; + color: #ffffff; + font-weight: bold; + font-size: 14px; + border: none; + border-radius: 8px; + padding: 10px 24px; +} + +QPushButton#receiveButton:hover { + background-color: #248a54; +} + +/* === Input Fields === */ +QLineEdit { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 6px; + padding: 6px 10px; + color: #1a1a2e; + font-size: 13px; + selection-background-color: rgba(45, 143, 94, 0.3); +} + +QLineEdit:focus { + border-color: #2d8f5e; +} + +QLineEdit:disabled { + background-color: #f0f2f5; + color: #a0a8b4; +} + +QValidatedLineEdit { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 6px; + padding: 6px 10px; + color: #1a1a2e; +} + +QValidatedLineEdit:focus { + border-color: #2d8f5e; +} + +QTextEdit, QPlainTextEdit { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 6px; + padding: 8px; + color: #1a1a2e; + selection-background-color: rgba(45, 143, 94, 0.3); +} + +QTextEdit:focus, QPlainTextEdit:focus { + border-color: #2d8f5e; +} + +/* === Combo Boxes === */ +QComboBox { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 6px; + padding: 6px 10px; + color: #1a1a2e; + min-height: 24px; +} + +QComboBox:hover { + border-color: #a0a8b4; +} + +QComboBox:focus { + border-color: #2d8f5e; +} + +QComboBox::drop-down { + border: none; + width: 24px; +} + +QComboBox QAbstractItemView { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 6px; + selection-background-color: rgba(45, 143, 94, 0.15); + selection-color: #2d8f5e; +} + +/* === Spin Boxes === */ +QSpinBox, QDoubleSpinBox { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 6px; + padding: 4px 8px; + color: #1a1a2e; +} + +QSpinBox:focus, QDoubleSpinBox:focus { + border-color: #2d8f5e; +} + +/* === Progress Bars === */ +QProgressBar { + background-color: #e8eaed; + border: 1px solid #d0d4da; + border-radius: 4px; + padding: 1px; + text-align: center; + color: #1a1a2e; + font-size: 11px; +} + +QProgressBar::chunk { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #2d8f5e, stop:1 #43b581); + border-radius: 3px; +} + +/* === Tab Widget === */ +QTabWidget::pane { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 0 0 8px 8px; +} + +QTabBar::tab { + background-color: #f0f2f5; + color: #5a6270; + border: 1px solid #d0d4da; + border-bottom: none; + padding: 8px 20px; + margin-right: 2px; + border-radius: 6px 6px 0 0; +} + +QTabBar::tab:selected { + background-color: #ffffff; + color: #2d8f5e; + font-weight: bold; + border-bottom: 2px solid #2d8f5e; +} + +QTabBar::tab:hover:!selected { + background-color: #e8eaed; + color: #1a1a2e; +} + +/* === Tables === */ +QTableView, QTreeView { + background-color: #ffffff; + alternate-background-color: #f8f9fa; + border: 1px solid #d0d4da; + border-radius: 6px; + gridline-color: #e8eaed; + selection-background-color: rgba(45, 143, 94, 0.15); + selection-color: #1a1a2e; +} + +QTableView::item:hover, QTreeView::item:hover { + background-color: rgba(45, 143, 94, 0.06); +} + +QHeaderView::section { + background-color: #f0f2f5; + color: #5a6270; + border: none; + border-bottom: 1px solid #d0d4da; + border-right: 1px solid #e8eaed; + padding: 8px 12px; + font-weight: bold; + font-size: 12px; +} + +QHeaderView::section:hover { + background-color: #e8eaed; + color: #1a1a2e; +} + +/* === List Views === */ +QListView { + background: transparent; + border: none; +} + +QListView#listTransactions { + background: transparent; + border: none; +} + +QListView::item { + border-radius: 6px; + padding: 4px; +} + +QListView::item:hover { + background-color: rgba(45, 143, 94, 0.06); +} + +/* === Scroll Bars === */ +QScrollBar:vertical { + background-color: transparent; + width: 10px; + margin: 0; +} + +QScrollBar::handle:vertical { + background-color: #d0d4da; + border-radius: 5px; + min-height: 30px; +} + +QScrollBar::handle:vertical:hover { + background-color: #a0a8b4; +} + +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + height: 0; +} + +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: transparent; +} + +QScrollBar:horizontal { + background-color: transparent; + height: 10px; + margin: 0; +} + +QScrollBar::handle:horizontal { + background-color: #d0d4da; + border-radius: 5px; + min-width: 30px; +} + +QScrollBar::handle:horizontal:hover { + background-color: #a0a8b4; +} + +QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { + width: 0; +} + +/* === Check Boxes & Radio Buttons === */ +QCheckBox, QRadioButton { + spacing: 8px; + color: #1a1a2e; +} + +QCheckBox::indicator, QRadioButton::indicator { + width: 18px; + height: 18px; +} + +/* === Group Boxes === */ +QGroupBox { + border: 1px solid #d0d4da; + border-radius: 8px; + margin-top: 12px; + padding-top: 16px; + background-color: #ffffff; +} + +QGroupBox::title { + color: #5a6270; + subcontrol-origin: margin; + left: 12px; + padding: 0 4px; +} + +/* === Dialogs === */ +QDialog { + background-color: #f8f9fa; +} + +/* === Modal Overlay === */ +QWidget#bgWidget { + background-color: rgba(0, 0, 0, 0.5); +} + +QWidget#contentWidget { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 12px; +} + +/* === RPC Console === */ +QWidget#RPCConsole { + background-color: #f8f9fa; +} + +QWidget#RPCConsole QTextEdit { + background-color: #1a1a2e; + color: #43b581; + font-family: "Consolas", "Courier New", monospace; + border: 1px solid #d0d4da; + border-radius: 6px; +} + +QWidget#RPCConsole QPlainTextEdit { + background-color: #1a1a2e; + color: #e8e8f0; + font-family: "Consolas", "Courier New", monospace; + border: 1px solid #d0d4da; + border-radius: 6px; +} + +QWidget#RPCConsole QLineEdit#lineEdit { + background-color: #ffffff; + color: #1a1a2e; + font-family: "Consolas", "Courier New", monospace; + border: 1px solid #d0d4da; + border-radius: 6px; + padding: 8px; +} + +/* === Send Page === */ +QFrame#SendCoins { + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #ffffff, stop:1 #f8f9fa); + border: 1px solid #d0d4da; + border-radius: 12px; + padding: 16px; +} + +QFrame#frameCoinControl { + background-color: #f0f2f5; + border: 1px solid #d0d4da; + border-radius: 8px; + padding: 12px; +} + +QFrame#frameFee { + background-color: #f0f2f5; + border: 1px solid #d0d4da; + border-radius: 8px; + padding: 12px; +} + +/* === Receive Page === */ +QFrame#frame2 { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 12px; + padding: 16px; +} + +/* === BitcoinAmountField === */ +QWidget#BitcoinAmountField QLineEdit { + font-size: 15px; + font-weight: bold; +} + +/* === Fee Section === */ +QLabel#labelSmartFee { + font-weight: bold; + font-size: 13px; + color: #2d8f5e; +} + +QLabel#labelSmartFee2, QLabel#labelFeeEstimation { + color: #5a6270; + font-size: 11px; +} + +QLabel#labelBalance { + font-weight: bold; + font-size: 14px; + color: #2d8f5e; +} + +QRadioButton#radioSmartFee:checked, +QRadioButton#radioCustomFee:checked { + background-color: rgba(45, 143, 94, 0.08); + border-radius: 6px; + padding: 4px 8px; +} + +/* === Coin Control Stats === */ +QLabel#labelCoinControlQuantityText, QLabel#labelCoinControlBytesText, +QLabel#labelCoinControlAmountText, QLabel#labelCoinControlFeeText, +QLabel#labelCoinControlAfterFeeText, QLabel#labelCoinControlChangeText { + color: #5a6270; + font-size: 11px; +} + +QLabel#labelCoinControlQuantity, QLabel#labelCoinControlBytes, +QLabel#labelCoinControlAmount, QLabel#labelCoinControlFee, +QLabel#labelCoinControlAfterFee, QLabel#labelCoinControlChange { + font-family: "Consolas", "Courier New", monospace; + color: #1a1a2e; + font-size: 12px; +} + +/* === Send Entry Buttons === */ +QPushButton#addressBookButton, QPushButton#pasteButton { + background-color: transparent; + border: 1px solid #d0d4da; + border-radius: 6px; + padding: 4px; + min-width: 28px; + min-height: 28px; +} + +QPushButton#addressBookButton:hover, QPushButton#pasteButton:hover { + background-color: rgba(45, 143, 94, 0.1); + border-color: #2d8f5e; +} + +QPushButton#deleteButton { + background-color: transparent; + border: 1px solid rgba(208, 64, 64, 0.3); + border-radius: 6px; + padding: 4px; +} + +QPushButton#deleteButton:hover { + background-color: rgba(208, 64, 64, 0.1); + border-color: #d04040; +} + +/* === Receive Page Buttons === */ +QPushButton#showRequestButton { + background-color: #ffffff; + color: #2d8f5e; + border: 1px solid #2d8f5e; + border-radius: 6px; + padding: 6px 16px; + font-weight: bold; +} + +QPushButton#showRequestButton:hover { + background-color: rgba(45, 143, 94, 0.1); +} + +QPushButton#removeRequestButton { + background-color: #ffffff; + color: #d04040; + border: 1px solid rgba(208, 64, 64, 0.4); + border-radius: 6px; + padding: 6px 16px; +} + +QPushButton#removeRequestButton:hover { + background-color: rgba(208, 64, 64, 0.08); + border-color: #d04040; +} + +/* === Address Book === */ +QPushButton#newAddress { + background-color: #2d8f5e; + color: #ffffff; + font-weight: bold; + border: none; + border-radius: 6px; + padding: 8px 16px; +} + +QPushButton#newAddress:hover { + background-color: #248a54; +} + +QPushButton#copyAddress, QPushButton#exportButton { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 6px; + padding: 8px 16px; +} + +QPushButton#copyAddress:hover, QPushButton#exportButton:hover { + background-color: #f0f2f5; + border-color: #2d8f5e; +} + +QPushButton#deleteAddress { + background-color: #ffffff; + color: #d04040; + border: 1px solid rgba(208, 64, 64, 0.3); + border-radius: 6px; + padding: 8px 16px; +} + +QPushButton#deleteAddress:hover { + background-color: rgba(208, 64, 64, 0.08); + border-color: #d04040; +} + +/* === Add/Clear Buttons === */ +QPushButton#addButton { + background-color: #ffffff; + color: #2d8f5e; + border: 1px solid #2d8f5e; + border-radius: 6px; + padding: 8px 16px; + font-weight: bold; +} + +QPushButton#addButton:hover { + background-color: rgba(45, 143, 94, 0.08); +} + +QPushButton#clearButton { + background-color: #ffffff; + color: #5a6270; + border: 1px solid #d0d4da; + border-radius: 6px; + padding: 8px 16px; +} + +QPushButton#clearButton:hover { + background-color: #f0f2f5; +} + +/* === Transaction View === */ +TransactionView QComboBox { + min-height: 28px; +} + +TransactionView QLineEdit { + min-height: 28px; +} + +TransactionView QTableView::item:hover { + background-color: rgba(45, 143, 94, 0.06); +} + +TransactionView QMenu { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 8px; + padding: 4px; +} + +TransactionView QMenu::item:selected { + background-color: rgba(45, 143, 94, 0.1); + color: #2d8f5e; +} + +/* === Coin Control Dialog === */ +CoinControlDialog QTreeWidget { + alternate-background-color: #f8f9fa; +} + +CoinControlDialog QTreeWidget::item:hover { + background-color: rgba(45, 143, 94, 0.06); +} + +CoinControlDialog QTreeWidget::item:selected { + background-color: rgba(45, 143, 94, 0.15); +} + +CoinControlDialog QPushButton#pushButtonSelectAll { + background-color: #ffffff; + color: #2d8f5e; + border: 1px solid #2d8f5e; + border-radius: 6px; + padding: 6px 14px; + font-weight: bold; +} + +CoinControlDialog QPushButton#pushButtonSelectAll:hover { + background-color: rgba(45, 143, 94, 0.08); +} + +QLabel#statusLabel { + color: #d04040; + font-weight: bold; +} + +/* === Date/Time === */ +QDateTimeEdit { + background-color: #ffffff; + border: 1px solid #d0d4da; + border-radius: 6px; + padding: 4px 8px; +} + +QDateTimeEdit:focus { + border-color: #2d8f5e; +} + +QCalendarWidget { + background-color: #ffffff; +} + +QCalendarWidget QAbstractItemView { + selection-background-color: #2d8f5e; + selection-color: #ffffff; + alternate-background-color: #f8f9fa; +} + +/* === Scroll Area === */ +QScrollArea { + background: transparent; + border: none; +} + +QScrollArea > QWidget > QWidget { + background: transparent; +} + +/* === Frames (separators) === */ +QFrame[frameShape="4"], QFrame[frameShape="5"] { + color: #d0d4da; +} + +/* === Slider (Fee) === */ +QSlider::groove:horizontal { + border: none; + height: 6px; + background-color: #e8eaed; + border-radius: 3px; +} + +QSlider::handle:horizontal { + background-color: #2d8f5e; + border: 2px solid #248a54; + width: 16px; + height: 16px; + margin: -6px 0; + border-radius: 9px; +} + +QSlider::handle:horizontal:hover { + background-color: #35a868; +} + +QSlider::sub-page:horizontal { + background-color: #2d8f5e; + border-radius: 3px; +} + +/* === Tooltips === */ +QToolTip { + background-color: #ffffff; + color: #1a1a2e; + border: 1px solid #2d8f5e; + border-radius: 6px; + padding: 6px 10px; + font-size: 12px; +} + +/* === Backup Wizard === */ +QDialog#BackupWizard { + background-color: #f8f9fa; +} + +/* === Peer Map === */ +PeerMapWidget { + background-color: #e8eaed; + border: 1px solid #d0d4da; + border-radius: 8px; +} + +/* === Receive Request Dialog (QR) === */ +QTextEdit#outUri { + background-color: #f0f2f5; + border: 1px solid #d0d4da; + border-radius: 6px; + color: #2d8f5e; + font-family: "Consolas", "Courier New", monospace; + font-size: 12px; + padding: 8px; +} + + +/* === QSplitter Handles === */ +QSplitter::handle { + background-color: transparent; +} +QSplitter::handle:horizontal { + width: 6px; + background-color: transparent; +} +QSplitter::handle:vertical { + height: 6px; + background-color: transparent; +} +QSplitter::handle:hover { + background-color: rgba(45, 143, 94, 0.2); + border-radius: 2px; +} +QSplitter::handle:pressed { + background-color: rgba(45, 143, 94, 0.4); +} diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index 5a1e8eca9..f172dbd9a 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -14,6 +14,8 @@ #include "guiutil.h" #include "platformstyle.h" #include "bantablemodel.h" +#include "peermapwidget.h" +#include "geoip.h" #include "chainparams.h" #include "netbase.h" @@ -385,6 +387,10 @@ RPCConsole::RPCConsole(const PlatformStyle *platformStyle, QWidget *parent) : ui->detailWidget->hide(); ui->peerHeading->setText(tr("Select a peer to view detailed information.")); + // Add Peer Map tab + peerMapWidget = new PeerMapWidget(); + ui->tabWidget->addTab(peerMapWidget, tr("Peer &Map")); + QSettings settings; consoleFontSize = settings.value(fontSizeSettingsKey, QFontInfo(QFont()).pointSize()).toInt(); clear(); @@ -509,6 +515,10 @@ void RPCConsole::setClientModel(ClientModel *model) this, SLOT(peerSelected(const QItemSelection &, const QItemSelection &))); // peer table signal handling - update peer details when new nodes are added to the model connect(model->getPeerTableModel(), SIGNAL(layoutChanged()), this, SLOT(peerLayoutChanged())); + // update peer map when peer list changes + connect(model->getPeerTableModel(), SIGNAL(layoutChanged()), this, SLOT(updatePeerMap())); + // Force initial peer map population + updatePeerMap(); // set up ban table ui->banlistWidget->setModel(model->getBanTableModel()); @@ -1047,3 +1057,37 @@ void RPCConsole::setTabFocus(enum TabTypes tabType) { ui->tabWidget->setCurrentIndex(tabType); } + +void RPCConsole::updatePeerMap() +{ + if (!clientModel || !clientModel->getPeerTableModel() || !peerMapWidget) + return; + + QVector peers; + PeerTableModel *peerTable = clientModel->getPeerTableModel(); + int rowCount = peerTable->getRowByNodeId(-1); // get total count indirectly + + // Iterate through all rows in the peer table + for (int i = 0; ; i++) { + const CNodeCombinedStats *stats = peerTable->getNodeStats(i); + if (!stats) + break; + + PeerMapNode node; + node.address = QString::fromStdString(stats->nodeStats.addrName); + node.subversion = QString::fromStdString(stats->nodeStats.cleanSubVer); + node.pingMs = (stats->nodeStats.dPingTime > 0) ? + (int)(stats->nodeStats.dPingTime * 1000) : -1; + node.bytesIn = stats->nodeStats.nRecvBytes; + node.bytesOut = stats->nodeStats.nSendBytes; + node.isInbound = stats->nodeStats.fInbound; + + QPair coords = GeoIP::lookup(node.address); + node.latitude = coords.first; + node.longitude = coords.second; + + peers.append(node); + } + + peerMapWidget->setPeers(peers); +} diff --git a/src/qt/rpcconsole.h b/src/qt/rpcconsole.h index 5fb6b5f1e..7a7c43e40 100644 --- a/src/qt/rpcconsole.h +++ b/src/qt/rpcconsole.h @@ -7,6 +7,7 @@ #include "guiutil.h" #include "peertablemodel.h" +#include "peermapwidget.h" #include "net.h" @@ -52,7 +53,8 @@ class RPCConsole: public QWidget TAB_INFO = 0, TAB_CONSOLE = 1, TAB_GRAPH = 2, - TAB_PEERS = 3 + TAB_PEERS = 3, + TAB_PEERMAP = 4 }; protected: @@ -109,6 +111,8 @@ public Q_SLOTS: void unbanSelectedNode(); /** set which tab has the focus (is visible) */ void setTabFocus(enum TabTypes tabType); + /** update peer map widget with current peer data */ + void updatePeerMap(); Q_SIGNALS: // For RPC command executor @@ -144,6 +148,7 @@ public Q_SLOTS: int consoleFontSize; QCompleter *autoCompleter; QThread thread; + PeerMapWidget *peerMapWidget; }; #endif // BITCOIN_QT_RPCCONSOLE_H diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index f9f482e23..67d78396d 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -288,7 +288,7 @@ void SendCoinsDialog::on_sendButton_clicked() if(txFee > 0) { // append fee string if a fee is required - questionString.append("
"); + questionString.append("
"); questionString.append(BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), txFee)); questionString.append(" "); questionString.append(tr("added as transaction fee")); @@ -752,7 +752,7 @@ void SendCoinsDialog::coinControlChangeEdited(const QString& text) { // Default to no change address until verified CoinControlDialog::coinControl->destChange = CNoDestination(); - ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:red;}"); + ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:#e05555;}"); const CTxDestination dest = DecodeDestination(text.toStdString()); @@ -763,7 +763,7 @@ void SendCoinsDialog::coinControlChangeEdited(const QString& text) else if (!IsValidDestination(dest)) { // Invalid address - ui->labelCoinControlChangeLabel->setText(tr("Warning: Invalid Bitcoin address")); + ui->labelCoinControlChangeLabel->setText(tr("Warning: Invalid Validity address")); } else { @@ -784,14 +784,14 @@ void SendCoinsDialog::coinControlChangeEdited(const QString& text) else { ui->lineEditCoinControlChange->setText(""); - ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:black;}"); + ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:#e8e8f0;}"); ui->labelCoinControlChangeLabel->setText(""); } } else { // Known change address - ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:black;}"); + ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:#e8e8f0;}"); // Query label QString associatedLabel = model->getAddressTableModel()->labelForAddress(text); diff --git a/src/qt/signverifymessagedialog.cpp b/src/qt/signverifymessagedialog.cpp index b9a151ae2..f0349333f 100644 --- a/src/qt/signverifymessagedialog.cpp +++ b/src/qt/signverifymessagedialog.cpp @@ -120,7 +120,7 @@ void SignVerifyMessageDialog::on_signMessageButton_SM_clicked() CTxDestination destination = DecodeDestination(ui->addressIn_SM->text().toStdString()); if (!IsValidDestination(destination)) { - ui->statusLabel_SM->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel_SM->setStyleSheet("QLabel { color: #e05555; }"); ui->statusLabel_SM->setText(tr("The entered address is invalid.") + QString(" ") + tr("Please check the address and try again.")); return; } @@ -128,7 +128,7 @@ void SignVerifyMessageDialog::on_signMessageButton_SM_clicked() if (!keyID) { ui->addressIn_SM->setValid(false); - ui->statusLabel_SM->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel_SM->setStyleSheet("QLabel { color: #e05555; }"); ui->statusLabel_SM->setText(tr("The entered address does not refer to a key.") + QString(" ") + tr("Please check the address and try again.")); return; } @@ -136,7 +136,7 @@ void SignVerifyMessageDialog::on_signMessageButton_SM_clicked() WalletModel::UnlockContext ctx(model->requestUnlock()); if (!ctx.isValid()) { - ui->statusLabel_SM->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel_SM->setStyleSheet("QLabel { color: #e05555; }"); ui->statusLabel_SM->setText(tr("Wallet unlock was cancelled.")); return; } @@ -144,7 +144,7 @@ void SignVerifyMessageDialog::on_signMessageButton_SM_clicked() CKey key; if (!pwalletMain->GetKey(*keyID, key)) { - ui->statusLabel_SM->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel_SM->setStyleSheet("QLabel { color: #e05555; }"); ui->statusLabel_SM->setText(tr("Private key for the entered address is not available.")); return; } @@ -156,12 +156,12 @@ void SignVerifyMessageDialog::on_signMessageButton_SM_clicked() std::vector vchSig; if (!key.SignCompact(ss.GetHash(), vchSig)) { - ui->statusLabel_SM->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel_SM->setStyleSheet("QLabel { color: #e05555; }"); ui->statusLabel_SM->setText(QString("") + tr("Message signing failed.") + QString("")); return; } - ui->statusLabel_SM->setStyleSheet("QLabel { color: green; }"); + ui->statusLabel_SM->setStyleSheet("QLabel { color: #43b581; }"); ui->statusLabel_SM->setText(QString("") + tr("Message signed.") + QString("")); ui->signatureOut_SM->setText(QString::fromStdString(EncodeBase64(&vchSig[0], vchSig.size()))); @@ -200,14 +200,14 @@ void SignVerifyMessageDialog::on_verifyMessageButton_VM_clicked() CTxDestination destination = DecodeDestination(ui->addressIn_VM->text().toStdString()); if (!IsValidDestination(destination)) { - ui->statusLabel_VM->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel_VM->setStyleSheet("QLabel { color: #e05555; }"); ui->statusLabel_VM->setText(tr("The entered address is invalid.") + QString(" ") + tr("Please check the address and try again.")); return; } if (!boost::get(&destination)) { ui->addressIn_VM->setValid(false); - ui->statusLabel_VM->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel_VM->setStyleSheet("QLabel { color: #e05555; }"); ui->statusLabel_VM->setText(tr("The entered address does not refer to a key.") + QString(" ") + tr("Please check the address and try again.")); return; } @@ -218,7 +218,7 @@ void SignVerifyMessageDialog::on_verifyMessageButton_VM_clicked() if (fInvalid) { ui->signatureIn_VM->setValid(false); - ui->statusLabel_VM->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel_VM->setStyleSheet("QLabel { color: #e05555; }"); ui->statusLabel_VM->setText(tr("The signature could not be decoded.") + QString(" ") + tr("Please check the signature and try again.")); return; } @@ -231,19 +231,19 @@ void SignVerifyMessageDialog::on_verifyMessageButton_VM_clicked() if (!pubkey.RecoverCompact(ss.GetHash(), vchSig)) { ui->signatureIn_VM->setValid(false); - ui->statusLabel_VM->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel_VM->setStyleSheet("QLabel { color: #e05555; }"); ui->statusLabel_VM->setText(tr("The signature did not match the message digest.") + QString(" ") + tr("Please check the signature and try again.")); return; } if (!(CTxDestination(pubkey.GetID()) == destination)) { - ui->statusLabel_VM->setStyleSheet("QLabel { color: red; }"); + ui->statusLabel_VM->setStyleSheet("QLabel { color: #e05555; }"); ui->statusLabel_VM->setText(QString("") + tr("Message verification failed.") + QString("")); return; } - ui->statusLabel_VM->setStyleSheet("QLabel { color: green; }"); + ui->statusLabel_VM->setStyleSheet("QLabel { color: #43b581; }"); ui->statusLabel_VM->setText(QString("") + tr("Message verified.") + QString("")); } diff --git a/src/qt/splashscreen.cpp b/src/qt/splashscreen.cpp index 0d90546d0..04e3e5679 100644 --- a/src/qt/splashscreen.cpp +++ b/src/qt/splashscreen.cpp @@ -1,230 +1,240 @@ -// Copyright (c) 2011-2015 The Bitcoin Core developers -// Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#if defined(HAVE_CONFIG_H) -#include "config/bitcoin-config.h" -#endif - -#include "splashscreen.h" - -#include "networkstyle.h" - -#include "clientversion.h" -#include "init.h" -#include "util.h" -#include "ui_interface.h" -#include "version.h" - -#ifdef ENABLE_WALLET -#include "wallet/wallet.h" -#endif - -#include -#include -#include -#include -#include - -SplashScreen::SplashScreen(Qt::WindowFlags f, const NetworkStyle *networkStyle) : - QWidget(0, f), curAlignment(0) -{ - // set reference point, paddings - int paddingRight = 15; - int paddingTop = 50; - int titleVersionVSpace = 30; - int titleVersionHSpace = -5; - int titleCopyrightVSpace = 50; - int titleHSpace = -35; - int titleCopyrightHSpace = -40; - - float fontFactor = 1.0; - float devicePixelRatio = 1.0; -#if QT_VERSION > 0x050100 - devicePixelRatio = ((QGuiApplication*)QCoreApplication::instance())->devicePixelRatio(); -#endif - - // define text to place - QString titleText = tr(PACKAGE_NAME); - QString versionText = QString(" %1").arg(QString::fromStdString(FormatFullVersion())); - - QString copyrightTextBitcoin = QChar(0xA9)+QString(" %1 ").arg(2009) + QString("The Bitcoin Core developers"); - QString copyrightTextBlackcoin = QChar(0xA9)+QString(" %1 ").arg(2014) + QString("The Blackcoin developers"); - QString copyrightTextBlackmore = QChar(0xA9)+QString(" %1 ").arg(2018) + QString("The Blackcoin More developers"); - QString copyrightTextRadiumcore = QChar(0xA9) + QString(" %1 ").arg(2019) + QString("The Radium Core developers"); - QString copyrightTextValidity = QChar(0xA9) + QString(" %1 ").arg(COPYRIGHT_YEAR) + QString("The Validity developers"); - - // QString copyrightText = QChar(0xA9)+QString(" %1-%2 ").arg(2009).arg(COPYRIGHT_YEAR) + QString::fromStdString(CopyrightHolders()); - - QString titleAddText = networkStyle->getTitleAddText(); - - QString font = QApplication::font().toString(); - - // create a bitmap according to device pixelratio - QSize splashSize(480*devicePixelRatio,320*devicePixelRatio); - pixmap = QPixmap(splashSize); - -#if QT_VERSION > 0x050100 - // change to HiDPI if it makes sense - pixmap.setDevicePixelRatio(devicePixelRatio); -#endif - - QPainter pixPaint(&pixmap); - pixPaint.setPen(QColor(100,100,100)); - - // draw a slightly radial gradient - QRadialGradient gradient(QPoint(0,0), splashSize.width()/devicePixelRatio); - gradient.setColorAt(0, Qt::white); - gradient.setColorAt(1, QColor(247,247,247)); - QRect rGradient(QPoint(0,0), splashSize); - pixPaint.fillRect(rGradient, gradient); - - // draw the bitcoin icon, expected size of PNG: 1024x1024 - QRect rectIcon(QPoint(-95,-65), QSize(345,345)); - - const QSize requiredSize(1024,1024); - QPixmap icon(networkStyle->getAppIcon().pixmap(requiredSize)); - - pixPaint.drawPixmap(rectIcon, icon); - - // check font size and drawing with - pixPaint.setFont(QFont(font, 33*fontFactor)); - QFontMetrics fm = pixPaint.fontMetrics(); - int titleTextWidth = fm.width(titleText); - if (titleTextWidth > 176) { - fontFactor = fontFactor * 176 / titleTextWidth; - } - - pixPaint.setFont(QFont(font, 33*fontFactor)); - fm = pixPaint.fontMetrics(); - titleTextWidth = fm.width(titleText); - pixPaint.drawText(pixmap.width()/devicePixelRatio-titleTextWidth-paddingRight + titleHSpace,paddingTop,titleText); - - pixPaint.setFont(QFont(font, 15*fontFactor)); - - // if the version string is to long, reduce size - fm = pixPaint.fontMetrics(); - int versionTextWidth = fm.width(versionText); - if(versionTextWidth > titleTextWidth+paddingRight-10) { - pixPaint.setFont(QFont(font, 10*fontFactor)); - titleVersionVSpace -= 5; - } - pixPaint.drawText(pixmap.width()/devicePixelRatio-titleTextWidth-paddingRight+titleVersionHSpace,paddingTop+titleVersionVSpace,versionText); - - // draw copyright stuff - { - pixPaint.setFont(QFont(font, 8*fontFactor)); - const int x = pixmap.width()/devicePixelRatio-titleTextWidth-paddingRight+titleCopyrightHSpace; - const int y = paddingTop+titleCopyrightVSpace; - pixPaint.drawText(x,y,copyrightTextBitcoin); - pixPaint.drawText(x,y+12,copyrightTextBlackcoin); - pixPaint.drawText(x,y+24,copyrightTextBlackmore); - pixPaint.drawText(x,y+36, copyrightTextRadiumcore); - pixPaint.drawText(x, y + 48, copyrightTextValidity); - } - - // draw additional text if special network - if(!titleAddText.isEmpty()) { - QFont boldFont = QFont(font, 10*fontFactor); - boldFont.setWeight(QFont::Bold); - pixPaint.setFont(boldFont); - fm = pixPaint.fontMetrics(); - int titleAddTextWidth = fm.width(titleAddText); - pixPaint.drawText(pixmap.width()/devicePixelRatio-titleAddTextWidth-10,15,titleAddText); - } - - pixPaint.end(); - - // Set window title - setWindowTitle(titleText + " " + titleAddText); - - // Resize window and move to center of desktop, disallow resizing - QRect r(QPoint(), QSize(pixmap.size().width()/devicePixelRatio,pixmap.size().height()/devicePixelRatio)); - resize(r.size()); - setFixedSize(r.size()); - move(QApplication::desktop()->screenGeometry().center() - r.center()); - - subscribeToCoreSignals(); -} - -SplashScreen::~SplashScreen() -{ - unsubscribeFromCoreSignals(); -} - -void SplashScreen::slotFinish(QWidget *mainWin) -{ - Q_UNUSED(mainWin); - - /* If the window is minimized, hide() will be ignored. */ - /* Make sure we de-minimize the splashscreen window before hiding */ - if (isMinimized()) - showNormal(); - hide(); - deleteLater(); // No more need for this -} - -static void InitMessage(SplashScreen *splash, const std::string &message) -{ - QMetaObject::invokeMethod(splash, "showMessage", - Qt::QueuedConnection, - Q_ARG(QString, QString::fromStdString(message)), - Q_ARG(int, Qt::AlignBottom|Qt::AlignHCenter), - Q_ARG(QColor, QColor(55,55,55))); -} - -static void ShowProgress(SplashScreen *splash, const std::string &title, int nProgress) -{ - InitMessage(splash, title + strprintf("%d", nProgress) + "%"); -} - -#ifdef ENABLE_WALLET -static void ConnectWallet(SplashScreen *splash, CWallet* wallet) -{ - wallet->ShowProgress.connect(boost::bind(ShowProgress, splash, _1, _2)); -} -#endif - -void SplashScreen::subscribeToCoreSignals() -{ - // Connect signals to client - uiInterface.InitMessage.connect(boost::bind(InitMessage, this, _1)); - uiInterface.ShowProgress.connect(boost::bind(ShowProgress, this, _1, _2)); -#ifdef ENABLE_WALLET - uiInterface.LoadWallet.connect(boost::bind(ConnectWallet, this, _1)); -#endif -} - -void SplashScreen::unsubscribeFromCoreSignals() -{ - // Disconnect signals from client - uiInterface.InitMessage.disconnect(boost::bind(InitMessage, this, _1)); - uiInterface.ShowProgress.disconnect(boost::bind(ShowProgress, this, _1, _2)); -#ifdef ENABLE_WALLET - if(pwalletMain) - pwalletMain->ShowProgress.disconnect(boost::bind(ShowProgress, this, _1, _2)); -#endif -} - -void SplashScreen::showMessage(const QString &message, int alignment, const QColor &color) -{ - curMessage = message; - curAlignment = alignment; - curColor = color; - update(); -} - -void SplashScreen::paintEvent(QPaintEvent *event) -{ - QPainter painter(this); - painter.drawPixmap(0, 0, pixmap); - QRect r = rect().adjusted(5, 5, -5, -5); - painter.setPen(curColor); - painter.drawText(r, curAlignment, curMessage); -} - -void SplashScreen::closeEvent(QCloseEvent *event) -{ - StartShutdown(); // allows an "emergency" shutdown during startup - event->ignore(); -} +// Copyright (c) 2011-2015 The Bitcoin Core developers +// Copyright (c) 2025-2026 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#if defined(HAVE_CONFIG_H) +#include "config/bitcoin-config.h" +#endif + +#include "splashscreen.h" + +#include "networkstyle.h" + +#include "clientversion.h" +#include "init.h" +#include "util.h" +#include "ui_interface.h" +#include "version.h" + +#ifdef ENABLE_WALLET +#include "wallet/wallet.h" +#endif + +#include +#include +#include +#include +#include +#include + +SplashScreen::SplashScreen(Qt::WindowFlags f, const NetworkStyle *networkStyle) : + QWidget(0, f), curAlignment(0) +{ + float devicePixelRatio = 1.0; +#if QT_VERSION > 0x050100 + devicePixelRatio = ((QGuiApplication*)QCoreApplication::instance())->devicePixelRatio(); +#endif + + // Create splash bitmap + QSize splashSize(520*devicePixelRatio, 340*devicePixelRatio); + pixmap = QPixmap(splashSize); + +#if QT_VERSION > 0x050100 + pixmap.setDevicePixelRatio(devicePixelRatio); +#endif + + QPainter pixPaint(&pixmap); + pixPaint.setRenderHint(QPainter::Antialiasing, true); + pixPaint.setRenderHint(QPainter::TextAntialiasing, true); + + int w = splashSize.width() / devicePixelRatio; + int h = splashSize.height() / devicePixelRatio; + + // === Dark cosmic gradient background === + QLinearGradient bgGradient(0, 0, 0, h); + bgGradient.setColorAt(0.0, QColor(10, 10, 26)); // #0a0a1a deep navy + bgGradient.setColorAt(0.5, QColor(15, 15, 36)); // #0f0f24 + bgGradient.setColorAt(1.0, QColor(26, 16, 48)); // #1a1030 dark purple + pixPaint.fillRect(QRect(0, 0, w, h), bgGradient); + + // === Subtle radial glow in center === + QRadialGradient centerGlow(w/2, h/2 - 30, 180); + centerGlow.setColorAt(0.0, QColor(67, 181, 129, 15)); // very faint green + centerGlow.setColorAt(1.0, QColor(67, 181, 129, 0)); + pixPaint.fillRect(QRect(0, 0, w, h), centerGlow); + + // === Draw the app icon (Validity diamond) === + const QSize requiredSize(256, 256); + QPixmap icon(networkStyle->getAppIcon().pixmap(requiredSize)); + int iconSize = 80; + int iconX = (w - iconSize) / 2; + int iconY = 40; + pixPaint.drawPixmap(QRect(iconX, iconY, iconSize, iconSize), icon); + + // === Title: "VAL" in white + "IDITY" in green === + QString font = QApplication::font().toString(); + QFont titleFont(font, 28); + titleFont.setWeight(QFont::Bold); + titleFont.setLetterSpacing(QFont::AbsoluteSpacing, 4); + pixPaint.setFont(titleFont); + + QString titlePart1 = "VAL"; + QString titlePart2 = "IDITY"; + QFontMetrics fm = pixPaint.fontMetrics(); + int totalWidth = fm.width(titlePart1) + fm.width(titlePart2); + int titleX = (w - totalWidth) / 2; + int titleY = iconY + iconSize + 40; + + pixPaint.setPen(QColor(232, 232, 240)); // #e8e8f0 white + pixPaint.drawText(titleX, titleY, titlePart1); + + pixPaint.setPen(QColor(67, 181, 129)); // #43b581 green + pixPaint.drawText(titleX + fm.width(titlePart1), titleY, titlePart2); + + // === Version text === + QString versionText = QString("v%1").arg(QString::fromStdString(FormatFullVersion())); + QFont versionFont(font, 11); + pixPaint.setFont(versionFont); + pixPaint.setPen(QColor(136, 136, 168)); // #8888a8 muted + fm = pixPaint.fontMetrics(); + int versionX = (w - fm.width(versionText)) / 2; + pixPaint.drawText(versionX, titleY + 24, versionText); + + // === Copyright text === + QFont copyrightFont(font, 8); + pixPaint.setFont(copyrightFont); + pixPaint.setPen(QColor(90, 90, 122)); // #5a5a7a dim + fm = pixPaint.fontMetrics(); + + QStringList copyrightLines; + copyrightLines << QChar(0xA9) + QString(" 2009-%1 The Bitcoin Core developers").arg(COPYRIGHT_YEAR); + copyrightLines << QChar(0xA9) + QString(" 2014-2018 The Blackcoin developers"); + copyrightLines << QChar(0xA9) + QString(" 2018-%1 The Blackcoin More developers").arg(COPYRIGHT_YEAR); + copyrightLines << QChar(0xA9) + QString(" 2019-%1 The Radium Core developers").arg(COPYRIGHT_YEAR); + copyrightLines << QChar(0xA9) + QString(" 2020-%1 The Validity developers").arg(COPYRIGHT_YEAR); + + int lineHeight = fm.height() + 2; + int copyrightY = titleY + 40; + for (int i = 0; i < copyrightLines.size(); i++) { + int cx = (w - fm.width(copyrightLines[i])) / 2; + pixPaint.drawText(cx, copyrightY + i * lineHeight, copyrightLines[i]); + } + + // === Bottom accent line === + QLinearGradient lineGradient(w * 0.2, 0, w * 0.8, 0); + lineGradient.setColorAt(0.0, QColor(67, 181, 129, 0)); + lineGradient.setColorAt(0.5, QColor(67, 181, 129, 180)); + lineGradient.setColorAt(1.0, QColor(67, 181, 129, 0)); + pixPaint.setPen(Qt::NoPen); + pixPaint.setBrush(lineGradient); + pixPaint.drawRect(QRectF(w * 0.2, h - 60, w * 0.6, 1)); + + // === Network name for testnet === + QString titleAddText = networkStyle->getTitleAddText(); + if(!titleAddText.isEmpty()) { + QFont boldFont(font, 10); + boldFont.setWeight(QFont::Bold); + pixPaint.setFont(boldFont); + pixPaint.setPen(QColor(67, 181, 129)); + fm = pixPaint.fontMetrics(); + int addX = (w - fm.width(titleAddText)) / 2; + pixPaint.drawText(addX, titleY + 72, titleAddText); + } + + pixPaint.end(); + + // Set window title + QString titleText = tr(PACKAGE_NAME); + setWindowTitle(titleText + " " + titleAddText); + + // Resize and center + QRect r(QPoint(), QSize(pixmap.size().width()/devicePixelRatio, pixmap.size().height()/devicePixelRatio)); + resize(r.size()); + setFixedSize(r.size()); + move(QApplication::desktop()->screenGeometry().center() - r.center()); + + subscribeToCoreSignals(); +} + +SplashScreen::~SplashScreen() +{ + unsubscribeFromCoreSignals(); +} + +void SplashScreen::slotFinish(QWidget *mainWin) +{ + Q_UNUSED(mainWin); + if (isMinimized()) + showNormal(); + hide(); + deleteLater(); +} + +static void InitMessage(SplashScreen *splash, const std::string &message) +{ + QMetaObject::invokeMethod(splash, "showMessage", + Qt::QueuedConnection, + Q_ARG(QString, QString::fromStdString(message)), + Q_ARG(int, Qt::AlignBottom|Qt::AlignHCenter), + Q_ARG(QColor, QColor(136, 136, 168))); // #8888a8 muted text +} + +static void ShowProgress(SplashScreen *splash, const std::string &title, int nProgress) +{ + InitMessage(splash, title + strprintf("%d", nProgress) + "%"); +} + +#ifdef ENABLE_WALLET +static void ConnectWallet(SplashScreen *splash, CWallet* wallet) +{ + wallet->ShowProgress.connect(boost::bind(ShowProgress, splash, _1, _2)); +} +#endif + +void SplashScreen::subscribeToCoreSignals() +{ + uiInterface.InitMessage.connect(boost::bind(InitMessage, this, _1)); + uiInterface.ShowProgress.connect(boost::bind(ShowProgress, this, _1, _2)); +#ifdef ENABLE_WALLET + uiInterface.LoadWallet.connect(boost::bind(ConnectWallet, this, _1)); +#endif +} + +void SplashScreen::unsubscribeFromCoreSignals() +{ + uiInterface.InitMessage.disconnect(boost::bind(InitMessage, this, _1)); + uiInterface.ShowProgress.disconnect(boost::bind(ShowProgress, this, _1, _2)); +#ifdef ENABLE_WALLET + if(pwalletMain) + pwalletMain->ShowProgress.disconnect(boost::bind(ShowProgress, this, _1, _2)); +#endif +} + +void SplashScreen::showMessage(const QString &message, int alignment, const QColor &color) +{ + curMessage = message; + curAlignment = alignment; + curColor = color; + update(); +} + +void SplashScreen::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + painter.drawPixmap(0, 0, pixmap); + + // Draw loading message at the bottom + QRect r = rect().adjusted(20, 5, -20, -12); + painter.setPen(curColor); + QFont msgFont = painter.font(); + msgFont.setPixelSize(11); + painter.setFont(msgFont); + painter.drawText(r, curAlignment, curMessage); +} + +void SplashScreen::closeEvent(QCloseEvent *event) +{ + StartShutdown(); + event->ignore(); +} diff --git a/src/qt/stakingchartwidget.cpp b/src/qt/stakingchartwidget.cpp new file mode 100644 index 000000000..e3c73141c --- /dev/null +++ b/src/qt/stakingchartwidget.cpp @@ -0,0 +1,258 @@ +// Copyright (c) 2025-2026 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "stakingchartwidget.h" +#include "bitcoinunits.h" + +#include +#include +#include +#include +#include + +static const int BAR_SPACING = 2; +static const int CHART_PADDING_LEFT = 60; +static const int CHART_PADDING_RIGHT = 10; +static const int CHART_PADDING_TOP = 10; +static const int CHART_PADDING_BOTTOM = 30; + +static const QColor COLOR_BAR_NORMAL(67, 181, 129); // #43b581 Validity green +static const QColor COLOR_BAR_HOVER(78, 204, 146); // #4ecc92 brighter green on hover +static const QColor COLOR_BAR_ZERO(67, 181, 129, 30); // very faint for zero-amount days +static const QColor COLOR_GRID(255, 255, 255, 12); // ultra-subtle grid lines +static const QColor COLOR_AXIS_TEXT(136, 136, 168); // #8888a8 muted text + +StakingChartWidget::StakingChartWidget(QWidget *parent) : + QWidget(parent), + displayUnit(0), + hoveredBar(-1), + cacheDirty(true), + cachedHoverBar(-1) +{ + setMouseTracking(true); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +void StakingChartWidget::setData(const QVector &data) +{ + chartData = data; + hoveredBar = -1; + cacheDirty = true; + update(); +} + +void StakingChartWidget::setUnit(int unit) +{ + displayUnit = unit; + cacheDirty = true; + update(); +} + +QSize StakingChartWidget::minimumSizeHint() const +{ + return QSize(200, 120); +} + +QSize StakingChartWidget::sizeHint() const +{ + return QSize(400, 180); +} + +void StakingChartWidget::resizeEvent(QResizeEvent *event) +{ + cacheDirty = true; + QWidget::resizeEvent(event); +} + +QRect StakingChartWidget::getBarRect(int index, int chartLeft, int chartTop, int chartWidth, int chartHeight, CAmount maxAmount) const +{ + if (chartData.isEmpty() || maxAmount <= 0) + return QRect(); + + int numBars = chartData.size(); + double barWidth = (double)(chartWidth - (numBars - 1) * BAR_SPACING) / numBars; + if (barWidth < 1) barWidth = 1; + + int x = chartLeft + (int)(index * (barWidth + BAR_SPACING)); + double ratio = (double)chartData[index].amount / (double)maxAmount; + int barHeight = (int)(ratio * chartHeight); + if (chartData[index].amount > 0 && barHeight < 2) + barHeight = 2; // minimum visible height for non-zero + + int y = chartTop + chartHeight - barHeight; + + return QRect(x, y, (int)barWidth, barHeight); +} + +void StakingChartWidget::rebuildCache() +{ + int w = width(); + int h = height(); + if (w <= 0 || h <= 0) return; + + paintCache = QPixmap(w, h); + paintCache.fill(Qt::transparent); + + QPainter painter(&paintCache); + painter.setRenderHint(QPainter::Antialiasing, true); + + int chartLeft = CHART_PADDING_LEFT; + int chartTop = CHART_PADDING_TOP; + int chartWidth = w - CHART_PADDING_LEFT - CHART_PADDING_RIGHT; + int chartHeight = h - CHART_PADDING_TOP - CHART_PADDING_BOTTOM; + + if (chartWidth <= 0 || chartHeight <= 0) + return; + + // Find max amount for scaling + CAmount maxAmount = 0; + for (int i = 0; i < chartData.size(); i++) { + if (chartData[i].amount > maxAmount) + maxAmount = chartData[i].amount; + } + + // If no data or all zeros, show empty state + if (chartData.isEmpty() || maxAmount == 0) { + painter.setPen(COLOR_AXIS_TEXT); + QFont emptyFont = painter.font(); + emptyFont.setPixelSize(12); + painter.setFont(emptyFont); + painter.drawText(QRect(0, 0, w, h), Qt::AlignCenter, "No staking rewards in the last 30 days"); + cacheDirty = false; + cachedHoverBar = hoveredBar; + return; + } + + // Add 10% headroom + maxAmount = (CAmount)(maxAmount * 1.1); + + // Draw horizontal grid lines (4 lines) + painter.setPen(QPen(COLOR_GRID, 1)); + QFont axisFont = painter.font(); + axisFont.setPixelSize(10); + painter.setFont(axisFont); + + for (int i = 0; i <= 4; i++) { + int y = chartTop + (int)((double)i / 4.0 * chartHeight); + painter.setPen(QPen(COLOR_GRID, 1, Qt::DotLine)); + painter.drawLine(chartLeft, y, chartLeft + chartWidth, y); + + // Y-axis label + CAmount labelAmount = (CAmount)((double)(4 - i) / 4.0 * maxAmount); + QString label = BitcoinUnits::format(displayUnit, labelAmount, false, BitcoinUnits::separatorAlways, 2); + painter.setPen(COLOR_AXIS_TEXT); + painter.drawText(0, y - 7, chartLeft - 5, 14, Qt::AlignRight | Qt::AlignVCenter, label); + } + + // Draw bars + int numBars = chartData.size(); + double barWidth = (double)(chartWidth - (numBars - 1) * BAR_SPACING) / numBars; + if (barWidth < 1) barWidth = 1; + + for (int i = 0; i < numBars; i++) { + QRect barRect = getBarRect(i, chartLeft, chartTop, chartWidth, chartHeight, maxAmount); + + QColor barColor; + if (chartData[i].amount == 0) { + barRect = QRect(barRect.x(), chartTop + chartHeight - 1, (int)barWidth, 1); + barColor = COLOR_BAR_ZERO; + } else if (i == hoveredBar) { + barColor = COLOR_BAR_HOVER; + } else { + barColor = COLOR_BAR_NORMAL; + } + + // Rounded top corners + QPainterPath path; + int radius = barWidth > 6 ? 3 : (barWidth > 3 ? 1 : 0); + if (barRect.height() > radius * 2) { + path.moveTo(barRect.bottomLeft()); + path.lineTo(barRect.left(), barRect.top() + radius); + path.quadTo(barRect.topLeft(), QPointF(barRect.left() + radius, barRect.top())); + path.lineTo(barRect.right() - radius, barRect.top()); + path.quadTo(barRect.topRight(), QPointF(barRect.right(), barRect.top() + radius)); + path.lineTo(barRect.bottomRight()); + path.closeSubpath(); + } else { + path.addRect(barRect); + } + + painter.fillPath(path, barColor); + + // X-axis date labels + int labelEvery = numBars > 15 ? 5 : (numBars > 7 ? 3 : 1); + if (i % labelEvery == 0 || i == numBars - 1) { + painter.setPen(COLOR_AXIS_TEXT); + int labelX = barRect.left(); + int labelW = (int)(barWidth * labelEvery); + if (i == numBars - 1) labelW = (int)barWidth + CHART_PADDING_RIGHT; + painter.drawText(labelX, chartTop + chartHeight + 4, labelW, 20, + Qt::AlignLeft | Qt::AlignTop, chartData[i].label); + } + } + + cacheDirty = false; + cachedHoverBar = hoveredBar; +} + +void StakingChartWidget::paintEvent(QPaintEvent *) +{ + // Only rebuild cache when data, size, or hover state changed + if (cacheDirty || cachedHoverBar != hoveredBar || paintCache.size() != size()) { + rebuildCache(); + } + + QPainter painter(this); + if (!paintCache.isNull()) { + painter.drawPixmap(0, 0, paintCache); + } +} + +void StakingChartWidget::mouseMoveEvent(QMouseEvent *event) +{ + if (chartData.isEmpty()) return; + + CAmount maxAmount = 0; + for (int i = 0; i < chartData.size(); i++) { + if (chartData[i].amount > maxAmount) + maxAmount = chartData[i].amount; + } + if (maxAmount == 0) return; + + int chartLeft = CHART_PADDING_LEFT; + int chartWidth = width() - CHART_PADDING_LEFT - CHART_PADDING_RIGHT; + + int newHover = -1; + int numBars = chartData.size(); + double barWidth = (double)(chartWidth - (numBars - 1) * BAR_SPACING) / numBars; + + int mouseX = event->pos().x(); + for (int i = 0; i < numBars; i++) { + int x = chartLeft + (int)(i * (barWidth + BAR_SPACING)); + if (mouseX >= x && mouseX < x + (int)barWidth) { + newHover = i; + break; + } + } + + if (newHover != hoveredBar) { + hoveredBar = newHover; + update(); // will rebuild cache with new hover bar + + if (hoveredBar >= 0 && hoveredBar < chartData.size()) { + QString tip = QString("%1\n%2") + .arg(chartData[hoveredBar].label) + .arg(BitcoinUnits::formatWithUnit(displayUnit, chartData[hoveredBar].amount, false, BitcoinUnits::separatorAlways, 2)); + QToolTip::showText(mapToGlobal(event->pos()), tip, this); + } + } +} + +void StakingChartWidget::leaveEvent(QEvent *) +{ + if (hoveredBar != -1) { + hoveredBar = -1; + update(); + } +} diff --git a/src/qt/stakingchartwidget.h b/src/qt/stakingchartwidget.h new file mode 100644 index 000000000..c8cad3170 --- /dev/null +++ b/src/qt/stakingchartwidget.h @@ -0,0 +1,53 @@ +// Copyright (c) 2025-2026 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_STAKINGCHARTWIDGET_H +#define BITCOIN_QT_STAKINGCHARTWIDGET_H + +#include "amount.h" + +#include +#include +#include +#include + +struct StakeDayData { + QString label; // e.g. "Feb 25" + CAmount amount; // satoshis +}; + +class StakingChartWidget : public QWidget +{ + Q_OBJECT + +public: + explicit StakingChartWidget(QWidget *parent = 0); + + void setData(const QVector &data); + void setUnit(int unit); + + QSize minimumSizeHint() const; + QSize sizeHint() const; + +protected: + void paintEvent(QPaintEvent *event); + void resizeEvent(QResizeEvent *event); + void mouseMoveEvent(QMouseEvent *event); + void leaveEvent(QEvent *event); + +private: + QVector chartData; + int displayUnit; + int hoveredBar; + + // Paint cache — avoids full repaint on every frame + QPixmap paintCache; + bool cacheDirty; + int cachedHoverBar; + void rebuildCache(); + + QRect getBarRect(int index, int chartLeft, int chartTop, int chartWidth, int chartHeight, CAmount maxAmount) const; +}; + +#endif // BITCOIN_QT_STAKINGCHARTWIDGET_H diff --git a/src/qt/thememanager.cpp b/src/qt/thememanager.cpp new file mode 100644 index 000000000..2e0e05d89 --- /dev/null +++ b/src/qt/thememanager.cpp @@ -0,0 +1,93 @@ +// Copyright (c) 2025 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "thememanager.h" + +#include +#include +#include +#include +#include + +ThemeManager::ThemeManager(QObject *parent) : + QObject(parent), + activeTheme(Dark) +{ +} + +void ThemeManager::setTheme(Theme theme) +{ + activeTheme = theme; + + QString qss; + if (theme == Dark) { + qss = loadStyleSheet(":/themes/dark"); + } else { + qss = loadStyleSheet(":/themes/light"); + } + + qApp->setStyleSheet(qss); + + // Update application palette to match theme (for PlatformStyle and palette-aware code) + QPalette pal = qApp->palette(); + if (theme == Dark) { + pal.setColor(QPalette::Window, QColor(15, 15, 36)); // #0f0f24 + pal.setColor(QPalette::WindowText, QColor(232, 232, 240)); // #e8e8f0 + pal.setColor(QPalette::Base, QColor(10, 10, 26)); // #0a0a1a + pal.setColor(QPalette::AlternateBase, QColor(17, 17, 40)); // #111128 + pal.setColor(QPalette::Text, QColor(232, 232, 240)); // #e8e8f0 + pal.setColor(QPalette::Button, QColor(26, 26, 58)); // #1a1a3a + pal.setColor(QPalette::ButtonText, QColor(232, 232, 240)); // #e8e8f0 + pal.setColor(QPalette::Highlight, QColor(67, 181, 129)); // #43b581 + pal.setColor(QPalette::HighlightedText, QColor(255, 255, 255)); + pal.setColor(QPalette::ToolTipBase, QColor(21, 21, 48)); // #151530 + pal.setColor(QPalette::ToolTipText, QColor(232, 232, 240)); // #e8e8f0 + pal.setColor(QPalette::Disabled, QPalette::Text, QColor(90, 90, 122)); // #5a5a7a + pal.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(90, 90, 122)); + } else { + // Reset to default system palette for light theme + pal = QPalette(); + } + qApp->setPalette(pal); + + // Save preference + QSettings settings; + settings.setValue("nTheme", static_cast(theme)); + + Q_EMIT themeChanged(static_cast(theme)); +} + +void ThemeManager::loadSavedTheme() +{ + QSettings settings; + int savedTheme = settings.value("nTheme", static_cast(Dark)).toInt(); + if (savedTheme == Light || savedTheme == Dark) { + setTheme(static_cast(savedTheme)); + } else { + setTheme(Dark); + } +} + +QString ThemeManager::loadStyleSheet(const QString &filename) const +{ + QFile file(filename); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + return QString(); + } + return file.readAll(); +} + +QStringList ThemeManager::availableThemes() +{ + return QStringList() << themeName(Light) << themeName(Dark); +} + +QString ThemeManager::themeName(Theme theme) +{ + switch (theme) { + case Light: return "Light"; + case Dark: return "Dark"; + default: return "Dark"; + } +} diff --git a/src/qt/thememanager.h b/src/qt/thememanager.h new file mode 100644 index 000000000..1b51ccd46 --- /dev/null +++ b/src/qt/thememanager.h @@ -0,0 +1,53 @@ +// Copyright (c) 2025 The Validity developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_THEMEMANAGER_H +#define BITCOIN_QT_THEMEMANAGER_H + +#include +#include + +class QApplication; + +class ThemeManager : public QObject +{ + Q_OBJECT + +public: + enum Theme { + Light = 0, + Dark = 1 + }; + + explicit ThemeManager(QObject *parent = 0); + + /** Apply a theme to the application */ + void setTheme(Theme theme); + + /** Get the currently active theme */ + Theme currentTheme() const { return activeTheme; } + + /** Load saved theme preference from QSettings */ + void loadSavedTheme(); + + /** Check if dark theme is active */ + bool isDark() const { return activeTheme == Dark; } + + /** Get list of available theme names for UI */ + static QStringList availableThemes(); + + /** Convert theme enum to display name */ + static QString themeName(Theme theme); + +Q_SIGNALS: + void themeChanged(int theme); + +private: + Theme activeTheme; + + /** Load QSS content from resource file */ + QString loadStyleSheet(const QString &filename) const; +}; + +#endif // BITCOIN_QT_THEMEMANAGER_H diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index 17b94b821..069405d4b 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -27,6 +27,8 @@ #include #include +#include +#include // Amount column is right-aligned it contains numbers static int column_alignments[] = { @@ -76,9 +78,11 @@ class TransactionTablePriv /* Query entire wallet anew from core. */ + // Background-loaded results (written by worker thread, read by UI thread after completion) + QList backgroundResult; + void refreshWallet() { - qDebug() << "TransactionTablePriv::refreshWallet"; cachedWallet.clear(); { LOCK2(cs_main, wallet->cs_wallet); @@ -90,6 +94,46 @@ class TransactionTablePriv } } + void refreshWalletBackground() + { + // Runs in a worker thread. + // CRITICAL: Use chunked locking so the UI thread can acquire + // cs_main/cs_wallet between batches (otherwise UI freezes + // waiting for locks held by this thread for 10+ seconds). + + // Pass 1: collect transaction hashes (fast, brief lock) + std::vector txHashes; + { + LOCK2(cs_main, wallet->cs_wallet); + for(std::map::iterator it = wallet->mapWallet.begin(); it != wallet->mapWallet.end(); ++it) + { + if(TransactionRecord::showTransaction(it->second)) + txHashes.push_back(it->first); + } + } + + // Pass 2: decompose in small batches, releasing locks between + QList result; + const size_t BATCH_SIZE = 200; + for(size_t i = 0; i < txHashes.size(); i += BATCH_SIZE) + { + { + LOCK2(cs_main, wallet->cs_wallet); + size_t end = std::min(i + BATCH_SIZE, txHashes.size()); + for(size_t j = i; j < end; j++) + { + std::map::iterator it = wallet->mapWallet.find(txHashes[j]); + if(it != wallet->mapWallet.end()) + result.append(TransactionRecord::decomposeTransaction(wallet, it->second)); + } + } + // Yield so UI/staking threads can acquire the locks + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + backgroundResult = result; + } + /* Update our model of the wallet incrementally, to synchronize our model of the wallet with that of the core. @@ -246,10 +290,17 @@ TransactionTableModel::TransactionTableModel(const PlatformStyle *platformStyle, walletModel(parent), priv(new TransactionTablePriv(wallet, this)), fProcessingQueuedTransactions(false), - platformStyle(platformStyle) + platformStyle(platformStyle), + backgroundLoadDone(false) { columns << QString() << QString() << tr("Date") << tr("Type") << tr("Label") << BitcoinUnits::getAmountColumnTitle(walletModel->getOptionsModel()->getDisplayUnit()); - priv->refreshWallet(); + + // Load all 36K+ wallet transactions in a background thread + // so the UI stays responsive during startup + std::thread([this]() { + priv->refreshWalletBackground(); + QMetaObject::invokeMethod(this, "onBackgroundRefreshComplete", Qt::QueuedConnection); + }).detach(); connect(walletModel->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit())); connect(this, SIGNAL(message(QString,QString,unsigned int)), walletModel, SIGNAL(message(QString,QString,unsigned int))); @@ -272,6 +323,10 @@ void TransactionTableModel::updateAmountColumnTitle() void TransactionTableModel::updateTransaction(const QString &hash, int status, bool showTransaction) { + // Skip incremental updates while background load is in progress + if (!backgroundLoadDone) + return; + uint256 updated; updated.SetHex(hash.toStdString()); @@ -280,6 +335,10 @@ void TransactionTableModel::updateTransaction(const QString &hash, int status, b void TransactionTableModel::updateConfirmations() { + // Skip while background load is in progress + if (!backgroundLoadDone) + return; + // Blocks came in since last poll. // Invalidate status (number of confirmations) and (possibly) description // for all rows. Qt is smart enough to only actually request the data for the @@ -717,6 +776,16 @@ void TransactionTableModel::updateDisplayUnit() Q_EMIT dataChanged(index(0, Amount), index(priv->size()-1, Amount)); } +void TransactionTableModel::onBackgroundRefreshComplete() +{ + // Swap in the background-loaded data + beginResetModel(); + priv->cachedWallet = priv->backgroundResult; + priv->backgroundResult.clear(); + backgroundLoadDone = true; + endResetModel(); +} + // queue notifications to show a non freezing progress dialog e.g. for rescan struct TransactionNotification { diff --git a/src/qt/transactiontablemodel.h b/src/qt/transactiontablemodel.h index 048b64451..abc1c96fd 100644 --- a/src/qt/transactiontablemodel.h +++ b/src/qt/transactiontablemodel.h @@ -9,6 +9,7 @@ #include #include +#include class PlatformStyle; class TransactionRecord; @@ -88,6 +89,7 @@ class TransactionTableModel : public QAbstractTableModel TransactionTablePriv *priv; bool fProcessingQueuedTransactions; const PlatformStyle *platformStyle; + std::atomic backgroundLoadDone; void subscribeToCoreSignals(); void unsubscribeFromCoreSignals(); @@ -113,6 +115,8 @@ public Q_SLOTS: void updateTransaction(const QString &hash, int status, bool showTransaction); void updateConfirmations(); void updateDisplayUnit(); + /** Called when background wallet refresh completes */ + void onBackgroundRefreshComplete(); /** Updates the column title to "Amount (DisplayUnit)" and emits headerDataChanged() signal for table headers to react. */ void updateAmountColumnTitle(); /* Needed to update fProcessingQueuedTransactions through a QueuedConnection */ diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index 5ccf33256..04d922a2a 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -165,6 +165,13 @@ void WalletFrame::backupWallet() walletView->backupWallet(); } +void WalletFrame::backupWizard() +{ + WalletView *walletView = currentWalletView(); + if (walletView) + walletView->backupWizard(); +} + void WalletFrame::changePassphrase() { WalletView *walletView = currentWalletView(); diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index 65cde1e16..773009725 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -75,6 +75,8 @@ public Q_SLOTS: void encryptWallet(bool status); /** Backup the wallet */ void backupWallet(); + /** Open backup wizard */ + void backupWizard(); /** Change encrypted wallet passphrase */ void changePassphrase(); /** Ask for passphrase to unlock wallet temporarily */ diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index 3983fff2d..003d7fb05 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -6,6 +6,7 @@ #include "addressbookpage.h" #include "askpassphrasedialog.h" +#include "backupwizard.h" #include "bitcoingui.h" #include "clientmodel.h" #include "guiutil.h" @@ -23,6 +24,7 @@ #include #include +#include #include #include #include @@ -35,7 +37,7 @@ WalletView::WalletView(const PlatformStyle *_platformStyle, const Config *cfg, Q walletModel(0), platformStyle(_platformStyle) { - // Create tabs + // Create all tabs (widgets are cheap; the heavy part is setModel) overviewPage = new OverviewPage(platformStyle); transactionsPage = new QWidget(this); @@ -80,6 +82,20 @@ WalletView::WalletView(const PlatformStyle *_platformStyle, const Config *cfg, Q connect(transactionView, SIGNAL(message(QString,QString,unsigned int)), this, SIGNAL(message(QString,QString,unsigned int))); } +void WalletView::deferredModelLoad() +{ + if (!walletModel) + return; + + // Set the heavy transaction model on the history view + if (transactionView) + transactionView->setModel(walletModel); + + // Connect balloon pop-up for new transactions (triggers model creation if not already done) + connect(walletModel->getTransactionTableModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(processNewTransaction(QModelIndex,int,int))); +} + WalletView::~WalletView() { } @@ -100,7 +116,7 @@ void WalletView::setBitcoinGUI(BitcoinGUI *gui) // Pass through transaction notifications connect(this, SIGNAL(incomingTransaction(QString,int,CAmount,QString,QString,QString)), gui, SLOT(incomingTransaction(QString,int,CAmount,QString,QString,QString))); - // Connect HD enabled state signal + // Connect HD enabled state signal connect(this, SIGNAL(hdEnabledStatusChanged(int)), gui, SLOT(setHDStatus(int))); } } @@ -117,13 +133,15 @@ void WalletView::setWalletModel(WalletModel *walletModel) { this->walletModel = walletModel; - // Put transaction list in tabs - transactionView->setModel(walletModel); + // Set models on lightweight pages immediately overviewPage->setWalletModel(walletModel); receiveCoinsPage->setModel(walletModel); sendCoinsPage->setModel(walletModel); - usedReceivingAddressesPage->setModel(walletModel->getAddressTableModel()); - usedSendingAddressesPage->setModel(walletModel->getAddressTableModel()); + usedReceivingAddressesPage->setModel(walletModel ? walletModel->getAddressTableModel() : 0); + usedSendingAddressesPage->setModel(walletModel ? walletModel->getAddressTableModel() : 0); + + // Defer the heavy transaction table model load so the dashboard renders first + QTimer::singleShot(750, this, SLOT(deferredModelLoad())); if (walletModel) { @@ -137,9 +155,8 @@ void WalletView::setWalletModel(WalletModel *walletModel) // update HD status Q_EMIT hdEnabledStatusChanged(walletModel->hdEnabled()); - // Balloon pop-up for new transaction - connect(walletModel->getTransactionTableModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), - this, SLOT(processNewTransaction(QModelIndex,int,int))); + // NOTE: getTransactionTableModel() connection moved to deferredModelLoad() + // to avoid triggering the full wallet scan during startup // Ask for passphrase if needed connect(walletModel, SIGNAL(requireUnlock()), this, SLOT(unlockWallet())); @@ -262,6 +279,12 @@ void WalletView::backupWallet() } } +void WalletView::backupWizard() +{ + BackupWizard wizard(walletModel, this); + wizard.exec(); +} + void WalletView::changePassphrase() { AskPassphraseDialog dlg(AskPassphraseDialog::ChangePass, this); diff --git a/src/qt/walletview.h b/src/qt/walletview.h index 816812a69..001621225 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -71,6 +71,9 @@ class WalletView : public QStackedWidget QProgressDialog *progressDialog; const PlatformStyle *platformStyle; +private Q_SLOTS: + void deferredModelLoad(); + public Q_SLOTS: /** Switch to overview (home) page */ void gotoOverviewPage(); @@ -95,6 +98,8 @@ public Q_SLOTS: void encryptWallet(bool status); /** Backup the wallet */ void backupWallet(); + /** Open backup wizard */ + void backupWizard(); /** Change encrypted wallet passphrase */ void changePassphrase(); /** Ask for passphrase to unlock wallet temporarily */