Compare commits
48 Commits
Author | SHA1 | Date |
---|---|---|
Martchus | 03eeecac9c | |
Martchus | dff8cca664 | |
Martchus | 297f34d5c6 | |
Martchus | 8955d5c27b | |
Martchus | 9574ed8b0e | |
Martchus | c5a658e2c2 | |
Martchus | 760f764b13 | |
Martchus | cabeca7756 | |
Martchus | 06ae52d0d8 | |
Martchus | 6ffa54088a | |
Martchus | 1b6b645f7a | |
Martchus | 1ce919397b | |
Martchus | 3c706b614e | |
Martchus | e095dc904f | |
Martchus | a37ee658a6 | |
Martchus | 9653b4c0d5 | |
Martchus | 613d62d042 | |
Martchus | 8319d4f485 | |
Martchus | c2fc538e2c | |
Martchus | f20c7b5d3d | |
Martchus | b2d85c7d53 | |
Martchus | 0f4c30c14f | |
Martchus | 21a16e0be0 | |
Martchus | ba9a6e26de | |
Martchus | 9ebffe6b16 | |
Martchus | 64a2738827 | |
Martchus | a098b40dab | |
Martchus | dbc2e9a4e2 | |
Martchus | f960aa80a1 | |
Martchus | b950451807 | |
Martchus | 20aea51fa1 | |
Martchus | 5706086456 | |
Martchus | b8d2422669 | |
Martchus | 7a08ee7d00 | |
Martchus | 7c03835fc3 | |
Martchus | 0791964c6f | |
Martchus | f6096f7232 | |
Martchus | 79a0ef031c | |
Martchus | cb6fe128c4 | |
Martchus | eed9d596ba | |
Martchus | cd23eb4add | |
Martchus | 4891ee0b32 | |
Martchus | 6972256727 | |
Martchus | 4bf6a91d72 | |
Martchus | 430e17416a | |
Martchus | 8f8eda8755 | |
Martchus | fcbc3cb4a4 | |
Martchus | 7b242732e8 |
|
@ -42,3 +42,6 @@ Makefile*
|
||||||
|
|
||||||
# clang-format
|
# clang-format
|
||||||
/.clang-format
|
/.clang-format
|
||||||
|
|
||||||
|
# Android-specific
|
||||||
|
/android/AndroidManifest.xml
|
||||||
|
|
|
@ -9,14 +9,15 @@ set(META_APP_NAME "Password Manager")
|
||||||
set(META_APP_CATEGORIES "Utility;Security;")
|
set(META_APP_CATEGORIES "Utility;Security;")
|
||||||
set(META_APP_AUTHOR "Martchus")
|
set(META_APP_AUTHOR "Martchus")
|
||||||
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
|
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_APP_DESCRIPTION "A simple password store using AES-256-CBC encryption via OpenSSL")
|
||||||
set(META_GUI_OPTIONAL YES)
|
set(META_GUI_OPTIONAL YES)
|
||||||
set(META_USE_QQC2 ON)
|
set(META_USE_QQC2 ON)
|
||||||
set(META_ANDROID_PACKAGE_NAME "org.martchus.passwordmanager")
|
set(META_ANDROID_PACKAGE_NAME "org.martchus.passwordmanager")
|
||||||
set(META_VERSION_MAJOR 4)
|
set(META_VERSION_MAJOR 4)
|
||||||
set(META_VERSION_MINOR 1)
|
set(META_VERSION_MINOR 2)
|
||||||
set(META_VERSION_PATCH 13)
|
set(META_VERSION_PATCH 1)
|
||||||
set(META_RELEASE_DATE "2023-11-21")
|
set(META_RELEASE_DATE "2024-04-02")
|
||||||
|
|
||||||
# add project files
|
# add project files
|
||||||
set(HEADER_FILES cli/cli.h model/entryfiltermodel.h model/entrymodel.h model/fieldmodel.h)
|
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)
|
if (ANDROID)
|
||||||
list(APPEND QML_HEADER_FILES quickgui/android.h)
|
list(APPEND QML_HEADER_FILES quickgui/android.h)
|
||||||
list(APPEND QML_SRC_FILES quickgui/android.cpp)
|
list(APPEND QML_SRC_FILES quickgui/android.cpp)
|
||||||
|
else ()
|
||||||
|
list(APPEND EXCLUDED_FILES quickgui/android.h quickgui/android.cpp)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
set(TS_FILES translations/${META_PROJECT_NAME}_de_DE.ts translations/${META_PROJECT_NAME}_en_US.ts)
|
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
|
preferences-desktop-locale
|
||||||
qtcreator
|
qtcreator
|
||||||
search
|
search
|
||||||
story-editor
|
view-list-details-symbolic
|
||||||
system-file-manager
|
system-file-manager
|
||||||
system-run
|
system-run
|
||||||
system-search
|
system-search
|
||||||
|
@ -117,24 +120,24 @@ find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.6.0 REQUIRED)
|
||||||
use_cpp_utilities()
|
use_cpp_utilities()
|
||||||
|
|
||||||
# apply basic configuration
|
# 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)
|
include(BasicConfig)
|
||||||
|
|
||||||
# find qtutilities
|
# find qtutilities
|
||||||
set(CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES
|
set(CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES
|
||||||
"${CONFIGURATION_PACKAGE_SUFFIX}"
|
"${CONFIGURATION_PACKAGE_SUFFIX}"
|
||||||
CACHE STRING "sets the suffix for qtutilities")
|
CACHE STRING "sets the suffix for qtutilities")
|
||||||
find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.12.0 REQUIRED)
|
find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.14.0 REQUIRED)
|
||||||
use_qt_utilities()
|
use_qt_utilities()
|
||||||
|
|
||||||
# find passwordfile
|
# find passwordfile
|
||||||
find_package(passwordfile${CONFIGURATION_PACKAGE_SUFFIX} 5.0.0 REQUIRED)
|
find_package(passwordfile${CONFIGURATION_PACKAGE_SUFFIX} 5.0.0 REQUIRED)
|
||||||
use_password_file()
|
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
|
# 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)
|
# well)
|
||||||
if (QUICK_GUI AND NOT WIDGETS_GUI)
|
if (QUICK_GUI AND NOT WIDGETS_GUI)
|
||||||
|
@ -147,20 +150,32 @@ if (QUICK_GUI AND NOT WIDGETS_GUI)
|
||||||
endif ()
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
# add further Qt/KF modules required by the Qt Quick GUI under Android
|
# deduce major Qt version from package prefix
|
||||||
if (ANDROID)
|
if (NOT QT_PACKAGE_PREFIX)
|
||||||
list(APPEND ADDITIONAL_QT_MODULES AndroidExtras)
|
set(MAJOR_QT_VERSION "5")
|
||||||
|
elseif (QT_PACKAGE_PREFIX MATCHES ".*Qt([0-9]+).*")
|
||||||
|
set(MAJOR_QT_VERSION "${CMAKE_MATCH_1}")
|
||||||
endif ()
|
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)
|
if (QUICK_GUI)
|
||||||
list(APPEND ADDITIONAL_KF_MODULES Kirigami2)
|
list(APPEND ADDITIONAL_KF_MODULES Kirigami)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
# add Qt-version-specific QML files
|
# add Qt-version-specific QML files
|
||||||
unset(QML_FILE)
|
unset(QML_FILE)
|
||||||
if (NOT QT_PACKAGE_PREFIX)
|
if (MAJOR_QT_VERSION)
|
||||||
set(QML_FILE "resources/qml5.qrc")
|
set(QML_FILE "resources/qml${MAJOR_QT_VERSION}.qrc")
|
||||||
elseif (QT_PACKAGE_PREFIX MATCHES ".*Qt([0-9]+).*")
|
|
||||||
set(QML_FILE "resources/qml${CMAKE_MATCH_1}.qrc")
|
|
||||||
endif ()
|
endif ()
|
||||||
if (NOT QML_FILE)
|
if (NOT QML_FILE)
|
||||||
message(FATAL_ERROR "Unable to add Qt-version-specific resource file for QT_PACKAGE_PREFIX \"${QT_PACKAGE_PREFIX}\".")
|
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 ()
|
endif ()
|
||||||
include(WindowsResources)
|
include(WindowsResources)
|
||||||
include(AppTarget)
|
include(AppTarget)
|
||||||
include(AndroidApk)
|
|
||||||
include(ShellCompletion)
|
include(ShellCompletion)
|
||||||
include(ConfigHeader)
|
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
|
# create desktop file using previously defined meta data
|
||||||
add_desktop_file()
|
add_desktop_file()
|
||||||
|
|
153
README.md
153
README.md
|
@ -4,7 +4,7 @@ A simple [password manager](https://en.wikipedia.org/wiki/Password_manager) with
|
||||||
## Features
|
## Features
|
||||||
* Cross-platform: tested under GNU/Linux, Android and Windows
|
* Cross-platform: tested under GNU/Linux, Android and Windows
|
||||||
* Qt Widgets GUI for desktop platforms
|
* 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
|
* Interactive command-line interface
|
||||||
* Simple architecture: All data is stored in ordinary files with AES-256-CBC applied. No cloud stuff. Use
|
* Simple architecture: All data is stored in ordinary files with AES-256-CBC applied. No cloud stuff. Use
|
||||||
eg. Syncthing for synchronization.
|
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
|
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:
|
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
|
* Creating custom Qt models
|
||||||
* Nested model and model with multiple columns
|
* Nested model and model with multiple columns
|
||||||
* Support Drag & Drop in `QTreeView`
|
* 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
|
* Integration with Qt Widgets' undo/redo framework
|
||||||
* Filtering
|
* Filtering
|
||||||
* Android tweaks
|
* Android tweaks
|
||||||
* Add CMake target to invoke `androiddeployqt`
|
* Create APK via CMake (using `androiddeployqt`)
|
||||||
* Customize activity
|
* Customize activity
|
||||||
* Customize gradle project to add additional Java dependency
|
* Customize gradle project to add additional Java dependency
|
||||||
* Adjust the window style of the activity
|
* Adjust the window style of the activity
|
||||||
|
@ -46,7 +46,7 @@ See the release section on GitHub.
|
||||||
* Arch Linux
|
* Arch Linux
|
||||||
* for PKGBUILDs checkout [my GitHub repository](https://github.com/Martchus/PKGBUILDs) or
|
* for PKGBUILDs checkout [my GitHub repository](https://github.com/Martchus/PKGBUILDs) or
|
||||||
[the AUR](https://aur.archlinux.org/packages?SeB=m&K=Martchus)
|
[the AUR](https://aur.archlinux.org/packages?SeB=m&K=Martchus)
|
||||||
* there is also a [binary repository](https://martchus.no-ip.biz/repo/arch/ownstuff)
|
* there is also a [binary repository](https://martchus.dyn.f3l.de/repo/arch/ownstuff)
|
||||||
* Tumbleweed, Leap, Fedora
|
* Tumbleweed, Leap, Fedora
|
||||||
* RPM \*.spec files and binaries are available via openSUSE Build Service
|
* RPM \*.spec files and binaries are available via openSUSE Build Service
|
||||||
* remarks
|
* remarks
|
||||||
|
@ -67,10 +67,14 @@ See the release section on GitHub.
|
||||||
the package `libopengl0` is installed on Debian/Ubuntu)
|
the package `libopengl0` is installed on Debian/Ubuntu)
|
||||||
* Supports X11 and Wayland (set the environment variable `QT_QPA_PLATFORM=xcb` to disable
|
* 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)
|
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
|
* Windows
|
||||||
* for binaries checkout the [release section on GitHub](https://github.com/Martchus/tageditor/releases)
|
* 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 version 1809 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
|
* 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)
|
* for mingw-w64 PKGBUILDs checkout [my GitHub repository](https://github.com/Martchus/PKGBUILDs)
|
||||||
|
|
||||||
## Build instructions
|
## Build instructions
|
||||||
|
@ -87,7 +91,7 @@ can be passed to CMake to influence the build.
|
||||||
### Optional dependencies
|
### Optional dependencies
|
||||||
* When building any Qt GUI, the library qtutilities is required.
|
* 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 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 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
|
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.
|
the desired location afterwards.
|
||||||
|
|
||||||
#### Concrete example of 3. for building an Android APK under Arch Linux
|
#### 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
|
# set variables for creating keystore and androiddeployqt to find it
|
||||||
keystore_dir=/path/to/keystore-dir
|
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
|
||||||
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>
|
|
||||||
|
|
||||||
# create keystore (do only once)
|
# create keystore (do only once)
|
||||||
pushd "$keystore_dir"
|
mkdir -p "${QT_ANDROID_KEYSTORE_PATH%/*}"
|
||||||
keytool -genkey -v -keystore "$keystore_alias" -alias "$keystore_alias" -keyalg RSA -keysize 2048 -validity 10000
|
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
|
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
|
# use Java 17 (the latest Java doesn't work at this point) and avoid unwanted Java options
|
||||||
_pkg_arch=aarch64
|
export PATH=/usr/lib/jvm/java-17-openjdk/bin:$PATH
|
||||||
_android_arch=arm64-v8a
|
export _JAVA_OPTIONS=
|
||||||
_android_arch2=arm64
|
|
||||||
_android_api_level=22
|
|
||||||
|
|
||||||
# set project name
|
# configure and build using helpers from android-cmake package
|
||||||
_reponame=passwordmanager
|
android_arch=aarch64
|
||||||
_pkgname=passwordmanager
|
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_PKGCONFIG" \
|
||||||
|
-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
|
# install the app
|
||||||
android_sdk_root=${ANDROID_SDK_ROOT:-/opt/android-sdk}
|
adb install "$build_dir/passwordmanager/android-build//build/outputs/apk/release/android-build-release-signed.apk"
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Notes
|
##### 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.
|
my [PKGBUILDs](http://github.com/Martchus/PKGBUILDs) repo.
|
||||||
* The latest Java I was able to use was version 8 (`jdk8-openjdk` package).
|
* 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
|
||||||
### Manual deployment of Android APK file
|
Android via a normal desktop build.
|
||||||
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`
|
|
||||||
|
|
||||||
### Building without Qt GUI
|
### 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.
|
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 notice and license
|
||||||
Copyright © 2015-2023 Marius Kittler
|
Copyright © 2015-2024 Marius Kittler
|
||||||
|
|
||||||
All code is licensed under [GPL-2-or-later](LICENSE).
|
All code is licensed under [GPL-2-or-later](LICENSE).
|
||||||
|
|
|
@ -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>
|
|
|
@ -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(",")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
android.useAndroidX=true
|
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name">@ANDROID_APK_APPLICATION_LABEL@</string>
|
|
||||||
</resources>
|
|
|
@ -7,9 +7,9 @@ import android.os.Bundle;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowManager.LayoutParams;
|
import android.view.WindowManager.LayoutParams;
|
||||||
import android.support.v4.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import org.qtproject.qt5.android.bindings.QtActivity;
|
import org.qtproject.qt.android.bindings.QtActivity;
|
||||||
|
|
||||||
public class Activity extends QtActivity {
|
public class Activity extends QtActivity {
|
||||||
private final int REQUEST_CODE_OPEN_EXISTING_FILE = 1;
|
private final int REQUEST_CODE_OPEN_EXISTING_FILE = 1;
|
||||||
|
|
92
cli/cli.cpp
92
cli/cli.cpp
|
@ -5,6 +5,7 @@
|
||||||
#include <passwordfile/io/field.h>
|
#include <passwordfile/io/field.h>
|
||||||
#include <passwordfile/io/parsingexception.h>
|
#include <passwordfile/io/parsingexception.h>
|
||||||
#include <passwordfile/io/passwordfile.h>
|
#include <passwordfile/io/passwordfile.h>
|
||||||
|
#include <passwordfile/util/openssl.h>
|
||||||
|
|
||||||
#include <c++utilities/application/commandlineutils.h>
|
#include <c++utilities/application/commandlineutils.h>
|
||||||
#include <c++utilities/conversion/stringconversion.h>
|
#include <c++utilities/conversion/stringconversion.h>
|
||||||
|
@ -15,7 +16,9 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
|
@ -89,32 +92,39 @@ InteractiveCli::InteractiveCli()
|
||||||
, m_modified(false)
|
, m_modified(false)
|
||||||
, m_quit(false)
|
, m_quit(false)
|
||||||
{
|
{
|
||||||
|
Util::OpenSsl::init();
|
||||||
CMD_UTILS_START_CONSOLE;
|
CMD_UTILS_START_CONSOLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractiveCli::run(const string &file)
|
InteractiveCli::~InteractiveCli()
|
||||||
|
{
|
||||||
|
Util::OpenSsl::clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
int InteractiveCli::run(string_view file)
|
||||||
{
|
{
|
||||||
if (!file.empty()) {
|
if (!file.empty()) {
|
||||||
openFile(file, PasswordFileOpenFlags::Default);
|
openFile(file, PasswordFileOpenFlags::Default);
|
||||||
}
|
}
|
||||||
string input;
|
auto input = std::string();
|
||||||
while (!m_quit) {
|
while (!m_quit) {
|
||||||
getline(m_i, input);
|
std::getline(m_i, input);
|
||||||
if (!input.empty()) {
|
if (!input.empty()) {
|
||||||
processCommand(input);
|
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 CMD(value) !paramMissing &&cmd == value
|
||||||
#define CMD2(value1, value2) !paramMissing && (cmd == value1 || cmd == value2)
|
#define CMD2(value1, value2) !paramMissing && (cmd == value1 || cmd == value2)
|
||||||
#define CMD_P(value) !paramMissing &&checkCommand(cmd, value, param, paramMissing)
|
#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))
|
#define CMD2_P(value1, value2) !paramMissing && (checkCommand(cmd, value1, param, paramMissing) || checkCommand(cmd, value2, param, paramMissing))
|
||||||
|
|
||||||
string param;
|
auto param = std::string();
|
||||||
bool paramMissing = false;
|
auto paramMissing = false;
|
||||||
if (CMD2("quit", "q")) {
|
if (CMD2("quit", "q")) {
|
||||||
quit();
|
quit();
|
||||||
} else if (CMD("q!")) {
|
} 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) == '/';
|
bool fromRoot = path.at(0) == '/';
|
||||||
if (fromRoot && parts.empty()) {
|
if (fromRoot && parts.empty()) {
|
||||||
return m_file.rootEntry();
|
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 ¶m, bool ¶mMissing)
|
bool InteractiveCli::checkCommand(const std::string &str, const char *phrase, std::string ¶m, bool ¶mMissing)
|
||||||
{
|
{
|
||||||
for (auto i = str.cbegin(), end = str.cend(); i != end; ++i, ++phrase) {
|
for (auto i = str.cbegin(), end = str.cend(); i != end; ++i, ++phrase) {
|
||||||
if (*phrase == 0) {
|
if (*phrase == 0) {
|
||||||
|
@ -234,13 +244,13 @@ bool InteractiveCli::checkCommand(const string &str, const char *phrase, std::st
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractiveCli::openFile(const string &file, PasswordFileOpenFlags openFlags)
|
void InteractiveCli::openFile(std::string_view file, PasswordFileOpenFlags openFlags)
|
||||||
{
|
{
|
||||||
if (m_file.isOpen()) {
|
if (m_file.isOpen()) {
|
||||||
m_o << "file \"" << m_file.path() << "\" currently open; close first" << endl;
|
m_o << "file \"" << m_file.path() << "\" currently open; close first" << endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_file.setPath(file);
|
m_file.setPath(std::string(file));
|
||||||
for (;;) {
|
for (;;) {
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
|
@ -322,7 +332,7 @@ void InteractiveCli::saveFile()
|
||||||
m_modified = false;
|
m_modified = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractiveCli::createFile(const string &file)
|
void InteractiveCli::createFile(const std::string &file)
|
||||||
{
|
{
|
||||||
if (m_file.isOpen()) {
|
if (m_file.isOpen()) {
|
||||||
m_o << "file \"" << m_file.path() << "\" currently open; close first" << endl;
|
m_o << "file \"" << m_file.path() << "\" currently open; close first" << endl;
|
||||||
|
@ -392,7 +402,7 @@ void InteractiveCli::pwd()
|
||||||
m_o << joinStrings(path, "/") << endl;
|
m_o << joinStrings(path, "/") << endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractiveCli::cd(const string &path)
|
void InteractiveCli::cd(const std::string &path)
|
||||||
{
|
{
|
||||||
if (!m_file.isOpen()) {
|
if (!m_file.isOpen()) {
|
||||||
m_o << "can not change directory; no file open" << endl;
|
m_o << "can not change directory; no file open" << endl;
|
||||||
|
@ -450,7 +460,7 @@ void InteractiveCli::tree()
|
||||||
printEntries(m_currentEntry, 0);
|
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()) {
|
if (!m_file.isOpen()) {
|
||||||
m_o << "can not make entry; no file open" << endl;
|
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()) {
|
if (!m_file.isOpen()) {
|
||||||
m_o << "can not remove entry; no file open" << endl;
|
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()) {
|
if (!m_file.isOpen()) {
|
||||||
m_o << "can not rename entry; no file open" << endl;
|
m_o << "can not rename entry; no file open" << endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Entry *entry = resolvePath(path)) {
|
if (Entry *entry = resolvePath(path)) {
|
||||||
string label;
|
auto label = std::string();
|
||||||
m_o << "enter new name: " << endl;
|
m_o << "enter new name: " << endl;
|
||||||
getline(m_i, label);
|
std::getline(m_i, label);
|
||||||
if (label.empty()) {
|
if (label.empty()) {
|
||||||
m_o << "can not rename; new name is empty" << endl;
|
m_o << "can not rename; new name is empty" << endl;
|
||||||
} else {
|
} 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()) {
|
if (!m_file.isOpen()) {
|
||||||
m_o << "can not rename entry; no file open" << endl;
|
m_o << "can not rename entry; no file open" << endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Entry *entry = resolvePath(path)) {
|
if (Entry *entry = resolvePath(path)) {
|
||||||
string newParentPath;
|
auto newParentPath = std::string();
|
||||||
m_o << "enter path of new parent: " << endl;
|
m_o << "enter path of new parent: " << endl;
|
||||||
getline(m_i, newParentPath);
|
std::getline(m_i, newParentPath);
|
||||||
if (newParentPath.empty()) {
|
if (newParentPath.empty()) {
|
||||||
m_o << "can not move; path of new parent is empty" << endl;
|
m_o << "can not move; path of new parent is empty" << endl;
|
||||||
} else {
|
} 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()) {
|
if (!m_file.isOpen()) {
|
||||||
m_o << "can not read field; no file open" << endl;
|
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()) {
|
if (!m_file.isOpen()) {
|
||||||
m_o << "can not set field; no file open" << endl;
|
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;
|
m_o << "can not set field; current entry is no account entry" << endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vector<Field> &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
|
auto &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
|
||||||
unsigned int valuesFound = 0;
|
auto valuesFound = unsigned();
|
||||||
string value;
|
auto value = std::string();
|
||||||
m_o << "enter new value: ";
|
m_o << "enter new value: ";
|
||||||
if (useMuter) {
|
if (useMuter) {
|
||||||
InputMuter m;
|
InputMuter m;
|
||||||
getline(m_i, value);
|
std::getline(m_i, value);
|
||||||
m_o << endl << "repeat: ";
|
m_o << endl << "repeat: ";
|
||||||
string repeat;
|
auto repeat = std::string();
|
||||||
getline(m_i, repeat);
|
std::getline(m_i, repeat);
|
||||||
if (value != repeat) {
|
if (value != repeat) {
|
||||||
m_o << "values do not match; field has not been altered" << endl;
|
m_o << "values do not match; field has not been altered" << endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getline(m_i, value);
|
std::getline(m_i, value);
|
||||||
}
|
}
|
||||||
for (Field &field : fields) {
|
for (Field &field : fields) {
|
||||||
if (field.name() == fieldName) {
|
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()) {
|
if (!m_file.isOpen()) {
|
||||||
m_o << "can not remove field; no file open" << endl;
|
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;
|
m_o << "can not remove field; current entry is no account entry" << endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vector<Field> &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
|
auto &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
|
||||||
unsigned int valuesFound = 0;
|
auto valuesFound = unsigned();
|
||||||
for (const Field &field : fields) {
|
for (const Field &field : fields) {
|
||||||
if (field.name() == fieldName) {
|
if (field.name() == fieldName) {
|
||||||
++valuesFound;
|
++valuesFound;
|
||||||
|
@ -730,7 +740,7 @@ void InteractiveCli::quit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string InteractiveCli::askForPassphrase(bool confirm)
|
std::string InteractiveCli::askForPassphrase(bool confirm)
|
||||||
{
|
{
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
m_o << "enter new passphrase: ";
|
m_o << "enter new passphrase: ";
|
||||||
|
@ -738,10 +748,10 @@ string InteractiveCli::askForPassphrase(bool confirm)
|
||||||
m_o << "enter passphrase: ";
|
m_o << "enter passphrase: ";
|
||||||
}
|
}
|
||||||
m_o.flush();
|
m_o.flush();
|
||||||
string input1;
|
auto input1 = std::string();
|
||||||
{
|
{
|
||||||
InputMuter m;
|
auto m = InputMuter();
|
||||||
getline(m_i, input1);
|
std::getline(m_i, input1);
|
||||||
}
|
}
|
||||||
m_o << endl;
|
m_o << endl;
|
||||||
if (input1.empty()) {
|
if (input1.empty()) {
|
||||||
|
@ -751,15 +761,15 @@ string InteractiveCli::askForPassphrase(bool confirm)
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
m_o << "confirm new passphrase: ";
|
m_o << "confirm new passphrase: ";
|
||||||
m_o.flush();
|
m_o.flush();
|
||||||
string input2;
|
auto input2 = std::string();
|
||||||
{
|
{
|
||||||
InputMuter m;
|
auto m = InputMuter();
|
||||||
getline(m_i, input2);
|
std::getline(m_i, input2);
|
||||||
}
|
}
|
||||||
m_o << endl;
|
m_o << endl;
|
||||||
if (input1 != input2) {
|
if (input1 != input2) {
|
||||||
m_o << "phrases do not match" << endl;
|
m_o << "phrases do not match" << endl;
|
||||||
throw runtime_error("confirmation failed");
|
throw std::runtime_error("confirmation failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return input1;
|
return input1;
|
||||||
|
|
11
cli/cli.h
11
cli/cli.h
|
@ -12,7 +12,7 @@
|
||||||
#include <istream>
|
#include <istream>
|
||||||
#include <ostream>
|
#include <ostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <string_view>
|
||||||
|
|
||||||
namespace Io {
|
namespace Io {
|
||||||
class Entry;
|
class Entry;
|
||||||
|
@ -23,7 +23,7 @@ namespace Cli {
|
||||||
|
|
||||||
class InputMuter {
|
class InputMuter {
|
||||||
public:
|
public:
|
||||||
InputMuter();
|
explicit InputMuter();
|
||||||
~InputMuter();
|
~InputMuter();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -39,9 +39,10 @@ void clearConsole();
|
||||||
|
|
||||||
class InteractiveCli {
|
class InteractiveCli {
|
||||||
public:
|
public:
|
||||||
InteractiveCli();
|
explicit InteractiveCli();
|
||||||
void run(const std::string &file = std::string());
|
~InteractiveCli();
|
||||||
void openFile(const std::string &file, Io::PasswordFileOpenFlags openFlags);
|
int run(std::string_view file);
|
||||||
|
void openFile(std::string_view file, Io::PasswordFileOpenFlags openFlags);
|
||||||
void closeFile();
|
void closeFile();
|
||||||
void saveFile();
|
void saveFile();
|
||||||
void createFile(const std::string &file);
|
void createFile(const std::string &file);
|
||||||
|
|
|
@ -7,7 +7,7 @@ namespace QtGui {
|
||||||
|
|
||||||
class FieldDelegate : public QStyledItemDelegate {
|
class FieldDelegate : public QStyledItemDelegate {
|
||||||
public:
|
public:
|
||||||
FieldDelegate(QObject *parent = nullptr);
|
explicit FieldDelegate(QObject *parent = nullptr);
|
||||||
|
|
||||||
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
|
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
#include "resources/config.h"
|
#include "resources/config.h"
|
||||||
#include "resources/qtconfig.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/importplugin.h>
|
||||||
#include <qtutilities/resources/qtconfigarguments.h>
|
#include <qtutilities/resources/qtconfigarguments.h>
|
||||||
#include <qtutilities/resources/resources.h>
|
#include <qtutilities/resources/resources.h>
|
||||||
|
@ -12,7 +14,6 @@
|
||||||
|
|
||||||
#include <passwordfile/util/openssl.h>
|
#include <passwordfile/util/openssl.h>
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
@ -31,7 +32,8 @@ int runWidgetsGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs,
|
||||||
OpenSsl::init();
|
OpenSsl::init();
|
||||||
|
|
||||||
// init application
|
// init application
|
||||||
QApplication application(argc, argv);
|
auto application = QApplication(argc, argv);
|
||||||
|
QObject::connect(&application, &QCoreApplication::aboutToQuit, &OpenSsl::clean);
|
||||||
|
|
||||||
// restore Qt settings
|
// restore Qt settings
|
||||||
auto qtSettings = QtSettings();
|
auto qtSettings = QtSettings();
|
||||||
|
@ -56,7 +58,6 @@ int runWidgetsGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
// start event loop
|
// start event loop
|
||||||
QObject::connect(&application, &QCoreApplication::aboutToQuit, &OpenSsl::clean);
|
|
||||||
auto res = application.exec();
|
auto res = application.exec();
|
||||||
|
|
||||||
// save settings to disk
|
// save settings to disk
|
||||||
|
|
|
@ -119,6 +119,21 @@ void MainWindow::updateStyleSheet()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Updates the columns sizing according to the checked action/option.
|
||||||
|
*/
|
||||||
|
void MainWindow::updateColumnSizing()
|
||||||
|
{
|
||||||
|
auto *const header = m_ui->tableView->horizontalHeader();
|
||||||
|
if (m_ui->actionColumnWidthCustom->isChecked()) {
|
||||||
|
header->setStretchLastSection(true);
|
||||||
|
header->setCascadingSectionResizes(true);
|
||||||
|
header->setSectionResizeMode(QHeaderView::Interactive);
|
||||||
|
} else {
|
||||||
|
header->setSectionResizeMode(QHeaderView::Stretch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Constructs a new main window.
|
* \brief Constructs a new main window.
|
||||||
*/
|
*/
|
||||||
|
@ -172,7 +187,18 @@ MainWindow::MainWindow(QSettings &settings, QtUtilities::QtSettings *qtSettings,
|
||||||
m_ui->treeView->setFrameShape(QFrame::StyledPanel);
|
m_ui->treeView->setFrameShape(QFrame::StyledPanel);
|
||||||
m_ui->tableView->setFrameShape(QFrame::StyledPanel);
|
m_ui->tableView->setFrameShape(QFrame::StyledPanel);
|
||||||
#endif
|
#endif
|
||||||
m_ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
|
||||||
|
// setup column sizing
|
||||||
|
auto *const columnSizingGroup = new QActionGroup(this);
|
||||||
|
columnSizingGroup->addAction(m_ui->actionColumnWidthAuto);
|
||||||
|
columnSizingGroup->addAction(m_ui->actionColumnWidthCustom);
|
||||||
|
if (settings.value(QStringLiteral("interactivecolumns")).toBool()) {
|
||||||
|
m_ui->actionColumnWidthCustom->setChecked(true);
|
||||||
|
} else {
|
||||||
|
m_ui->actionColumnWidthAuto->setChecked(true);
|
||||||
|
}
|
||||||
|
updateColumnSizing();
|
||||||
|
|
||||||
// splitter sizes
|
// splitter sizes
|
||||||
m_ui->splitter->setSizes(QList<int>() << 100 << 800);
|
m_ui->splitter->setSizes(QList<int>() << 100 << 800);
|
||||||
|
|
||||||
|
@ -225,6 +251,7 @@ MainWindow::MainWindow(QSettings &settings, QtUtilities::QtSettings *qtSettings,
|
||||||
connect(m_undoStack, &QUndoStack::canRedoChanged, m_ui->actionRedo, &QAction::setEnabled);
|
connect(m_undoStack, &QUndoStack::canRedoChanged, m_ui->actionRedo, &QAction::setEnabled);
|
||||||
// -> view
|
// -> view
|
||||||
connect(passwordVisibilityGroup, &QActionGroup::triggered, this, &MainWindow::setPasswordVisibility);
|
connect(passwordVisibilityGroup, &QActionGroup::triggered, this, &MainWindow::setPasswordVisibility);
|
||||||
|
connect(columnSizingGroup, &QActionGroup::triggered, this, &MainWindow::updateColumnSizing);
|
||||||
connect(m_ui->actionShowUndoStack, &QAction::triggered, this, &MainWindow::showUndoView);
|
connect(m_ui->actionShowUndoStack, &QAction::triggered, this, &MainWindow::showUndoView);
|
||||||
// -> models
|
// -> models
|
||||||
connect(m_ui->treeView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &MainWindow::accountSelected);
|
connect(m_ui->treeView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &MainWindow::accountSelected);
|
||||||
|
@ -254,6 +281,9 @@ bool MainWindow::event(QEvent *event)
|
||||||
{
|
{
|
||||||
switch (event->type()) {
|
switch (event->type()) {
|
||||||
case QEvent::PaletteChange:
|
case QEvent::PaletteChange:
|
||||||
|
if (m_qtSettings) {
|
||||||
|
m_qtSettings->reevaluatePaletteAndDefaultIconTheme();
|
||||||
|
}
|
||||||
updateStyleSheet();
|
updateStyleSheet();
|
||||||
break;
|
break;
|
||||||
default:;
|
default:;
|
||||||
|
@ -278,7 +308,8 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event)
|
||||||
QString data;
|
QString data;
|
||||||
const QMimeData *mimeData = dropEvent->mimeData();
|
const QMimeData *mimeData = dropEvent->mimeData();
|
||||||
if (mimeData->hasUrls()) {
|
if (mimeData->hasUrls()) {
|
||||||
const QUrl url = mimeData->urls().front();
|
const auto urls = mimeData->urls();
|
||||||
|
const auto &url = urls.front();
|
||||||
if (url.scheme() == QLatin1String("file")) {
|
if (url.scheme() == QLatin1String("file")) {
|
||||||
data = url.path();
|
data = url.path();
|
||||||
}
|
}
|
||||||
|
@ -329,6 +360,7 @@ void MainWindow::closeEvent(QCloseEvent *event)
|
||||||
pwVisibility = QStringLiteral("editing");
|
pwVisibility = QStringLiteral("editing");
|
||||||
}
|
}
|
||||||
m_settings.setValue(QStringLiteral("pwvisibility"), QVariant(pwVisibility));
|
m_settings.setValue(QStringLiteral("pwvisibility"), QVariant(pwVisibility));
|
||||||
|
m_settings.setValue(QStringLiteral("interactivecolumns"), m_ui->actionColumnWidthCustom->isChecked());
|
||||||
m_settings.endGroup();
|
m_settings.endGroup();
|
||||||
if (m_qtSettings) {
|
if (m_qtSettings) {
|
||||||
m_qtSettings->save(m_settings);
|
m_qtSettings->save(m_settings);
|
||||||
|
|
|
@ -110,6 +110,7 @@ private Q_SLOTS:
|
||||||
void setSomethingChanged();
|
void setSomethingChanged();
|
||||||
void setSomethingChanged(bool somethingChanged);
|
void setSomethingChanged(bool somethingChanged);
|
||||||
void updateStyleSheet();
|
void updateStyleSheet();
|
||||||
|
void updateColumnSizing();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// showing conditional messages/prompts
|
// showing conditional messages/prompts
|
||||||
|
|
|
@ -249,7 +249,15 @@
|
||||||
<addaction name="actionShowOnlyWhenEditing"/>
|
<addaction name="actionShowOnlyWhenEditing"/>
|
||||||
<addaction name="actionHideAlways"/>
|
<addaction name="actionHideAlways"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuColumnWiths">
|
||||||
|
<property name="title">
|
||||||
|
<string>Column withs</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionColumnWidthAuto"/>
|
||||||
|
<addaction name="actionColumnWidthCustom"/>
|
||||||
|
</widget>
|
||||||
<addaction name="menuPassword_visibility"/>
|
<addaction name="menuPassword_visibility"/>
|
||||||
|
<addaction name="menuColumnWiths"/>
|
||||||
<addaction name="actionShowUndoStack"/>
|
<addaction name="actionShowUndoStack"/>
|
||||||
</widget>
|
</widget>
|
||||||
<addaction name="menuProgramm"/>
|
<addaction name="menuProgramm"/>
|
||||||
|
@ -569,6 +577,22 @@
|
||||||
<string>Details ...</string>
|
<string>Details ...</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionColumnWidthAuto">
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Auto</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionColumnWidthCustom">
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Custom</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<layoutdefault spacing="6" margin="11"/>
|
<layoutdefault spacing="6" margin="11"/>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
|
|
@ -298,6 +298,17 @@
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</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>
|
<resources>
|
||||||
<include location="../resources/icons.qrc"/>
|
<include location="../resources/icons.qrc"/>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -17,7 +17,7 @@ class StackSupport {
|
||||||
friend class StackAbsorper;
|
friend class StackAbsorper;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
StackSupport(QUndoStack *undoStack = nullptr);
|
explicit StackSupport(QUndoStack *undoStack = nullptr);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QUndoStack *undoStack();
|
QUndoStack *undoStack();
|
||||||
|
@ -68,7 +68,7 @@ inline void StackSupport::clearUndoStack()
|
||||||
*/
|
*/
|
||||||
class StackAbsorper {
|
class StackAbsorper {
|
||||||
public:
|
public:
|
||||||
StackAbsorper(StackSupport *supported);
|
explicit StackAbsorper(StackSupport *supported);
|
||||||
~StackAbsorper();
|
~StackAbsorper();
|
||||||
QUndoStack *stack();
|
QUndoStack *stack();
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,15 @@ void CustomUndoCommand::undo()
|
||||||
* \brief Sets the value for the specified index and role in the specified field model.
|
* \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.
|
* \brief Constructs a new command.
|
||||||
*/
|
*/
|
||||||
|
@ -75,7 +84,6 @@ FieldModelSetValueCommand::FieldModelSetValueCommand(FieldModel *model, const QM
|
||||||
, m_oldValue(model->data(index, role))
|
, m_oldValue(model->data(index, role))
|
||||||
, m_role(role)
|
, m_role(role)
|
||||||
{
|
{
|
||||||
QString fieldName = model->index(m_row, 0, index.parent()).data().toString();
|
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
case Qt::EditRole:
|
case Qt::EditRole:
|
||||||
|
@ -88,16 +96,16 @@ FieldModelSetValueCommand::FieldModelSetValueCommand(FieldModel *model, const QM
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
if (fieldName.isEmpty()) {
|
if (const auto fieldName = getFieldName(model, m_row, index); !fieldName.isEmpty()) {
|
||||||
setText(QApplication::translate("undocommands", "setting value of empty field"));
|
|
||||||
} else {
|
|
||||||
setText(QApplication::translate("undocommands", "setting value of »%1« field").arg(fieldName));
|
setText(QApplication::translate("undocommands", "setting value of »%1« field").arg(fieldName));
|
||||||
|
} else {
|
||||||
|
setText(QApplication::translate("undocommands", "setting value of empty field"));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case FieldTypeRole:
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
setText(QApplication::translate("undocommands", "setting field property in row »%1«").arg(m_row + 1));
|
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.
|
* \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();
|
res.clear();
|
||||||
if (Entry *entry = model->entry(index)) {
|
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.
|
* \brief Fetches the entry for the specified \a model and \a path.
|
||||||
* \remarks The \a path will be modified. To prevent this use entryFromPathCpy().
|
* \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()) {
|
if (NodeEntry *rootEntry = model->rootEntry()) {
|
||||||
return rootEntry->entryByPath(path);
|
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.
|
* \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);
|
return entryFromPath(model, path);
|
||||||
}
|
}
|
||||||
|
@ -316,7 +324,7 @@ EntryModelModifyRowsCommand::~EntryModelModifyRowsCommand()
|
||||||
*/
|
*/
|
||||||
bool EntryModelModifyRowsCommand::insert()
|
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)) {
|
if (m_model->insertEntries(m_row, m_model->index(parentEntry), m_values)) {
|
||||||
m_values.clear();
|
m_values.clear();
|
||||||
return true;
|
return true;
|
||||||
|
@ -334,7 +342,7 @@ bool EntryModelModifyRowsCommand::insert()
|
||||||
*/
|
*/
|
||||||
bool EntryModelModifyRowsCommand::remove()
|
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));
|
m_values = m_model->takeEntries(m_row, m_count, m_model->index(parentEntry));
|
||||||
return !m_values.isEmpty();
|
return !m_values.isEmpty();
|
||||||
}
|
}
|
||||||
|
@ -433,29 +441,28 @@ bool EntryModelMoveRowsCommand::internalRedo()
|
||||||
|
|
||||||
bool EntryModelMoveRowsCommand::internalUndo()
|
bool EntryModelMoveRowsCommand::internalUndo()
|
||||||
{
|
{
|
||||||
if (m_count) {
|
if (!m_count) {
|
||||||
Entry *sourceParentEntry = entryFromPathCpy(m_model, m_sourceParentPath);
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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
|
} // namespace QtGui
|
||||||
|
|
83
main.cpp
83
main.cpp
|
@ -1,4 +1,5 @@
|
||||||
#include "./cli/cli.h"
|
#include "./cli/cli.h"
|
||||||
|
|
||||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
||||||
#include "./gui/initiategui.h"
|
#include "./gui/initiategui.h"
|
||||||
#endif
|
#endif
|
||||||
|
@ -9,13 +10,12 @@
|
||||||
#include "resources/config.h"
|
#include "resources/config.h"
|
||||||
#include "resources/qtconfig.h"
|
#include "resources/qtconfig.h"
|
||||||
|
|
||||||
#include <passwordfile/util/openssl.h>
|
|
||||||
|
|
||||||
#include <c++utilities/application/argumentparser.h>
|
#include <c++utilities/application/argumentparser.h>
|
||||||
#include <c++utilities/application/commandlineutils.h>
|
#include <c++utilities/application/commandlineutils.h>
|
||||||
#include <c++utilities/misc/parseerror.h>
|
#include <c++utilities/misc/parseerror.h>
|
||||||
|
|
||||||
#if defined(PASSWORD_MANAGER_GUI_QTWIDGETS) || defined(PASSWORD_MANAGER_GUI_QTQUICK)
|
#if defined(PASSWORD_MANAGER_GUI_QTWIDGETS) || defined(PASSWORD_MANAGER_GUI_QTQUICK)
|
||||||
|
#define PASSWORD_MANAGER_GUI_QTWIDGETS_OR_QTQUICK
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <qtutilities/resources/qtconfigarguments.h>
|
#include <qtutilities/resources/qtconfigarguments.h>
|
||||||
|
@ -24,33 +24,39 @@ ENABLE_QT_RESOURCES_OF_STATIC_DEPENDENCIES
|
||||||
#include <c++utilities/application/fakeqtconfigarguments.h>
|
#include <c++utilities/application/fakeqtconfigarguments.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
// force (preferably Qt Quick) GUI under Android
|
// force (preferably Qt Quick) GUI under Android
|
||||||
#ifdef Q_OS_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
|
#define PASSWORD_MANAGER_FORCE_GUI
|
||||||
#else
|
#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
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
using namespace std;
|
|
||||||
using namespace CppUtilities;
|
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[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
CMD_UTILS_CONVERT_ARGS_TO_UTF8;
|
CMD_UTILS_CONVERT_ARGS_TO_UTF8;
|
||||||
SET_APPLICATION_INFO;
|
SET_APPLICATION_INFO;
|
||||||
QT_CONFIG_ARGUMENTS qtConfigArgs;
|
|
||||||
int returnCode = 0;
|
|
||||||
|
|
||||||
|
// parse CLI arguments
|
||||||
|
auto qtConfigArgs = QT_CONFIG_ARGUMENTS();
|
||||||
#ifndef PASSWORD_MANAGER_FORCE_GUI
|
#ifndef PASSWORD_MANAGER_FORCE_GUI
|
||||||
// setup argument parser
|
auto parser = ArgumentParser();
|
||||||
ArgumentParser parser;
|
auto fileArg = Argument("file", 'f', "specifies the file to be opened (or created when using --modify)");
|
||||||
// file argument
|
|
||||||
Argument fileArg("file", 'f', "specifies the file to be opened (or created when using --modify)");
|
|
||||||
fileArg.setValueNames({ "path" });
|
fileArg.setValueNames({ "path" });
|
||||||
fileArg.setRequiredValueCount(1);
|
fileArg.setRequiredValueCount(1);
|
||||||
fileArg.setCombinable(true);
|
fileArg.setCombinable(true);
|
||||||
|
@ -58,63 +64,44 @@ int main(int argc, char *argv[])
|
||||||
fileArg.setImplicit(true);
|
fileArg.setImplicit(true);
|
||||||
qtConfigArgs.qtWidgetsGuiArg().addSubArgument(&fileArg);
|
qtConfigArgs.qtWidgetsGuiArg().addSubArgument(&fileArg);
|
||||||
qtConfigArgs.qtQuickGuiArg().addSubArgument(&fileArg);
|
qtConfigArgs.qtQuickGuiArg().addSubArgument(&fileArg);
|
||||||
// cli argument
|
auto cliArg = Argument("interactive-cli", 'i', "starts the interactive command line interface");
|
||||||
Argument cliArg("interactive-cli", 'i', "starts the interactive command line interface");
|
|
||||||
cliArg.setDenotesOperation(true);
|
cliArg.setDenotesOperation(true);
|
||||||
cliArg.setSubArguments({ &fileArg });
|
cliArg.setSubArguments({ &fileArg });
|
||||||
// help argument
|
auto helpArg = HelpArgument(parser);
|
||||||
HelpArgument helpArg(parser);
|
|
||||||
parser.setMainArguments({ &qtConfigArgs.qtWidgetsGuiArg(), &qtConfigArgs.qtQuickGuiArg(), &cliArg, &helpArg });
|
parser.setMainArguments({ &qtConfigArgs.qtWidgetsGuiArg(), &qtConfigArgs.qtQuickGuiArg(), &cliArg, &helpArg });
|
||||||
// parse the specified arguments
|
|
||||||
parser.parseArgs(argc, argv);
|
parser.parseArgs(argc, argv);
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef PASSWORD_MANAGER_FORCE_GUI
|
// run CLI if CLI-argument is present
|
||||||
// start either interactive CLI or GUI
|
|
||||||
if (cliArg.isPresent()) {
|
if (cliArg.isPresent()) {
|
||||||
// init OpenSSL
|
return Cli::InteractiveCli().run(fileArg.isPresent() ? std::string(fileArg.firstValue()) : std::string());
|
||||||
OpenSsl::init();
|
}
|
||||||
|
|
||||||
Cli::InteractiveCli cli;
|
// run GUI depending on which GUI-argument is present
|
||||||
if (fileArg.isPresent()) {
|
if (qtConfigArgs.areQtGuiArgsPresent()) {
|
||||||
cli.run(fileArg.firstValue());
|
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS_OR_QTQUICK
|
||||||
} else {
|
const auto file = fileArg.isPresent() ? QString::fromLocal8Bit(fileArg.firstValue()) : QString();
|
||||||
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());
|
|
||||||
#endif
|
#endif
|
||||||
if (qtConfigArgs.qtWidgetsGuiArg().isPresent()) {
|
if (qtConfigArgs.qtWidgetsGuiArg().isPresent()) {
|
||||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
||||||
returnCode = QtGui::runWidgetsGui(argc, argv, qtConfigArgs, file);
|
return QtGui::runWidgetsGui(argc, argv, qtConfigArgs, file);
|
||||||
#else
|
#else
|
||||||
CMD_UTILS_START_CONSOLE;
|
return fail("The application has not been built with Qt Widgets GUI support.");
|
||||||
cerr << "The application has not been built with Qt widgets support." << endl;
|
|
||||||
#endif
|
#endif
|
||||||
} else if (qtConfigArgs.qtQuickGuiArg().isPresent()) {
|
} else if (qtConfigArgs.qtQuickGuiArg().isPresent()) {
|
||||||
#ifdef PASSWORD_MANAGER_GUI_QTQUICK
|
#ifdef PASSWORD_MANAGER_GUI_QTQUICK
|
||||||
returnCode = QtGui::runQuickGui(argc, argv, qtConfigArgs, file);
|
return QtGui::runQuickGui(argc, argv, qtConfigArgs, file);
|
||||||
#else
|
#else
|
||||||
CMD_UTILS_START_CONSOLE;
|
return fail("The application has not been built with Qt Quick GUI support.");
|
||||||
cerr << "The application has not been built with Qt quick support." << endl;
|
|
||||||
#endif
|
#endif
|
||||||
} else {
|
|
||||||
CMD_UTILS_START_CONSOLE;
|
|
||||||
cerr << "See --help for usage." << endl;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return fail("See --help for usage.");
|
||||||
|
|
||||||
#else // PASSWORD_MANAGER_FORCE_GUI
|
#else // PASSWORD_MANAGER_FORCE_GUI
|
||||||
#ifdef PASSWORD_MANAGER_GUI_QTQUICK
|
#ifdef PASSWORD_MANAGER_GUI_QTQUICK
|
||||||
returnCode = QtGui::runQuickGui(argc, argv, qtConfigArgs, QString());
|
return QtGui::runQuickGui(argc, argv, qtConfigArgs, QString());
|
||||||
#else
|
#else
|
||||||
returnCode = QtGui::runWidgetsGui(argc, argv, qtConfigArgs, QString());
|
return QtGui::runWidgetsGui(argc, argv, qtConfigArgs, QString());
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return returnCode;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -452,31 +452,39 @@ bool EntryModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co
|
||||||
if (undoStack()) {
|
if (undoStack()) {
|
||||||
return push(make_unique<EntryModelMoveRowsCommand>(this, sourceParent, sourceRow, count, destinationParent, destinationChild));
|
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
|
#endif
|
||||||
// check validation of specified arguments: source and destination parent entries need to be node entries
|
// check validation of specified arguments: source and destination parent entries need to be node entries
|
||||||
if (sourceRow < 0 || count <= 0) {
|
if (sourceRow < 0 || count <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto *const srcParentEntry = entry(sourceParent);
|
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) {
|
if (!srcParentEntry || !destParentEntry || srcParentEntry->type() != EntryType::Node || destParentEntry->type() != EntryType::Node) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// determine the source parent entry and dest parent entry as node entries
|
// determine the source parent entry and dest parent entry as node entries
|
||||||
auto *const srcParentNodeEntry = static_cast<NodeEntry *>(sourceParent.internalPointer());
|
auto *const srcParentNodeEntry = static_cast<NodeEntry *>(sourceParent.internalPointer());
|
||||||
auto *const destParentNodeEntry = static_cast<NodeEntry *>(destinationParent.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
|
// source rows must be within the valid range
|
||||||
if (static_cast<size_t>(sourceRow + count) > srcParentNodeEntry->children().size()
|
if (static_cast<std::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))) {
|
|
||||||
return false;
|
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
|
// do not move a row to one of its own children! -> check before
|
||||||
for (int index = 0; index < count; ++index) {
|
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) {
|
if (toMove->type() != EntryType::Node) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -485,9 +493,11 @@ bool EntryModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// actually perform the move operation
|
// 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) {
|
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) {
|
if (srcParentNodeEntry == destParentNodeEntry && sourceRow < destinationChild) {
|
||||||
toMove->setParent(destParentNodeEntry, destinationChild + index - 1);
|
toMove->setParent(destParentNodeEntry, destinationChild + index - 1);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -233,17 +233,15 @@ bool FieldModel::setData(const QModelIndex &index, const QVariant &value, int ro
|
||||||
switch (index.column()) {
|
switch (index.column()) {
|
||||||
case 0:
|
case 0:
|
||||||
beginInsertRows(index.parent(), rowCount(), rowCount());
|
beginInsertRows(index.parent(), rowCount(), rowCount());
|
||||||
m_fields->emplace_back(m_accountEntry);
|
m_fields->emplace_back(m_accountEntry).setName(value.toString().toStdString());
|
||||||
m_fields->back().setName(value.toString().toStdString());
|
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
roles << Qt::DisplayRole << Qt::EditRole << IsLastRow;
|
roles << Qt::DisplayRole << Qt::EditRole << Key << IsLastRow;
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
beginInsertRows(index.parent(), rowCount(), rowCount());
|
beginInsertRows(index.parent(), rowCount(), rowCount());
|
||||||
m_fields->emplace_back(m_accountEntry);
|
m_fields->emplace_back(m_accountEntry).setValue(value.toString().toStdString());
|
||||||
m_fields->back().setValue(value.toString().toStdString());
|
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
roles << Qt::DisplayRole << Qt::EditRole << IsLastRow;
|
roles << Qt::DisplayRole << Qt::EditRole << Value << IsLastRow;
|
||||||
break;
|
break;
|
||||||
default:;
|
default:;
|
||||||
}
|
}
|
||||||
|
@ -335,25 +333,23 @@ bool FieldModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co
|
||||||
|
|
||||||
// validate input parameter
|
// validate input parameter
|
||||||
if (sourceParent.isValid() || destinationParent.isValid() || sourceRow < 0 || count <= 0 || destinationChild < 0
|
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))) {
|
|| (destinationChild >= sourceRow && destinationChild < (sourceRow + count))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// begin the move
|
// begin the move
|
||||||
if (destinationChild > sourceRow) {
|
// note: When moving rows down (destinationChild > sourceRow) the third param is still counted in the initial array!
|
||||||
// move rows down: the third param is still counted in the initial array!
|
if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent,
|
||||||
beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild + count);
|
destinationChild > sourceRow ? destinationChild + count : destinationChild)) {
|
||||||
} else {
|
return false;
|
||||||
// move rows up
|
|
||||||
beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reserve space for temporary copies (FIXME: possible to avoid this?)
|
// reserve space for temporary copies (FIXME: possible to avoid this?)
|
||||||
m_fields->reserve(m_fields->size() + static_cast<size_t>(count));
|
m_fields->reserve(m_fields->size() + static_cast<std::size_t>(count));
|
||||||
vector<Io::Field> tmp(static_cast<size_t>(count));
|
auto tmp = std::vector<Io::Field>(static_cast<std::size_t>(count));
|
||||||
// move rows to temporary array
|
// 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
|
// erase slots of rows to be moved
|
||||||
m_fields->erase(m_fields->begin() + sourceRow, m_fields->begin() + sourceRow + count);
|
m_fields->erase(m_fields->begin() + sourceRow, m_fields->begin() + sourceRow + count);
|
||||||
// insert rows again at their new position
|
// 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
|
const Field *FieldModel::field(size_t row) const
|
||||||
{
|
{
|
||||||
if (m_fields && row < m_fields->size()) {
|
return m_fields && row < m_fields->size() ? &(*m_fields)[row] : nullptr;
|
||||||
return &(*m_fields)[row];
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QtGui
|
} // namespace QtGui
|
||||||
|
|
|
@ -7,8 +7,7 @@ BasicDialog {
|
||||||
id: aboutDialog
|
id: aboutDialog
|
||||||
standardButtons: Controls.Dialog.Ok
|
standardButtons: Controls.Dialog.Ok
|
||||||
padding: Kirigami.Units.largeSpacing
|
padding: Kirigami.Units.largeSpacing
|
||||||
|
contentItem: ColumnLayout {
|
||||||
ColumnLayout {
|
|
||||||
width: aboutDialog.availableWidth
|
width: aboutDialog.availableWidth
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
|
@ -43,16 +42,8 @@ BasicDialog {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: "<a href=\"" + app.organizationDomain + "\">" + app.organizationDomain + "</a>"
|
text: "<a href=\"" + app.organizationDomain + "\">" + app.organizationDomain + "</a>"
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
onLinkActivated: openWebsite()
|
onLinkActivated: Qt.openUrlExternally(app.organizationDomain)
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: parent.openWebsite()
|
|
||||||
}
|
|
||||||
function openWebsite() {
|
|
||||||
Qt.openUrlExternally(app.organizationDomain)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Controls.Label {
|
Controls.Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
|
@ -6,9 +6,7 @@ Controls.Dialog {
|
||||||
modal: true
|
modal: true
|
||||||
focus: true
|
focus: true
|
||||||
parent: applicationWindow().overlay
|
parent: applicationWindow().overlay
|
||||||
//anchors.centerIn: parent // enable if requiring at least Qt 5.12 instead of setting x and y manually
|
anchors.centerIn: parent
|
||||||
x: (parent.width - width) / 2
|
|
||||||
y: (parent.height - height) / 2
|
|
||||||
width: Math.min(parent.width, Kirigami.Units.gridUnit * 30)
|
width: Math.min(parent.width, Kirigami.Units.gridUnit * 30)
|
||||||
|
|
||||||
function acceptOnReturn(event) {
|
function acceptOnReturn(event) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import QtQuick 2.4
|
import QtQuick 2.15
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQml.Models 2.2
|
import QtQml.Models 2.2
|
||||||
import QtQuick.Controls 2.4 as Controls
|
import QtQuick.Controls 2.4 as Controls
|
||||||
|
@ -15,17 +15,17 @@ Kirigami.ScrollablePage {
|
||||||
var currentEntryName = entryModel.data(rootIndex)
|
var currentEntryName = entryModel.data(rootIndex)
|
||||||
return currentEntryName ? currentEntryName : ""
|
return currentEntryName ? currentEntryName : ""
|
||||||
}
|
}
|
||||||
actions {
|
actions:[
|
||||||
main: Kirigami.Action {
|
Kirigami.Action {
|
||||||
iconName: "list-add"
|
icon.name: "list-add"
|
||||||
text: qsTr("Add account")
|
text: qsTr("Add account")
|
||||||
visible: !nativeInterface.hasEntryFilter
|
visible: !nativeInterface.hasEntryFilter
|
||||||
enabled: !nativeInterface.hasEntryFilter
|
enabled: !nativeInterface.hasEntryFilter
|
||||||
onTriggered: insertEntry("Account")
|
onTriggered: insertEntry("Account")
|
||||||
shortcut: "Ctrl+A"
|
shortcut: "Ctrl+A"
|
||||||
}
|
},
|
||||||
left: Kirigami.Action {
|
Kirigami.Action {
|
||||||
iconName: "edit-paste"
|
icon.name: "edit-paste"
|
||||||
text: qsTr("Paste account")
|
text: qsTr("Paste account")
|
||||||
visible: !nativeInterface.hasEntryFilter
|
visible: !nativeInterface.hasEntryFilter
|
||||||
enabled: nativeInterface.canPaste && !nativeInterface.hasEntryFilter
|
enabled: nativeInterface.canPaste && !nativeInterface.hasEntryFilter
|
||||||
|
@ -40,19 +40,16 @@ Kirigami.ScrollablePage {
|
||||||
pastedEntries.join(", ")))
|
pastedEntries.join(", ")))
|
||||||
}
|
}
|
||||||
shortcut: StandardKey.Paste
|
shortcut: StandardKey.Paste
|
||||||
}
|
},
|
||||||
right: Kirigami.Action {
|
Kirigami.Action {
|
||||||
iconName: "folder-add"
|
icon.name: "folder-add"
|
||||||
text: qsTr("Add category")
|
text: qsTr("Add category")
|
||||||
visible: !nativeInterface.hasEntryFilter
|
visible: !nativeInterface.hasEntryFilter
|
||||||
enabled: !nativeInterface.hasEntryFilter
|
enabled: !nativeInterface.hasEntryFilter
|
||||||
onTriggered: insertEntry("Node")
|
onTriggered: insertEntry("Node")
|
||||||
shortcut: "Ctrl+Shift+A"
|
shortcut: "Ctrl+Shift+A"
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
background: Rectangle {
|
|
||||||
color: Kirigami.Theme.backgroundColor
|
|
||||||
}
|
|
||||||
|
|
||||||
// dialog to confirm deletion of an entry
|
// dialog to confirm deletion of an entry
|
||||||
BasicDialog {
|
BasicDialog {
|
||||||
|
@ -64,8 +61,7 @@ Kirigami.ScrollablePage {
|
||||||
standardButtons: Controls.Dialog.Ok | Controls.Dialog.Cancel
|
standardButtons: Controls.Dialog.Ok | Controls.Dialog.Cancel
|
||||||
title: qsTr("Delete %1?").arg(entryDesc)
|
title: qsTr("Delete %1?").arg(entryDesc)
|
||||||
onAccepted: entryModel.removeRows(this.entryIndex, 1, rootIndex)
|
onAccepted: entryModel.removeRows(this.entryIndex, 1, rootIndex)
|
||||||
|
contentItem: ColumnLayout {
|
||||||
ColumnLayout {
|
|
||||||
Controls.Label {
|
Controls.Label {
|
||||||
text: " "
|
text: " "
|
||||||
}
|
}
|
||||||
|
@ -109,13 +105,12 @@ Kirigami.ScrollablePage {
|
||||||
entryModel.removeRows(this.entryIndex, 1, rootIndex)
|
entryModel.removeRows(this.entryIndex, 1, rootIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
contentItem: ColumnLayout {
|
||||||
ColumnLayout {
|
|
||||||
Controls.TextField {
|
Controls.TextField {
|
||||||
id: entryNameTextField
|
id: entryNameTextField
|
||||||
Layout.preferredWidth: renameDialog.availableWidth
|
Layout.preferredWidth: renameDialog.availableWidth
|
||||||
placeholderText: qsTr("enter new name here")
|
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
|
// list view to display one hierarchy level of entry model
|
||||||
ListView {
|
ListView {
|
||||||
id: entriesListView
|
id: entriesListView
|
||||||
|
@ -257,12 +143,16 @@ Kirigami.ScrollablePage {
|
||||||
easing.type: Easing.InOutQuad
|
easing.type: Easing.InOutQuad
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
reuseItems: true
|
||||||
model: DelegateModel {
|
model: DelegateModel {
|
||||||
id: delegateModel
|
id: delegateModel
|
||||||
|
delegate: EntryDelegate {
|
||||||
delegate: Kirigami.DelegateRecycler {
|
width: entriesListView.width
|
||||||
width: parent ? parent.width : implicitWidth
|
view: entriesListView
|
||||||
sourceComponent: listDelegateComponent
|
onMoveRequested:
|
||||||
|
(oldIndex, newIndex) => {
|
||||||
|
entryModel.moveRows(rootIndex, oldIndex, 1, rootIndex, oldIndex < newIndex ? newIndex + 1 : newIndex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNode(rowNumber) {
|
function isNode(rowNumber) {
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import QtQuick 2.4
|
import QtQuick 2.15
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQml.Models 2.2
|
import QtQml.Models 2.2
|
||||||
import QtQuick.Controls 2.4 as Controls
|
import QtQuick.Controls 2.4 as Controls
|
||||||
|
@ -10,9 +10,21 @@ Kirigami.ScrollablePage {
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
title: nativeInterface.currentAccountName
|
title: nativeInterface.currentAccountName
|
||||||
background: Rectangle {
|
actions:[
|
||||||
color: Kirigami.Theme.backgroundColor
|
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
|
// dialog to edit certain field
|
||||||
BasicDialog {
|
BasicDialog {
|
||||||
|
@ -32,8 +44,7 @@ Kirigami.ScrollablePage {
|
||||||
fieldsListView.model.setData(column0, isPassword ? 1 : 0,
|
fieldsListView.model.setData(column0, isPassword ? 1 : 0,
|
||||||
0x0100 + 1)
|
0x0100 + 1)
|
||||||
}
|
}
|
||||||
|
contentItem: ColumnLayout {
|
||||||
ColumnLayout {
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
Layout.preferredWidth: fieldDialog.availableWidth
|
Layout.preferredWidth: fieldDialog.availableWidth
|
||||||
columns: 2
|
columns: 2
|
||||||
|
@ -43,7 +54,7 @@ Kirigami.ScrollablePage {
|
||||||
id: fieldNameEdit
|
id: fieldNameEdit
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: fieldDialog.fieldName
|
text: fieldDialog.fieldName
|
||||||
Keys.onPressed: fieldDialog.acceptOnReturn(event)
|
Keys.onPressed: (event) => fieldDialog.acceptOnReturn(event)
|
||||||
}
|
}
|
||||||
Controls.RoundButton {
|
Controls.RoundButton {
|
||||||
flat: true
|
flat: true
|
||||||
|
@ -69,7 +80,7 @@ Kirigami.ScrollablePage {
|
||||||
// fix ugly bullet points under Android
|
// fix ugly bullet points under Android
|
||||||
font.pointSize: hideCharacters ? fieldNameEdit.font.pointSize
|
font.pointSize: hideCharacters ? fieldNameEdit.font.pointSize
|
||||||
* 0.5 : fieldNameEdit.font.pointSize
|
* 0.5 : fieldNameEdit.font.pointSize
|
||||||
Keys.onPressed: fieldDialog.acceptOnReturn(event)
|
Keys.onPressed: (event) => fieldDialog.acceptOnReturn(event)
|
||||||
}
|
}
|
||||||
Controls.RoundButton {
|
Controls.RoundButton {
|
||||||
flat: true
|
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
|
// list view to edit the currently selected account
|
||||||
ListView {
|
ListView {
|
||||||
id: fieldsListView
|
id: fieldsListView
|
||||||
implicitWidth: Kirigami.Units.gridUnit * 30
|
implicitWidth: Kirigami.Units.gridUnit * 30
|
||||||
model: nativeInterface.fieldModel
|
model: nativeInterface.fieldModel
|
||||||
|
reuseItems: true
|
||||||
moveDisplaced: Transition {
|
moveDisplaced: Transition {
|
||||||
YAnimator {
|
YAnimator {
|
||||||
duration: Kirigami.Units.longDuration
|
duration: Kirigami.Units.longDuration
|
||||||
easing.type: Easing.InOutQuad
|
easing.type: Easing.InOutQuad
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delegate: Kirigami.DelegateRecycler {
|
delegate: FieldsDelegate {
|
||||||
width: parent ? parent.width : implicitWidth
|
width: fieldsListView.width
|
||||||
sourceComponent: fieldsListDelegateComponent
|
view: fieldsListView
|
||||||
|
onMoveRequested:
|
||||||
|
(oldIndex, newIndex) => {
|
||||||
|
const model = fieldsListView.model
|
||||||
|
const invalidIndex = model.index(-1, 0)
|
||||||
|
model.moveRows(invalidIndex, oldIndex, 1, invalidIndex, newIndex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import QtQuick.Dialogs as Dialogs
|
import QtQuick.Dialogs 6.2 as Dialogs
|
||||||
|
|
||||||
Dialogs.FileDialog {
|
Dialogs.FileDialog {
|
||||||
id: fileDialog
|
id: fileDialog
|
||||||
|
|
|
@ -30,8 +30,7 @@ BasicDialog {
|
||||||
qsTr("You aborted. The password has not been altered."))
|
qsTr("You aborted. The password has not been altered."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
contentItem: ColumnLayout {
|
||||||
ColumnLayout {
|
|
||||||
Controls.Label {
|
Controls.Label {
|
||||||
id: instructionLabel
|
id: instructionLabel
|
||||||
Layout.preferredWidth: passwordDialog.availableWidth
|
Layout.preferredWidth: passwordDialog.availableWidth
|
||||||
|
@ -42,26 +41,18 @@ BasicDialog {
|
||||||
id: passwordTextField
|
id: passwordTextField
|
||||||
Layout.preferredWidth: passwordDialog.availableWidth
|
Layout.preferredWidth: passwordDialog.availableWidth
|
||||||
echoMode: showCharactersCheckBox.checked ? TextInput.Normal : TextInput.Password
|
echoMode: showCharactersCheckBox.checked ? TextInput.Normal : TextInput.Password
|
||||||
placeholderText: qsTr("enter password here, leave empty for no encryption")
|
placeholderText: newPassword
|
||||||
color: "#101010"
|
? qsTr("enter password here, leave empty for no encryption")
|
||||||
placeholderTextColor: "#505050"
|
: qsTr("enter password here")
|
||||||
background: Rectangle {
|
Keys.onPressed: (event) => passwordDialog.acceptOnReturn(event)
|
||||||
border.color: "#5d5e6d"
|
|
||||||
}
|
|
||||||
Keys.onPressed: passwordDialog.acceptOnReturn(event)
|
|
||||||
}
|
}
|
||||||
Controls.TextField {
|
Controls.TextField {
|
||||||
id: repeatPasswordTextField
|
id: repeatPasswordTextField
|
||||||
Layout.preferredWidth: passwordDialog.availableWidth
|
Layout.preferredWidth: passwordDialog.availableWidth
|
||||||
visible: passwordDialog.newPassword
|
visible: passwordDialog.newPassword
|
||||||
&& !showCharactersCheckBox.checked
|
enabled: visible && !showCharactersCheckBox.checked
|
||||||
echoMode: TextInput.Password
|
echoMode: TextInput.Password
|
||||||
placeholderText: qsTr("repeat password")
|
placeholderText: qsTr("repeat password")
|
||||||
color: "#101010"
|
|
||||||
placeholderTextColor: "#505050"
|
|
||||||
background: Rectangle {
|
|
||||||
border.color: passwordDialog.canAccept ? "#089900" : "#ff0000"
|
|
||||||
}
|
|
||||||
Keys.onPressed: passwordDialog.acceptOnReturn(event)
|
Keys.onPressed: passwordDialog.acceptOnReturn(event)
|
||||||
}
|
}
|
||||||
Controls.CheckBox {
|
Controls.CheckBox {
|
||||||
|
|
83
qml/main.qml
83
qml/main.qml
|
@ -1,6 +1,8 @@
|
||||||
import QtQuick 2.7
|
import QtQuick 2.7
|
||||||
import QtQuick.Templates 2.0 as T2
|
import QtQuick.Templates 2.0 as T2
|
||||||
import QtQuick.Controls 2.1 as Controls
|
import QtQuick.Controls 2.1 as Controls
|
||||||
|
import QtQuick.Controls.Material
|
||||||
|
import QtQuick.Controls.Universal
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import org.kde.kirigami 2.4 as Kirigami
|
import org.kde.kirigami 2.4 as Kirigami
|
||||||
|
|
||||||
|
@ -9,13 +11,14 @@ Kirigami.ApplicationWindow {
|
||||||
property var fieldsPage: undefined
|
property var fieldsPage: undefined
|
||||||
property var lastEntriesPage: undefined
|
property var lastEntriesPage: undefined
|
||||||
|
|
||||||
|
Material.theme: nativeInterface.darkModeEnabled ? Material.Dark : Material.Light
|
||||||
|
Universal.theme: nativeInterface.darkModeEnabled ? Universal.Dark : Universal.Light
|
||||||
|
|
||||||
globalDrawer: Kirigami.GlobalDrawer {
|
globalDrawer: Kirigami.GlobalDrawer {
|
||||||
id: leftMenu
|
id: leftMenu
|
||||||
property bool showNoPasswordWarning: nativeInterface.fileOpen
|
property bool showNoPasswordWarning: nativeInterface.fileOpen
|
||||||
&& !nativeInterface.passwordSet
|
&& !nativeInterface.passwordSet
|
||||||
|
|
||||||
title: app.applicationName
|
|
||||||
titleIcon: "qrc://icons/hicolor/scalable/apps/passwordmanager.svg"
|
|
||||||
visible: true
|
visible: true
|
||||||
resetMenuOnTriggered: false
|
resetMenuOnTriggered: false
|
||||||
topContent: ColumnLayout {
|
topContent: ColumnLayout {
|
||||||
|
@ -101,20 +104,20 @@ Kirigami.ApplicationWindow {
|
||||||
actions: [
|
actions: [
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: qsTr("Create new file")
|
text: qsTr("Create new file")
|
||||||
iconName: "document-new"
|
icon.name: "document-new"
|
||||||
onTriggered: fileDialog.createNew()
|
onTriggered: fileDialog.createNew()
|
||||||
shortcut: StandardKey.New
|
shortcut: StandardKey.New
|
||||||
},
|
},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: qsTr("Open existing file")
|
text: qsTr("Open existing file")
|
||||||
iconName: "document-open"
|
icon.name: "document-open"
|
||||||
onTriggered: fileDialog.openExisting()
|
onTriggered: fileDialog.openExisting()
|
||||||
shortcut: StandardKey.Open
|
shortcut: StandardKey.Open
|
||||||
},
|
},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
id: recentlyOpenedAction
|
id: recentlyOpenedAction
|
||||||
text: qsTr("Recently opened ...")
|
text: qsTr("Recently opened ...")
|
||||||
iconName: "document-open-recent"
|
icon.name: "document-open-recent"
|
||||||
children: createRecentlyOpenedActions(
|
children: createRecentlyOpenedActions(
|
||||||
nativeInterface.recentFiles)
|
nativeInterface.recentFiles)
|
||||||
visible: nativeInterface.recentFiles.length > 0
|
visible: nativeInterface.recentFiles.length > 0
|
||||||
|
@ -123,14 +126,14 @@ Kirigami.ApplicationWindow {
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: qsTr("Save modifications")
|
text: qsTr("Save modifications")
|
||||||
enabled: nativeInterface.fileOpen
|
enabled: nativeInterface.fileOpen
|
||||||
iconName: "document-save"
|
icon.name: "document-save"
|
||||||
onTriggered: nativeInterface.save()
|
onTriggered: nativeInterface.save()
|
||||||
shortcut: StandardKey.Save
|
shortcut: StandardKey.Save
|
||||||
},
|
},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: qsTr("Save as")
|
text: qsTr("Save as")
|
||||||
enabled: nativeInterface.fileOpen
|
enabled: nativeInterface.fileOpen
|
||||||
iconName: "document-save-as"
|
icon.name: "document-save-as"
|
||||||
onTriggered: fileDialog.saveAs()
|
onTriggered: fileDialog.saveAs()
|
||||||
shortcut: StandardKey.SaveAs
|
shortcut: StandardKey.SaveAs
|
||||||
},
|
},
|
||||||
|
@ -138,7 +141,7 @@ Kirigami.ApplicationWindow {
|
||||||
text: nativeInterface.passwordSet ? qsTr("Change password") : qsTr(
|
text: nativeInterface.passwordSet ? qsTr("Change password") : qsTr(
|
||||||
"Add password")
|
"Add password")
|
||||||
enabled: nativeInterface.fileOpen
|
enabled: nativeInterface.fileOpen
|
||||||
iconName: "document-encrypt"
|
icon.name: "document-encrypt"
|
||||||
onTriggered: enterPasswordDialog.askForNewPassword(
|
onTriggered: enterPasswordDialog.askForNewPassword(
|
||||||
qsTr("Change password for %1").arg(
|
qsTr("Change password for %1").arg(
|
||||||
nativeInterface.filePath))
|
nativeInterface.filePath))
|
||||||
|
@ -147,7 +150,7 @@ Kirigami.ApplicationWindow {
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: qsTr("Details")
|
text: qsTr("Details")
|
||||||
enabled: nativeInterface.fileOpen
|
enabled: nativeInterface.fileOpen
|
||||||
iconName: "document-properties"
|
icon.name: "document-properties"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
leftMenu.resetMenu()
|
leftMenu.resetMenu()
|
||||||
fileSummaryDialog.show()
|
fileSummaryDialog.show()
|
||||||
|
@ -159,7 +162,7 @@ Kirigami.ApplicationWindow {
|
||||||
"Adjust search")
|
"Adjust search")
|
||||||
enabled: nativeInterface.fileOpen
|
enabled: nativeInterface.fileOpen
|
||||||
visible: nativeInterface.filterAsDialog
|
visible: nativeInterface.filterAsDialog
|
||||||
iconName: "search"
|
icon.name: "search"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
leftMenu.resetMenu()
|
leftMenu.resetMenu()
|
||||||
filterDialog.open()
|
filterDialog.open()
|
||||||
|
@ -171,7 +174,7 @@ Kirigami.ApplicationWindow {
|
||||||
enabled: nativeInterface.fileOpen
|
enabled: nativeInterface.fileOpen
|
||||||
visible: nativeInterface.filterAsDialog
|
visible: nativeInterface.filterAsDialog
|
||||||
&& nativeInterface.entryFilter.length > 0
|
&& nativeInterface.entryFilter.length > 0
|
||||||
iconName: "edit-clear"
|
icon.name: "edit-clear"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
leftMenu.resetMenu()
|
leftMenu.resetMenu()
|
||||||
nativeInterface.entryFilter = ""
|
nativeInterface.entryFilter = ""
|
||||||
|
@ -183,7 +186,7 @@ Kirigami.ApplicationWindow {
|
||||||
visible: nativeInterface.undoText.length !== 0
|
visible: nativeInterface.undoText.length !== 0
|
||||||
&& nativeInterface.entryFilter.length === 0
|
&& nativeInterface.entryFilter.length === 0
|
||||||
enabled: visible
|
enabled: visible
|
||||||
iconName: "edit-undo"
|
icon.name: "edit-undo"
|
||||||
shortcut: StandardKey.Undo
|
shortcut: StandardKey.Undo
|
||||||
onTriggered: nativeInterface.undo()
|
onTriggered: nativeInterface.undo()
|
||||||
},
|
},
|
||||||
|
@ -192,29 +195,30 @@ Kirigami.ApplicationWindow {
|
||||||
visible: nativeInterface.redoText.length !== 0
|
visible: nativeInterface.redoText.length !== 0
|
||||||
&& nativeInterface.entryFilter.length === 0
|
&& nativeInterface.entryFilter.length === 0
|
||||||
enabled: visible
|
enabled: visible
|
||||||
iconName: "edit-redo"
|
icon.name: "edit-redo"
|
||||||
shortcut: StandardKey.Redo
|
shortcut: StandardKey.Redo
|
||||||
onTriggered: nativeInterface.redo()
|
onTriggered: nativeInterface.redo()
|
||||||
},
|
},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: qsTr("Close file")
|
text: qsTr("Close file")
|
||||||
enabled: nativeInterface.fileOpen
|
enabled: nativeInterface.fileOpen
|
||||||
iconName: "document-close"
|
icon.name: "document-close"
|
||||||
shortcut: StandardKey.Close
|
shortcut: StandardKey.Close
|
||||||
onTriggered: nativeInterface.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 {
|
contextDrawer: Kirigami.ContextDrawer {
|
||||||
id: contextDrawer
|
id: contextDrawer
|
||||||
|
@ -239,9 +243,9 @@ Kirigami.ApplicationWindow {
|
||||||
id: fileSummaryDialog
|
id: fileSummaryDialog
|
||||||
standardButtons: Controls.Dialog.Ok
|
standardButtons: Controls.Dialog.Ok
|
||||||
title: qsTr("File details")
|
title: qsTr("File details")
|
||||||
|
contentItem: Controls.TextArea {
|
||||||
Controls.Label {
|
|
||||||
id: fileSummaryLabel
|
id: fileSummaryLabel
|
||||||
|
readOnly: true
|
||||||
text: "No file summary available"
|
text: "No file summary available"
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
|
@ -287,8 +291,7 @@ Kirigami.ApplicationWindow {
|
||||||
Controls.DialogButtonBox.buttonRole: Controls.DialogButtonBox.RejectRole
|
Controls.DialogButtonBox.buttonRole: Controls.DialogButtonBox.RejectRole
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
contentItem: ColumnLayout {
|
||||||
ColumnLayout {
|
|
||||||
Controls.TextField {
|
Controls.TextField {
|
||||||
id: filterDialogTextField
|
id: filterDialogTextField
|
||||||
Layout.preferredWidth: filterDialog.availableWidth
|
Layout.preferredWidth: filterDialog.availableWidth
|
||||||
|
@ -299,12 +302,12 @@ Kirigami.ApplicationWindow {
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: nativeInterface
|
target: nativeInterface
|
||||||
onEntryFilterChanged: {
|
function onEntryFilterChanged(newFilter) {
|
||||||
if (filterTextField.text !== newFilter) {
|
if (filterTextField.text !== newFilter) {
|
||||||
filterTextField.text = newFilter
|
filterTextField.text = newFilter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onFileError: {
|
function onFileError(errorMessage, retryAction) {
|
||||||
var retryMethod = null
|
var retryMethod = null
|
||||||
if (retryAction === "load" || retryAction === "save") {
|
if (retryAction === "load" || retryAction === "save") {
|
||||||
retryMethod = retryAction
|
retryMethod = retryAction
|
||||||
|
@ -318,16 +321,16 @@ Kirigami.ApplicationWindow {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onSettingsError: {
|
function onSettingsError(errorMessage) {
|
||||||
showPassiveNotification(errorMessage)
|
showPassiveNotification(errorMessage)
|
||||||
}
|
}
|
||||||
onPasswordRequired: {
|
function onPasswordRequired(filePath) {
|
||||||
enterPasswordDialog.askForExistingPassword(
|
enterPasswordDialog.askForExistingPassword(
|
||||||
qsTr("Password required to open %1").arg(
|
qsTr("Password required to open %1").arg(
|
||||||
nativeInterface.filePath))
|
nativeInterface.filePath))
|
||||||
leftMenu.resetMenu()
|
leftMenu.resetMenu()
|
||||||
}
|
}
|
||||||
onFileOpenChanged: {
|
function onFileOpenChanged(fileOpen) {
|
||||||
clearStack()
|
clearStack()
|
||||||
if (!nativeInterface.fileOpen) {
|
if (!nativeInterface.fileOpen) {
|
||||||
showPassiveNotification(qsTr("%1 closed").arg(
|
showPassiveNotification(qsTr("%1 closed").arg(
|
||||||
|
@ -339,20 +342,20 @@ Kirigami.ApplicationWindow {
|
||||||
nativeInterface.fileName))
|
nativeInterface.fileName))
|
||||||
leftMenu.close()
|
leftMenu.close()
|
||||||
}
|
}
|
||||||
onFileSaved: {
|
function onFileSaved() {
|
||||||
showPassiveNotification(qsTr("%1 saved").arg(
|
showPassiveNotification(qsTr("%1 saved").arg(
|
||||||
nativeInterface.fileName))
|
nativeInterface.fileName))
|
||||||
}
|
}
|
||||||
onNewNotification: {
|
function onNewNotification(message) {
|
||||||
showPassiveNotification(message)
|
showPassiveNotification(message)
|
||||||
}
|
}
|
||||||
onCurrentAccountChanged: {
|
function onCurrentAccountChanged() {
|
||||||
// remove the fields page if the current account has been removed
|
// remove the fields page if the current account has been removed
|
||||||
if (!nativeInterface.hasCurrentAccount) {
|
if (!nativeInterface.hasCurrentAccount) {
|
||||||
pageStack.pop(lastEntriesPage)
|
pageStack.pop(lastEntriesPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onEntryAboutToBeRemoved: {
|
function onEntryAboutToBeRemoved(removedIndex) {
|
||||||
// get the filter entry index
|
// get the filter entry index
|
||||||
if (nativeInterface.hasEntryFilter) {
|
if (nativeInterface.hasEntryFilter) {
|
||||||
removedIndex = nativeInterface.filterEntryIndex(removedIndex)
|
removedIndex = nativeInterface.filterEntryIndex(removedIndex)
|
||||||
|
@ -370,7 +373,7 @@ Kirigami.ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onHasEntryFilterChanged: {
|
function onHasEntryFilterChanged(hasEntryFilter) {
|
||||||
if (nativeInterface.fileOpen) {
|
if (nativeInterface.fileOpen) {
|
||||||
pageStack.clear()
|
pageStack.clear()
|
||||||
initStack()
|
initStack()
|
||||||
|
@ -395,7 +398,7 @@ Kirigami.ApplicationWindow {
|
||||||
id: clearRecentFilesActionComponent
|
id: clearRecentFilesActionComponent
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: qsTr("Clear recently opened files")
|
text: qsTr("Clear recently opened files")
|
||||||
iconName: "edit-clear"
|
icon.name: "edit-clear"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
nativeInterface.clearRecentFiles()
|
nativeInterface.clearRecentFiles()
|
||||||
leftMenu.resetMenu()
|
leftMenu.resetMenu()
|
||||||
|
|
|
@ -5,12 +5,11 @@
|
||||||
|
|
||||||
#include <c++utilities/conversion/stringbuilder.h>
|
#include <c++utilities/conversion/stringbuilder.h>
|
||||||
|
|
||||||
#include <QAndroidJniObject>
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
#include <QJniObject>
|
||||||
#include <QMessageLogContext>
|
#include <QMessageLogContext>
|
||||||
#include <QMetaObject>
|
#include <QMetaObject>
|
||||||
#include <QtAndroid>
|
|
||||||
|
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
|
||||||
|
@ -35,9 +34,9 @@ static Controller *controllerForAndroid = nullptr;
|
||||||
|
|
||||||
void applyThemingForAndroid()
|
void applyThemingForAndroid()
|
||||||
{
|
{
|
||||||
QtAndroid::runOnAndroidThread([=]() {
|
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() {
|
||||||
const auto color = QColor(QLatin1String("#2c714a")).rgba();
|
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>("addFlags", "(I)V", Android::WindowManager::LayoutParams::DrawsSystemBarBackgrounds);
|
||||||
window.callMethod<void>("clearFlags", "(I)V", Android::WindowManager::LayoutParams::TranslucentStatus);
|
window.callMethod<void>("clearFlags", "(I)V", Android::WindowManager::LayoutParams::TranslucentStatus);
|
||||||
window.callMethod<void>("setStatusBarColor", "(I)V", color);
|
window.callMethod<void>("setStatusBarColor", "(I)V", color);
|
||||||
|
@ -52,13 +51,14 @@ void registerControllerForAndroid(Controller *controller)
|
||||||
|
|
||||||
bool showAndroidFileDialog(bool existing, bool createNew)
|
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)
|
int openFileDescriptorFromAndroidContentUrl(const QString &url, const QString &mode)
|
||||||
{
|
{
|
||||||
return QtAndroid::androidActivity().callMethod<jint>("openFileDescriptorFromAndroidContentUri", "(Ljava/lang/String;Ljava/lang/String;)I",
|
return QJniObject(QNativeInterface::QAndroidApplication::context())
|
||||||
QAndroidJniObject::fromString(url).object<jstring>(), QAndroidJniObject::fromString(mode).object<jstring>());
|
.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)
|
void writeToAndroidLog(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||||
|
@ -102,19 +102,20 @@ void setupAndroidSpecifics()
|
||||||
static void onAndroidError(JNIEnv *, jobject, jstring message)
|
static void onAndroidError(JNIEnv *, jobject, jstring message)
|
||||||
{
|
{
|
||||||
QMetaObject::invokeMethod(
|
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)
|
static void onAndroidFileDialogAccepted(JNIEnv *, jobject, jstring fileName, jboolean existing, jboolean createNew)
|
||||||
{
|
{
|
||||||
QMetaObject::invokeMethod(QtGui::controllerForAndroid, "handleFileSelectionAccepted", Qt::QueuedConnection,
|
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,
|
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));
|
Q_ARG(int, fileHandle), Q_ARG(bool, existing), Q_ARG(bool, createNew));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,14 +43,15 @@ Controller::Controller(QSettings &settings, const QString &filePath, QObject *pa
|
||||||
#endif
|
#endif
|
||||||
, m_fileOpen(false)
|
, m_fileOpen(false)
|
||||||
, m_fileModified(false)
|
, m_fileModified(false)
|
||||||
, m_useNativeFileDialog(false)
|
, m_useNativeFileDialog(supportsNativeFileDialog())
|
||||||
, m_filterAsDialog(
|
, m_filterAsDialog(
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
true
|
true
|
||||||
#else
|
#else
|
||||||
false
|
false
|
||||||
#endif
|
#endif
|
||||||
)
|
)
|
||||||
|
, m_darkModeEnabled(false)
|
||||||
{
|
{
|
||||||
m_fieldModel.setPasswordVisibility(PasswordVisibility::Never);
|
m_fieldModel.setPasswordVisibility(PasswordVisibility::Never);
|
||||||
m_entryFilterModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
|
m_entryFilterModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||||
|
|
|
@ -44,6 +44,7 @@ class Controller : public QObject {
|
||||||
Q_PROPERTY(QUndoStack *undoStack READ undoStack NOTIFY undoStackChanged)
|
Q_PROPERTY(QUndoStack *undoStack READ undoStack NOTIFY undoStackChanged)
|
||||||
Q_PROPERTY(QString undoText READ undoText NOTIFY undoTextChanged)
|
Q_PROPERTY(QString undoText READ undoText NOTIFY undoTextChanged)
|
||||||
Q_PROPERTY(QString redoText READ redoText NOTIFY redoTextChanged)
|
Q_PROPERTY(QString redoText READ redoText NOTIFY redoTextChanged)
|
||||||
|
Q_PROPERTY(bool darkModeEnabled READ isDarkModeEnabled WRITE setDarkModeEnabled NOTIFY darkModeEnabledChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Controller(QSettings &settings, const QString &filePath = QString(), QObject *parent = nullptr);
|
explicit Controller(QSettings &settings, const QString &filePath = QString(), QObject *parent = nullptr);
|
||||||
|
@ -86,6 +87,8 @@ public:
|
||||||
QString undoText() const;
|
QString undoText() const;
|
||||||
QString redoText() const;
|
QString redoText() const;
|
||||||
Io::PasswordFileSaveFlags prepareSaving();
|
Io::PasswordFileSaveFlags prepareSaving();
|
||||||
|
bool isDarkModeEnabled() const;
|
||||||
|
void setDarkModeEnabled(bool darkModeEnabled);
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
void init();
|
void init();
|
||||||
|
@ -129,6 +132,7 @@ Q_SIGNALS:
|
||||||
void undoTextChanged(const QString &undoText);
|
void undoTextChanged(const QString &undoText);
|
||||||
void redoTextChanged(const QString &redoText);
|
void redoTextChanged(const QString &redoText);
|
||||||
void settingsError(const QString &errorMessage);
|
void settingsError(const QString &errorMessage);
|
||||||
|
void darkModeEnabledChanged(bool darkModeEnabled);
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void handleEntriesRemoved(const QModelIndex &parentIndex, int first, int last);
|
void handleEntriesRemoved(const QModelIndex &parentIndex, int first, int last);
|
||||||
|
@ -160,6 +164,7 @@ private:
|
||||||
bool m_fileModified;
|
bool m_fileModified;
|
||||||
bool m_useNativeFileDialog;
|
bool m_useNativeFileDialog;
|
||||||
bool m_filterAsDialog;
|
bool m_filterAsDialog;
|
||||||
|
bool m_darkModeEnabled;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline QModelIndex Controller::ensureSourceEntryIndex(const QModelIndex &entryIndexMaybeFromFilterModel) const
|
inline QModelIndex Controller::ensureSourceEntryIndex(const QModelIndex &entryIndexMaybeFromFilterModel) const
|
||||||
|
@ -345,6 +350,18 @@ inline QString Controller::redoText() const
|
||||||
#endif
|
#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()
|
inline void Controller::undo()
|
||||||
{
|
{
|
||||||
#ifdef PASSWORD_MANAGER_UNDO_SUPPORT
|
#ifdef PASSWORD_MANAGER_UNDO_SUPPORT
|
||||||
|
|
|
@ -10,25 +10,29 @@
|
||||||
// enable inline helper functions for Qt Quick provided by qtutilities
|
// enable inline helper functions for Qt Quick provided by qtutilities
|
||||||
#define QT_UTILITIES_GUI_QTQUICK
|
#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/qtconfigarguments.h>
|
||||||
#include <qtutilities/resources/resources.h>
|
#include <qtutilities/resources/resources.h>
|
||||||
|
#include <qtutilities/settingsdialog/qtsettings.h>
|
||||||
|
|
||||||
#include <passwordfile/util/openssl.h>
|
#include <passwordfile/util/openssl.h>
|
||||||
|
|
||||||
#include <QGuiApplication>
|
#include <QDebug>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QtQml>
|
|
||||||
#ifdef Q_OS_ANDROID
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QDirIterator>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
#include <cstdlib>
|
||||||
#include <QApplication>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using namespace CppUtilities;
|
using namespace CppUtilities;
|
||||||
using namespace Util;
|
using namespace Util;
|
||||||
|
@ -42,55 +46,53 @@ int runQuickGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, c
|
||||||
setupAndroidSpecifics();
|
setupAndroidSpecifics();
|
||||||
#endif
|
#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
|
// init OpenSSL
|
||||||
OpenSsl::init();
|
OpenSsl::init();
|
||||||
|
|
||||||
// init application
|
// init application
|
||||||
SET_QT_APPLICATION_INFO;
|
SET_QT_APPLICATION_INFO;
|
||||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
auto application = App(argc, argv);
|
||||||
QApplication application(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
|
#else
|
||||||
QGuiApplication application(argc, argv);
|
const auto isDarkModeEnabled = QtUtilities::isDarkModeEnabled().value_or(false);
|
||||||
|
qtSettings.reapplyDefaultIconTheme(isDarkModeEnabled);
|
||||||
|
controller.setDarkModeEnabled(isDarkModeEnabled);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// apply settings specified via command line args
|
// apply settings specified via command line args
|
||||||
qtConfigArgs.applySettings();
|
qtConfigArgs.applySettings(qtSettings.hasCustomFont());
|
||||||
qtConfigArgs.applySettingsForQuickGui();
|
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;
|
LOAD_QT_TRANSLATIONS;
|
||||||
|
|
||||||
// init Quick GUI
|
// init QML engine
|
||||||
auto engine = QQmlApplicationEngine();
|
auto engine = QQmlApplicationEngine();
|
||||||
auto controller = Controller(*settings, file);
|
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
registerControllerForAndroid(&controller);
|
registerControllerForAndroid(&controller);
|
||||||
#endif
|
#endif
|
||||||
auto *const context(engine.rootContext());
|
auto *const context = engine.rootContext();
|
||||||
context->setContextProperty(QStringLiteral("nativeInterface"), &controller);
|
context->setContextProperty(QStringLiteral("nativeInterface"), &controller);
|
||||||
context->setContextProperty(QStringLiteral("app"), &application);
|
context->setContextProperty(QStringLiteral("app"), &application);
|
||||||
context->setContextProperty(QStringLiteral("description"), QStringLiteral(APP_DESCRIPTION));
|
context->setContextProperty(QStringLiteral("description"), QStringLiteral(APP_DESCRIPTION));
|
||||||
|
@ -101,10 +103,18 @@ int runQuickGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, c
|
||||||
engine.addImportPath(path);
|
engine.addImportPath(path);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml")));
|
|
||||||
|
|
||||||
// run event loop
|
// load main QML file; run event loop or exit if it cannot be loaded
|
||||||
QObject::connect(&application, &QCoreApplication::aboutToQuit, &OpenSsl::clean);
|
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();
|
return application.exec();
|
||||||
}
|
}
|
||||||
} // namespace QtGui
|
} // namespace QtGui
|
||||||
|
|
|
@ -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>
|
|
@ -8,6 +8,8 @@
|
||||||
<file alias="AboutDialog.qml">../qml/AboutDialog.qml</file>
|
<file alias="AboutDialog.qml">../qml/AboutDialog.qml</file>
|
||||||
<file alias="PasswordDialog.qml">../qml/PasswordDialog.qml</file>
|
<file alias="PasswordDialog.qml">../qml/PasswordDialog.qml</file>
|
||||||
<file alias="EntriesPage.qml">../qml/EntriesPage.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="FieldsPage.qml">../qml/FieldsPage.qml</file>
|
||||||
|
<file alias="FieldsDelegate.qml">../qml/FieldsDelegate.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue