Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ bitcoin_safe.dist-info
*.xcf

screenshots*/
.codex-artifacts/
.DS_Store

profile.html
Expand Down
8 changes: 5 additions & 3 deletions bitcoin_safe/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@
from PyQt6.QtWidgets import QApplication # noqa: E402

from bitcoin_safe.compatibility import check_compatibility # noqa: E402
from bitcoin_safe.gnome_darkmode import is_gnome_dark_mode, set_dark_palette # noqa: E402
from bitcoin_safe.config import UserConfig # noqa: E402
from bitcoin_safe.gui.qt.main import MainWindow # noqa: E402
from bitcoin_safe.gui.qt.startup_window_probe import StartupWindowProbe # noqa: E402
from bitcoin_safe.gui.qt.util import custom_exception_handler # noqa: E402
from bitcoin_safe.theme import apply_theme_mode # noqa: E402

logger = logging.getLogger(__name__)
NETWORK_ARG_NAMES = tuple(sorted(network.name.lower() for network in bdk.Network))
Expand Down Expand Up @@ -130,11 +131,12 @@ def main(args: StartupArgs | None = None) -> None:

check_compatibility()

if is_gnome_dark_mode():
set_dark_palette(app)
config = UserConfig.from_file() if UserConfig.exists() else None
apply_theme_mode(app, config.theme_mode if config else UserConfig().theme_mode)

window = MainWindow(
network=startup_args.network,
config=config,
open_files_at_startup=startup_args.open_files_at_startup,
)
if startup_args.trace_startup_windows:
Expand Down
5 changes: 4 additions & 1 deletion bitcoin_safe/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from bitcoin_safe.fx_types import FXProvider
from bitcoin_safe.gui.qt.unique_deque import UniqueDeque
from bitcoin_safe.pythonbdk_types import BlockchainType
from bitcoin_safe.theme import ThemeMode
from bitcoin_safe.util import current_project_dir

from .execute_config import DEFAULT_LANG_CODE, DEFAULT_MAINNET
Expand Down Expand Up @@ -98,8 +99,9 @@ class UserConfig(BaseSaveableClass):
BitcoinSymbol.__name__: BitcoinSymbol,
BtcPayInvoiceDetails.__name__: BtcPayInvoiceDetails,
FXProvider.__name__: FXProvider,
ThemeMode.__name__: ThemeMode,
}
VERSION = "0.3.8"
VERSION = "0.3.9"

app_name = "bitcoin_safe"
locales_path = current_project_dir() / "gui" / "locales"
Expand Down Expand Up @@ -130,6 +132,7 @@ def __init__(self) -> None:
self.language_code: str = DEFAULT_LANG_CODE
self.currency: str = "USD"
self.bitcoin_symbol: BitcoinSymbol = BitcoinSymbol.ISO
self.theme_mode: ThemeMode = ThemeMode.SYSTEM
self.rates: dict[str, dict[str, Any]] = {}
self.historical_rates: dict[float, dict[str, float]] = {}
self.last_tab_title: list[str] = []
Expand Down
12 changes: 4 additions & 8 deletions bitcoin_safe/gui/qt/address_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from bitcoin_safe_lib.util_os import webopen
from PyQt6 import QtCore, QtGui
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QMessageBox, QSizePolicy
from PyQt6.QtWidgets import QApplication, QMessageBox, QSizePolicy

from bitcoin_safe.gui.qt.analyzers import AddressAnalyzer
from bitcoin_safe.gui.qt.buttonedit import ButtonEdit, SquareButton
Expand Down Expand Up @@ -212,17 +212,13 @@ def get_color(is_change: bool) -> QtGui.QColor:

def format_address_field(self, wallet: Wallet | None) -> None:
"""Format address field."""
palette = QtGui.QPalette()
background_color = None

background_color = None
palette = QtGui.QPalette(QApplication.palette())
if wallet:
background_color = self.color_address(
self.address, wallet, wallet_signals=self.wallet_functions.wallet_signals[wallet.id]
)

if background_color:
palette.setColor(QtGui.QPalette.ColorRole.Base, background_color)
if background_color:
palette.setColor(QtGui.QPalette.ColorRole.Base, background_color)

self.input_field.setPalette(palette)
self.input_field.update()
Expand Down
1 change: 1 addition & 0 deletions bitcoin_safe/gui/qt/address_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ def update_content(self) -> None:
"""Update content."""
if self.maybe_defer_update():
return
ColorScheme.update_from_widget(self)
logger.debug(f"{self.__class__.__name__} update")
self._before_update_content()

Expand Down
29 changes: 25 additions & 4 deletions bitcoin_safe/gui/qt/buttonedit.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from bitcoin_qr_tools.gui.bitcoin_video_widget import BitcoinVideoWidget
from bitcoin_safe_lib.gui.qt.signal_tracker import SignalProtocol, SignalTools, SignalTracker
from bitcoin_safe_lib.gui.qt.util import question_dialog
from PyQt6.QtCore import QObject, QSize, Qt, pyqtSignal
from PyQt6.QtCore import QEvent, QObject, QSize, Qt, pyqtSignal
from PyQt6.QtGui import QIcon, QResizeEvent, QTextCharFormat
from PyQt6.QtWidgets import (
QApplication,
Expand All @@ -61,19 +61,40 @@
AnalyzerState,
AnalyzerTextEdit,
)
from bitcoin_safe.gui.qt.util import Message, MessageType, clear_layout, do_copy, get_icon_path, svg_tools
from bitcoin_safe.gui.qt.util import (
Message,
MessageType,
clear_layout,
do_copy,
get_icon_path,
should_process_theme_change,
svg_tools,
)
from bitcoin_safe.i18n import translate

logger = logging.getLogger(__name__)


class SquareButton(QToolButton):
def __init__(self, qicon: QIcon, parent) -> None:
def __init__(self, qicon: QIcon | str, parent) -> None:
"""Initialize instance."""
super().__init__(parent)
self._icon_name: str | None = None
self._icon: QIcon | None = None
self.setIcon(qicon)
self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)

def setIcon(self, icon: QIcon | str) -> None:
self._icon_name = icon if isinstance(icon, str) else None
self._icon = icon if isinstance(icon, QIcon) else None
effective_icon = svg_tools.get_QIcon(self._icon_name) if self._icon_name else self._icon
super().setIcon(effective_icon if effective_icon else QIcon())

def changeEvent(self, a0: QEvent | None) -> None:
super().changeEvent(a0)
if self._icon_name and should_process_theme_change(self, a0):
self.setIcon(self._icon_name)


class ButtonsField(QWidget):
def __init__(self, vertical_align: Qt.AlignmentFlag = Qt.AlignmentFlag.AlignBottom, parent=None) -> None:
Expand Down Expand Up @@ -272,7 +293,7 @@ def updateUi(self) -> None:

def add_button(self, icon_path: str | None, button_callback: Callable, tooltip: str = "") -> SquareButton:
"""Add button."""
button = SquareButton(svg_tools.get_QIcon(icon_path), parent=self) # Create the button with the icon
button = SquareButton(icon_path if icon_path else QIcon(), parent=self)
if tooltip:
button.setToolTip(tooltip)
button.clicked.connect(button_callback) # Connect the button's clicked signal to the callback
Expand Down
59 changes: 50 additions & 9 deletions bitcoin_safe/gui/qt/card_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@

import enum
from collections.abc import Callable
from pathlib import Path
from typing import cast

from bitcoin_safe_lib.gui.qt.signal_tracker import SignalProtocol
from PyQt6.QtCore import QEvent, QObject, QSize, Qt, pyqtSignal
from PyQt6.QtGui import QIcon, QMouseEvent, QPalette, QPixmap
from PyQt6.QtWidgets import (
QApplication,
QFrame,
QHBoxLayout,
QLabel,
Expand All @@ -49,7 +51,14 @@
from bitcoin_safe.gui.qt.invisible_scroll_area import InvisibleScrollArea

from .styled_card_frame import BaseCardFrame
from .util import get_neutral_surface_colors, set_no_margins, svg_tools, to_color_name
from .util import (
SvgTools,
get_neutral_surface_colors,
set_no_margins,
should_process_theme_change,
svg_tools,
to_color_name,
)


class CardExpansionMode(enum.Enum):
Expand All @@ -73,6 +82,9 @@ def __init__(
self._body_content_visible = True
self._header_clickable = False
self._header_click_targets: list[QWidget] = []
self._icon_source: QIcon | QPixmap | Path | str | None = None
self._icon_size: tuple[int, int] | None = None
self._icon_svg_tools: SvgTools | None = None

self.root_layout = QVBoxLayout(self)

Expand Down Expand Up @@ -110,10 +122,6 @@ def __init__(

self.header_subtitle = QLabel(self.header_text_widget)
self.header_subtitle.setWordWrap(True)
subtitle_palette = self.header_subtitle.palette()
surface_colors = get_neutral_surface_colors()
subtitle_palette.setColor(self.header_subtitle.foregroundRole(), surface_colors.muted_text)
self.header_subtitle.setPalette(subtitle_palette)
self.header_text_layout.addWidget(self.header_subtitle)

self.header_right_widget = QWidget(self.header_widget)
Expand Down Expand Up @@ -144,7 +152,7 @@ def __init__(
self._refresh_body_visibility()
self._update_header_cursor()

self.refresh_style()
self._refresh_theme_dependent_ui()

@property
def is_expanded(self) -> bool:
Expand Down Expand Up @@ -198,9 +206,13 @@ def set_subtitle(self, subtitle: str) -> None:

def set_icon(
self,
icon: QIcon | QPixmap | str | None,
icon: QIcon | QPixmap | Path | str | None,
size: tuple[int, int] | None = None,
svg_tools_custom: SvgTools | None = None,
) -> None:
self._icon_source = icon
self._icon_size = size
self._icon_svg_tools = svg_tools_custom
if icon is None:
self.header_icon.clear()
return
Expand Down Expand Up @@ -293,9 +305,37 @@ def _update_header_cursor(self) -> None:
for widget in self._header_click_targets:
widget.setCursor(cursor_shape)

def changeEvent(self, a0: QEvent | None) -> None:
"""Refresh theme-dependent card colors and SVG icons."""
super().changeEvent(a0)
if should_process_theme_change(self, a0):
self._on_theme_change()

def _on_theme_change(self) -> None:
self._refresh_theme_dependent_ui()

def _refresh_theme_dependent_ui(self) -> None:
surface_colors = get_neutral_surface_colors()
self.refresh_style()
self.hline.setStyleSheet(f"color: {to_color_name(QPalette.ColorRole.Mid)}")

title_palette = self.header_title.palette()
title_palette.setColor(
self.header_title.foregroundRole(),
QApplication.palette().color(QPalette.ColorRole.WindowText),
)
self.header_title.setPalette(title_palette)

subtitle_palette = self.header_subtitle.palette()
subtitle_palette.setColor(self.header_subtitle.foregroundRole(), surface_colors.muted_text)
self.header_subtitle.setPalette(subtitle_palette)

if self._icon_source is not None:
self.set_icon(self._icon_source, self._icon_size, svg_tools_custom=self._icon_svg_tools)

def _coerce_pixmap(
self,
icon: QIcon | QPixmap | str,
icon: QIcon | QPixmap | Path | str,
size: tuple[int, int],
) -> QPixmap:
if isinstance(icon, QPixmap):
Expand All @@ -306,7 +346,8 @@ def _coerce_pixmap(
)
if isinstance(icon, QIcon):
return icon.pixmap(QSize(*size), self.devicePixelRatioF())
return svg_tools.get_pixmap(icon, size=size)
icon_svg_tools = self._icon_svg_tools if self._icon_svg_tools else svg_tools
return icon_svg_tools.get_pixmap(str(icon), size=size)


class CardList(QWidget):
Expand Down
6 changes: 3 additions & 3 deletions bitcoin_safe/gui/qt/color_corrected_treeview.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
from PyQt6.QtCore import QEvent
from PyQt6.QtWidgets import QApplication, QTreeView, QWidget

from .util import is_theme_change_event


class ColorCorrectedTreeView(QTreeView):
"""QTreeView variant that fixes selected item text colors for Windows styles."""
Expand All @@ -49,9 +51,7 @@ def __init__(self, parent: QWidget | None = None) -> None:
def changeEvent(self, event: QEvent | None) -> None:
"""React to palette and style changes that affect selection colors."""
super().changeEvent(event)
if not event:
return
if event.type() in {QEvent.Type.StyleChange, QEvent.Type.PaletteChange}:
if is_theme_change_event(event, include_style_change=True):
self._refresh_selection_style_sheet()

def setStyleSheet(self, styleSheet: str | None) -> None:
Expand Down
14 changes: 13 additions & 1 deletion bitcoin_safe/gui/qt/custom_edits.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@

import bdkpython as bdk
from bitcoin_safe_lib.gui.qt.signal_tracker import SignalProtocol
from PyQt6.QtCore import QRect, QSize, QStringListModel, Qt, pyqtSignal
from PyQt6.QtCore import QEvent, QRect, QSize, QStringListModel, Qt, pyqtSignal
from PyQt6.QtGui import QFocusEvent, QKeyEvent, QPainter, QPaintEvent, QPalette
from PyQt6.QtWidgets import QApplication, QCompleter, QLineEdit, QTextEdit, QWidget

from .util import should_process_theme_change

logger = logging.getLogger(__name__)
ENABLE_COMPLETERS = True

Expand Down Expand Up @@ -259,6 +261,11 @@ def paintEvent(self, a0: QPaintEvent | None) -> None:
self.displayText(),
)

def changeEvent(self, a0: QEvent | None) -> None:
super().changeEvent(a0)
if should_process_theme_change(self, a0):
self.update()


class FlexibleHeightTextedit(QTextEdit):
def sizeHint(self) -> QSize:
Expand Down Expand Up @@ -319,6 +326,11 @@ def setCursorPosition(self, a0: int) -> None:
def setText(self, a0: str | None) -> None: # type: ignore
self.setPlainText(a0 or "")

def changeEvent(self, e: QEvent | None) -> None:
super().changeEvent(e)
if should_process_theme_change(self, e):
self.update()


class QCompleterLineEdit(AnalyzerLineEdit):
signal_focus_out = cast(SignalProtocol[[]], pyqtSignal())
Expand Down
8 changes: 6 additions & 2 deletions bitcoin_safe/gui/qt/hist_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
from bitcoin_safe_lib.util import fast_version, time_logger
from bitcoin_safe_lib.util_os import webopen
from PyQt6.QtCore import QMimeData, QModelIndex, QPoint, Qt, pyqtSignal
from PyQt6.QtGui import QBrush, QColor, QFont, QStandardItem
from PyQt6.QtGui import QBrush, QColor, QFont, QIcon, QStandardItem
from PyQt6.QtWidgets import QAbstractItemView, QFileDialog, QWidget

from bitcoin_safe.client import ProgressInfo, SyncStatus
Expand Down Expand Up @@ -955,7 +955,7 @@ def __init__(self, hist_list: HistList, config: UserConfig, parent: QWidget | No

self.sync_button = SpinningButton(
signal_stop_spinning=self.signal_disable_spinning_button,
enabled_icon=svg_tools.get_QIcon("bi--arrow-clockwise.svg"),
enabled_icon=QIcon(),
parent=self,
timeout=60 * 60,
text="",
Expand All @@ -979,6 +979,7 @@ def __init__(self, hist_list: HistList, config: UserConfig, parent: QWidget | No
self.hist_list.signals.language_switch.connect(self.updateUi)
for wallet in self.hist_list.wallets:
self.hist_list.wallet_functions.wallet_signals[wallet.id].updated.connect(self.update_with_filter)
self.updateUi()

def _on_sync_button_clicked(self):
"""On sync button clicked."""
Expand All @@ -997,6 +998,9 @@ def update_with_filter(self, update_filter: UpdateFilter):
def updateUi(self) -> None:
"""UpdateUi."""
super().updateUi()
self.sync_button.enabled_icon = svg_tools.get_QIcon("bi--arrow-clockwise.svg")
if not self.sync_button._spinning:
self.sync_button.setIcon(self.sync_button.enabled_icon)
if self.balance_label:
balance_total = Satoshis(self.hist_list.balance, self.config.network)
self.balance_label.setText(
Expand Down
Loading
Loading