Compare commits

...

52 Commits

Author SHA1 Message Date
Martchus cabeca7756 Don't build the Qt Quick GUI by default for Windows and Apple platforms
It uses Kirigami which is probably not present/wanted on those platforms.
2024-04-13 16:50:59 +02:00
Martchus 06ae52d0d8 Update translations 2024-04-08 12:40:05 +02:00
Martchus 6ffa54088a Ensure desktop file name is set when initializing GUIs 2024-04-08 12:39:46 +02:00
Martchus 1b6b645f7a Apply cmake-format 2024-04-08 12:39:05 +02:00
Martchus 1ce919397b Bump patch version 2024-04-08 12:38:47 +02:00
Martchus 3c706b614e Clarify condition in Qt version check for Quick GUI 2024-04-02 15:19:49 +02:00
Martchus e095dc904f Update release date 2024-04-02 11:09:47 +02:00
Martchus a37ee658a6 Update translations 2024-04-01 15:41:34 +02:00
Martchus 9653b4c0d5 Bump minor version after all the Android and Qt Quick GUI related changes 2024-04-01 15:41:09 +02:00
Martchus 613d62d042 Elide entry names in entries/fields list items 2024-04-01 15:32:47 +02:00
Martchus 8319d4f485 Extend Android-related remarks in README 2024-04-01 14:55:58 +02:00
Martchus c2fc538e2c Avoid hidden swipe actions in mobile/tablet mode as those are broken
When clicking on an action the action is not triggered but the whole list
item enters a broken state instead. This can also be reproduced on the
desktop by setting `QT_QUICK_CONTROLS_MOBILE=1`.
2024-04-01 14:40:19 +02:00
Martchus f20c7b5d3d Avoid using `story-editor` icon as it is not actually dark in breeze-dark 2024-04-01 14:24:28 +02:00
Martchus b2d85c7d53 Refactor various aspects of the code
* Prefer using auto
* Reduce nesting in certain places
* Simplify code in main function
2024-04-01 13:59:22 +02:00
Martchus 0f4c30c14f Format Android-specific files (also when not building for Android) 2024-03-31 23:04:39 +02:00
Martchus 21a16e0be0 Add approach for handling darkmode changes dynamically
This seems to work for plain QCC2 parts but unfortunately doesn't help with
Kirigami (see added not). Hence this is disabled for now.
2024-03-31 22:53:32 +02:00
Martchus ba9a6e26de Clean code for initializing Qt Quick GUI 2024-03-31 22:46:01 +02:00
Martchus 9ebffe6b16 Exit application when the main QML file cannot be loaded 2024-03-31 22:43:48 +02:00
Martchus 64a2738827 Update translations 2024-03-31 13:50:00 +02:00
Martchus a098b40dab Apply clang-format 2024-03-31 13:49:44 +02:00
Martchus dbc2e9a4e2 Select correct icon theme for Qt Quick GUI depending on darkmode setting 2024-03-31 13:49:34 +02:00
Martchus f960aa80a1 Remove Android-specific debugging code for Qt Quick app 2024-03-31 13:44:48 +02:00
Martchus b950451807 Avoid explicit color definitions in Qt Quick GUI
Those might break dark mode and using custom color palettes in general.
2024-03-31 12:57:35 +02:00
Martchus 20aea51fa1 Update translations 2024-03-26 22:54:47 +01:00
Martchus 5706086456 Streamline setup of Qt Quick GUI with Qt Widgets GUI 2024-03-26 22:54:10 +01:00
Martchus b8d2422669 Bundle icons and translations when building for Android 2024-03-26 22:52:21 +01:00
Martchus 7a08ee7d00 Adapt README to latest changes 2024-03-26 22:50:20 +01:00
Martchus 7c03835fc3 Improve documentation of keystore handling 2024-03-26 21:58:35 +01:00
Martchus 0791964c6f Apply clang-format and cmake-format 2024-03-25 18:34:22 +01:00
Martchus f6096f7232 Avoid showing title/icon in Qt Quick GUI
It doesn't look as good in the KF6-based version anymore and cannot be made
clickable anyway.
2024-03-25 18:34:22 +01:00
Martchus 79a0ef031c Improve specifying dependencies for Qt Quick GUI
* Remove QuickControls2 as the CMake module from qtutilities take care of
  it
* Replace Kirigami2 with Kirigami because the former is deprecated
2024-03-25 18:34:22 +01:00
Martchus cb6fe128c4 Set app domain for the sake of displaying the URL in the Qt Quick GUI 2024-03-25 18:34:22 +01:00
Martchus eed9d596ba Simplify code for about dialog of Qt Quick GUI 2024-03-25 18:34:22 +01:00
Martchus cd23eb4add Avoid warning about missing version when building Qt Quick GUI 2024-03-25 18:34:22 +01:00
Martchus 4891ee0b32 Update translations 2024-03-25 18:34:22 +01:00
Martchus 6972256727 Improve Qt Quick GUI after porting to Qt 6
* Avoid use of deprecated QML features
* Fix problems when moving items by making the delegates own items
* Improve code in models, especially functions for moving rows
* Fix sizing of dialogs
* Replace code relying on `bannerClicked` event which has been removed in
  KF6
* Use the native file dialog if supported (and otherwise not); this should
  give the desired behavior under each platform out of the box
* Update versioning requirements in README
2024-03-24 22:26:44 +01:00
Martchus 4bf6a91d72 Port Qt Quick GUI for Android to Qt 6 2024-03-17 00:34:00 +01:00
Martchus 430e17416a Update copyright date 2024-02-24 21:16:53 +01:00
Martchus 8f8eda8755 Mention key used for signing binaries in README 2024-02-07 19:27:58 +01:00
Martchus fcbc3cb4a4 Add tab-stops explicitly where default order is wrong 2024-01-07 23:41:39 +01:00
Martchus 7b242732e8 Bump patch version 2024-01-07 23:40:42 +01:00
Martchus 343121ae90 Update release date 2023-11-21 21:53:07 +01:00
Martchus a8023d255a State minimum required Windows 10 version 2023-11-18 21:42:07 +01:00
Martchus 5ebf64d1de Enable creating a backup file by default 2023-11-18 21:13:57 +01:00
Martchus 9aa8d9c9ac Bump patch version 2023-11-18 21:13:36 +01:00
Martchus 1993046e96 Avoid CMake deprecation warning by bumping version 2023-07-23 21:06:56 +02:00
Martchus a93e04fd14 Update release date 2023-04-04 21:22:05 +02:00
Martchus ef6b6085b1 Update translations 2023-03-30 00:05:08 +02:00
Martchus 5acf8b0cc0 Apply Qt settings immediately 2023-03-30 00:04:39 +02:00
Martchus 0ead95e749 Update style sheet on palette changes accordingly 2023-03-30 00:04:17 +02:00
Martchus a45734de1d Remove accidentally hard-coded font 2023-03-30 00:02:01 +02:00
Martchus e407f0b455 Bump patch release 2023-03-30 00:00:49 +02:00
40 changed files with 1402 additions and 1257 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ Makefile*
# clang-format
/.clang-format
# Android-specific
/android/AndroidManifest.xml

View File

@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.1.0 FATAL_ERROR)
cmake_minimum_required(VERSION 3.17.0 FATAL_ERROR)
# set meta data
project(passwordmanager)
@ -9,14 +9,15 @@ set(META_APP_NAME "Password Manager")
set(META_APP_CATEGORIES "Utility;Security;")
set(META_APP_AUTHOR "Martchus")
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
set(META_APP_DOMAIN "${META_APP_URL}")
set(META_APP_DESCRIPTION "A simple password store using AES-256-CBC encryption via OpenSSL")
set(META_GUI_OPTIONAL YES)
set(META_USE_QQC2 ON)
set(META_ANDROID_PACKAGE_NAME "org.martchus.passwordmanager")
set(META_VERSION_MAJOR 4)
set(META_VERSION_MINOR 1)
set(META_VERSION_PATCH 11)
set(META_RELEASE_DATE "2023-03-07")
set(META_VERSION_MINOR 2)
set(META_VERSION_PATCH 1)
set(META_RELEASE_DATE "2024-04-02")
# add project files
set(HEADER_FILES cli/cli.h model/entryfiltermodel.h model/entrymodel.h model/fieldmodel.h)
@ -42,6 +43,8 @@ set(QML_SRC_FILES quickgui/controller.cpp quickgui/initiatequick.cpp resources/i
if (ANDROID)
list(APPEND QML_HEADER_FILES quickgui/android.h)
list(APPEND QML_SRC_FILES quickgui/android.cpp)
else ()
list(APPEND EXCLUDED_FILES quickgui/android.h quickgui/android.cpp)
endif ()
set(TS_FILES translations/${META_PROJECT_NAME}_de_DE.ts translations/${META_PROJECT_NAME}_en_US.ts)
@ -102,7 +105,7 @@ set(REQUIRED_ICONS
preferences-desktop-locale
qtcreator
search
story-editor
view-list-details-symbolic
system-file-manager
system-run
system-search
@ -117,24 +120,24 @@ find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.6.0 REQUIRED)
use_cpp_utilities()
# apply basic configuration
set(BUILD_QUICK_GUI_BY_DEFAULT ON)
if (WIN32 OR APPLE)
set(BUILD_QUICK_GUI_BY_DEFAULT OFF)
endif ()
option(QUICK_GUI "enables/disables building the Qt Quick GUI using Kirigami" "${BUILD_QUICK_GUI_BY_DEFAULT}")
include(BasicConfig)
# find qtutilities
set(CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES
"${CONFIGURATION_PACKAGE_SUFFIX}"
CACHE STRING "sets the suffix for qtutilities")
find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.11.0 REQUIRED)
find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.14.0 REQUIRED)
use_qt_utilities()
# find passwordfile
find_package(passwordfile${CONFIGURATION_PACKAGE_SUFFIX} 5.0.0 REQUIRED)
use_password_file()
# require at least Qt 5.8 for the Qt Quick GUI
if (QUICK_GUI)
set(META_QT5_VERSION 5.8)
endif ()
# allow to enable undo support from the widgets GUI in the quick GUI as well (so the quick GUI will depend on Qt Widgets as
# well)
if (QUICK_GUI AND NOT WIDGETS_GUI)
@ -147,20 +150,32 @@ if (QUICK_GUI AND NOT WIDGETS_GUI)
endif ()
endif ()
# add further Qt/KF modules required by the Qt Quick GUI under Android
if (ANDROID)
list(APPEND ADDITIONAL_QT_MODULES AndroidExtras)
# deduce major Qt version from package prefix
if (NOT QT_PACKAGE_PREFIX)
set(MAJOR_QT_VERSION "5")
elseif (QT_PACKAGE_PREFIX MATCHES ".*Qt([0-9]+).*")
set(MAJOR_QT_VERSION "${CMAKE_MATCH_1}")
endif ()
# require Qt 6 for the Qt Quick GUI
if (QUICK_GUI AND (MAJOR_QT_VERSION VERSION_LESS 6 OR MAJOR_QT_VERSION VERSION_GREATER_EQUAL 7))
message(FATAL_ERROR "The Qt Quick GUI is only compatible with Qt 6 (but Qt ${MAJOR_QT_VERSION} was found).")
endif ()
# workaround "ld: error: undefined symbol: qt_resourceFeatureZstd" when Qt 6 is not configured with zstd support
if (MAJOR_QT_VERSION GREATER_EQUAL 6 AND NOT QT_FEATURE_zstd)
set(CMAKE_AUTORCC_OPTIONS "--no-zstd")
endif ()
# add further Qt/KF modules required by Qt Quick GUI
if (QUICK_GUI)
list(APPEND ADDITIONAL_KF_MODULES Kirigami2)
list(APPEND ADDITIONAL_KF_MODULES Kirigami)
endif ()
# add Qt-version-specific QML files
unset(QML_FILE)
if (NOT QT_PACKAGE_PREFIX)
set(QML_FILE "resources/qml5.qrc")
elseif (QT_PACKAGE_PREFIX MATCHES ".*Qt([0-9]+).*")
set(QML_FILE "resources/qml${CMAKE_MATCH_1}.qrc")
if (MAJOR_QT_VERSION)
set(QML_FILE "resources/qml${MAJOR_QT_VERSION}.qrc")
endif ()
if (NOT QML_FILE)
message(FATAL_ERROR "Unable to add Qt-version-specific resource file for QT_PACKAGE_PREFIX \"${QT_PACKAGE_PREFIX}\".")
@ -180,9 +195,28 @@ if (WIDGETS_GUI OR QUICK_GUI)
endif ()
include(WindowsResources)
include(AppTarget)
include(AndroidApk)
include(ShellCompletion)
include(ConfigHeader)
# configure creating an Android package using androiddeployqt
if (ANDROID)
set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
set_target_properties(${META_TARGET_NAME} PROPERTIES QT_ANDROID_PACKAGE_SOURCE_DIR "${ANDROID_PACKAGE_SOURCE_DIR}")
set(ANDROID_MANIFEST_PATH "${ANDROID_PACKAGE_SOURCE_DIR}/AndroidManifest.xml")
configure_file("resources/AndroidManifest.xml.in" "${ANDROID_MANIFEST_PATH}")
# bundle OpenMP (used by Kirigami) explicitly as it is otherwise not bundled
find_package(OpenMP)
if (OpenMP_CXX_FOUND)
message(STATUS "Bundling OpenMP library for Kirigami: ${OpenMP_omp_LIBRARY}")
set_target_properties(${META_TARGET_NAME} PROPERTIES QT_ANDROID_EXTRA_LIBS "${OpenMP_omp_LIBRARY}")
endif ()
set(QT_ANDROID_SIGN_APK ON)
qt_android_generate_deployment_settings(${META_TARGET_NAME})
qt_android_add_apk_target(${META_TARGET_NAME})
endif ()
# create desktop file using previously defined meta data
add_desktop_file()

153
README.md
View File

@ -4,7 +4,7 @@ A simple [password manager](https://en.wikipedia.org/wiki/Password_manager) with
## Features
* Cross-platform: tested under GNU/Linux, Android and Windows
* Qt Widgets GUI for desktop platforms
* Qt Quick GUI (using Qt Quick Controls 2 and Kirigami 2) for mobile platforms
* Qt Quick GUI (using Qt Quick Controls 2 and Kirigami) for mobile platforms
* Interactive command-line interface
* Simple architecture: All data is stored in ordinary files with AES-256-CBC applied. No cloud stuff. Use
eg. Syncthing for synchronization.
@ -13,7 +13,7 @@ A simple [password manager](https://en.wikipedia.org/wiki/Password_manager) with
I've mainly started this project to learn C++ and Qt programming. So beside the mentioned features this project
and the underlying libraries serve as an example project covering some interesting C++/Qt topics:
* Basic use of Qt Widgets, Qt Quick and Kirigami 2
* Basic use of Qt Widgets, Qt Quick and Kirigami
* Creating custom Qt models
* Nested model and model with multiple columns
* Support Drag & Drop in `QTreeView`
@ -22,7 +22,7 @@ and the underlying libraries serve as an example project covering some interesti
* Integration with Qt Widgets' undo/redo framework
* Filtering
* Android tweaks
* Add CMake target to invoke `androiddeployqt`
* Create APK via CMake (using `androiddeployqt`)
* Customize activity
* Customize gradle project to add additional Java dependency
* Adjust the window style of the activity
@ -67,10 +67,14 @@ See the release section on GitHub.
the package `libopengl0` is installed on Debian/Ubuntu)
* Supports X11 and Wayland (set the environment variable `QT_QPA_PLATFORM=xcb` to disable
native Wayland support if it does not work on your system)
* Binaries are signed with the GPG key
[`B9E36A7275FC61B464B67907E06FE8F53CDC6A4C`](https://keyserver.ubuntu.com/pks/lookup?search=B9E36A7275FC61B464B67907E06FE8F53CDC6A4C&fingerprint=on&op=index).
* Windows
* for binaries checkout the [release section on GitHub](https://github.com/Martchus/tageditor/releases)
* the Qt 6 based version is stable and preferable but only supports Windows 10 and newer
* the Qt 6 based version is stable and preferable but only supports Windows 10 version 1809 and newer
* the Qt 5 based version should still work on older versions down to Windows 7 although this is not regularly checked
* Binaries are signed with the GPG key
[`B9E36A7275FC61B464B67907E06FE8F53CDC6A4C`](https://keyserver.ubuntu.com/pks/lookup?search=B9E36A7275FC61B464B67907E06FE8F53CDC6A4C&fingerprint=on&op=index).
* for mingw-w64 PKGBUILDs checkout [my GitHub repository](https://github.com/Martchus/PKGBUILDs)
## Build instructions
@ -87,7 +91,7 @@ can be passed to CMake to influence the build.
### Optional dependencies
* When building any Qt GUI, the library qtutilities is required.
* When building with Qt Widgets GUI support, the following Qt modules are required (version 5.6 or higher): core gui widgets
* When building with support for the experimental Qt Quick GUI, the following Qt/KDE modules are required (version 5.12 or higher): core gui qml quick quickcontrols2 kirigami
* When building with support for the experimental Qt Quick GUI, the following Qt/KDE modules are required (version 6.6 or higher): core gui qml quick quickcontrols2 kirigami
To specify the major Qt version to use, set `QT_PACKAGE_PREFIX` (e.g. add `-DQT_PACKAGE_PREFIX:STRING=Qt6`
to the CMake arguments). There's also `KF_PACKAGE_PREFIX` for KDE dependencies. Note that the Qt Quick GUI
@ -120,131 +124,46 @@ always requires the same major Qt version as your KDE modules use.
the desired location afterwards.
#### Concrete example of 3. for building an Android APK under Arch Linux
Create stuff for signing the package (remove `-DANDROID_APK_FORCE_DEBUG=ON` line in the CMake invocation to actually use this):
Create a key for signing the package (always required; otherwise the APK file won't install):
```
# locate keystore
keystore_dir=/path/to/keystore-dir
keystore_alias=$USER
keystore_url=$keystore_dir/$keystore_alias
# make up some password to protect the store; enter this on keytool invocation
keystore_password=<password>
# set variables for creating keystore and androiddeployqt to find it
export QT_ANDROID_KEYSTORE_PATH=/path/to/keystore-dir QT_ANDROID_KEYSTORE_ALIAS=$USER-devel QT_ANDROID_KEYSTORE_STORE_PASS=$USER-devel QT_ANDROID_KEYSTORE_KEY_PASS=$USER-devel
# create keystore (do only once)
pushd "$keystore_dir"
keytool -genkey -v -keystore "$keystore_alias" -alias "$keystore_alias" -keyalg RSA -keysize 2048 -validity 10000
mkdir -p "${QT_ANDROID_KEYSTORE_PATH%/*}"
pushd "${QT_ANDROID_KEYSTORE_PATH%/*}"
keytool -genkey -v -keystore "$QT_ANDROID_KEYSTORE_ALIAS" -alias "$QT_ANDROID_KEYSTORE_ALIAS" -keyalg RSA -keysize 2048 -validity 10000
popd
```
Build c++utilities, passwordfile, qtutilities and passwordmanager in one step to create an Android APK for arm64-v8a:
Build c++utilities, passwordfile, qtutilities and passwordmanager in one step to create an Android APK for aarch64:
```
# specify Android platform
_pkg_arch=aarch64
_android_arch=arm64-v8a
_android_arch2=arm64
_android_api_level=22
# use Java 17 (the latest Java doesn't work at this point) and avoid unwanted Java options
export PATH=/usr/lib/jvm/java-17-openjdk/bin:$PATH
export _JAVA_OPTIONS=
# set project name
_reponame=passwordmanager
_pkgname=passwordmanager
# configure and build using helpers from android-cmake package
android_arch=aarch64
build_dir=$BUILD_DIR/../manual/passwordmanager-android-$android_arch-release
source /usr/bin/android-env $android_arch
android-$android_arch-cmake -G Ninja -S . -B "$build_dir" \
-DCMAKE_FIND_ROOT_PATH="${ANDROID_PREFIX}" -DANDROID_SDK_ROOT="${ANDROID_HOME}" \
-DPKG_CONFIG_EXECUTABLE:FILEPATH=/usr/bin/android-$android_arch-pkg-config \
-DBUILTIN_ICON_THEMES='breeze;breeze-dark' -DBUILTIN_TRANSLATIONS=ON \
-DQT_PACKAGE_PREFIX:STRING=Qt6 -DKF_PACKAGE_PREFIX:STRING=KF6
cmake --build "$build_dir"
# locate SDK, NDK and further libraries
android_sdk_root=${ANDROID_SDK_ROOT:-/opt/android-sdk}
android_ndk_root=${ANDROID_NDK_ROOT:-/opt/android-ndk}
build_tools_version=$(pacman -Q android-sdk-build-tools | sed 's/.* r\(.*\)-.*/\1/')
other_libs_root=/opt/android-libs/$_pkg_arch
other_libs_include=$other_libs_root/include
root="$android_ndk_root/sysroot;$other_libs_root"
# use Java 8 which seems to be the latest version which works
export PATH=/usr/lib/jvm/java-8-openjdk/jre/bin/:$PATH
# configure with the toolchain file provided by the Android NDK (still WIP)
# note: This configuration is likely required in the future to resolve https://gitlab.kitware.com/cmake/cmake/issues/18739. But for now
# better keep using CMake's internal Android support because this config has its own pitfalls (see CMAKE_CXX_FLAGS).
cmake \
-DCMAKE_BUILD_TYPE=Release \
-DANDROID_ABI=$_android_arch \
-DANDROID_PLATFORM=$_android_api_level \
-DCMAKE_TOOLCHAIN_FILE=$android_ndk_root/build/cmake/android.toolchain.cmake \
-DCMAKE_SYSTEM_NAME=Android \
-DCMAKE_SYSTEM_VERSION=$_android_api_level \
-DCMAKE_ANDROID_ARCH_ABI=$_android_arch \
-DCMAKE_ANDROID_NDK="$android_ndk_root" \
-DCMAKE_ANDROID_SDK="$android_sdk_root" \
-DCMAKE_ANDROID_STL_TYPE=c++_shared \
-DCMAKE_INSTALL_PREFIX=$other_libs_root \
-DCMAKE_PREFIX_PATH="$root" \
-DCMAKE_FIND_ROOT_PATH="$root;$root/libs" \
-DCMAKE_CXX_FLAGS="-include $android_ndk_root/sysroot/usr/include/math.h -include $android_ndk_root/sources/cxx-stl/llvm-libc++/include/math.h -I$other_libs_include" \
-DBUILD_SHARED_LIBS=ON \
-DZLIB_LIBRARY="$android_ndk_root/platforms/android-$_android_api_level/arch-$_android_arch2/usr/lib/libz.so" \
-DCLANG_FORMAT_ENABLED=ON \
-DUSE_NATIVE_FILE_BUFFER=ON \
-DUSE_STANDARD_FILESYSTEM=OFF \
-DNO_DOXYGEN=ON \
-DWIDGETS_GUI=OFF \
-DQUICK_GUI=ON \
-DBUILTIN_ICON_THEMES=breeze \
-DBUILTIN_TRANSLATIONS=ON \
-DANDROID_APK_TOOLCHAIN_VERSION=4.9 \
-DANDROID_APK_CXX_STANDARD_LIBRARY="$android_ndk_root/platforms/android-$_android_api_level/arch-$_android_arch2/usr/lib/libstdc++.so" \
-DANDROID_APK_FORCE_DEBUG=ON \
-DANDROID_APK_KEYSTORE_URL="$keystore_url" \
-DANDROID_APK_KEYSTORE_ALIAS="$keystore_alias" \
-DANDROID_APK_KEYSTORE_PASSWORD="$keystore_password" \
-DANDROID_APK_APPLICATION_ID_SUFFIX=".unstable" \
-DANDROID_APK_APPLICATION_LABEL="Password Manager (unstable)" \
$SOURCES/subdirs/$_reponame
# configure with CMake's internal Android support
# note: Requires workaround with Android NDK r19: https://gitlab.kitware.com/cmake/cmake/issues/18739#note_498676
cmake \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_SYSTEM_NAME=Android \
-DCMAKE_SYSTEM_VERSION=$_android_api_level \
-DCMAKE_ANDROID_ARCH_ABI=$_android_arch \
-DCMAKE_ANDROID_NDK="$android_ndk_root" \
-DCMAKE_ANDROID_SDK="$android_sdk_root" \
-DCMAKE_ANDROID_STL_TYPE=c++_shared \
-DCMAKE_INSTALL_PREFIX=$other_libs_root \
-DCMAKE_PREFIX_PATH="$root" \
-DCMAKE_FIND_ROOT_PATH="$root;$root/libs" \
-DCMAKE_CXX_FLAGS="-D__ANDROID_API__=$_android_api_level" \
-DCLANG_FORMAT_ENABLED=ON \
-DBUILD_SHARED_LIBS=ON \
-DUSE_NATIVE_FILE_BUFFER=ON \
-DUSE_STANDARD_FILESYSTEM=OFF \
-DNO_DOXYGEN=ON \
-DWIDGETS_GUI=OFF \
-DQUICK_GUI=ON \
-DBUILTIN_ICON_THEMES=breeze \
-DBUILTIN_TRANSLATIONS=ON \
-DANDROID_APK_FORCE_DEBUG=ON \
-DANDROID_APK_KEYSTORE_URL="$keystore_url" \
-DANDROID_APK_KEYSTORE_ALIAS="$keystore_alias" \
-DANDROID_APK_KEYSTORE_PASSWORD="$keystore_password" \
-DANDROID_APK_APPLICATION_ID_SUFFIX=".unstable" \
-DANDROID_APK_APPLICATION_LABEL="Password Manager (unstable)" \
$SOURCES/subdirs/$_reponame
# build all binaries and make APK file using all CPU cores
make passwordmanager_apk -j$(nproc)
# install app on USB-connected phone
make passwordmanager_deploy_apk
# install the app
adb install "$build_dir/passwordmanager/android-build//build/outputs/apk/release/android-build-release-signed.apk"
```
##### Notes
* The Android packages for the dependencies Qt, iconv, OpenSSL and Kirigami 2 are provided in
* The Android packages for the dependencies Boost, Qt, iconv, OpenSSL and Kirigami are provided in
my [PKGBUILDs](http://github.com/Martchus/PKGBUILDs) repo.
* The latest Java I was able to use was version 8 (`jdk8-openjdk` package).
### Manual deployment of Android APK file
1. Find device ID: `adb devices`
2. Install App on phone: `adb -s <DEVICE_ID> install -r $BUILD_DIR/passwordmanager_build_apk/build/outputs/apk/passwordmanager_build_apk-debug.apk`
3. View log: `adb -s <DEVICE_ID> logcat`
* The latest Java I was able to use was version 17.
* Use `QT_QUICK_CONTROLS_STYLE=Material` and `QT_QUICK_CONTROLS_MOBILE=1` to test the Qt Quick GUI like it would be shown under
Android via a normal desktop build.
### Building without Qt GUI
It is possible to build without the GUI if only the CLI is needed. In this case no Qt dependencies (including qtutilities) are required.
@ -255,6 +174,6 @@ To build without GUI, add the following parameters to the CMake call:
```
## Copyright notice and license
Copyright © 2015-2023 Marius Kittler
Copyright © 2015-2024 Marius Kittler
All code is licensed under [GPL-2-or-later](LICENSE).

View File

@ -1,54 +0,0 @@
<?xml version="1.0"?>
<manifest package="@META_ANDROID_PACKAGE_NAME@" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="@META_APP_VERSION@" android:versionCode="@META_VERSION_MAJOR@" android:installLocation="auto">
<application
android:icon="@mipmap/ic_launcher"
android:name="org.qtproject.qt5.android.bindings.QtApplication"
android:label="@string/app_name"
android:resizeableActivity="true">
<activity
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
android:name="@META_ANDROID_PACKAGE_NAME@.Activity"
android:label="@string/app_name"
android:screenOrientation="unspecified"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
<!-- Deploy Qt libs as part of package -->
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.bundled_in_lib_resource_id" android:resource="@array/bundled_in_lib"/>
<meta-data android:name="android.app.bundled_in_assets_resource_id" android:resource="@array/bundled_in_assets"/>
<!-- Run with local libs -->
<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
<!--<meta-data android:name="android.app.load_local_libs" android:value="-- %%INSERT_LOCAL_LIBS%% --"/>-->
<meta-data android:name="android.app.load_local_libs" android:value="plugins/platforms/android/libqtforandroid.so:plugins/bearer/libqandroidbearer.so:lib/libQt5QuickParticles.so"/>
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
<!-- Messages maps -->
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
<!-- Splash screen -->
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splash"/>
<!-- Splash screen -->
</activity>
</application>
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23"/>
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
Remove the comment if you do not require these default permissions. -->
<!-- %%INSERT_PERMISSIONS -->
<!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
Remove the comment if you do not require these default features. -->
<!-- %%INSERT_FEATURES -->
</manifest>

83
android/build.gradle Normal file
View File

@ -0,0 +1,83 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.1'
}
}
repositories {
google()
mavenCentral()
}
apply plugin: 'com.android.application'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation "androidx.documentfile:documentfile:1.0.1"
implementation 'androidx.core:core:1.10.1'
}
android {
/*******************************************************
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qtAndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
* are defined in gradle.properties file. This file is
* updated by QtCreator and androiddeployqt tools.
* Changing them manually might break the compilation!
*******************************************************/
compileSdkVersion androidCompileSdkVersion
buildToolsVersion androidBuildToolsVersion
ndkVersion androidNdkVersion
// Extract native libraries from the APK
packagingOptions.jniLibs.useLegacyPackaging true
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = [qtAndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qtAndroidDir + '/res', 'res']
resources.srcDirs = ['resources']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
tasks.withType(JavaCompile) {
options.incremental = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
abortOnError false
}
// Do not compress Qt binary resources file
aaptOptions {
noCompress 'rcc'
}
defaultConfig {
resConfig "en"
minSdkVersion qtMinSdkVersion
targetSdkVersion qtTargetSdkVersion
ndk.abiFilters = qtTargetAbiList.split(",")
}
}

View File

@ -1,63 +0,0 @@
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.0'
}
}
repositories {
google()
jcenter()
}
apply plugin: 'com.android.application'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:support-v4:27.1.0'
}
android {
/*******************************************************
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
* are defined in gradle.properties file. This file is
* updated by QtCreator and androiddeployqt tools.
* Changing them manually might break the compilation!
*******************************************************/
compileSdkVersion androidCompileSdkVersion.toInteger()
buildToolsVersion androidBuildToolsVersion
defaultConfig {
applicationId "@META_ANDROID_PACKAGE_NAME@"
applicationIdSuffix "@ANDROID_APK_APPLICATION_ID_SUFFIX@"
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qt5AndroidDir + '/res', 'res']
resources.srcDirs = ['src']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
lintOptions {
abortOnError false
}
}

View File

@ -0,0 +1 @@
android.useAndroidX=true

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">@ANDROID_APK_APPLICATION_LABEL@</string>
</resources>

View File

@ -7,9 +7,9 @@ import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.view.Window;
import android.view.WindowManager.LayoutParams;
import android.support.v4.provider.DocumentFile;
import androidx.documentfile.provider.DocumentFile;
import java.io.FileNotFoundException;
import org.qtproject.qt5.android.bindings.QtActivity;
import org.qtproject.qt.android.bindings.QtActivity;
public class Activity extends QtActivity {
private final int REQUEST_CODE_OPEN_EXISTING_FILE = 1;

View File

@ -5,6 +5,7 @@
#include <passwordfile/io/field.h>
#include <passwordfile/io/parsingexception.h>
#include <passwordfile/io/passwordfile.h>
#include <passwordfile/util/openssl.h>
#include <c++utilities/application/commandlineutils.h>
#include <c++utilities/conversion/stringconversion.h>
@ -15,7 +16,9 @@
#endif
#include <algorithm>
#include <cstdlib>
#include <functional>
#include <stdexcept>
using namespace std;
using namespace std::placeholders;
@ -89,32 +92,39 @@ InteractiveCli::InteractiveCli()
, m_modified(false)
, m_quit(false)
{
Util::OpenSsl::init();
CMD_UTILS_START_CONSOLE;
}
void InteractiveCli::run(const string &file)
InteractiveCli::~InteractiveCli()
{
Util::OpenSsl::clean();
}
int InteractiveCli::run(string_view file)
{
if (!file.empty()) {
openFile(file, PasswordFileOpenFlags::Default);
}
string input;
auto input = std::string();
while (!m_quit) {
getline(m_i, input);
std::getline(m_i, input);
if (!input.empty()) {
processCommand(input);
}
}
return EXIT_SUCCESS;
}
void InteractiveCli::processCommand(const string &cmd)
void InteractiveCli::processCommand(const std::string &cmd)
{
#define CMD(value) !paramMissing &&cmd == value
#define CMD2(value1, value2) !paramMissing && (cmd == value1 || cmd == value2)
#define CMD_P(value) !paramMissing &&checkCommand(cmd, value, param, paramMissing)
#define CMD2_P(value1, value2) !paramMissing && (checkCommand(cmd, value1, param, paramMissing) || checkCommand(cmd, value2, param, paramMissing))
string param;
bool paramMissing = false;
auto param = std::string();
auto paramMissing = false;
if (CMD2("quit", "q")) {
quit();
} else if (CMD("q!")) {
@ -175,9 +185,9 @@ void InteractiveCli::processCommand(const string &cmd)
}
}
Entry *InteractiveCli::resolvePath(const string &path)
Entry *InteractiveCli::resolvePath(const std::string &path)
{
auto parts = splitString<vector<string>>(path, "/", EmptyPartsTreat::Merge);
auto parts = splitString<std::vector<std::string>>(path, "/", EmptyPartsTreat::Merge);
bool fromRoot = path.at(0) == '/';
if (fromRoot && parts.empty()) {
return m_file.rootEntry();
@ -213,7 +223,7 @@ Entry *InteractiveCli::resolvePath(const string &path)
}
}
bool InteractiveCli::checkCommand(const string &str, const char *phrase, std::string &param, bool &paramMissing)
bool InteractiveCli::checkCommand(const std::string &str, const char *phrase, std::string &param, bool &paramMissing)
{
for (auto i = str.cbegin(), end = str.cend(); i != end; ++i, ++phrase) {
if (*phrase == 0) {
@ -234,13 +244,13 @@ bool InteractiveCli::checkCommand(const string &str, const char *phrase, std::st
return false;
}
void InteractiveCli::openFile(const string &file, PasswordFileOpenFlags openFlags)
void InteractiveCli::openFile(std::string_view file, PasswordFileOpenFlags openFlags)
{
if (m_file.isOpen()) {
m_o << "file \"" << m_file.path() << "\" currently open; close first" << endl;
return;
}
m_file.setPath(file);
m_file.setPath(std::string(file));
for (;;) {
try {
try {
@ -322,7 +332,7 @@ void InteractiveCli::saveFile()
m_modified = false;
}
void InteractiveCli::createFile(const string &file)
void InteractiveCli::createFile(const std::string &file)
{
if (m_file.isOpen()) {
m_o << "file \"" << m_file.path() << "\" currently open; close first" << endl;
@ -392,7 +402,7 @@ void InteractiveCli::pwd()
m_o << joinStrings(path, "/") << endl;
}
void InteractiveCli::cd(const string &path)
void InteractiveCli::cd(const std::string &path)
{
if (!m_file.isOpen()) {
m_o << "can not change directory; no file open" << endl;
@ -450,7 +460,7 @@ void InteractiveCli::tree()
printEntries(m_currentEntry, 0);
}
void InteractiveCli::makeEntry(EntryType entryType, const string &label)
void InteractiveCli::makeEntry(EntryType entryType, const std::string &label)
{
if (!m_file.isOpen()) {
m_o << "can not make entry; no file open" << endl;
@ -473,7 +483,7 @@ void InteractiveCli::makeEntry(EntryType entryType, const string &label)
}
}
void InteractiveCli::removeEntry(const string &path)
void InteractiveCli::removeEntry(const std::string &path)
{
if (!m_file.isOpen()) {
m_o << "can not remove entry; no file open" << endl;
@ -493,16 +503,16 @@ void InteractiveCli::removeEntry(const string &path)
}
}
void InteractiveCli::renameEntry(const string &path)
void InteractiveCli::renameEntry(const std::string &path)
{
if (!m_file.isOpen()) {
m_o << "can not rename entry; no file open" << endl;
return;
}
if (Entry *entry = resolvePath(path)) {
string label;
auto label = std::string();
m_o << "enter new name: " << endl;
getline(m_i, label);
std::getline(m_i, label);
if (label.empty()) {
m_o << "can not rename; new name is empty" << endl;
} else {
@ -513,16 +523,16 @@ void InteractiveCli::renameEntry(const string &path)
}
}
void InteractiveCli::moveEntry(const string &path)
void InteractiveCli::moveEntry(const std::string &path)
{
if (!m_file.isOpen()) {
m_o << "can not rename entry; no file open" << endl;
return;
}
if (Entry *entry = resolvePath(path)) {
string newParentPath;
auto newParentPath = std::string();
m_o << "enter path of new parent: " << endl;
getline(m_i, newParentPath);
std::getline(m_i, newParentPath);
if (newParentPath.empty()) {
m_o << "can not move; path of new parent is empty" << endl;
} else {
@ -547,7 +557,7 @@ void InteractiveCli::moveEntry(const string &path)
}
}
void InteractiveCli::readField(const string &fieldName)
void InteractiveCli::readField(const std::string &fieldName)
{
if (!m_file.isOpen()) {
m_o << "can not read field; no file open" << endl;
@ -571,7 +581,7 @@ void InteractiveCli::readField(const string &fieldName)
}
}
void InteractiveCli::setField(bool useMuter, const string &fieldName)
void InteractiveCli::setField(bool useMuter, const std::string &fieldName)
{
if (!m_file.isOpen()) {
m_o << "can not set field; no file open" << endl;
@ -581,22 +591,22 @@ void InteractiveCli::setField(bool useMuter, const string &fieldName)
m_o << "can not set field; current entry is no account entry" << endl;
return;
}
vector<Field> &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
unsigned int valuesFound = 0;
string value;
auto &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
auto valuesFound = unsigned();
auto value = std::string();
m_o << "enter new value: ";
if (useMuter) {
InputMuter m;
getline(m_i, value);
std::getline(m_i, value);
m_o << endl << "repeat: ";
string repeat;
getline(m_i, repeat);
auto repeat = std::string();
std::getline(m_i, repeat);
if (value != repeat) {
m_o << "values do not match; field has not been altered" << endl;
return;
}
} else {
getline(m_i, value);
std::getline(m_i, value);
}
for (Field &field : fields) {
if (field.name() == fieldName) {
@ -643,7 +653,7 @@ void InteractiveCli::setField(bool useMuter, const string &fieldName)
}
}
void InteractiveCli::removeField(const string &fieldName)
void InteractiveCli::removeField(const std::string &fieldName)
{
if (!m_file.isOpen()) {
m_o << "can not remove field; no file open" << endl;
@ -653,8 +663,8 @@ void InteractiveCli::removeField(const string &fieldName)
m_o << "can not remove field; current entry is no account entry" << endl;
return;
}
vector<Field> &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
unsigned int valuesFound = 0;
auto &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
auto valuesFound = unsigned();
for (const Field &field : fields) {
if (field.name() == fieldName) {
++valuesFound;
@ -730,7 +740,7 @@ void InteractiveCli::quit()
}
}
string InteractiveCli::askForPassphrase(bool confirm)
std::string InteractiveCli::askForPassphrase(bool confirm)
{
if (confirm) {
m_o << "enter new passphrase: ";
@ -738,10 +748,10 @@ string InteractiveCli::askForPassphrase(bool confirm)
m_o << "enter passphrase: ";
}
m_o.flush();
string input1;
auto input1 = std::string();
{
InputMuter m;
getline(m_i, input1);
auto m = InputMuter();
std::getline(m_i, input1);
}
m_o << endl;
if (input1.empty()) {
@ -751,15 +761,15 @@ string InteractiveCli::askForPassphrase(bool confirm)
if (confirm) {
m_o << "confirm new passphrase: ";
m_o.flush();
string input2;
auto input2 = std::string();
{
InputMuter m;
getline(m_i, input2);
auto m = InputMuter();
std::getline(m_i, input2);
}
m_o << endl;
if (input1 != input2) {
m_o << "phrases do not match" << endl;
throw runtime_error("confirmation failed");
throw std::runtime_error("confirmation failed");
}
}
return input1;

View File

@ -12,7 +12,7 @@
#include <istream>
#include <ostream>
#include <string>
#include <vector>
#include <string_view>
namespace Io {
class Entry;
@ -23,7 +23,7 @@ namespace Cli {
class InputMuter {
public:
InputMuter();
explicit InputMuter();
~InputMuter();
private:
@ -39,9 +39,10 @@ void clearConsole();
class InteractiveCli {
public:
InteractiveCli();
void run(const std::string &file = std::string());
void openFile(const std::string &file, Io::PasswordFileOpenFlags openFlags);
explicit InteractiveCli();
~InteractiveCli();
int run(std::string_view file);
void openFile(std::string_view file, Io::PasswordFileOpenFlags openFlags);
void closeFile();
void saveFile();
void createFile(const std::string &file);

View File

@ -7,7 +7,7 @@ namespace QtGui {
class FieldDelegate : public QStyledItemDelegate {
public:
FieldDelegate(QObject *parent = nullptr);
explicit FieldDelegate(QObject *parent = nullptr);
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
};

View File

@ -5,6 +5,8 @@
#include "resources/config.h"
#include "resources/qtconfig.h"
#include <QApplication> // ensure QGuiApplication is defined before resources.h for desktop file name
#include <qtutilities/resources/importplugin.h>
#include <qtutilities/resources/qtconfigarguments.h>
#include <qtutilities/resources/resources.h>
@ -12,7 +14,6 @@
#include <passwordfile/util/openssl.h>
#include <QApplication>
#include <QFile>
#include <QMessageBox>
#include <QSettings>
@ -31,12 +32,14 @@ int runWidgetsGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs,
OpenSsl::init();
// init application
QApplication application(argc, argv);
auto application = QApplication(argc, argv);
QObject::connect(&application, &QCoreApplication::aboutToQuit, &OpenSsl::clean);
// restore Qt settings
auto qtSettings = QtSettings();
auto settings = QtUtilities::getSettings(QStringLiteral(PROJECT_NAME));
auto settingsError = QtUtilities::errorMessageForSettings(*settings);
qtSettings.disableNotices();
qtSettings.restore(*settings);
qtSettings.apply();
@ -55,7 +58,6 @@ int runWidgetsGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs,
}
// start event loop
QObject::connect(&application, &QCoreApplication::aboutToQuit, &OpenSsl::clean);
auto res = application.exec();
// save settings to disk

View File

@ -106,6 +106,19 @@ void MainWindow::setSomethingChanged(bool somethingChanged)
}
}
/*!
* \brief Updates the style sheet.
*/
void MainWindow::updateStyleSheet()
{
#ifdef Q_OS_WINDOWS
const auto p = palette();
setStyleSheet(QStringLiteral("%1 #splitter QWidget { background-color: palette(base); color: palette(text); } #splitter QWidget *, #splitter "
"QWidget * { background-color: none; } #leftWidget { border-right: 1px solid %2; }")
.arg(dialogStyleForPalette(p), windowFrameColorForPalette(p).name()));
#endif
}
/*!
* \brief Constructs a new main window.
*/
@ -120,12 +133,9 @@ MainWindow::MainWindow(QSettings &settings, QtUtilities::QtSettings *qtSettings,
, m_settingsDlg(nullptr)
{
// setup ui
updateStyleSheet();
m_ui->setupUi(this);
#ifdef Q_OS_WIN32
setStyleSheet(QStringLiteral("%1 #splitter QWidget { background-color: palette(base); color: palette(text); } #splitter QWidget *, #splitter "
"QWidget * { background-color: none; } #leftWidget { border-right: 1px solid %2; }")
.arg(dialogStyle(), windowFrameColor().name()));
#endif
// set default values
setSomethingChanged(false);
m_dontUpdateSelection = false;
@ -227,7 +237,7 @@ MainWindow::MainWindow(QSettings &settings, QtUtilities::QtSettings *qtSettings,
connect(m_ui->accountFilterLineEdit, &QLineEdit::textChanged, this, &MainWindow::applyFilter);
// setup other controls
m_ui->actionAlwaysCreateBackup->setChecked(settings.value(QStringLiteral("alwayscreatebackup"), false).toBool());
m_ui->actionAlwaysCreateBackup->setChecked(settings.value(QStringLiteral("alwayscreatebackup"), true).toBool());
m_ui->accountFilterLineEdit->setText(settings.value(QStringLiteral("accountfilter"), QString()).toString());
m_ui->centralWidget->installEventFilter(this);
settings.endGroup();
@ -240,6 +250,17 @@ MainWindow::~MainWindow()
{
}
bool MainWindow::event(QEvent *event)
{
switch (event->type()) {
case QEvent::PaletteChange:
updateStyleSheet();
break;
default:;
}
return QMainWindow::event(event);
}
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (obj == m_undoView) {
@ -332,6 +353,7 @@ void MainWindow::showSettingsDialog()
if (m_qtSettings) {
m_settingsDlg->setWindowTitle(tr("Qt settings"));
m_settingsDlg->setSingleCategory(m_qtSettings->category());
connect(m_settingsDlg, &SettingsDialog::applied, this, [this] { m_qtSettings->apply(); });
}
}
if (m_settingsDlg->isHidden()) {

View File

@ -74,6 +74,7 @@ public:
bool openFile(const QString &path, Io::PasswordFileOpenFlags openFlags); // can not be a slot in Qt 6 without further effort, see QTBUG-86424
protected:
bool event(QEvent *event) override;
bool eventFilter(QObject *obj, QEvent *event) override;
void closeEvent(QCloseEvent *event) override;
void timerEvent(QTimerEvent *event) override;
@ -108,6 +109,7 @@ private Q_SLOTS:
void clearClipboard();
void setSomethingChanged();
void setSomethingChanged(bool somethingChanged);
void updateStyleSheet();
private:
// showing conditional messages/prompts

View File

@ -43,11 +43,8 @@ PasswordGeneratorDialog::PasswordGeneratorDialog(QWidget *parent)
: QDialog(parent)
, m_ui(new Ui::PasswordGeneratorDialog)
{
updateStyleSheet();
m_ui->setupUi(this);
#ifdef Q_OS_WIN32
setStyleSheet(QStringLiteral("%1 QCommandLinkButton { font-size: 12pt; color: %2; font-weight: normal; }")
.arg(dialogStyle(), instructionTextColor().name()));
#endif
setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
connect(m_ui->generatePassowordCommandLinkButton, &QCommandLinkButton::clicked, this, &PasswordGeneratorDialog::generateNewPassword);
@ -75,6 +72,17 @@ PasswordGeneratorDialog::~PasswordGeneratorDialog()
{
}
bool PasswordGeneratorDialog::event(QEvent *event)
{
switch (event->type()) {
case QEvent::PaletteChange:
updateStyleSheet();
break;
default:;
}
return QDialog::event(event);
}
/*!
* \brief Generates and shows a new password.
*/
@ -162,6 +170,17 @@ void PasswordGeneratorDialog::handlePasswordChanged()
m_ui->copyPasswordCommandLinkButton->setEnabled(m_ui->passwordLineEdit->text().size() > 0);
}
/*!
* \brief Updates the style sheet.
*/
void PasswordGeneratorDialog::updateStyleSheet()
{
#ifdef Q_OS_WINDOWS
setStyleSheet(QStringLiteral("%1 QCommandLinkButton { font-size: 12pt; color: %2; font-weight: normal; }")
.arg(dialogStyleForPalette(palette()), instructionTextColor().name()));
#endif
}
#ifndef QT_NO_CLIPBOARD
/*!
* \brief Copies the current password to the clipboard.

View File

@ -21,10 +21,14 @@ public:
explicit PasswordGeneratorDialog(QWidget *parent = nullptr);
~PasswordGeneratorDialog() override;
protected:
bool event(QEvent *event) override;
private Q_SLOTS:
void generateNewPassword();
void handleCheckedCategoriesChanged();
void handlePasswordChanged();
void updateStyleSheet();
#ifndef QT_NO_CLIPBOARD
void copyPassword();
#endif

View File

@ -281,22 +281,12 @@
</item>
<item>
<widget class="QPushButton" name="closePushButton">
<property name="font">
<font>
<family>Segoe UI,Sans</family>
<pointsize>9</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Close</string>
</property>
<property name="icon">
<iconset theme="window-close">
<normaloff/>
</iconset>
<normaloff>.</normaloff>.</iconset>
</property>
<property name="checkable">
<bool>false</bool>
@ -308,6 +298,17 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>useSmallLettersCheckBox</tabstop>
<tabstop>useCapitalLettersCheckBox</tabstop>
<tabstop>useDigitsCheckBox</tabstop>
<tabstop>otherCharsLineEdit</tabstop>
<tabstop>LengthSpinBox</tabstop>
<tabstop>passwordLineEdit</tabstop>
<tabstop>generatePassowordCommandLinkButton</tabstop>
<tabstop>copyPasswordCommandLinkButton</tabstop>
<tabstop>closePushButton</tabstop>
</tabstops>
<resources>
<include location="../resources/icons.qrc"/>
</resources>

View File

@ -17,7 +17,7 @@ class StackSupport {
friend class StackAbsorper;
public:
StackSupport(QUndoStack *undoStack = nullptr);
explicit StackSupport(QUndoStack *undoStack = nullptr);
protected:
QUndoStack *undoStack();
@ -68,7 +68,7 @@ inline void StackSupport::clearUndoStack()
*/
class StackAbsorper {
public:
StackAbsorper(StackSupport *supported);
explicit StackAbsorper(StackSupport *supported);
~StackAbsorper();
QUndoStack *stack();

View File

@ -62,6 +62,15 @@ void CustomUndoCommand::undo()
* \brief Sets the value for the specified index and role in the specified field model.
*/
/// \cond
static QString getFieldName(FieldModel *model, int row, const QModelIndex &index)
{
return model->index(row, 0, index.parent()).data().toString();
}
/// \endcond
/*!
* \brief Constructs a new command.
*/
@ -75,7 +84,6 @@ FieldModelSetValueCommand::FieldModelSetValueCommand(FieldModel *model, const QM
, m_oldValue(model->data(index, role))
, m_role(role)
{
QString fieldName = model->index(m_row, 0, index.parent()).data().toString();
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole:
@ -88,16 +96,16 @@ FieldModelSetValueCommand::FieldModelSetValueCommand(FieldModel *model, const QM
}
break;
case 1:
if (fieldName.isEmpty()) {
setText(QApplication::translate("undocommands", "setting value of empty field"));
} else {
if (const auto fieldName = getFieldName(model, m_row, index); !fieldName.isEmpty()) {
setText(QApplication::translate("undocommands", "setting value of »%1« field").arg(fieldName));
} else {
setText(QApplication::translate("undocommands", "setting value of empty field"));
}
break;
}
break;
case FieldTypeRole:
setText(QApplication::translate("undocommands", "setting type of »%1« field").arg(fieldName));
setText(QApplication::translate("undocommands", "setting type of »%1« field").arg(getFieldName(model, m_row, index)));
break;
default:
setText(QApplication::translate("undocommands", "setting field property in row »%1«").arg(m_row + 1));
@ -197,7 +205,7 @@ bool FieldModelRemoveRowsCommand::internalUndo()
/*!
* \brief Stores the entry path for the specified \a model and \a index in \a res.
*/
void indexToPath(EntryModel *model, const QModelIndex &index, list<string> &res)
static void indexToPath(EntryModel *model, const QModelIndex &index, list<string> &res)
{
res.clear();
if (Entry *entry = model->entry(index)) {
@ -209,7 +217,7 @@ void indexToPath(EntryModel *model, const QModelIndex &index, list<string> &res)
* \brief Fetches the entry for the specified \a model and \a path.
* \remarks The \a path will be modified. To prevent this use entryFromPathCpy().
*/
Entry *entryFromPath(EntryModel *model, list<string> &path)
static Entry *entryFromPath(EntryModel *model, list<string> &path)
{
if (NodeEntry *rootEntry = model->rootEntry()) {
return rootEntry->entryByPath(path);
@ -220,7 +228,7 @@ Entry *entryFromPath(EntryModel *model, list<string> &path)
/*!
* \brief Fetches the entry for the specified \a model and \a path.
*/
Entry *entryFromPathCpy(EntryModel *model, list<string> path)
static Entry *entryFromPathCpy(EntryModel *model, list<string> path)
{
return entryFromPath(model, path);
}
@ -316,7 +324,7 @@ EntryModelModifyRowsCommand::~EntryModelModifyRowsCommand()
*/
bool EntryModelModifyRowsCommand::insert()
{
if (Entry *parentEntry = entryFromPathCpy(m_model, m_parentPath)) {
if (Entry *const parentEntry = entryFromPathCpy(m_model, m_parentPath)) {
if (m_model->insertEntries(m_row, m_model->index(parentEntry), m_values)) {
m_values.clear();
return true;
@ -334,7 +342,7 @@ bool EntryModelModifyRowsCommand::insert()
*/
bool EntryModelModifyRowsCommand::remove()
{
if (Entry *parentEntry = entryFromPathCpy(m_model, m_parentPath)) {
if (Entry *const parentEntry = entryFromPathCpy(m_model, m_parentPath)) {
m_values = m_model->takeEntries(m_row, m_count, m_model->index(parentEntry));
return !m_values.isEmpty();
}
@ -433,29 +441,28 @@ bool EntryModelMoveRowsCommand::internalRedo()
bool EntryModelMoveRowsCommand::internalUndo()
{
if (m_count) {
Entry *sourceParentEntry = entryFromPathCpy(m_model, m_sourceParentPath);
Entry *destParentEntry = entryFromPathCpy(m_model, m_destParentPath);
if (sourceParentEntry && destParentEntry) {
int sourceRow = m_destChild;
int destChild = m_sourceRow;
// moves within the same parent needs special consideration
if (sourceParentEntry == destParentEntry) {
// move entry down
if (m_sourceRow < m_destChild) {
sourceRow -= m_count;
// move entry up
} else if (m_sourceRow > m_destChild) {
destChild += m_count;
// keep entry were it is
} else {
return true;
}
}
return m_model->moveRows(m_model->index(destParentEntry), sourceRow, m_count, m_model->index(sourceParentEntry), destChild);
}
return false;
if (!m_count) {
return true;
}
return true;
Entry *const sourceParentEntry = entryFromPathCpy(m_model, m_sourceParentPath);
Entry *const destParentEntry = entryFromPathCpy(m_model, m_destParentPath);
if (sourceParentEntry && destParentEntry) {
auto sourceRow = m_destChild, destChild = m_sourceRow;
// moves within the same parent needs special consideration
if (sourceParentEntry == destParentEntry) {
// move entry down
if (m_sourceRow < m_destChild) {
sourceRow -= m_count;
// move entry up
} else if (m_sourceRow > m_destChild) {
destChild += m_count;
// keep entry were it is
} else {
return true;
}
}
return m_model->moveRows(m_model->index(destParentEntry), sourceRow, m_count, m_model->index(sourceParentEntry), destChild);
}
return false;
}
} // namespace QtGui

View File

@ -1,4 +1,5 @@
#include "./cli/cli.h"
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
#include "./gui/initiategui.h"
#endif
@ -9,13 +10,12 @@
#include "resources/config.h"
#include "resources/qtconfig.h"
#include <passwordfile/util/openssl.h>
#include <c++utilities/application/argumentparser.h>
#include <c++utilities/application/commandlineutils.h>
#include <c++utilities/misc/parseerror.h>
#if defined(PASSWORD_MANAGER_GUI_QTWIDGETS) || defined(PASSWORD_MANAGER_GUI_QTQUICK)
#define PASSWORD_MANAGER_GUI_QTWIDGETS_OR_QTQUICK
#include <QCoreApplication>
#include <QString>
#include <qtutilities/resources/qtconfigarguments.h>
@ -24,33 +24,39 @@ ENABLE_QT_RESOURCES_OF_STATIC_DEPENDENCIES
#include <c++utilities/application/fakeqtconfigarguments.h>
#endif
#include <cstdlib>
#include <iostream>
// force (preferably Qt Quick) GUI under Android
#ifdef Q_OS_ANDROID
#if defined(PASSWORD_MANAGER_GUI_QTWIDGETS) || defined(PASSWORD_MANAGER_GUI_QTQUICK)
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS_OR_QTQUICK
#define PASSWORD_MANAGER_FORCE_GUI
#else
#error "Must build at least one kind of GUI under Android."
#error "Must configure building at least one kind of GUI under Android."
#endif
#endif
using namespace std;
using namespace CppUtilities;
using namespace Util;
#ifndef PASSWORD_MANAGER_FORCE_GUI
static int fail(std::string_view error)
{
CMD_UTILS_START_CONSOLE;
std::cerr << error << std::endl;
return EXIT_FAILURE;
}
#endif
int main(int argc, char *argv[])
{
CMD_UTILS_CONVERT_ARGS_TO_UTF8;
SET_APPLICATION_INFO;
QT_CONFIG_ARGUMENTS qtConfigArgs;
int returnCode = 0;
// parse CLI arguments
auto qtConfigArgs = QT_CONFIG_ARGUMENTS();
#ifndef PASSWORD_MANAGER_FORCE_GUI
// setup argument parser
ArgumentParser parser;
// file argument
Argument fileArg("file", 'f', "specifies the file to be opened (or created when using --modify)");
auto parser = ArgumentParser();
auto fileArg = Argument("file", 'f', "specifies the file to be opened (or created when using --modify)");
fileArg.setValueNames({ "path" });
fileArg.setRequiredValueCount(1);
fileArg.setCombinable(true);
@ -58,63 +64,44 @@ int main(int argc, char *argv[])
fileArg.setImplicit(true);
qtConfigArgs.qtWidgetsGuiArg().addSubArgument(&fileArg);
qtConfigArgs.qtQuickGuiArg().addSubArgument(&fileArg);
// cli argument
Argument cliArg("interactive-cli", 'i', "starts the interactive command line interface");
auto cliArg = Argument("interactive-cli", 'i', "starts the interactive command line interface");
cliArg.setDenotesOperation(true);
cliArg.setSubArguments({ &fileArg });
// help argument
HelpArgument helpArg(parser);
auto helpArg = HelpArgument(parser);
parser.setMainArguments({ &qtConfigArgs.qtWidgetsGuiArg(), &qtConfigArgs.qtQuickGuiArg(), &cliArg, &helpArg });
// parse the specified arguments
parser.parseArgs(argc, argv);
#endif
#ifndef PASSWORD_MANAGER_FORCE_GUI
// start either interactive CLI or GUI
// run CLI if CLI-argument is present
if (cliArg.isPresent()) {
// init OpenSSL
OpenSsl::init();
return Cli::InteractiveCli().run(fileArg.isPresent() ? std::string(fileArg.firstValue()) : std::string());
}
Cli::InteractiveCli cli;
if (fileArg.isPresent()) {
cli.run(fileArg.firstValue());
} else {
cli.run();
}
// clean OpenSSL
OpenSsl::clean();
} else if (qtConfigArgs.areQtGuiArgsPresent()) {
#if defined(PASSWORD_MANAGER_GUI_QTWIDGETS) || defined(PASSWORD_MANAGER_GUI_QTQUICK)
const auto file(fileArg.isPresent() ? QString::fromLocal8Bit(fileArg.firstValue()) : QString());
// run GUI depending on which GUI-argument is present
if (qtConfigArgs.areQtGuiArgsPresent()) {
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS_OR_QTQUICK
const auto file = fileArg.isPresent() ? QString::fromLocal8Bit(fileArg.firstValue()) : QString();
#endif
if (qtConfigArgs.qtWidgetsGuiArg().isPresent()) {
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
returnCode = QtGui::runWidgetsGui(argc, argv, qtConfigArgs, file);
return QtGui::runWidgetsGui(argc, argv, qtConfigArgs, file);
#else
CMD_UTILS_START_CONSOLE;
cerr << "The application has not been built with Qt widgets support." << endl;
return fail("The application has not been built with Qt Widgets GUI support.");
#endif
} else if (qtConfigArgs.qtQuickGuiArg().isPresent()) {
#ifdef PASSWORD_MANAGER_GUI_QTQUICK
returnCode = QtGui::runQuickGui(argc, argv, qtConfigArgs, file);
return QtGui::runQuickGui(argc, argv, qtConfigArgs, file);
#else
CMD_UTILS_START_CONSOLE;
cerr << "The application has not been built with Qt quick support." << endl;
return fail("The application has not been built with Qt Quick GUI support.");
#endif
} else {
CMD_UTILS_START_CONSOLE;
cerr << "See --help for usage." << endl;
}
}
return fail("See --help for usage.");
#else // PASSWORD_MANAGER_FORCE_GUI
#ifdef PASSWORD_MANAGER_GUI_QTQUICK
returnCode = QtGui::runQuickGui(argc, argv, qtConfigArgs, QString());
return QtGui::runQuickGui(argc, argv, qtConfigArgs, QString());
#else
returnCode = QtGui::runWidgetsGui(argc, argv, qtConfigArgs, QString());
return QtGui::runWidgetsGui(argc, argv, qtConfigArgs, QString());
#endif
#endif
return returnCode;
}

View File

@ -452,31 +452,39 @@ bool EntryModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co
if (undoStack()) {
return push(make_unique<EntryModelMoveRowsCommand>(this, sourceParent, sourceRow, count, destinationParent, destinationChild));
}
#endif
#if CPP_UTILITIES_DEBUG_BUILD
std::cout << "sourceRow: " << sourceRow << endl;
std::cout << "destinationChild: " << destinationChild << endl;
#endif
// check validation of specified arguments: source and destination parent entries need to be node entries
if (sourceRow < 0 || count <= 0) {
return false;
}
const auto *const srcParentEntry = entry(sourceParent);
const auto *const destParentEntry = entry(sourceParent);
const auto *const destParentEntry = entry(destinationParent);
if (!srcParentEntry || !destParentEntry || srcParentEntry->type() != EntryType::Node || destParentEntry->type() != EntryType::Node) {
return false;
}
// determine the source parent entry and dest parent entry as node entries
auto *const srcParentNodeEntry = static_cast<NodeEntry *>(sourceParent.internalPointer());
auto *const destParentNodeEntry = static_cast<NodeEntry *>(destinationParent.internalPointer());
#if CPP_UTILITIES_DEBUG_BUILD
cout << "destinationChild: " << destinationChild << endl;
#endif
// source rows must be within the valid range
if (static_cast<size_t>(sourceRow + count) > srcParentNodeEntry->children().size()
// if source and destination parent are the same the destination child mustn't be in the source range
|| !(srcParentNodeEntry != destParentNodeEntry || (destinationChild < sourceRow || (sourceRow + count) < destinationChild))) {
if (static_cast<std::size_t>(sourceRow + count) > srcParentNodeEntry->children().size()) {
return false;
}
// if source and destination parent are the same the destination child mustn't be in the source range
if (srcParentNodeEntry == destParentNodeEntry) {
if (destinationChild == sourceRow) {
return true;
}
if (!(destinationChild < sourceRow || (sourceRow + count) < destinationChild)) {
return false;
}
}
// do not move a row to one of its own children! -> check before
for (int index = 0; index < count; ++index) {
Entry *const toMove = srcParentNodeEntry->children()[static_cast<size_t>(sourceRow + index)];
Entry *const toMove = srcParentNodeEntry->children()[static_cast<std::size_t>(sourceRow + index)];
if (toMove->type() != EntryType::Node) {
continue;
}
@ -485,9 +493,11 @@ bool EntryModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co
}
}
// actually perform the move operation
beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild);
if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild)) {
return false;
}
for (int index = 0; index < count; ++index) {
Entry *toMove = srcParentNodeEntry->children()[static_cast<size_t>(sourceRow + index)];
Entry *toMove = srcParentNodeEntry->children()[static_cast<std::size_t>(sourceRow + index)];
if (srcParentNodeEntry == destParentNodeEntry && sourceRow < destinationChild) {
toMove->setParent(destParentNodeEntry, destinationChild + index - 1);
} else {

View File

@ -233,17 +233,15 @@ bool FieldModel::setData(const QModelIndex &index, const QVariant &value, int ro
switch (index.column()) {
case 0:
beginInsertRows(index.parent(), rowCount(), rowCount());
m_fields->emplace_back(m_accountEntry);
m_fields->back().setName(value.toString().toStdString());
m_fields->emplace_back(m_accountEntry).setName(value.toString().toStdString());
endInsertRows();
roles << Qt::DisplayRole << Qt::EditRole << IsLastRow;
roles << Qt::DisplayRole << Qt::EditRole << Key << IsLastRow;
break;
case 1:
beginInsertRows(index.parent(), rowCount(), rowCount());
m_fields->emplace_back(m_accountEntry);
m_fields->back().setValue(value.toString().toStdString());
m_fields->emplace_back(m_accountEntry).setValue(value.toString().toStdString());
endInsertRows();
roles << Qt::DisplayRole << Qt::EditRole << IsLastRow;
roles << Qt::DisplayRole << Qt::EditRole << Value << IsLastRow;
break;
default:;
}
@ -335,25 +333,23 @@ bool FieldModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co
// validate input parameter
if (sourceParent.isValid() || destinationParent.isValid() || sourceRow < 0 || count <= 0 || destinationChild < 0
|| static_cast<size_t>(sourceRow + count) > m_fields->size() || static_cast<size_t>(destinationChild) >= m_fields->size()
|| static_cast<std::size_t>(sourceRow + count) > m_fields->size() || static_cast<std::size_t>(destinationChild) >= m_fields->size()
|| (destinationChild >= sourceRow && destinationChild < (sourceRow + count))) {
return false;
}
// begin the move
if (destinationChild > sourceRow) {
// move rows down: the third param is still counted in the initial array!
beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild + count);
} else {
// move rows up
beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild);
// note: When moving rows down (destinationChild > sourceRow) the third param is still counted in the initial array!
if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent,
destinationChild > sourceRow ? destinationChild + count : destinationChild)) {
return false;
}
// reserve space for temporary copies (FIXME: possible to avoid this?)
m_fields->reserve(m_fields->size() + static_cast<size_t>(count));
vector<Io::Field> tmp(static_cast<size_t>(count));
m_fields->reserve(m_fields->size() + static_cast<std::size_t>(count));
auto tmp = std::vector<Io::Field>(static_cast<std::size_t>(count));
// move rows to temporary array
move(m_fields->begin() + sourceRow, m_fields->begin() + sourceRow + count, tmp.begin());
std::move(m_fields->begin() + sourceRow, m_fields->begin() + sourceRow + count, tmp.begin());
// erase slots of rows to be moved
m_fields->erase(m_fields->begin() + sourceRow, m_fields->begin() + sourceRow + count);
// insert rows again at their new position
@ -398,10 +394,7 @@ QMimeData *FieldModel::mimeData(const QModelIndexList &indices) const
*/
const Field *FieldModel::field(size_t row) const
{
if (m_fields && row < m_fields->size()) {
return &(*m_fields)[row];
}
return nullptr;
return m_fields && row < m_fields->size() ? &(*m_fields)[row] : nullptr;
}
} // namespace QtGui

View File

@ -7,8 +7,7 @@ BasicDialog {
id: aboutDialog
standardButtons: Controls.Dialog.Ok
padding: Kirigami.Units.largeSpacing
ColumnLayout {
contentItem: ColumnLayout {
width: aboutDialog.availableWidth
Image {
@ -43,16 +42,8 @@ BasicDialog {
Layout.fillWidth: true
text: "<a href=\"" + app.organizationDomain + "\">" + app.organizationDomain + "</a>"
horizontalAlignment: Text.AlignHCenter
onLinkActivated: openWebsite()
onLinkActivated: Qt.openUrlExternally(app.organizationDomain)
wrapMode: Text.Wrap
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: parent.openWebsite()
}
function openWebsite() {
Qt.openUrlExternally(app.organizationDomain)
}
}
Controls.Label {
Layout.fillWidth: true

View File

@ -6,9 +6,7 @@ Controls.Dialog {
modal: true
focus: true
parent: applicationWindow().overlay
//anchors.centerIn: parent // enable if requiring at least Qt 5.12 instead of setting x and y manually
x: (parent.width - width) / 2
y: (parent.height - height) / 2
anchors.centerIn: parent
width: Math.min(parent.width, Kirigami.Units.gridUnit * 30)
function acceptOnReturn(event) {

View File

@ -1,4 +1,4 @@
import QtQuick 2.4
import QtQuick 2.15
import QtQuick.Layouts 1.2
import QtQml.Models 2.2
import QtQuick.Controls 2.4 as Controls
@ -15,17 +15,17 @@ Kirigami.ScrollablePage {
var currentEntryName = entryModel.data(rootIndex)
return currentEntryName ? currentEntryName : ""
}
actions {
main: Kirigami.Action {
iconName: "list-add"
actions:[
Kirigami.Action {
icon.name: "list-add"
text: qsTr("Add account")
visible: !nativeInterface.hasEntryFilter
enabled: !nativeInterface.hasEntryFilter
onTriggered: insertEntry("Account")
shortcut: "Ctrl+A"
}
left: Kirigami.Action {
iconName: "edit-paste"
},
Kirigami.Action {
icon.name: "edit-paste"
text: qsTr("Paste account")
visible: !nativeInterface.hasEntryFilter
enabled: nativeInterface.canPaste && !nativeInterface.hasEntryFilter
@ -40,19 +40,16 @@ Kirigami.ScrollablePage {
pastedEntries.join(", ")))
}
shortcut: StandardKey.Paste
}
right: Kirigami.Action {
iconName: "folder-add"
},
Kirigami.Action {
icon.name: "folder-add"
text: qsTr("Add category")
visible: !nativeInterface.hasEntryFilter
enabled: !nativeInterface.hasEntryFilter
onTriggered: insertEntry("Node")
shortcut: "Ctrl+Shift+A"
}
}
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
]
// dialog to confirm deletion of an entry
BasicDialog {
@ -64,8 +61,7 @@ Kirigami.ScrollablePage {
standardButtons: Controls.Dialog.Ok | Controls.Dialog.Cancel
title: qsTr("Delete %1?").arg(entryDesc)
onAccepted: entryModel.removeRows(this.entryIndex, 1, rootIndex)
ColumnLayout {
contentItem: ColumnLayout {
Controls.Label {
text: " "
}
@ -109,13 +105,12 @@ Kirigami.ScrollablePage {
entryModel.removeRows(this.entryIndex, 1, rootIndex)
}
}
ColumnLayout {
contentItem: ColumnLayout {
Controls.TextField {
id: entryNameTextField
Layout.preferredWidth: renameDialog.availableWidth
placeholderText: qsTr("enter new name here")
Keys.onPressed: renameDialog.acceptOnReturn(event)
Keys.onPressed: (event) => renameDialog.acceptOnReturn(event)
}
}
@ -138,115 +133,6 @@ Kirigami.ScrollablePage {
}
}
// component representing an entry
Component {
id: listDelegateComponent
Kirigami.SwipeListItem {
id: listItem
contentItem: RowLayout {
Kirigami.ListItemDragHandle {
listItem: listItem
listView: entriesListView
enabled: !nativeInterface.hasEntryFilter
// FIXME: not sure why newIndex + 1 is required to be able to move a row at the end
onMoveRequested: entryModel.moveRows(
rootIndex, oldIndex, 1, rootIndex,
oldIndex < newIndex ? newIndex + 1 : newIndex)
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
RowLayout {
anchors.fill: parent
Kirigami.Icon {
width: Kirigami.Units.iconSizes.smallMedium
height: Kirigami.Units.iconSizes.smallMedium
Layout.fillHeight: true
source: delegateModel.isNode(
index) ? "folder-symbolic" : "story-editor"
}
Controls.Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: model.name
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.RightButton) {
entryContextMenu.popup()
return
}
delegateModel.handleEntryClicked(index, model.name)
}
onPressAndHold: entryContextMenu.popup()
}
Controls.Menu {
id: entryContextMenu
Controls.MenuItem {
icon.name: "edit-cut"
text: qsTr("Cut")
enabled: !nativeInterface.hasEntryFilter
onTriggered: {
nativeInterface.cutEntry(
entryModel.index(index, 0,
rootIndex))
showPassiveNotification(qsTr("Cut %1").arg(
model.name))
}
}
Controls.MenuItem {
icon.name: "edit-delete"
text: qsTr("Delete")
enabled: !nativeInterface.hasEntryFilter
onTriggered: confirmDeletionDialog.confirmDeletion(
model.name, index)
}
Controls.MenuItem {
icon.name: "edit-rename"
text: qsTr("Rename")
enabled: !nativeInterface.hasEntryFilter
onTriggered: renameDialog.renameEntry(model.name,
index)
}
}
}
}
actions: [
Kirigami.Action {
iconName: "edit-cut"
text: qsTr("Cut")
enabled: !nativeInterface.hasEntryFilter
onTriggered: {
nativeInterface.cutEntry(entryModel.index(index, 0,
rootIndex))
showPassiveNotification(text + " " + model.name)
}
shortcut: StandardKey.Cut
},
Kirigami.Action {
iconName: "edit-delete"
text: qsTr("Delete")
enabled: !nativeInterface.hasEntryFilter
onTriggered: confirmDeletionDialog.confirmDeletion(
model.name, index)
shortcut: StandardKey.Delete
},
Kirigami.Action {
iconName: "edit-rename"
text: qsTr("Rename")
enabled: !nativeInterface.hasEntryFilter
onTriggered: renameDialog.renameEntry(model.name, index)
shortcut: "F2"
}
]
}
}
// list view to display one hierarchy level of entry model
ListView {
id: entriesListView
@ -257,12 +143,16 @@ Kirigami.ScrollablePage {
easing.type: Easing.InOutQuad
}
}
reuseItems: true
model: DelegateModel {
id: delegateModel
delegate: Kirigami.DelegateRecycler {
width: parent ? parent.width : implicitWidth
sourceComponent: listDelegateComponent
delegate: EntryDelegate {
width: entriesListView.width
view: entriesListView
onMoveRequested:
(oldIndex, newIndex) => {
entryModel.moveRows(rootIndex, oldIndex, 1, rootIndex, oldIndex < newIndex ? newIndex + 1 : newIndex)
}
}
function isNode(rowNumber) {

119
qml/EntryDelegate.qml Normal file
View File

@ -0,0 +1,119 @@
import QtQuick 2.4
import QtQuick.Layouts 1.2
import QtQml.Models 2.2
import QtQuick.Controls 2.4 as Controls
import org.kde.kirigami 2.5 as Kirigami
Item {
id: delegate
property ListView view
implicitHeight : listItem.implicitHeight
signal moveRequested(int oldIndex, int newIndex)
Kirigami.SwipeListItem {
id: listItem
alwaysVisibleActions: true // default is broken in mobile/tablet mode
contentItem: RowLayout {
Kirigami.ListItemDragHandle {
listItem: listItem
listView: view
enabled: !nativeInterface.hasEntryFilter
onMoveRequested:
(oldIndex, newIndex) => delegate.moveRequested(oldIndex, newIndex)
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
RowLayout {
anchors.fill: parent
Kirigami.Icon {
width: Kirigami.Units.iconSizes.smallMedium
height: Kirigami.Units.iconSizes.smallMedium
Layout.fillHeight: true
source: delegateModel.isNode(
index) ? "folder-symbolic" : "view-list-details-symbolic"
}
Controls.Label {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: availableWidth - listItem.overlayWidth
elide: Text.ElideRight
text: model.name
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
entryContextMenu.popup()
return
}
delegateModel.handleEntryClicked(index, model.name)
}
onPressAndHold: entryContextMenu.popup()
}
Controls.Menu {
id: entryContextMenu
Controls.MenuItem {
icon.name: "edit-cut"
text: qsTr("Cut")
enabled: !nativeInterface.hasEntryFilter
onTriggered: {
nativeInterface.cutEntry(
entryModel.index(index, 0,
rootIndex))
showPassiveNotification(qsTr("Cut %1").arg(
model.name))
}
}
Controls.MenuItem {
icon.name: "edit-delete"
text: qsTr("Delete")
enabled: !nativeInterface.hasEntryFilter
onTriggered: confirmDeletionDialog.confirmDeletion(
model.name, index)
}
Controls.MenuItem {
icon.name: "edit-rename"
text: qsTr("Rename")
enabled: !nativeInterface.hasEntryFilter
onTriggered: renameDialog.renameEntry(model.name,
index)
}
}
}
}
actions: [
Kirigami.Action {
icon.name: "edit-cut"
text: qsTr("Cut")
enabled: !nativeInterface.hasEntryFilter
onTriggered: {
nativeInterface.cutEntry(entryModel.index(index, 0,
rootIndex))
showPassiveNotification(text + " " + model.name)
}
shortcut: StandardKey.Cut
},
Kirigami.Action {
icon.name: "edit-delete"
text: qsTr("Delete")
enabled: !nativeInterface.hasEntryFilter
onTriggered: confirmDeletionDialog.confirmDeletion(
model.name, index)
shortcut: StandardKey.Delete
},
Kirigami.Action {
icon.name: "edit-rename"
text: qsTr("Rename")
enabled: !nativeInterface.hasEntryFilter
onTriggered: renameDialog.renameEntry(model.name, index)
shortcut: "F2"
}
]
}
}

129
qml/FieldsDelegate.qml Normal file
View File

@ -0,0 +1,129 @@
import QtQuick 2.4
import QtQuick.Layouts 1.2
import QtQml.Models 2.2
import QtQuick.Controls 2.4 as Controls
import org.kde.kirigami 2.5 as Kirigami
Item {
id: delegate
property ListView view
implicitHeight : listItem.implicitHeight
signal moveRequested(int oldIndex, int newIndex)
Kirigami.SwipeListItem {
id: listItem
alwaysVisibleActions: true // default is broken in mobile/tablet mode
visible: !model.isLastRow
contentItem: RowLayout {
id: fieldRow
Kirigami.ListItemDragHandle {
listItem: listItem
listView: view
onMoveRequested: (oldIndex, newIndex) => delegate.moveRequested(oldIndex, newIndex)
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
RowLayout {
anchors.fill: parent
Controls.Label {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: availableWidth - listItem.overlayWidth
elide: Text.ElideRight
text: {
let pieces = []
if (model.key) {
pieces.push(model.key)
}
if (model.value) {
pieces.push(model.value)
}
return pieces.join(": ")
}
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
return fieldContextMenu.popup()
}
fieldDialog.init(model, index)
fieldDialog.open()
}
onPressAndHold: fieldContextMenu.popup()
}
Controls.Menu {
id: fieldContextMenu
Controls.MenuItem {
icon.name: !model.isPassword ? "password-show-off" : "password-show-on"
text: model.isPassword ? qsTr("Mark as normal field") : qsTr(
"Mark as password field")
onClicked: view.model.setData(
view.model.index(index,
0),
model.isPassword ? 0 : 1, 0x0100 + 1)
}
Controls.MenuItem {
icon.name: "edit-copy"
text: model.isPassword ? qsTr("Copy password") : qsTr(
"Copy value")
onClicked: showPassiveNotification(
nativeInterface.copyToClipboard(
model.actualValue) ? qsTr("Copied") : qsTr(
"Unable to access clipboard"))
}
Controls.MenuItem {
icon.name: "edit-delete"
text: qsTr("Delete field")
onClicked: view.model.removeRows(index, 1)
}
Controls.MenuItem {
icon.name: "list-add"
text: qsTr("Insert empty field after this")
onClicked: view.model.insertRows(
index + 1, 1)
}
}
}
}
actions: [
Kirigami.Action {
icon.name: !model.isPassword ? "password-show-off" : "password-show-on"
text: model.isPassword ? qsTr(
"Mark as normal field") : qsTr(
"Mark as password field")
onTriggered: view.model.setData(
view.model.index(index, 0),
model.isPassword ? 0 : 1, 0x0100 + 1)
},
Kirigami.Action {
icon.name: "edit-copy"
text: model.isPassword ? qsTr("Copy password") : qsTr(
"Copy value")
onTriggered: showPassiveNotification(
nativeInterface.copyToClipboard(
model.actualValue) ? qsTr("Copied") : qsTr(
"Unable to access clipboard"))
shortcut: StandardKey.Cut
},
Kirigami.Action {
icon.name: "edit-delete"
text: qsTr("Delete field")
onTriggered: view.model.removeRows(index, 1)
shortcut: StandardKey.Delete
},
Kirigami.Action {
icon.name: "list-add"
text: qsTr("Insert empty field after this")
enabled: !nativeInterface.hasEntryFilter
onTriggered: view.model.insertRows(index + 1, 1)
}
]
}
}

View File

@ -1,4 +1,4 @@
import QtQuick 2.4
import QtQuick 2.15
import QtQuick.Layouts 1.2
import QtQml.Models 2.2
import QtQuick.Controls 2.4 as Controls
@ -10,9 +10,21 @@ Kirigami.ScrollablePage {
Layout.fillWidth: true
title: nativeInterface.currentAccountName
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
actions:[
Kirigami.Action {
icon.name: "list-add"
text: qsTr("Add field")
visible: !nativeInterface.hasEntryFilter
enabled: !nativeInterface.hasEntryFilter
onTriggered: {
const delegateModel = fieldsListView.model
const row = delegateModel.rowCount() - 1
fieldDialog.init(delegateModel, row)
fieldDialog.open()
}
shortcut: "Ctrl+Shift+A"
}
]
// dialog to edit certain field
BasicDialog {
@ -32,8 +44,7 @@ Kirigami.ScrollablePage {
fieldsListView.model.setData(column0, isPassword ? 1 : 0,
0x0100 + 1)
}
ColumnLayout {
contentItem: ColumnLayout {
GridLayout {
Layout.preferredWidth: fieldDialog.availableWidth
columns: 2
@ -43,7 +54,7 @@ Kirigami.ScrollablePage {
id: fieldNameEdit
Layout.fillWidth: true
text: fieldDialog.fieldName
Keys.onPressed: fieldDialog.acceptOnReturn(event)
Keys.onPressed: (event) => fieldDialog.acceptOnReturn(event)
}
Controls.RoundButton {
flat: true
@ -69,7 +80,7 @@ Kirigami.ScrollablePage {
// fix ugly bullet points under Android
font.pointSize: hideCharacters ? fieldNameEdit.font.pointSize
* 0.5 : fieldNameEdit.font.pointSize
Keys.onPressed: fieldDialog.acceptOnReturn(event)
Keys.onPressed: (event) => fieldDialog.acceptOnReturn(event)
}
Controls.RoundButton {
flat: true
@ -107,169 +118,27 @@ Kirigami.ScrollablePage {
}
}
// component representing a field
Component {
id: fieldsListDelegateComponent
Kirigami.SwipeListItem {
id: fieldsListItem
contentItem: RowLayout {
id: fieldRow
readonly property bool isLast: model.isLastRow
Kirigami.ListItemDragHandle {
listItem: fieldsListItem
listView: fieldsListView
onMoveRequested: fieldsListView.model.moveRows(
fieldsListView.model.index(-1, 0),
oldIndex, 1,
fieldsListView.model.index(-1, 0),
newIndex)
visible: !fieldRow.isLast
}
Kirigami.Icon {
width: Kirigami.Units.iconSizes.smallMedium
height: width
source: "list-add"
opacity: 0.6
visible: fieldRow.isLast
MouseArea {
anchors.fill: parent
onClicked: {
fieldDialog.init(model, index)
fieldDialog.open()
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
RowLayout {
anchors.fill: parent
Controls.Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: {
if (fieldRow.isLast) {
return qsTr("click to append new field")
}
var pieces = []
if (model.key) {
pieces.push(model.key)
}
if (model.value) {
pieces.push(model.value)
}
return pieces.join(": ")
}
color: fieldRow.isLast ? "gray" : palette.text
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.RightButton) {
if (!fieldRow.isLast) {
fieldContextMenu.popup()
}
return
}
fieldDialog.init(model, index)
fieldDialog.open()
}
onPressAndHold: fieldContextMenu.popup()
}
Controls.Menu {
id: fieldContextMenu
Controls.MenuItem {
icon.name: !model.isPassword ? "password-show-off" : "password-show-on"
text: model.isPassword ? qsTr("Mark as normal field") : qsTr(
"Mark as password field")
onClicked: fieldsListView.model.setData(
fieldsListView.model.index(index,
0),
model.isPassword ? 0 : 1, 0x0100 + 1)
}
Controls.MenuItem {
icon.name: "edit-copy"
text: model.isPassword ? qsTr("Copy password") : qsTr(
"Copy value")
onClicked: showPassiveNotification(
nativeInterface.copyToClipboard(
model.actualValue) ? qsTr("Copied") : qsTr(
"Unable to access clipboard"))
}
Controls.MenuItem {
icon.name: "edit-delete"
text: qsTr("Delete field")
onClicked: fieldsListView.model.removeRows(index, 1)
}
Controls.MenuItem {
icon.name: "list-add"
text: qsTr("Insert empty field after this")
onClicked: fieldsListView.model.insertRows(
index + 1, 1)
}
}
}
}
actions: [
Kirigami.Action {
iconName: !model.isPassword ? "password-show-off" : "password-show-on"
text: model.isPassword ? qsTr(
"Mark as normal field") : qsTr(
"Mark as password field")
onTriggered: fieldsListView.model.setData(
fieldsListView.model.index(index, 0),
model.isPassword ? 0 : 1, 0x0100 + 1)
visible: !fieldRow.isLast
},
Kirigami.Action {
iconName: "edit-copy"
text: model.isPassword ? qsTr("Copy password") : qsTr(
"Copy value")
onTriggered: showPassiveNotification(
nativeInterface.copyToClipboard(
model.actualValue) ? qsTr("Copied") : qsTr(
"Unable to access clipboard"))
shortcut: StandardKey.Cut
visible: !fieldRow.isLast
},
Kirigami.Action {
iconName: "edit-delete"
text: qsTr("Delete field")
onTriggered: fieldsListView.model.removeRows(index, 1)
shortcut: StandardKey.Delete
visible: !fieldRow.isLast
},
Kirigami.Action {
iconName: "list-add"
text: qsTr("Insert empty field after this")
enabled: !nativeInterface.hasEntryFilter
onTriggered: fieldsListView.model.insertRows(index + 1, 1)
visible: !fieldRow.isLast
}
]
}
}
// list view to edit the currently selected account
ListView {
id: fieldsListView
implicitWidth: Kirigami.Units.gridUnit * 30
model: nativeInterface.fieldModel
reuseItems: true
moveDisplaced: Transition {
YAnimator {
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
}
}
delegate: Kirigami.DelegateRecycler {
width: parent ? parent.width : implicitWidth
sourceComponent: fieldsListDelegateComponent
delegate: FieldsDelegate {
width: fieldsListView.width
view: fieldsListView
onMoveRequested:
(oldIndex, newIndex) => {
const model = fieldsListView.model
const invalidIndex = model.index(-1, 0)
model.moveRows(invalidIndex, oldIndex, 1, invalidIndex, newIndex)
}
}
}
}

View File

@ -1,4 +1,4 @@
import QtQuick.Dialogs as Dialogs
import QtQuick.Dialogs 6.2 as Dialogs
Dialogs.FileDialog {
id: fileDialog

View File

@ -30,8 +30,7 @@ BasicDialog {
qsTr("You aborted. The password has not been altered."))
}
}
ColumnLayout {
contentItem: ColumnLayout {
Controls.Label {
id: instructionLabel
Layout.preferredWidth: passwordDialog.availableWidth
@ -42,26 +41,18 @@ BasicDialog {
id: passwordTextField
Layout.preferredWidth: passwordDialog.availableWidth
echoMode: showCharactersCheckBox.checked ? TextInput.Normal : TextInput.Password
placeholderText: qsTr("enter password here, leave empty for no encryption")
color: "#101010"
placeholderTextColor: "#505050"
background: Rectangle {
border.color: "#5d5e6d"
}
Keys.onPressed: passwordDialog.acceptOnReturn(event)
placeholderText: newPassword
? qsTr("enter password here, leave empty for no encryption")
: qsTr("enter password here")
Keys.onPressed: (event) => passwordDialog.acceptOnReturn(event)
}
Controls.TextField {
id: repeatPasswordTextField
Layout.preferredWidth: passwordDialog.availableWidth
visible: passwordDialog.newPassword
&& !showCharactersCheckBox.checked
enabled: visible && !showCharactersCheckBox.checked
echoMode: TextInput.Password
placeholderText: qsTr("repeat password")
color: "#101010"
placeholderTextColor: "#505050"
background: Rectangle {
border.color: passwordDialog.canAccept ? "#089900" : "#ff0000"
}
Keys.onPressed: passwordDialog.acceptOnReturn(event)
}
Controls.CheckBox {

View File

@ -1,6 +1,8 @@
import QtQuick 2.7
import QtQuick.Templates 2.0 as T2
import QtQuick.Controls 2.1 as Controls
import QtQuick.Controls.Material
import QtQuick.Controls.Universal
import QtQuick.Layouts 1.2
import org.kde.kirigami 2.4 as Kirigami
@ -9,13 +11,14 @@ Kirigami.ApplicationWindow {
property var fieldsPage: undefined
property var lastEntriesPage: undefined
Material.theme: nativeInterface.darkModeEnabled ? Material.Dark : Material.Light
Universal.theme: nativeInterface.darkModeEnabled ? Universal.Dark : Universal.Light
globalDrawer: Kirigami.GlobalDrawer {
id: leftMenu
property bool showNoPasswordWarning: nativeInterface.fileOpen
&& !nativeInterface.passwordSet
title: app.applicationName
titleIcon: "qrc://icons/hicolor/scalable/apps/passwordmanager.svg"
visible: true
resetMenuOnTriggered: false
topContent: ColumnLayout {
@ -101,20 +104,20 @@ Kirigami.ApplicationWindow {
actions: [
Kirigami.Action {
text: qsTr("Create new file")
iconName: "document-new"
icon.name: "document-new"
onTriggered: fileDialog.createNew()
shortcut: StandardKey.New
},
Kirigami.Action {
text: qsTr("Open existing file")
iconName: "document-open"
icon.name: "document-open"
onTriggered: fileDialog.openExisting()
shortcut: StandardKey.Open
},
Kirigami.Action {
id: recentlyOpenedAction
text: qsTr("Recently opened ...")
iconName: "document-open-recent"
icon.name: "document-open-recent"
children: createRecentlyOpenedActions(
nativeInterface.recentFiles)
visible: nativeInterface.recentFiles.length > 0
@ -123,14 +126,14 @@ Kirigami.ApplicationWindow {
Kirigami.Action {
text: qsTr("Save modifications")
enabled: nativeInterface.fileOpen
iconName: "document-save"
icon.name: "document-save"
onTriggered: nativeInterface.save()
shortcut: StandardKey.Save
},
Kirigami.Action {
text: qsTr("Save as")
enabled: nativeInterface.fileOpen
iconName: "document-save-as"
icon.name: "document-save-as"
onTriggered: fileDialog.saveAs()
shortcut: StandardKey.SaveAs
},
@ -138,7 +141,7 @@ Kirigami.ApplicationWindow {
text: nativeInterface.passwordSet ? qsTr("Change password") : qsTr(
"Add password")
enabled: nativeInterface.fileOpen
iconName: "document-encrypt"
icon.name: "document-encrypt"
onTriggered: enterPasswordDialog.askForNewPassword(
qsTr("Change password for %1").arg(
nativeInterface.filePath))
@ -147,7 +150,7 @@ Kirigami.ApplicationWindow {
Kirigami.Action {
text: qsTr("Details")
enabled: nativeInterface.fileOpen
iconName: "document-properties"
icon.name: "document-properties"
onTriggered: {
leftMenu.resetMenu()
fileSummaryDialog.show()
@ -159,7 +162,7 @@ Kirigami.ApplicationWindow {
"Adjust search")
enabled: nativeInterface.fileOpen
visible: nativeInterface.filterAsDialog
iconName: "search"
icon.name: "search"
onTriggered: {
leftMenu.resetMenu()
filterDialog.open()
@ -171,7 +174,7 @@ Kirigami.ApplicationWindow {
enabled: nativeInterface.fileOpen
visible: nativeInterface.filterAsDialog
&& nativeInterface.entryFilter.length > 0
iconName: "edit-clear"
icon.name: "edit-clear"
onTriggered: {
leftMenu.resetMenu()
nativeInterface.entryFilter = ""
@ -183,7 +186,7 @@ Kirigami.ApplicationWindow {
visible: nativeInterface.undoText.length !== 0
&& nativeInterface.entryFilter.length === 0
enabled: visible
iconName: "edit-undo"
icon.name: "edit-undo"
shortcut: StandardKey.Undo
onTriggered: nativeInterface.undo()
},
@ -192,29 +195,30 @@ Kirigami.ApplicationWindow {
visible: nativeInterface.redoText.length !== 0
&& nativeInterface.entryFilter.length === 0
enabled: visible
iconName: "edit-redo"
icon.name: "edit-redo"
shortcut: StandardKey.Redo
onTriggered: nativeInterface.redo()
},
Kirigami.Action {
text: qsTr("Close file")
enabled: nativeInterface.fileOpen
iconName: "document-close"
icon.name: "document-close"
shortcut: StandardKey.Close
onTriggered: nativeInterface.close()
},
Kirigami.Action {
separator: true
},
Kirigami.Action {
text: qsTr("About")
icon.name: "help-about"
shortcut: "Ctrl+?"
onTriggered: {
leftMenu.resetMenu()
aboutDialog.open()
}
}
]
onBannerClicked: {
leftMenu.resetMenu()
aboutDialog.open()
}
Controls.Switch {
text: qsTr("Use native file dialog")
checked: nativeInterface.useNativeFileDialog
visible: nativeInterface.supportsNativeFileDialog
onCheckedChanged: nativeInterface.useNativeFileDialog = checked
}
}
contextDrawer: Kirigami.ContextDrawer {
id: contextDrawer
@ -239,9 +243,9 @@ Kirigami.ApplicationWindow {
id: fileSummaryDialog
standardButtons: Controls.Dialog.Ok
title: qsTr("File details")
Controls.Label {
contentItem: Controls.TextArea {
id: fileSummaryLabel
readOnly: true
text: "No file summary available"
textFormat: Text.RichText
wrapMode: Text.Wrap
@ -287,8 +291,7 @@ Kirigami.ApplicationWindow {
Controls.DialogButtonBox.buttonRole: Controls.DialogButtonBox.RejectRole
}
}
ColumnLayout {
contentItem: ColumnLayout {
Controls.TextField {
id: filterDialogTextField
Layout.preferredWidth: filterDialog.availableWidth
@ -299,12 +302,12 @@ Kirigami.ApplicationWindow {
Connections {
target: nativeInterface
onEntryFilterChanged: {
function onEntryFilterChanged(newFilter) {
if (filterTextField.text !== newFilter) {
filterTextField.text = newFilter
}
}
onFileError: {
function onFileError(errorMessage, retryAction) {
var retryMethod = null
if (retryAction === "load" || retryAction === "save") {
retryMethod = retryAction
@ -318,16 +321,16 @@ Kirigami.ApplicationWindow {
})
}
}
onSettingsError: {
function onSettingsError(errorMessage) {
showPassiveNotification(errorMessage)
}
onPasswordRequired: {
function onPasswordRequired(filePath) {
enterPasswordDialog.askForExistingPassword(
qsTr("Password required to open %1").arg(
nativeInterface.filePath))
leftMenu.resetMenu()
}
onFileOpenChanged: {
function onFileOpenChanged(fileOpen) {
clearStack()
if (!nativeInterface.fileOpen) {
showPassiveNotification(qsTr("%1 closed").arg(
@ -339,20 +342,20 @@ Kirigami.ApplicationWindow {
nativeInterface.fileName))
leftMenu.close()
}
onFileSaved: {
function onFileSaved() {
showPassiveNotification(qsTr("%1 saved").arg(
nativeInterface.fileName))
}
onNewNotification: {
function onNewNotification(message) {
showPassiveNotification(message)
}
onCurrentAccountChanged: {
function onCurrentAccountChanged() {
// remove the fields page if the current account has been removed
if (!nativeInterface.hasCurrentAccount) {
pageStack.pop(lastEntriesPage)
}
}
onEntryAboutToBeRemoved: {
function onEntryAboutToBeRemoved(removedIndex) {
// get the filter entry index
if (nativeInterface.hasEntryFilter) {
removedIndex = nativeInterface.filterEntryIndex(removedIndex)
@ -370,7 +373,7 @@ Kirigami.ApplicationWindow {
}
}
}
onHasEntryFilterChanged: {
function onHasEntryFilterChanged(hasEntryFilter) {
if (nativeInterface.fileOpen) {
pageStack.clear()
initStack()
@ -395,7 +398,7 @@ Kirigami.ApplicationWindow {
id: clearRecentFilesActionComponent
Kirigami.Action {
text: qsTr("Clear recently opened files")
iconName: "edit-clear"
icon.name: "edit-clear"
onTriggered: {
nativeInterface.clearRecentFiles()
leftMenu.resetMenu()

View File

@ -5,12 +5,11 @@
#include <c++utilities/conversion/stringbuilder.h>
#include <QAndroidJniObject>
#include <QColor>
#include <QCoreApplication>
#include <QJniObject>
#include <QMessageLogContext>
#include <QMetaObject>
#include <QtAndroid>
#include <android/log.h>
@ -35,9 +34,9 @@ static Controller *controllerForAndroid = nullptr;
void applyThemingForAndroid()
{
QtAndroid::runOnAndroidThread([=]() {
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() {
const auto color = QColor(QLatin1String("#2c714a")).rgba();
QAndroidJniObject window = QtAndroid::androidActivity().callObjectMethod("getWindow", "()Landroid/view/Window;");
QJniObject window = QJniObject(QNativeInterface::QAndroidApplication::context()).callObjectMethod("getWindow", "()Landroid/view/Window;");
window.callMethod<void>("addFlags", "(I)V", Android::WindowManager::LayoutParams::DrawsSystemBarBackgrounds);
window.callMethod<void>("clearFlags", "(I)V", Android::WindowManager::LayoutParams::TranslucentStatus);
window.callMethod<void>("setStatusBarColor", "(I)V", color);
@ -52,13 +51,14 @@ void registerControllerForAndroid(Controller *controller)
bool showAndroidFileDialog(bool existing, bool createNew)
{
return QtAndroid::androidActivity().callMethod<jboolean>("showAndroidFileDialog", "(ZZ)Z", existing, createNew);
return QJniObject(QNativeInterface::QAndroidApplication::context()).callMethod<jboolean>("showAndroidFileDialog", "(ZZ)Z", existing, createNew);
}
int openFileDescriptorFromAndroidContentUrl(const QString &url, const QString &mode)
{
return QtAndroid::androidActivity().callMethod<jint>("openFileDescriptorFromAndroidContentUri", "(Ljava/lang/String;Ljava/lang/String;)I",
QAndroidJniObject::fromString(url).object<jstring>(), QAndroidJniObject::fromString(mode).object<jstring>());
return QJniObject(QNativeInterface::QAndroidApplication::context())
.callMethod<jint>("openFileDescriptorFromAndroidContentUri", "(Ljava/lang/String;Ljava/lang/String;)I",
QJniObject::fromString(url).object<jstring>(), QJniObject::fromString(mode).object<jstring>());
}
void writeToAndroidLog(QtMsgType type, const QMessageLogContext &context, const QString &msg)
@ -102,19 +102,20 @@ void setupAndroidSpecifics()
static void onAndroidError(JNIEnv *, jobject, jstring message)
{
QMetaObject::invokeMethod(
QtGui::controllerForAndroid, "newNotification", Qt::QueuedConnection, Q_ARG(QString, QAndroidJniObject::fromLocalRef(message).toString()));
QtGui::controllerForAndroid, "newNotification", Qt::QueuedConnection, Q_ARG(QString, QJniObject::fromLocalRef(message).toString()));
}
static void onAndroidFileDialogAccepted(JNIEnv *, jobject, jstring fileName, jboolean existing, jboolean createNew)
{
QMetaObject::invokeMethod(QtGui::controllerForAndroid, "handleFileSelectionAccepted", Qt::QueuedConnection,
Q_ARG(QString, QAndroidJniObject::fromLocalRef(fileName).toString()), Q_ARG(bool, existing), Q_ARG(bool, createNew));
Q_ARG(QString, QJniObject::fromLocalRef(fileName).toString()), Q_ARG(bool, existing), Q_ARG(bool, createNew));
}
static void onAndroidFileDialogAcceptedDescriptor(JNIEnv *, jobject, jstring nativeUrl, jstring fileName, jint fileHandle, jboolean existing, jboolean createNew)
static void onAndroidFileDialogAcceptedDescriptor(
JNIEnv *, jobject, jstring nativeUrl, jstring fileName, jint fileHandle, jboolean existing, jboolean createNew)
{
QMetaObject::invokeMethod(QtGui::controllerForAndroid, "handleFileSelectionAcceptedDescriptor", Qt::QueuedConnection,
Q_ARG(QString, QAndroidJniObject::fromLocalRef(nativeUrl).toString()), Q_ARG(QString, QAndroidJniObject::fromLocalRef(fileName).toString()),
Q_ARG(QString, QJniObject::fromLocalRef(nativeUrl).toString()), Q_ARG(QString, QJniObject::fromLocalRef(fileName).toString()),
Q_ARG(int, fileHandle), Q_ARG(bool, existing), Q_ARG(bool, createNew));
}

View File

@ -43,14 +43,15 @@ Controller::Controller(QSettings &settings, const QString &filePath, QObject *pa
#endif
, m_fileOpen(false)
, m_fileModified(false)
, m_useNativeFileDialog(false)
, m_useNativeFileDialog(supportsNativeFileDialog())
, m_filterAsDialog(
#ifdef Q_OS_ANDROID
true
#else
false
#endif
)
)
, m_darkModeEnabled(false)
{
m_fieldModel.setPasswordVisibility(PasswordVisibility::Never);
m_entryFilterModel.setFilterCaseSensitivity(Qt::CaseInsensitive);

View File

@ -44,6 +44,7 @@ class Controller : public QObject {
Q_PROPERTY(QUndoStack *undoStack READ undoStack NOTIFY undoStackChanged)
Q_PROPERTY(QString undoText READ undoText NOTIFY undoTextChanged)
Q_PROPERTY(QString redoText READ redoText NOTIFY redoTextChanged)
Q_PROPERTY(bool darkModeEnabled READ isDarkModeEnabled WRITE setDarkModeEnabled NOTIFY darkModeEnabledChanged)
public:
explicit Controller(QSettings &settings, const QString &filePath = QString(), QObject *parent = nullptr);
@ -86,6 +87,8 @@ public:
QString undoText() const;
QString redoText() const;
Io::PasswordFileSaveFlags prepareSaving();
bool isDarkModeEnabled() const;
void setDarkModeEnabled(bool darkModeEnabled);
public Q_SLOTS:
void init();
@ -129,6 +132,7 @@ Q_SIGNALS:
void undoTextChanged(const QString &undoText);
void redoTextChanged(const QString &redoText);
void settingsError(const QString &errorMessage);
void darkModeEnabledChanged(bool darkModeEnabled);
private Q_SLOTS:
void handleEntriesRemoved(const QModelIndex &parentIndex, int first, int last);
@ -160,6 +164,7 @@ private:
bool m_fileModified;
bool m_useNativeFileDialog;
bool m_filterAsDialog;
bool m_darkModeEnabled;
};
inline QModelIndex Controller::ensureSourceEntryIndex(const QModelIndex &entryIndexMaybeFromFilterModel) const
@ -345,6 +350,18 @@ inline QString Controller::redoText() const
#endif
}
inline bool Controller::isDarkModeEnabled() const
{
return m_darkModeEnabled;
}
inline void Controller::setDarkModeEnabled(bool darkModeEnabled)
{
if (darkModeEnabled != m_darkModeEnabled) {
emit darkModeEnabledChanged(m_darkModeEnabled = darkModeEnabled);
}
}
inline void Controller::undo()
{
#ifdef PASSWORD_MANAGER_UNDO_SUPPORT

View File

@ -10,25 +10,29 @@
// enable inline helper functions for Qt Quick provided by qtutilities
#define QT_UTILITIES_GUI_QTQUICK
// ensure QGuiApplication is defined before resources.h for desktop file name
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
#include <QApplication>
using App = QApplication;
#else
#include <QGuiApplication>
using App = QGuiApplication;
#endif
#include <qtutilities/misc/desktoputils.h>
#include <qtutilities/resources/qtconfigarguments.h>
#include <qtutilities/resources/resources.h>
#include <qtutilities/settingsdialog/qtsettings.h>
#include <passwordfile/util/openssl.h>
#include <QGuiApplication>
#include <QDebug>
#include <QIcon>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QSettings>
#include <QtQml>
#ifdef Q_OS_ANDROID
#include <QDebug>
#include <QDirIterator>
#endif
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
#include <QApplication>
#endif
#include <cstdlib>
using namespace CppUtilities;
using namespace Util;
@ -42,55 +46,53 @@ int runQuickGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, c
setupAndroidSpecifics();
#endif
// work around kirigami plugin trying to be clever
if (!qEnvironmentVariableIsSet("XDG_CURRENT_DESKTOP")) {
qputenv("XDG_CURRENT_DESKTOP", QByteArray("please don't override my settings"));
}
// init OpenSSL
OpenSsl::init();
// init application
SET_QT_APPLICATION_INFO;
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
QApplication application(argc, argv);
auto application = App(argc, argv);
QObject::connect(&application, &QCoreApplication::aboutToQuit, &OpenSsl::clean);
// restore Qt settings
auto qtSettings = QtUtilities::QtSettings();
auto settings = QtUtilities::getSettings(QStringLiteral(PROJECT_NAME));
if (auto settingsError = QtUtilities::errorMessageForSettings(*settings); !settingsError.isEmpty()) {
qDebug() << settingsError;
}
qtSettings.restore(*settings);
qtSettings.apply();
// create controller and handle dark mode
// note: Not handling changes of the dark mode setting dynamically yet because it does not work with Kirigami.
// It looks like Kirigami does not follow the QCC2 theme (the Material.theme/Material.theme settings) but
// instead uses colors based on the initial palette. Not sure how to toggle Kirigami's palette in accordance
// with the QCC2 theme. Hence this code is disabled via APPLY_COLOR_SCHEME_DYNAMICALLY for now.
auto controller = Controller(*settings, file);
#ifdef APPLY_COLOR_SCHEME_DYNAMICALLY
QtUtilities::onDarkModeChanged(
[&qtSettings, &controller](bool isDarkModeEnabled) {
qtSettings.reapplyDefaultIconTheme(isDarkModeEnabled);
controller.setDarkModeEnabled(isDarkModeEnabled);
},
&controller);
#else
QGuiApplication application(argc, argv);
const auto isDarkModeEnabled = QtUtilities::isDarkModeEnabled().value_or(false);
qtSettings.reapplyDefaultIconTheme(isDarkModeEnabled);
controller.setDarkModeEnabled(isDarkModeEnabled);
#endif
// apply settings specified via command line args
qtConfigArgs.applySettings();
qtConfigArgs.applySettings(qtSettings.hasCustomFont());
qtConfigArgs.applySettingsForQuickGui();
// assume we're bundling breeze icons
if (QIcon::themeName().isEmpty()) {
QIcon::setThemeName(QStringLiteral("breeze"));
}
// log resource information
#if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_DEBUG_BUILD)
qDebug() << "Using icon theme" << QIcon::themeName();
qDebug() << "Icon theme search paths" << QIcon::themeSearchPaths();
qDebug() << "Resources:";
QDirIterator it(QStringLiteral(":/"), QDirIterator::Subdirectories);
while (it.hasNext()) {
qDebug() << it.next();
}
#endif
// load settings from configuration file
auto settings = QtUtilities::getSettings(QStringLiteral(PROJECT_NAME));
// load translations
LOAD_QT_TRANSLATIONS;
// init Quick GUI
// init QML engine
auto engine = QQmlApplicationEngine();
auto controller = Controller(*settings, file);
#ifdef Q_OS_ANDROID
registerControllerForAndroid(&controller);
#endif
auto *const context(engine.rootContext());
auto *const context = engine.rootContext();
context->setContextProperty(QStringLiteral("nativeInterface"), &controller);
context->setContextProperty(QStringLiteral("app"), &application);
context->setContextProperty(QStringLiteral("description"), QStringLiteral(APP_DESCRIPTION));
@ -101,10 +103,18 @@ int runQuickGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, c
engine.addImportPath(path);
}
#endif
engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml")));
// run event loop
QObject::connect(&application, &QCoreApplication::aboutToQuit, &OpenSsl::clean);
// load main QML file; run event loop or exit if it cannot be loaded
const auto mainUrl = QUrl(QStringLiteral("qrc:/qml/main.qml"));
QObject::connect(
&engine, &QQmlApplicationEngine::objectCreated, &application,
[&mainUrl](QObject *obj, const QUrl &objUrl) {
if (!obj && objUrl == mainUrl) {
QCoreApplication::exit(EXIT_FAILURE);
}
},
Qt::QueuedConnection);
engine.load(mainUrl);
return application.exec();
}
} // namespace QtGui

View File

@ -0,0 +1,50 @@
<?xml version="1.0"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="@META_ANDROID_PACKAGE_NAME@"
android:label="@META_APP_NAME@"
android:installLocation="auto"
android:versionName="@META_APP_VERSION@"
android:versionCode="@META_VERSION_MAJOR@">
<!-- %%INSERT_PERMISSIONS -->
<!-- %%INSERT_FEATURES -->
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true" />
<application
android:name="org.qtproject.qt.android.bindings.QtApplication"
android:icon="@mipmap/ic_launcher"
android:hardwareAccelerated="true"
android:label="@META_APP_NAME@"
android:requestLegacyExternalStorage="true"
android:allowNativeHeapPointerTagging="false"
android:allowBackup="true"
android:fullBackupOnly="false">
<activity
android:name="org.martchus.passwordmanager.Activity"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
android:label="@META_APP_NAME@"
android:launchMode="singleTop"
android:screenOrientation="unspecified"
android:exported="true"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="-- %%INSERT_APP_LIB_NAME%% --" />
<meta-data
android:name="android.app.arguments"
android:value="-- %%INSERT_APP_ARGUMENTS%% --" />
<meta-data
android:name="android.app.extract_android_style"
android:value="minimal" />
<meta-data
android:name="android.app.splash_screen_drawable"
android:resource="@drawable/splash" />
</activity>
</application>
</manifest>

View File

@ -8,6 +8,8 @@
<file alias="AboutDialog.qml">../qml/AboutDialog.qml</file>
<file alias="PasswordDialog.qml">../qml/PasswordDialog.qml</file>
<file alias="EntriesPage.qml">../qml/EntriesPage.qml</file>
<file alias="EntryDelegate.qml">../qml/EntryDelegate.qml</file>
<file alias="FieldsPage.qml">../qml/FieldsPage.qml</file>
<file alias="FieldsDelegate.qml">../qml/FieldsDelegate.qml</file>
</qresource>
</RCC>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff