diff --git a/qt/Makefile.am b/qt/Makefile.am index ff92c40..48ebb59 100644 --- a/qt/Makefile.am +++ b/qt/Makefile.am @@ -1,75 +1,79 @@ # Makefile.am # Copyright (C) 2002 g10 Code GmbH, Klarälvdalens Datakonsult AB # Copyright (C) 2008, 2015 g10 Code GmbH # # This file is part of PINENTRY. # # PINENTRY is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # PINENTRY is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, see . # SPDX-License-Identifier: GPL-2.0+ ## Process this file with automake to produce Makefile.in bin_PROGRAMS = pinentry-qt EXTRA_DIST = pinentryrc.qrc SUBDIRS = icons if FALLBACK_CURSES ncurses_include = $(NCURSES_INCLUDE) libcurses = ../pinentry/libpinentry-curses.a $(LIBCURSES) $(LIBICONV) else ncurses_include = libcurses = endif AM_CPPFLAGS = $(COMMON_CFLAGS) \ -I$(top_srcdir) -I$(top_srcdir)/secmem \ $(ncurses_include) -I$(top_srcdir)/pinentry AM_CXXFLAGS = $(PINENTRY_QT5_CFLAGS) pinentry_qt_LDADD = \ ../pinentry/libpinentry.a $(top_builddir)/secmem/libsecmem.a \ $(COMMON_LIBS) $(PINENTRY_QT5_LIBS) $(libcurses) pinentry_qt_LDFLAGS = $(PINENTRY_QT5_LDFLAGS) if BUILD_PINENTRY_QT5 BUILT_SOURCES = \ pinentryconfirm.moc pinentrydialog.moc pinlineedit.moc capslock.moc \ + focusframe.moc \ + keyboardfocusindication.moc \ pinentryrc.cpp endif CLEANFILES = $(BUILT_SOURCES) if HAVE_W32_SYSTEM pinentry_qt_platform_SOURCES = capslock_win.cpp else pinentry_qt_platform_SOURCES = capslock_unix.cpp endif pinentry_qt_SOURCES = pinentrydialog.h pinentrydialog.cpp \ main.cpp pinentryconfirm.cpp pinentryconfirm.h \ pinlineedit.h pinlineedit.cpp capslock.cpp capslock.h capslock_p.h \ pinentry_debug.cpp pinentry_debug.h util.h accessibility.cpp \ accessibility.h qti18n.cpp pinentryrc.qrc \ + focusframe.h focusframe.cpp \ + keyboardfocusindication.h keyboardfocusindication.cpp \ $(pinentry_qt_platform_SOURCES) nodist_pinentry_qt_SOURCES = $(BUILT_SOURCES) .h.moc: $(MOC5) `test -f '$<' || echo '$(srcdir)/'`$< -o $@ pinentryrc.cpp: pinentryrc.qrc $(RCC5) `test -f '$<' || echo '$(srcdir)/'`$< -o $@ diff --git a/qt/focusframe.cpp b/qt/focusframe.cpp new file mode 100644 index 0000000..c4d4681 --- /dev/null +++ b/qt/focusframe.cpp @@ -0,0 +1,74 @@ +/* focusframe.cpp - A focus indicator for labels. + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "focusframe.h" + +#if QT_CONFIG(graphicseffect) +#include +#endif +#include +#include + +static QRect effectiveWidgetRect(const QWidget *w) +{ + // based on QWidgetPrivate::effectiveRectFor +#if QT_CONFIG(graphicseffect) + const auto* const graphicsEffect = w->graphicsEffect(); + if (graphicsEffect && graphicsEffect->isEnabled()) + return graphicsEffect->boundingRectFor(w->rect()).toAlignedRect(); +#endif // QT_CONFIG(graphicseffect) + return w->rect(); +} + +static QRect clipRect(const QWidget *w) +{ + // based on QWidgetPrivate::clipRect + if (!w->isVisible()) { + return QRect(); + } + QRect r = effectiveWidgetRect(w); + int ox = 0; + int oy = 0; + while (w && w->isVisible() && !w->isWindow() && w->parentWidget()) { + ox -= w->x(); + oy -= w->y(); + w = w->parentWidget(); + r &= QRect(ox, oy, w->width(), w->height()); + } + return r; +} + +void FocusFrame::paintEvent(QPaintEvent *) +{ + if (!widget()) { + return; + } + + QStylePainter p{this}; + QStyleOptionFocusRect option; + initStyleOption(&option); + const int vmargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &option); + const int hmargin = style()->pixelMetric(QStyle::PM_FocusFrameHMargin, &option); + const QRect rect = clipRect(widget()).adjusted(0, 0, hmargin*2, vmargin*2); + p.setClipRect(rect); + p.drawPrimitive(QStyle::PE_FrameFocusRect, option); +} + +#include "focusframe.moc" diff --git a/qt/focusframe.h b/qt/focusframe.h new file mode 100644 index 0000000..3d2231e --- /dev/null +++ b/qt/focusframe.h @@ -0,0 +1,36 @@ +/* focusframe.h - A focus indicator for labels. + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __FOCUSFRAME_H__ +#define __FOCUSFRAME_H__ + +#include + +class FocusFrame : public QFocusFrame +{ + Q_OBJECT +public: + using QFocusFrame::QFocusFrame; + +protected: + void paintEvent(QPaintEvent *event) override; +}; + +#endif // __FOCUSFRAME_H__ diff --git a/qt/keyboardfocusindication.cpp b/qt/keyboardfocusindication.cpp new file mode 100644 index 0000000..27e7231 --- /dev/null +++ b/qt/keyboardfocusindication.cpp @@ -0,0 +1,45 @@ +/* keyboardfocusindication.cpp - Helper for extended keyboard focus indication. + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "keyboardfocusindication.h" + +#include "focusframe.h" + +#include + +KeyboardFocusIndication::KeyboardFocusIndication(QObject *parent) + : QObject{parent} +{ + connect(qApp, &QApplication::focusChanged, this, &KeyboardFocusIndication::updateFocusFrame); +} + +void KeyboardFocusIndication::updateFocusFrame(QWidget *, QWidget *focusWidget) +{ + if (focusWidget && focusWidget->inherits("QLabel") && focusWidget->window()->testAttribute(Qt::WA_KeyboardFocusChange)) { + if (!focusFrame) { + focusFrame = new FocusFrame{focusWidget}; + } + focusFrame->setWidget(focusWidget); + } else if (focusFrame) { + focusFrame->setWidget(nullptr); + } +} + +#include "keyboardfocusindication.moc" diff --git a/qt/keyboardfocusindication.h b/qt/keyboardfocusindication.h new file mode 100644 index 0000000..e86a317 --- /dev/null +++ b/qt/keyboardfocusindication.h @@ -0,0 +1,42 @@ +/* keyboardfocusindication.h - Helper for extended keyboard focus indication. + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __KEYBOARDFOCUSINDICATION_H__ +#define __KEYBOARDFOCUSINDICATION_H__ + +#include +#include +#include + +class FocusFrame; + +class KeyboardFocusIndication : public QObject +{ + Q_OBJECT +public: + KeyboardFocusIndication(QObject *parent); + +private: + void updateFocusFrame(QWidget *, QWidget *); + + QPointer focusFrame; +}; + +#endif // __KEYBOARDFOCUSINDICATION_H__ diff --git a/qt/main.cpp b/qt/main.cpp index 47d15d2..8c8ab48 100644 --- a/qt/main.cpp +++ b/qt/main.cpp @@ -1,429 +1,431 @@ /* main.cpp - A Qt dialog for PIN entry. * Copyright (C) 2002, 2008 Klarälvdalens Datakonsult AB (KDAB) * Copyright (C) 2003, 2021 g10 Code GmbH * Copyright 2007 Ingo Klöcker * * Written by Steffen Hansen . * Modified by Marcus Brinkmann . * Modified by Marc Mutz * Software engineering by Ingo Klöcker * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: GPL-2.0+ */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "accessibility.h" +#include "keyboardfocusindication.h" #include "pinentryconfirm.h" #include "pinentrydialog.h" #include "pinentry.h" #include "util.h" #include #include #include #include #include #include #include #if QT_VERSION >= 0x050000 #include #endif #include #include #include #include #ifdef FALLBACK_CURSES #include #endif #if QT_VERSION >= 0x050000 && defined(QT_STATIC) #include #ifdef Q_OS_WIN #include #include Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin) #elif defined(Q_OS_MAC) Q_IMPORT_PLUGIN(QCocoaIntegrationPlugin) #else Q_IMPORT_PLUGIN(QXcbIntegrationPlugin) #endif #endif #ifdef Q_OS_WIN #include #endif #include "pinentry_debug.h" static QString escape_accel(const QString &s) { QString result; result.reserve(s.size()); bool afterUnderscore = false; for (unsigned int i = 0, end = s.size() ; i != end ; ++i) { const QChar ch = s[i]; if (ch == QLatin1Char('_')) { if (afterUnderscore) { // escaped _ result += QLatin1Char('_'); afterUnderscore = false; } else { // accel afterUnderscore = true; } } else { if (afterUnderscore || // accel ch == QLatin1Char('&')) { // escape & from being interpreted by Qt result += QLatin1Char('&'); } result += ch; afterUnderscore = false; } } if (afterUnderscore) // trailing single underscore: shouldn't happen, but deal with it robustly: { result += QLatin1Char('_'); } return result; } namespace { class InvalidUtf8 : public std::invalid_argument { public: InvalidUtf8() : std::invalid_argument("invalid utf8") {} ~InvalidUtf8() throw() {} }; } static const bool GPG_AGENT_IS_PORTED_TO_ONLY_SEND_UTF8 = false; static QString from_utf8(const char *s) { const QString result = QString::fromUtf8(s); if (result.contains(QChar::ReplacementCharacter)) { if (GPG_AGENT_IS_PORTED_TO_ONLY_SEND_UTF8) { throw InvalidUtf8(); } else { return QString::fromLocal8Bit(s); } } return result; } static void setup_foreground_window(QWidget *widget, WId parentWid) { #if QT_VERSION >= 0x050000 /* For windows set the desktop window as the transient parent */ QWindow *parentWindow = nullptr; if (parentWid) { parentWindow = QWindow::fromWinId(parentWid); } #ifdef Q_OS_WIN if (!parentWindow) { HWND desktop = GetDesktopWindow(); if (desktop) { parentWindow = QWindow::fromWinId((WId) desktop); } } #endif if (parentWindow) { // Ensure that we have a native wid widget->winId(); QWindow *wndHandle = widget->windowHandle(); if (wndHandle) { wndHandle->setTransientParent(parentWindow); } } #endif widget->setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowStaysOnTopHint | Qt::WindowMinimizeButtonHint); } static int qt_cmd_handler(pinentry_t pe) { int want_pass = !!pe->pin; const QString ok = pe->ok ? escape_accel(from_utf8(pe->ok)) : pe->default_ok ? escape_accel(from_utf8(pe->default_ok)) : /* else */ QLatin1String("&OK") ; const QString cancel = pe->cancel ? escape_accel(from_utf8(pe->cancel)) : pe->default_cancel ? escape_accel(from_utf8(pe->default_cancel)) : /* else */ QLatin1String("&Cancel") ; unique_malloced_ptr str{pinentry_get_title(pe)}; const QString title = str ? from_utf8(str.get()) : /* else */ QLatin1String("pinentry-qt") ; const QString repeatError = pe->repeat_error_string ? from_utf8(pe->repeat_error_string) : QLatin1String("Passphrases do not match"); const QString repeatString = pe->repeat_passphrase ? from_utf8(pe->repeat_passphrase) : QString(); const QString visibilityTT = pe->default_tt_visi ? from_utf8(pe->default_tt_visi) : QLatin1String("Show passphrase"); const QString hideTT = pe->default_tt_hide ? from_utf8(pe->default_tt_hide) : QLatin1String("Hide passphrase"); const QString capsLockHint = pe->default_capshint ? from_utf8(pe->default_capshint) : QLatin1String("Caps Lock is on"); const QString generateLbl = pe->genpin_label ? from_utf8(pe->genpin_label) : QString(); const QString generateTT = pe->genpin_tt ? from_utf8(pe->genpin_tt) : QString(); if (want_pass) { PinEntryDialog pinentry(nullptr, 0, pe->timeout, true, !!pe->quality_bar, repeatString, visibilityTT, hideTT); setup_foreground_window(&pinentry, pe->parent_wid); pinentry.setPinentryInfo(pe); pinentry.setPrompt(escape_accel(from_utf8(pe->prompt))); pinentry.setDescription(from_utf8(pe->description)); pinentry.setRepeatErrorText(repeatError); pinentry.setGenpinLabel(generateLbl); pinentry.setGenpinTT(generateTT); pinentry.setCapsLockHint(capsLockHint); pinentry.setFormattedPassphrase({ bool(pe->formatted_passphrase), from_utf8(pe->formatted_passphrase_hint)}); pinentry.setConstraintsOptions({ bool(pe->constraints_enforce), from_utf8(pe->constraints_hint_short), from_utf8(pe->constraints_hint_long), from_utf8(pe->constraints_error_title) }); if (!title.isEmpty()) { pinentry.setWindowTitle(title); } /* If we reuse the same dialog window. */ pinentry.setPin(QString()); pinentry.setOkText(ok); pinentry.setCancelText(cancel); if (pe->error) { pinentry.setError(from_utf8(pe->error)); } if (pe->quality_bar) { pinentry.setQualityBar(from_utf8(pe->quality_bar)); } if (pe->quality_bar_tt) { pinentry.setQualityBarTT(from_utf8(pe->quality_bar_tt)); } bool ret = pinentry.exec(); if (!ret) { if (pinentry.timedOut()) pe->specific_err = gpg_error (GPG_ERR_TIMEOUT); return -1; } const QString pinStr = pinentry.pin(); QByteArray pin = pinStr.toUtf8(); if (!!pe->repeat_passphrase) { /* Should not have been possible to accept the dialog in that case but we do a safety check here */ pe->repeat_okay = (pinStr == pinentry.repeatedPin()); } int len = strlen(pin.constData()); if (len >= 0) { pinentry_setbufferlen(pe, len + 1); if (pe->pin) { strcpy(pe->pin, pin.constData()); return len; } } return -1; } else { const QString desc = pe->description ? from_utf8(pe->description) : QString(); const QString notok = pe->notok ? escape_accel(from_utf8(pe->notok)) : QString(); const QMessageBox::StandardButtons buttons = pe->one_button ? QMessageBox::Ok : pe->notok ? QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel : /* else */ QMessageBox::Ok | QMessageBox::Cancel ; PinentryConfirm box{QMessageBox::Information, title, desc, buttons}; box.setTextFormat(Qt::PlainText); box.setTextInteractionFlags(Qt::TextSelectableByMouse); box.setTimeout(std::chrono::seconds{pe->timeout}); setup_foreground_window(&box, pe->parent_wid); const struct { QMessageBox::StandardButton button; QString label; } buttonLabels[] = { { QMessageBox::Ok, ok }, { QMessageBox::Yes, ok }, { QMessageBox::No, notok }, { QMessageBox::Cancel, cancel }, }; for (size_t i = 0 ; i < sizeof buttonLabels / sizeof * buttonLabels ; ++i) if ((buttons & buttonLabels[i].button) && !buttonLabels[i].label.isEmpty()) { box.button(buttonLabels[i].button)->setText(buttonLabels[i].label); Accessibility::setDescription(box.button(buttonLabels[i].button), buttonLabels[i].label); } box.setIconPixmap(applicationIconPixmap()); if (!pe->one_button) { box.setDefaultButton(QMessageBox::Cancel); } box.show(); raiseWindow(&box); const int rc = box.exec(); if (rc == QMessageBox::Cancel) { pe->canceled = true; } if (box.timedOut()) { pe->specific_err = gpg_error (GPG_ERR_TIMEOUT); } return rc == QMessageBox::Ok || rc == QMessageBox::Yes ; } } static int qt_cmd_handler_ex(pinentry_t pe) { try { return qt_cmd_handler(pe); } catch (const InvalidUtf8 &) { pe->locale_err = true; return pe->pin ? -1 : false ; } catch (...) { pe->canceled = true; return pe->pin ? -1 : false ; } } pinentry_cmd_handler_t pinentry_cmd_handler = qt_cmd_handler_ex; int main(int argc, char *argv[]) { pinentry_init("pinentry-qt"); QApplication *app = NULL; int new_argc = 0; #ifdef FALLBACK_CURSES #if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) // check a few environment variables that are usually set on X11 or Wayland sessions const bool hasWaylandDisplay = qEnvironmentVariableIsSet("WAYLAND_DISPLAY"); const bool isWaylandSessionType = qgetenv("XDG_SESSION_TYPE") == "wayland"; const bool hasX11Display = pinentry_have_display(argc, argv); const bool isX11SessionType = qgetenv("XDG_SESSION_TYPE") == "x11"; const bool isGUISession = hasWaylandDisplay || isWaylandSessionType || hasX11Display || isX11SessionType; qCDebug(PINENTRY_LOG) << "hasWaylandDisplay:" << hasWaylandDisplay; qCDebug(PINENTRY_LOG) << "isWaylandSessionType:" << isWaylandSessionType; qCDebug(PINENTRY_LOG) << "hasX11Display:" << hasX11Display; qCDebug(PINENTRY_LOG) << "isX11SessionType:" << isX11SessionType; qCDebug(PINENTRY_LOG) << "isGUISession:" << isGUISession; #else const bool isGUISession = pinentry_have_display(argc, argv); #endif if (!isGUISession) { pinentry_cmd_handler = curses_cmd_handler; pinentry_set_flavor_flag ("curses"); } else #endif { /* Qt does only understand -display but not --display; thus we are fixing that here. The code is pretty simply and may get confused if an argument is called "--display". */ char **new_argv, *p; size_t n; int i, done; for (n = 0, i = 0; i < argc; i++) { n += strlen(argv[i]) + 1; } n++; new_argv = (char **)calloc(argc + 1, sizeof * new_argv); if (new_argv) { *new_argv = (char *)malloc(n); } if (!new_argv || !*new_argv) { fprintf(stderr, "pinentry-qt: can't fixup argument list: %s\n", strerror(errno)); exit(EXIT_FAILURE); } for (done = 0, p = *new_argv, i = 0; i < argc; i++) if (!done && !strcmp(argv[i], "--display")) { new_argv[i] = strcpy(p, argv[i] + 1); p += strlen(argv[i] + 1) + 1; done = 1; } else { new_argv[i] = strcpy(p, argv[i]); p += strlen(argv[i]) + 1; } /* Note: QApplication uses int &argc so argc has to be valid * for the full lifetime of the application. * * As Qt might modify argc / argv we use copies here so that * we do not loose options that are handled in both. e.g. display. */ new_argc = argc; Q_ASSERT (new_argc); app = new QApplication(new_argc, new_argv); app->setWindowIcon(QIcon(QLatin1String(":/icons/document-encrypt.png"))); + (void) new KeyboardFocusIndication{app}; } pinentry_parse_opts(argc, argv); int rc = pinentry_loop(); delete app; return rc ? EXIT_FAILURE : EXIT_SUCCESS ; }