Compare commits
90 Commits
Author | SHA1 | Date |
---|---|---|
|
cabeca7756 | |
|
06ae52d0d8 | |
|
6ffa54088a | |
|
1b6b645f7a | |
|
1ce919397b | |
|
3c706b614e | |
|
e095dc904f | |
|
a37ee658a6 | |
|
9653b4c0d5 | |
|
613d62d042 | |
|
8319d4f485 | |
|
c2fc538e2c | |
|
f20c7b5d3d | |
|
b2d85c7d53 | |
|
0f4c30c14f | |
|
21a16e0be0 | |
|
ba9a6e26de | |
|
9ebffe6b16 | |
|
64a2738827 | |
|
a098b40dab | |
|
dbc2e9a4e2 | |
|
f960aa80a1 | |
|
b950451807 | |
|
20aea51fa1 | |
|
5706086456 | |
|
b8d2422669 | |
|
7a08ee7d00 | |
|
7c03835fc3 | |
|
0791964c6f | |
|
f6096f7232 | |
|
79a0ef031c | |
|
cb6fe128c4 | |
|
eed9d596ba | |
|
cd23eb4add | |
|
4891ee0b32 | |
|
6972256727 | |
|
4bf6a91d72 | |
|
430e17416a | |
|
8f8eda8755 | |
|
fcbc3cb4a4 | |
|
7b242732e8 | |
|
343121ae90 | |
|
a8023d255a | |
|
5ebf64d1de | |
|
9aa8d9c9ac | |
|
1993046e96 | |
|
a93e04fd14 | |
|
ef6b6085b1 | |
|
5acf8b0cc0 | |
|
0ead95e749 | |
|
a45734de1d | |
|
e407f0b455 | |
|
5ef3ac49bf | |
|
991873c149 | |
|
34e7fb123a | |
|
140d2e1782 | |
|
e8c5fdfb56 | |
|
a829ca0e65 | |
|
05e0ad5696 | |
|
e1f753cd89 | |
|
25ef1dbe91 | |
|
a8364be203 | |
|
b32c29c29f | |
|
3a1495ce53 | |
|
9e944618b7 | |
|
16876710e0 | |
|
9c3a236672 | |
|
eb1b73f4be | |
|
217942e946 | |
|
3f7ea3095e | |
|
46d35530c4 | |
|
24300cd8da | |
|
37ee70dfa4 | |
|
2b155c925f | |
|
7cf5972d27 | |
|
d020aaab63 | |
|
eb6fc17f6b | |
|
16b1051c68 | |
|
418104b6cb | |
|
cfb4423f79 | |
|
2428c3e656 | |
|
d97d291b16 | |
|
7e9f319d2d | |
|
54e21607df | |
|
6684f1b627 | |
|
641e02f64c | |
|
3a01f2de31 | |
|
acbfd6ec6b | |
|
4c85e7710b | |
|
bdefea2e59 |
|
@ -0,0 +1,19 @@
|
|||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- feature request
|
||||
- enhancement
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
|
@ -42,3 +42,6 @@ Makefile*
|
|||
|
||||
# clang-format
|
||||
/.clang-format
|
||||
|
||||
# Android-specific
|
||||
/android/AndroidManifest.xml
|
||||
|
|
100
CMakeLists.txt
100
CMakeLists.txt
|
@ -1,4 +1,4 @@
|
|||
cmake_minimum_required(VERSION 3.1.0 FATAL_ERROR)
|
||||
cmake_minimum_required(VERSION 3.17.0 FATAL_ERROR)
|
||||
|
||||
# set meta data
|
||||
project(passwordmanager)
|
||||
|
@ -9,13 +9,15 @@ set(META_APP_NAME "Password Manager")
|
|||
set(META_APP_CATEGORIES "Utility;Security;")
|
||||
set(META_APP_AUTHOR "Martchus")
|
||||
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
|
||||
set(META_APP_DOMAIN "${META_APP_URL}")
|
||||
set(META_APP_DESCRIPTION "A simple password store using AES-256-CBC encryption via OpenSSL")
|
||||
set(META_GUI_OPTIONAL YES)
|
||||
set(META_USE_QQC2 ON)
|
||||
set(META_ANDROID_PACKAGE_NAME "org.martchus.passwordmanager")
|
||||
set(META_VERSION_MAJOR 4)
|
||||
set(META_VERSION_MINOR 1)
|
||||
set(META_VERSION_PATCH 6)
|
||||
set(META_VERSION_MINOR 2)
|
||||
set(META_VERSION_PATCH 1)
|
||||
set(META_RELEASE_DATE "2024-04-02")
|
||||
|
||||
# add project files
|
||||
set(HEADER_FILES cli/cli.h model/entryfiltermodel.h model/entrymodel.h model/fieldmodel.h)
|
||||
|
@ -24,13 +26,8 @@ set(SRC_FILES cli/cli.cpp main.cpp)
|
|||
set(GUI_HEADER_FILES model/entryfiltermodel.cpp model/entrymodel.cpp model/fieldmodel.cpp)
|
||||
set(GUI_SRC_FILES model/entryfiltermodel.cpp model/entrymodel.cpp model/fieldmodel.cpp)
|
||||
|
||||
set(WIDGETS_HEADER_FILES
|
||||
gui/fielddelegate.h
|
||||
gui/initiategui.h
|
||||
gui/mainwindow.h
|
||||
gui/passwordgeneratordialog.h
|
||||
gui/stacksupport.h
|
||||
gui/undocommands.h)
|
||||
set(WIDGETS_HEADER_FILES gui/fielddelegate.h gui/initiategui.h gui/mainwindow.h gui/passwordgeneratordialog.h
|
||||
gui/stacksupport.h gui/undocommands.h)
|
||||
set(WIDGETS_SRC_FILES
|
||||
gui/fielddelegate.cpp
|
||||
gui/initiategui.cpp
|
||||
|
@ -46,6 +43,8 @@ set(QML_SRC_FILES quickgui/controller.cpp quickgui/initiatequick.cpp resources/i
|
|||
if (ANDROID)
|
||||
list(APPEND QML_HEADER_FILES quickgui/android.h)
|
||||
list(APPEND QML_SRC_FILES quickgui/android.cpp)
|
||||
else ()
|
||||
list(APPEND EXCLUDED_FILES quickgui/android.h quickgui/android.cpp)
|
||||
endif ()
|
||||
|
||||
set(TS_FILES translations/${META_PROJECT_NAME}_de_DE.ts translations/${META_PROJECT_NAME}_en_US.ts)
|
||||
|
@ -106,7 +105,7 @@ set(REQUIRED_ICONS
|
|||
preferences-desktop-locale
|
||||
qtcreator
|
||||
search
|
||||
story-editor
|
||||
view-list-details-symbolic
|
||||
system-file-manager
|
||||
system-run
|
||||
system-search
|
||||
|
@ -114,29 +113,31 @@ set(REQUIRED_ICONS
|
|||
window-close)
|
||||
|
||||
# find c++utilities
|
||||
set(CONFIGURATION_PACKAGE_SUFFIX ""
|
||||
set(CONFIGURATION_PACKAGE_SUFFIX
|
||||
""
|
||||
CACHE STRING "sets the suffix for find_package() calls to packages configured via c++utilities")
|
||||
find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.6.0 REQUIRED)
|
||||
use_cpp_utilities()
|
||||
|
||||
# apply basic configuration
|
||||
set(BUILD_QUICK_GUI_BY_DEFAULT ON)
|
||||
if (WIN32 OR APPLE)
|
||||
set(BUILD_QUICK_GUI_BY_DEFAULT OFF)
|
||||
endif ()
|
||||
option(QUICK_GUI "enables/disables building the Qt Quick GUI using Kirigami" "${BUILD_QUICK_GUI_BY_DEFAULT}")
|
||||
include(BasicConfig)
|
||||
|
||||
# find qtutilities
|
||||
set(CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES "${CONFIGURATION_PACKAGE_SUFFIX}"
|
||||
set(CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES
|
||||
"${CONFIGURATION_PACKAGE_SUFFIX}"
|
||||
CACHE STRING "sets the suffix for qtutilities")
|
||||
find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.0.0 REQUIRED)
|
||||
find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.14.0 REQUIRED)
|
||||
use_qt_utilities()
|
||||
|
||||
# find passwordfile
|
||||
find_package(passwordfile${CONFIGURATION_PACKAGE_SUFFIX} 5.0.0 REQUIRED)
|
||||
use_password_file()
|
||||
|
||||
# require at least Qt 5.8 for the Qt Quick GUI
|
||||
if (QUICK_GUI)
|
||||
set(META_QT5_VERSION 5.8)
|
||||
endif ()
|
||||
|
||||
# allow to enable undo support from the widgets GUI in the quick GUI as well (so the quick GUI will depend on Qt Widgets as
|
||||
# well)
|
||||
if (QUICK_GUI AND NOT WIDGETS_GUI)
|
||||
|
@ -149,12 +150,44 @@ if (QUICK_GUI AND NOT WIDGETS_GUI)
|
|||
endif ()
|
||||
endif ()
|
||||
|
||||
# add further Qt/KF modules required by the Qt Quick GUI under Android
|
||||
if (ANDROID AND QUICK_GUI)
|
||||
list(APPEND ADDITIONAL_QT_MODULES AndroidExtras)
|
||||
list(APPEND ADDITIONAL_KF_MODULES Kirigami2)
|
||||
# deduce major Qt version from package prefix
|
||||
if (NOT QT_PACKAGE_PREFIX)
|
||||
set(MAJOR_QT_VERSION "5")
|
||||
elseif (QT_PACKAGE_PREFIX MATCHES ".*Qt([0-9]+).*")
|
||||
set(MAJOR_QT_VERSION "${CMAKE_MATCH_1}")
|
||||
endif ()
|
||||
|
||||
# require Qt 6 for the Qt Quick GUI
|
||||
if (QUICK_GUI AND (MAJOR_QT_VERSION VERSION_LESS 6 OR MAJOR_QT_VERSION VERSION_GREATER_EQUAL 7))
|
||||
message(FATAL_ERROR "The Qt Quick GUI is only compatible with Qt 6 (but Qt ${MAJOR_QT_VERSION} was found).")
|
||||
endif ()
|
||||
|
||||
# workaround "ld: error: undefined symbol: qt_resourceFeatureZstd" when Qt 6 is not configured with zstd support
|
||||
if (MAJOR_QT_VERSION GREATER_EQUAL 6 AND NOT QT_FEATURE_zstd)
|
||||
set(CMAKE_AUTORCC_OPTIONS "--no-zstd")
|
||||
endif ()
|
||||
|
||||
# add further Qt/KF modules required by Qt Quick GUI
|
||||
if (QUICK_GUI)
|
||||
list(APPEND ADDITIONAL_KF_MODULES Kirigami)
|
||||
endif ()
|
||||
|
||||
# add Qt-version-specific QML files
|
||||
unset(QML_FILE)
|
||||
if (MAJOR_QT_VERSION)
|
||||
set(QML_FILE "resources/qml${MAJOR_QT_VERSION}.qrc")
|
||||
endif ()
|
||||
if (NOT QML_FILE)
|
||||
message(FATAL_ERROR "Unable to add Qt-version-specific resource file for QT_PACKAGE_PREFIX \"${QT_PACKAGE_PREFIX}\".")
|
||||
endif ()
|
||||
message(STATUS "Adding Qt-version-specific resource file \"${QML_FILE}\" to build")
|
||||
list(APPEND QML_SRC_FILES "${QML_FILE}")
|
||||
|
||||
# add other QML files to "EXCLUDED_FILES" so they're still considered for translations in any case
|
||||
file(GLOB OTHER_QML_FILES "resources/qml*.qrc")
|
||||
list(REMOVE_ITEM OTHER_QML_FILES ${QML_SRC_FILES})
|
||||
list(APPEND EXCLUDED_FILES ${OTHER_QML_FILES})
|
||||
|
||||
# apply further configuration
|
||||
if (WIDGETS_GUI OR QUICK_GUI)
|
||||
include(QtGuiConfig)
|
||||
|
@ -162,9 +195,28 @@ if (WIDGETS_GUI OR QUICK_GUI)
|
|||
endif ()
|
||||
include(WindowsResources)
|
||||
include(AppTarget)
|
||||
include(AndroidApk)
|
||||
include(ShellCompletion)
|
||||
include(ConfigHeader)
|
||||
|
||||
# configure creating an Android package using androiddeployqt
|
||||
if (ANDROID)
|
||||
set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
|
||||
set_target_properties(${META_TARGET_NAME} PROPERTIES QT_ANDROID_PACKAGE_SOURCE_DIR "${ANDROID_PACKAGE_SOURCE_DIR}")
|
||||
|
||||
set(ANDROID_MANIFEST_PATH "${ANDROID_PACKAGE_SOURCE_DIR}/AndroidManifest.xml")
|
||||
configure_file("resources/AndroidManifest.xml.in" "${ANDROID_MANIFEST_PATH}")
|
||||
|
||||
# bundle OpenMP (used by Kirigami) explicitly as it is otherwise not bundled
|
||||
find_package(OpenMP)
|
||||
if (OpenMP_CXX_FOUND)
|
||||
message(STATUS "Bundling OpenMP library for Kirigami: ${OpenMP_omp_LIBRARY}")
|
||||
set_target_properties(${META_TARGET_NAME} PROPERTIES QT_ANDROID_EXTRA_LIBS "${OpenMP_omp_LIBRARY}")
|
||||
endif ()
|
||||
|
||||
set(QT_ANDROID_SIGN_APK ON)
|
||||
qt_android_generate_deployment_settings(${META_TARGET_NAME})
|
||||
qt_android_add_apk_target(${META_TARGET_NAME})
|
||||
endif ()
|
||||
|
||||
# create desktop file using previously defined meta data
|
||||
add_desktop_file()
|
||||
|
|
File diff suppressed because it is too large
Load Diff
216
README.md
216
README.md
|
@ -4,7 +4,7 @@ A simple [password manager](https://en.wikipedia.org/wiki/Password_manager) with
|
|||
## Features
|
||||
* Cross-platform: tested under GNU/Linux, Android and Windows
|
||||
* Qt Widgets GUI for desktop platforms
|
||||
* Qt Quick GUI (using Qt Quick Controls 2 and Kirigami 2) for mobile platforms
|
||||
* Qt Quick GUI (using Qt Quick Controls 2 and Kirigami) for mobile platforms
|
||||
* Interactive command-line interface
|
||||
* Simple architecture: All data is stored in ordinary files with AES-256-CBC applied. No cloud stuff. Use
|
||||
eg. Syncthing for synchronization.
|
||||
|
@ -13,7 +13,7 @@ A simple [password manager](https://en.wikipedia.org/wiki/Password_manager) with
|
|||
I've mainly started this project to learn C++ and Qt programming. So beside the mentioned features this project
|
||||
and the underlying libraries serve as an example project covering some interesting C++/Qt topics:
|
||||
|
||||
* Basic use of Qt Widgets, Qt Quick and Kirigami 2
|
||||
* Basic use of Qt Widgets, Qt Quick and Kirigami
|
||||
* Creating custom Qt models
|
||||
* Nested model and model with multiple columns
|
||||
* Support Drag & Drop in `QTreeView`
|
||||
|
@ -22,7 +22,7 @@ and the underlying libraries serve as an example project covering some interesti
|
|||
* Integration with Qt Widgets' undo/redo framework
|
||||
* Filtering
|
||||
* Android tweaks
|
||||
* Add CMake target to invoke `androiddeployqt`
|
||||
* Create APK via CMake (using `androiddeployqt`)
|
||||
* Customize activity
|
||||
* Customize gradle project to add additional Java dependency
|
||||
* Adjust the window style of the activity
|
||||
|
@ -46,177 +46,124 @@ See the release section on GitHub.
|
|||
* Arch Linux
|
||||
* for PKGBUILDs checkout [my GitHub repository](https://github.com/Martchus/PKGBUILDs) or
|
||||
[the AUR](https://aur.archlinux.org/packages?SeB=m&K=Martchus)
|
||||
* for a binary repository checkout [my website](http://martchus.no-ip.biz/website/page.php?name=programming)
|
||||
* there is also a [binary repository](https://martchus.no-ip.biz/repo/arch/ownstuff)
|
||||
* Tumbleweed, Leap, Fedora
|
||||
* for RPM \*.spec files and binary repository checkout
|
||||
[openSUSE Build Servide](https://build.opensuse.org/project/show/home:mkittler)
|
||||
* there's also a [repo with builds of Git master](https://build.opensuse.org/project/show/home:mkittler:vcs)
|
||||
* RPM \*.spec files and binaries are available via openSUSE Build Service
|
||||
* remarks
|
||||
* Be sure to add the repository that matches the version of your OS and to keep it
|
||||
in sync when upgrading.
|
||||
* The linked download pages might be incomplete, use the repositories URL for a full
|
||||
list.
|
||||
* latest releases: [download page](https://software.opensuse.org/download.html?project=home:mkittler&package=passwordmanager),
|
||||
[repositories URL](https://download.opensuse.org/repositories/home:/mkittler),
|
||||
[project page](https://build.opensuse.org/project/show/home:mkittler)
|
||||
* Git master: [download page](https://software.opensuse.org/download.html?project=home:mkittler:vcs&package=passwordmanager),
|
||||
[repositories URL](https://download.opensuse.org/repositories/home:/mkittler:/vcs),
|
||||
[project page](https://build.opensuse.org/project/show/home:mkittler:vcs)
|
||||
* Other GNU/Linux systems
|
||||
* [AppImage repository for releases on the openSUSE Build Service](https://download.opensuse.org/repositories/home:/mkittler:/appimage/AppImage)
|
||||
* [AppImage repository for builds from Git master the openSUSE Build Service](https://download.opensuse.org/repositories/home:/mkittler:/appimage:/vcs/AppImage/)
|
||||
* for generic, self-contained binaries checkout the [release section on GitHub](https://github.com/Martchus/passwordmanager/releases)
|
||||
* Requires glibc>=2.26, OpenGL and libX11
|
||||
* openSUSE Leap 15, Fedora 27, Debian 10 and Ubuntu 18.04 are recent enough (be sure
|
||||
the package `libopengl0` is installed on Debian/Ubuntu)
|
||||
* Supports X11 and Wayland (set the environment variable `QT_QPA_PLATFORM=xcb` to disable
|
||||
native Wayland support if it does not work on your system)
|
||||
* Binaries are signed with the GPG key
|
||||
[`B9E36A7275FC61B464B67907E06FE8F53CDC6A4C`](https://keyserver.ubuntu.com/pks/lookup?search=B9E36A7275FC61B464B67907E06FE8F53CDC6A4C&fingerprint=on&op=index).
|
||||
* Windows
|
||||
* for binaries checkout the [release section on GitHub](https://github.com/Martchus/tageditor/releases)
|
||||
* the Qt 6 based version is stable and preferable but only supports Windows 10 version 1809 and newer
|
||||
* the Qt 5 based version should still work on older versions down to Windows 7 although this is not regularly checked
|
||||
* Binaries are signed with the GPG key
|
||||
[`B9E36A7275FC61B464B67907E06FE8F53CDC6A4C`](https://keyserver.ubuntu.com/pks/lookup?search=B9E36A7275FC61B464B67907E06FE8F53CDC6A4C&fingerprint=on&op=index).
|
||||
* for mingw-w64 PKGBUILDs checkout [my GitHub repository](https://github.com/Martchus/PKGBUILDs)
|
||||
* for statically linked binaries checkout the [release section on GitHub](https://github.com/Martchus/tageditor/releases)
|
||||
* [my website](http://martchus.no-ip.biz/website/page.php?name=programming) also contains an occasionally
|
||||
updated archive with a dynamically linked executable
|
||||
|
||||
## Build instructions
|
||||
The Password Manager depends on c++utilities and passwordfile. Checkout the README of c++utilities for more details. Note that this project is not built differently than any other CMake project.
|
||||
The application depends on [c++utilities](https://github.com/Martchus/cpp-utilities) and
|
||||
[passwordfile](https://github.com/Martchus/passwordfile) and is built the same way as these libraries.
|
||||
For basic instructions checkout the README file of [c++utilities](https://github.com/Martchus/cpp-utilities).
|
||||
When the Qt GUI is enabled, Qt and [qtutilities](https://github.com/Martchus/qtutilities) are required, too.
|
||||
|
||||
To avoid building c++utilities/passwordfile/qtutilities separately, follow the instructions under
|
||||
"Building this straight". There's also documentation about
|
||||
[various build variables](https://github.com/Martchus/cpp-utilities/blob/master/doc/buildvariables.md) which
|
||||
can be passed to CMake to influence the build.
|
||||
|
||||
### Optional dependencies
|
||||
* When building any Qt GUI, the library qtutilities is required.
|
||||
* When building with Qt Widgets GUI support, the following Qt modules are required (version 5.6 or higher): core gui widgets
|
||||
* When building with support for the experimental Qt Quick GUI, the following Qt/KDE modules are required (version 5.12 or higher): core gui qml quick quickcontrols2 kirigami
|
||||
* When building with support for the experimental Qt Quick GUI, the following Qt/KDE modules are required (version 6.6 or higher): core gui qml quick quickcontrols2 kirigami
|
||||
|
||||
To specify the major Qt version to use, set `QT_PACKAGE_PREFIX` (e.g. add `-DQT_PACKAGE_PREFIX:STRING=Qt6`
|
||||
to the CMake arguments). There's also `KF_PACKAGE_PREFIX` for KDE dependencies. Note that the Qt Quick GUI
|
||||
always requires the same major Qt version as your KDE modules use.
|
||||
|
||||
### Building this straight
|
||||
1. Install (preferably the latest version of) GCC or Clang, the required Qt modules, CMake and Ninja/Make. OpenSSL, iconv and
|
||||
zlib are required as well but likely already installed.
|
||||
2. Get the sources of additional dependencies and the password manager itself. For the lastest version from Git clone the following repositories:
|
||||
0. Install (preferably the latest version of) the GCC toolchain or Clang, the required Qt modules, OpenSSL, iconv,
|
||||
zlib, CMake and Ninja.
|
||||
1. Get the sources of additional dependencies and the password manager itself. For the latest version from Git clone the following repositories:
|
||||
```
|
||||
cd $SOURCES
|
||||
cd "$SOURCES"
|
||||
git clone https://github.com/Martchus/cpp-utilities.git c++utilities
|
||||
git clone https://github.com/Martchus/passwordfile.git
|
||||
git clone https://github.com/Martchus/qtutilities.git # only required for Qt GUI
|
||||
git clone https://github.com/Martchus/passwordmanager.git
|
||||
git clone https://github.com/Martchus/subdirs.git
|
||||
```
|
||||
3. Build and install everything in one step:
|
||||
2. Build and install everything in one step:
|
||||
```
|
||||
cd $BUILD_DIR
|
||||
cd "$BUILD_DIR"
|
||||
cmake \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="/install/prefix" \
|
||||
$SOURCES/subdirs/passwordmanager
|
||||
"$SOURCES/subdirs/passwordmanager"
|
||||
ninja install
|
||||
```
|
||||
* If the install directory is not writable, do **not** conduct the build as root. Instead, set `DESTDIR` to a
|
||||
writable location (e.g. `DESTDIR="temporary/install/dir" ninja install`) and move the files from there to
|
||||
the desired location afterwards.
|
||||
|
||||
#### Concrete example of 3. for building an Android APK under Arch Linux
|
||||
Create stuff for signing the package (remove `-DANDROID_APK_FORCE_DEBUG=ON` line in the CMake invocation to actually use this):
|
||||
Create a key for signing the package (always required; otherwise the APK file won't install):
|
||||
```
|
||||
# locate keystore
|
||||
keystore_dir=/path/to/keystore-dir
|
||||
keystore_alias=$USER
|
||||
keystore_url=$keystore_dir/$keystore_alias
|
||||
|
||||
# make up some password to protect the store; enter this on keytool invocation
|
||||
keystore_password=<password>
|
||||
# set variables for creating keystore and androiddeployqt to find it
|
||||
export QT_ANDROID_KEYSTORE_PATH=/path/to/keystore-dir QT_ANDROID_KEYSTORE_ALIAS=$USER-devel QT_ANDROID_KEYSTORE_STORE_PASS=$USER-devel QT_ANDROID_KEYSTORE_KEY_PASS=$USER-devel
|
||||
|
||||
# create keystore (do only once)
|
||||
pushd "$keystore_dir"
|
||||
keytool -genkey -v -keystore "$keystore_alias" -alias "$keystore_alias" -keyalg RSA -keysize 2048 -validity 10000
|
||||
mkdir -p "${QT_ANDROID_KEYSTORE_PATH%/*}"
|
||||
pushd "${QT_ANDROID_KEYSTORE_PATH%/*}"
|
||||
keytool -genkey -v -keystore "$QT_ANDROID_KEYSTORE_ALIAS" -alias "$QT_ANDROID_KEYSTORE_ALIAS" -keyalg RSA -keysize 2048 -validity 10000
|
||||
popd
|
||||
```
|
||||
|
||||
Build c++utilities, passwordfile, qtutilities and passwordmanager in one step to create an Android APK for arm64-v8a:
|
||||
Build c++utilities, passwordfile, qtutilities and passwordmanager in one step to create an Android APK for aarch64:
|
||||
|
||||
```
|
||||
# specify Android platform
|
||||
_pkg_arch=aarch64
|
||||
_android_arch=arm64-v8a
|
||||
_android_arch2=arm64
|
||||
_android_api_level=22
|
||||
# use Java 17 (the latest Java doesn't work at this point) and avoid unwanted Java options
|
||||
export PATH=/usr/lib/jvm/java-17-openjdk/bin:$PATH
|
||||
export _JAVA_OPTIONS=
|
||||
|
||||
# set project name
|
||||
_reponame=passwordmanager
|
||||
_pkgname=passwordmanager
|
||||
# configure and build using helpers from android-cmake package
|
||||
android_arch=aarch64
|
||||
build_dir=$BUILD_DIR/../manual/passwordmanager-android-$android_arch-release
|
||||
source /usr/bin/android-env $android_arch
|
||||
android-$android_arch-cmake -G Ninja -S . -B "$build_dir" \
|
||||
-DCMAKE_FIND_ROOT_PATH="${ANDROID_PREFIX}" -DANDROID_SDK_ROOT="${ANDROID_HOME}" \
|
||||
-DPKG_CONFIG_EXECUTABLE:FILEPATH=/usr/bin/android-$android_arch-pkg-config \
|
||||
-DBUILTIN_ICON_THEMES='breeze;breeze-dark' -DBUILTIN_TRANSLATIONS=ON \
|
||||
-DQT_PACKAGE_PREFIX:STRING=Qt6 -DKF_PACKAGE_PREFIX:STRING=KF6
|
||||
cmake --build "$build_dir"
|
||||
|
||||
# locate SDK, NDK and further libraries
|
||||
android_sdk_root=${ANDROID_SDK_ROOT:-/opt/android-sdk}
|
||||
android_ndk_root=${ANDROID_NDK_ROOT:-/opt/android-ndk}
|
||||
build_tools_version=$(pacman -Q android-sdk-build-tools | sed 's/.* r\(.*\)-.*/\1/')
|
||||
other_libs_root=/opt/android-libs/$_pkg_arch
|
||||
other_libs_include=$other_libs_root/include
|
||||
root="$android_ndk_root/sysroot;$other_libs_root"
|
||||
|
||||
# use Java 8 which seems to be the latest version which works
|
||||
export PATH=/usr/lib/jvm/java-8-openjdk/jre/bin/:$PATH
|
||||
|
||||
# configure with the toolchain file provided by the Android NDK (still WIP)
|
||||
# note: This configuration is likely required in the future to resolve https://gitlab.kitware.com/cmake/cmake/issues/18739. But for now
|
||||
# better keep using CMake's internal Android support because this config has its own pitfalls (see CMAKE_CXX_FLAGS).
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DANDROID_ABI=$_android_arch \
|
||||
-DANDROID_PLATFORM=$_android_api_level \
|
||||
-DCMAKE_TOOLCHAIN_FILE=$android_ndk_root/build/cmake/android.toolchain.cmake \
|
||||
-DCMAKE_SYSTEM_NAME=Android \
|
||||
-DCMAKE_SYSTEM_VERSION=$_android_api_level \
|
||||
-DCMAKE_ANDROID_ARCH_ABI=$_android_arch \
|
||||
-DCMAKE_ANDROID_NDK="$android_ndk_root" \
|
||||
-DCMAKE_ANDROID_SDK="$android_sdk_root" \
|
||||
-DCMAKE_ANDROID_STL_TYPE=c++_shared \
|
||||
-DCMAKE_INSTALL_PREFIX=$other_libs_root \
|
||||
-DCMAKE_PREFIX_PATH="$root" \
|
||||
-DCMAKE_FIND_ROOT_PATH="$root;$root/libs" \
|
||||
-DCMAKE_CXX_FLAGS="-include $android_ndk_root/sysroot/usr/include/math.h -include $android_ndk_root/sources/cxx-stl/llvm-libc++/include/math.h -I$other_libs_include" \
|
||||
-DBUILD_SHARED_LIBS=ON \
|
||||
-DZLIB_LIBRARY="$android_ndk_root/platforms/android-$_android_api_level/arch-$_android_arch2/usr/lib/libz.so" \
|
||||
-DCLANG_FORMAT_ENABLED=ON \
|
||||
-DUSE_NATIVE_FILE_BUFFER=ON \
|
||||
-DUSE_STANDARD_FILESYSTEM=OFF \
|
||||
-DNO_DOXYGEN=ON \
|
||||
-DWIDGETS_GUI=OFF \
|
||||
-DQUICK_GUI=ON \
|
||||
-DBUILTIN_ICON_THEMES=breeze \
|
||||
-DBUILTIN_TRANSLATIONS=ON \
|
||||
-DANDROID_APK_TOOLCHAIN_VERSION=4.9 \
|
||||
-DANDROID_APK_CXX_STANDARD_LIBRARY="$android_ndk_root/platforms/android-$_android_api_level/arch-$_android_arch2/usr/lib/libstdc++.so" \
|
||||
-DANDROID_APK_FORCE_DEBUG=ON \
|
||||
-DANDROID_APK_KEYSTORE_URL="$keystore_url" \
|
||||
-DANDROID_APK_KEYSTORE_ALIAS="$keystore_alias" \
|
||||
-DANDROID_APK_KEYSTORE_PASSWORD="$keystore_password" \
|
||||
-DANDROID_APK_APPLICATION_ID_SUFFIX=".unstable" \
|
||||
-DANDROID_APK_APPLICATION_LABEL="Password Manager (unstable)" \
|
||||
$SOURCES/subdirs/$_reponame
|
||||
|
||||
# configure with CMake's internal Android support
|
||||
# note: Requires workaround with Android NDK r19: https://gitlab.kitware.com/cmake/cmake/issues/18739#note_498676
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_SYSTEM_NAME=Android \
|
||||
-DCMAKE_SYSTEM_VERSION=$_android_api_level \
|
||||
-DCMAKE_ANDROID_ARCH_ABI=$_android_arch \
|
||||
-DCMAKE_ANDROID_NDK="$android_ndk_root" \
|
||||
-DCMAKE_ANDROID_SDK="$android_sdk_root" \
|
||||
-DCMAKE_ANDROID_STL_TYPE=c++_shared \
|
||||
-DCMAKE_INSTALL_PREFIX=$other_libs_root \
|
||||
-DCMAKE_PREFIX_PATH="$root" \
|
||||
-DCMAKE_FIND_ROOT_PATH="$root;$root/libs" \
|
||||
-DCMAKE_CXX_FLAGS="-D__ANDROID_API__=$_android_api_level" \
|
||||
-DCLANG_FORMAT_ENABLED=ON \
|
||||
-DBUILD_SHARED_LIBS=ON \
|
||||
-DUSE_NATIVE_FILE_BUFFER=ON \
|
||||
-DUSE_STANDARD_FILESYSTEM=OFF \
|
||||
-DNO_DOXYGEN=ON \
|
||||
-DWIDGETS_GUI=OFF \
|
||||
-DQUICK_GUI=ON \
|
||||
-DBUILTIN_ICON_THEMES=breeze \
|
||||
-DBUILTIN_TRANSLATIONS=ON \
|
||||
-DANDROID_APK_FORCE_DEBUG=ON \
|
||||
-DANDROID_APK_KEYSTORE_URL="$keystore_url" \
|
||||
-DANDROID_APK_KEYSTORE_ALIAS="$keystore_alias" \
|
||||
-DANDROID_APK_KEYSTORE_PASSWORD="$keystore_password" \
|
||||
-DANDROID_APK_APPLICATION_ID_SUFFIX=".unstable" \
|
||||
-DANDROID_APK_APPLICATION_LABEL="Password Manager (unstable)" \
|
||||
$SOURCES/subdirs/$_reponame
|
||||
|
||||
# build all binaries and make APK file using all CPU cores
|
||||
make passwordmanager_apk -j$(nproc)
|
||||
|
||||
# install app on USB-connected phone
|
||||
make passwordmanager_deploy_apk
|
||||
# install the app
|
||||
adb install "$build_dir/passwordmanager/android-build//build/outputs/apk/release/android-build-release-signed.apk"
|
||||
```
|
||||
|
||||
##### Notes
|
||||
* The Android packages for the dependencies Qt, iconv, OpenSSL and Kirigami 2 are provided in
|
||||
* The Android packages for the dependencies Boost, Qt, iconv, OpenSSL and Kirigami are provided in
|
||||
my [PKGBUILDs](http://github.com/Martchus/PKGBUILDs) repo.
|
||||
* The lastest Java I was able to use was version 8 (`jdk8-openjdk` package).
|
||||
|
||||
### Manual deployment of Android APK file
|
||||
1. Find device ID: `adb devices`
|
||||
2. Install App on phone: `adb -s <DEVICE_ID> install -r $BUILD_DIR/passwordmanager_build_apk/build/outputs/apk/passwordmanager_build_apk-debug.apk`
|
||||
3. View log: `adb -s <DEVICE_ID> logcat`
|
||||
* The latest Java I was able to use was version 17.
|
||||
* Use `QT_QUICK_CONTROLS_STYLE=Material` and `QT_QUICK_CONTROLS_MOBILE=1` to test the Qt Quick GUI like it would be shown under
|
||||
Android via a normal desktop build.
|
||||
|
||||
### Building without Qt GUI
|
||||
It is possible to build without the GUI if only the CLI is needed. In this case no Qt dependencies (including qtutilities) are required.
|
||||
|
@ -225,3 +172,8 @@ To build without GUI, add the following parameters to the CMake call:
|
|||
```
|
||||
-DWIDGETS_GUI=OFF -DQUICK_GUI=OFF
|
||||
```
|
||||
|
||||
## Copyright notice and license
|
||||
Copyright © 2015-2024 Marius Kittler
|
||||
|
||||
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.view.Window;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
import android.support.v4.provider.DocumentFile;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import java.io.FileNotFoundException;
|
||||
import org.qtproject.qt5.android.bindings.QtActivity;
|
||||
import org.qtproject.qt.android.bindings.QtActivity;
|
||||
|
||||
public class Activity extends QtActivity {
|
||||
private final int REQUEST_CODE_OPEN_EXISTING_FILE = 1;
|
||||
|
|
112
cli/cli.cpp
112
cli/cli.cpp
|
@ -5,6 +5,7 @@
|
|||
#include <passwordfile/io/field.h>
|
||||
#include <passwordfile/io/parsingexception.h>
|
||||
#include <passwordfile/io/passwordfile.h>
|
||||
#include <passwordfile/util/openssl.h>
|
||||
|
||||
#include <c++utilities/application/commandlineutils.h>
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
|
@ -15,7 +16,9 @@
|
|||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace std;
|
||||
using namespace std::placeholders;
|
||||
|
@ -89,32 +92,39 @@ InteractiveCli::InteractiveCli()
|
|||
, m_modified(false)
|
||||
, m_quit(false)
|
||||
{
|
||||
Util::OpenSsl::init();
|
||||
CMD_UTILS_START_CONSOLE;
|
||||
}
|
||||
|
||||
void InteractiveCli::run(const string &file)
|
||||
InteractiveCli::~InteractiveCli()
|
||||
{
|
||||
Util::OpenSsl::clean();
|
||||
}
|
||||
|
||||
int InteractiveCli::run(string_view file)
|
||||
{
|
||||
if (!file.empty()) {
|
||||
openFile(file, PasswordFileOpenFlags::Default);
|
||||
}
|
||||
string input;
|
||||
auto input = std::string();
|
||||
while (!m_quit) {
|
||||
getline(m_i, input);
|
||||
std::getline(m_i, input);
|
||||
if (!input.empty()) {
|
||||
processCommand(input);
|
||||
}
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
void InteractiveCli::processCommand(const string &cmd)
|
||||
void InteractiveCli::processCommand(const std::string &cmd)
|
||||
{
|
||||
#define CMD(value) !paramMissing && cmd == value
|
||||
#define CMD(value) !paramMissing &&cmd == value
|
||||
#define CMD2(value1, value2) !paramMissing && (cmd == value1 || cmd == value2)
|
||||
#define CMD_P(value) !paramMissing && checkCommand(cmd, value, param, paramMissing)
|
||||
#define CMD_P(value) !paramMissing &&checkCommand(cmd, value, param, paramMissing)
|
||||
#define CMD2_P(value1, value2) !paramMissing && (checkCommand(cmd, value1, param, paramMissing) || checkCommand(cmd, value2, param, paramMissing))
|
||||
|
||||
string param;
|
||||
bool paramMissing = false;
|
||||
auto param = std::string();
|
||||
auto paramMissing = false;
|
||||
if (CMD2("quit", "q")) {
|
||||
quit();
|
||||
} else if (CMD("q!")) {
|
||||
|
@ -175,9 +185,9 @@ void InteractiveCli::processCommand(const string &cmd)
|
|||
}
|
||||
}
|
||||
|
||||
Entry *InteractiveCli::resolvePath(const string &path)
|
||||
Entry *InteractiveCli::resolvePath(const std::string &path)
|
||||
{
|
||||
auto parts = splitString<vector<string>>(path, "/", EmptyPartsTreat::Merge);
|
||||
auto parts = splitString<std::vector<std::string>>(path, "/", EmptyPartsTreat::Merge);
|
||||
bool fromRoot = path.at(0) == '/';
|
||||
if (fromRoot && parts.empty()) {
|
||||
return m_file.rootEntry();
|
||||
|
@ -213,7 +223,7 @@ Entry *InteractiveCli::resolvePath(const string &path)
|
|||
}
|
||||
}
|
||||
|
||||
bool InteractiveCli::checkCommand(const string &str, const char *phrase, std::string ¶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) {
|
||||
if (*phrase == 0) {
|
||||
|
@ -234,13 +244,13 @@ bool InteractiveCli::checkCommand(const string &str, const char *phrase, std::st
|
|||
return false;
|
||||
}
|
||||
|
||||
void InteractiveCli::openFile(const string &file, PasswordFileOpenFlags openFlags)
|
||||
void InteractiveCli::openFile(std::string_view file, PasswordFileOpenFlags openFlags)
|
||||
{
|
||||
if (m_file.isOpen()) {
|
||||
m_o << "file \"" << m_file.path() << "\" currently open; close first" << endl;
|
||||
return;
|
||||
}
|
||||
m_file.setPath(file);
|
||||
m_file.setPath(std::string(file));
|
||||
for (;;) {
|
||||
try {
|
||||
try {
|
||||
|
@ -252,13 +262,13 @@ void InteractiveCli::openFile(const string &file, PasswordFileOpenFlags openFlag
|
|||
m_currentEntry = m_file.rootEntry();
|
||||
m_o << "file \"" << file << "\" opened" << endl;
|
||||
} catch (const ParsingException &) {
|
||||
m_o << "error occured when parsing file \"" << file << "\"" << endl;
|
||||
m_o << "error occurred when parsing file \"" << file << "\"" << endl;
|
||||
throw;
|
||||
} catch (const CryptoException &) {
|
||||
m_o << "error occured when decrypting file \"" << file << "\"" << endl;
|
||||
m_o << "error occurred when decrypting file \"" << file << "\"" << endl;
|
||||
throw;
|
||||
} catch (const std::ios_base::failure &) {
|
||||
m_o << "IO error occured when opening file \"" << file << "\"" << endl;
|
||||
m_o << "IO error occurred when opening file \"" << file << "\"" << endl;
|
||||
throw;
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
|
@ -303,13 +313,13 @@ void InteractiveCli::saveFile()
|
|||
m_file.save(flags);
|
||||
m_o << "file \"" << m_file.path() << "\" saved" << endl;
|
||||
} catch (const ParsingException &) {
|
||||
m_o << "error occured when parsing file \"" << m_file.path() << "\"" << endl;
|
||||
m_o << "error occurred when parsing file \"" << m_file.path() << "\"" << endl;
|
||||
throw;
|
||||
} catch (const CryptoException &) {
|
||||
m_o << "error occured when encrypting file \"" << m_file.path() << "\"" << endl;
|
||||
m_o << "error occurred when encrypting file \"" << m_file.path() << "\"" << endl;
|
||||
throw;
|
||||
} catch (const std::ios_base::failure &) {
|
||||
m_o << "IO error occured when saving file \"" << m_file.path() << "\"" << endl;
|
||||
m_o << "IO error occurred when saving file \"" << m_file.path() << "\"" << endl;
|
||||
throw;
|
||||
}
|
||||
} catch (const exception &e) {
|
||||
|
@ -322,7 +332,7 @@ void InteractiveCli::saveFile()
|
|||
m_modified = false;
|
||||
}
|
||||
|
||||
void InteractiveCli::createFile(const string &file)
|
||||
void InteractiveCli::createFile(const std::string &file)
|
||||
{
|
||||
if (m_file.isOpen()) {
|
||||
m_o << "file \"" << m_file.path() << "\" currently open; close first" << endl;
|
||||
|
@ -337,7 +347,7 @@ void InteractiveCli::createFile(const string &file)
|
|||
m_currentEntry = m_file.rootEntry();
|
||||
m_o << "file \"" << file << "\" created and opened" << endl;
|
||||
} catch (const std::ios_base::failure &) {
|
||||
m_o << "IO error occured when creating file \"" << file << "\"" << endl;
|
||||
m_o << "IO error occurred when creating file \"" << file << "\"" << endl;
|
||||
throw;
|
||||
}
|
||||
} catch (const exception &e) {
|
||||
|
@ -392,7 +402,7 @@ void InteractiveCli::pwd()
|
|||
m_o << joinStrings(path, "/") << endl;
|
||||
}
|
||||
|
||||
void InteractiveCli::cd(const string &path)
|
||||
void InteractiveCli::cd(const std::string &path)
|
||||
{
|
||||
if (!m_file.isOpen()) {
|
||||
m_o << "can not change directory; no file open" << endl;
|
||||
|
@ -407,7 +417,7 @@ void InteractiveCli::cd(const string &path)
|
|||
void InteractiveCli::ls()
|
||||
{
|
||||
if (!m_file.isOpen()) {
|
||||
m_o << "can not list any entires; no file open" << endl;
|
||||
m_o << "can not list any entries; no file open" << endl;
|
||||
return;
|
||||
}
|
||||
switch (m_currentEntry->type()) {
|
||||
|
@ -450,7 +460,7 @@ void InteractiveCli::tree()
|
|||
printEntries(m_currentEntry, 0);
|
||||
}
|
||||
|
||||
void InteractiveCli::makeEntry(EntryType entryType, const string &label)
|
||||
void InteractiveCli::makeEntry(EntryType entryType, const std::string &label)
|
||||
{
|
||||
if (!m_file.isOpen()) {
|
||||
m_o << "can not make entry; no file open" << endl;
|
||||
|
@ -473,7 +483,7 @@ void InteractiveCli::makeEntry(EntryType entryType, const string &label)
|
|||
}
|
||||
}
|
||||
|
||||
void InteractiveCli::removeEntry(const string &path)
|
||||
void InteractiveCli::removeEntry(const std::string &path)
|
||||
{
|
||||
if (!m_file.isOpen()) {
|
||||
m_o << "can not remove entry; no file open" << endl;
|
||||
|
@ -493,16 +503,16 @@ void InteractiveCli::removeEntry(const string &path)
|
|||
}
|
||||
}
|
||||
|
||||
void InteractiveCli::renameEntry(const string &path)
|
||||
void InteractiveCli::renameEntry(const std::string &path)
|
||||
{
|
||||
if (!m_file.isOpen()) {
|
||||
m_o << "can not rename entry; no file open" << endl;
|
||||
return;
|
||||
}
|
||||
if (Entry *entry = resolvePath(path)) {
|
||||
string label;
|
||||
auto label = std::string();
|
||||
m_o << "enter new name: " << endl;
|
||||
getline(m_i, label);
|
||||
std::getline(m_i, label);
|
||||
if (label.empty()) {
|
||||
m_o << "can not rename; new name is empty" << endl;
|
||||
} else {
|
||||
|
@ -513,16 +523,16 @@ void InteractiveCli::renameEntry(const string &path)
|
|||
}
|
||||
}
|
||||
|
||||
void InteractiveCli::moveEntry(const string &path)
|
||||
void InteractiveCli::moveEntry(const std::string &path)
|
||||
{
|
||||
if (!m_file.isOpen()) {
|
||||
m_o << "can not rename entry; no file open" << endl;
|
||||
return;
|
||||
}
|
||||
if (Entry *entry = resolvePath(path)) {
|
||||
string newParentPath;
|
||||
auto newParentPath = std::string();
|
||||
m_o << "enter path of new parent: " << endl;
|
||||
getline(m_i, newParentPath);
|
||||
std::getline(m_i, newParentPath);
|
||||
if (newParentPath.empty()) {
|
||||
m_o << "can not move; path of new parent is empty" << endl;
|
||||
} else {
|
||||
|
@ -547,7 +557,7 @@ void InteractiveCli::moveEntry(const string &path)
|
|||
}
|
||||
}
|
||||
|
||||
void InteractiveCli::readField(const string &fieldName)
|
||||
void InteractiveCli::readField(const std::string &fieldName)
|
||||
{
|
||||
if (!m_file.isOpen()) {
|
||||
m_o << "can not read field; no file open" << endl;
|
||||
|
@ -571,7 +581,7 @@ void InteractiveCli::readField(const string &fieldName)
|
|||
}
|
||||
}
|
||||
|
||||
void InteractiveCli::setField(bool useMuter, const string &fieldName)
|
||||
void InteractiveCli::setField(bool useMuter, const std::string &fieldName)
|
||||
{
|
||||
if (!m_file.isOpen()) {
|
||||
m_o << "can not set field; no file open" << endl;
|
||||
|
@ -581,22 +591,22 @@ void InteractiveCli::setField(bool useMuter, const string &fieldName)
|
|||
m_o << "can not set field; current entry is no account entry" << endl;
|
||||
return;
|
||||
}
|
||||
vector<Field> &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
|
||||
unsigned int valuesFound = 0;
|
||||
string value;
|
||||
auto &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
|
||||
auto valuesFound = unsigned();
|
||||
auto value = std::string();
|
||||
m_o << "enter new value: ";
|
||||
if (useMuter) {
|
||||
InputMuter m;
|
||||
getline(m_i, value);
|
||||
std::getline(m_i, value);
|
||||
m_o << endl << "repeat: ";
|
||||
string repeat;
|
||||
getline(m_i, repeat);
|
||||
auto repeat = std::string();
|
||||
std::getline(m_i, repeat);
|
||||
if (value != repeat) {
|
||||
m_o << "values do not match; field has not been altered" << endl;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
getline(m_i, value);
|
||||
std::getline(m_i, value);
|
||||
}
|
||||
for (Field &field : fields) {
|
||||
if (field.name() == fieldName) {
|
||||
|
@ -643,7 +653,7 @@ void InteractiveCli::setField(bool useMuter, const string &fieldName)
|
|||
}
|
||||
}
|
||||
|
||||
void InteractiveCli::removeField(const string &fieldName)
|
||||
void InteractiveCli::removeField(const std::string &fieldName)
|
||||
{
|
||||
if (!m_file.isOpen()) {
|
||||
m_o << "can not remove field; no file open" << endl;
|
||||
|
@ -653,8 +663,8 @@ void InteractiveCli::removeField(const string &fieldName)
|
|||
m_o << "can not remove field; current entry is no account entry" << endl;
|
||||
return;
|
||||
}
|
||||
vector<Field> &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
|
||||
unsigned int valuesFound = 0;
|
||||
auto &fields = static_cast<AccountEntry *>(m_currentEntry)->fields();
|
||||
auto valuesFound = unsigned();
|
||||
for (const Field &field : fields) {
|
||||
if (field.name() == fieldName) {
|
||||
++valuesFound;
|
||||
|
@ -730,7 +740,7 @@ void InteractiveCli::quit()
|
|||
}
|
||||
}
|
||||
|
||||
string InteractiveCli::askForPassphrase(bool confirm)
|
||||
std::string InteractiveCli::askForPassphrase(bool confirm)
|
||||
{
|
||||
if (confirm) {
|
||||
m_o << "enter new passphrase: ";
|
||||
|
@ -738,10 +748,10 @@ string InteractiveCli::askForPassphrase(bool confirm)
|
|||
m_o << "enter passphrase: ";
|
||||
}
|
||||
m_o.flush();
|
||||
string input1;
|
||||
auto input1 = std::string();
|
||||
{
|
||||
InputMuter m;
|
||||
getline(m_i, input1);
|
||||
auto m = InputMuter();
|
||||
std::getline(m_i, input1);
|
||||
}
|
||||
m_o << endl;
|
||||
if (input1.empty()) {
|
||||
|
@ -751,15 +761,15 @@ string InteractiveCli::askForPassphrase(bool confirm)
|
|||
if (confirm) {
|
||||
m_o << "confirm new passphrase: ";
|
||||
m_o.flush();
|
||||
string input2;
|
||||
auto input2 = std::string();
|
||||
{
|
||||
InputMuter m;
|
||||
getline(m_i, input2);
|
||||
auto m = InputMuter();
|
||||
std::getline(m_i, input2);
|
||||
}
|
||||
m_o << endl;
|
||||
if (input1 != input2) {
|
||||
m_o << "phrases do not match" << endl;
|
||||
throw runtime_error("confirmation failed");
|
||||
throw std::runtime_error("confirmation failed");
|
||||
}
|
||||
}
|
||||
return input1;
|
||||
|
|
11
cli/cli.h
11
cli/cli.h
|
@ -12,7 +12,7 @@
|
|||
#include <istream>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <string_view>
|
||||
|
||||
namespace Io {
|
||||
class Entry;
|
||||
|
@ -23,7 +23,7 @@ namespace Cli {
|
|||
|
||||
class InputMuter {
|
||||
public:
|
||||
InputMuter();
|
||||
explicit InputMuter();
|
||||
~InputMuter();
|
||||
|
||||
private:
|
||||
|
@ -39,9 +39,10 @@ void clearConsole();
|
|||
|
||||
class InteractiveCli {
|
||||
public:
|
||||
InteractiveCli();
|
||||
void run(const std::string &file = std::string());
|
||||
void openFile(const std::string &file, Io::PasswordFileOpenFlags openFlags);
|
||||
explicit InteractiveCli();
|
||||
~InteractiveCli();
|
||||
int run(std::string_view file);
|
||||
void openFile(std::string_view file, Io::PasswordFileOpenFlags openFlags);
|
||||
void closeFile();
|
||||
void saveFile();
|
||||
void createFile(const std::string &file);
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace QtGui {
|
|||
|
||||
class FieldDelegate : public QStyledItemDelegate {
|
||||
public:
|
||||
FieldDelegate(QObject *parent = nullptr);
|
||||
explicit FieldDelegate(QObject *parent = nullptr);
|
||||
|
||||
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
|
||||
};
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
#include "resources/config.h"
|
||||
#include "resources/qtconfig.h"
|
||||
|
||||
#include <QApplication> // ensure QGuiApplication is defined before resources.h for desktop file name
|
||||
|
||||
#include <qtutilities/resources/importplugin.h>
|
||||
#include <qtutilities/resources/qtconfigarguments.h>
|
||||
#include <qtutilities/resources/resources.h>
|
||||
|
@ -12,8 +14,8 @@
|
|||
|
||||
#include <passwordfile/util/openssl.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QSettings>
|
||||
|
||||
using namespace CppUtilities;
|
||||
|
@ -30,18 +32,15 @@ int runWidgetsGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs,
|
|||
OpenSsl::init();
|
||||
|
||||
// init application
|
||||
QApplication application(argc, argv);
|
||||
auto application = QApplication(argc, argv);
|
||||
QObject::connect(&application, &QCoreApplication::aboutToQuit, &OpenSsl::clean);
|
||||
|
||||
// restore Qt settings
|
||||
QtSettings qtSettings;
|
||||
QSettings settings(QSettings::IniFormat, QSettings::UserScope, QStringLiteral(PROJECT_NAME));
|
||||
|
||||
// move old config to new location
|
||||
const QString oldConfig
|
||||
= QSettings(QSettings::IniFormat, QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()).fileName();
|
||||
QFile::rename(oldConfig, settings.fileName()) || QFile::remove(oldConfig);
|
||||
settings.sync();
|
||||
qtSettings.restore(settings);
|
||||
auto qtSettings = QtSettings();
|
||||
auto settings = QtUtilities::getSettings(QStringLiteral(PROJECT_NAME));
|
||||
auto settingsError = QtUtilities::errorMessageForSettings(*settings);
|
||||
qtSettings.disableNotices();
|
||||
qtSettings.restore(*settings);
|
||||
qtSettings.apply();
|
||||
|
||||
// apply settings specified via command line args
|
||||
|
@ -49,14 +48,27 @@ int runWidgetsGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs,
|
|||
LOAD_QT_TRANSLATIONS;
|
||||
|
||||
// init widgets GUI
|
||||
MainWindow w(settings, &qtSettings);
|
||||
if (!settingsError.isEmpty()) {
|
||||
QMessageBox::critical(nullptr, QCoreApplication::applicationName(), settingsError);
|
||||
}
|
||||
auto w = MainWindow(*settings, &qtSettings);
|
||||
w.show();
|
||||
if (!file.isEmpty()) {
|
||||
w.openFile(file);
|
||||
}
|
||||
|
||||
// start event loop
|
||||
QObject::connect(&application, &QCoreApplication::aboutToQuit, &OpenSsl::clean);
|
||||
return application.exec();
|
||||
auto res = application.exec();
|
||||
|
||||
// save settings to disk
|
||||
settings->sync();
|
||||
if (settingsError.isEmpty()) {
|
||||
settingsError = QtUtilities::errorMessageForSettings(*settings);
|
||||
if (!settingsError.isEmpty()) {
|
||||
QMessageBox::critical(nullptr, QCoreApplication::applicationName(), settingsError);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
} // namespace QtGui
|
||||
|
|
|
@ -106,6 +106,19 @@ void MainWindow::setSomethingChanged(bool somethingChanged)
|
|||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Updates the style sheet.
|
||||
*/
|
||||
void MainWindow::updateStyleSheet()
|
||||
{
|
||||
#ifdef Q_OS_WINDOWS
|
||||
const auto p = palette();
|
||||
setStyleSheet(QStringLiteral("%1 #splitter QWidget { background-color: palette(base); color: palette(text); } #splitter QWidget *, #splitter "
|
||||
"QWidget * { background-color: none; } #leftWidget { border-right: 1px solid %2; }")
|
||||
.arg(dialogStyleForPalette(p), windowFrameColorForPalette(p).name()));
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new main window.
|
||||
*/
|
||||
|
@ -120,12 +133,9 @@ MainWindow::MainWindow(QSettings &settings, QtUtilities::QtSettings *qtSettings,
|
|||
, m_settingsDlg(nullptr)
|
||||
{
|
||||
// setup ui
|
||||
updateStyleSheet();
|
||||
m_ui->setupUi(this);
|
||||
#ifdef Q_OS_WIN32
|
||||
setStyleSheet(QStringLiteral("%1 #splitter QWidget { background-color: palette(base); color: palette(text); } #splitter QWidget *, #splitter "
|
||||
"QWidget * { background-color: none; } #leftWidget { border-right: 1px solid %2; }")
|
||||
.arg(dialogStyle(), windowFrameColor().name()));
|
||||
#endif
|
||||
|
||||
// set default values
|
||||
setSomethingChanged(false);
|
||||
m_dontUpdateSelection = false;
|
||||
|
@ -227,7 +237,7 @@ MainWindow::MainWindow(QSettings &settings, QtUtilities::QtSettings *qtSettings,
|
|||
connect(m_ui->accountFilterLineEdit, &QLineEdit::textChanged, this, &MainWindow::applyFilter);
|
||||
|
||||
// setup other controls
|
||||
m_ui->actionAlwaysCreateBackup->setChecked(settings.value(QStringLiteral("alwayscreatebackup"), false).toBool());
|
||||
m_ui->actionAlwaysCreateBackup->setChecked(settings.value(QStringLiteral("alwayscreatebackup"), true).toBool());
|
||||
m_ui->accountFilterLineEdit->setText(settings.value(QStringLiteral("accountfilter"), QString()).toString());
|
||||
m_ui->centralWidget->installEventFilter(this);
|
||||
settings.endGroup();
|
||||
|
@ -240,6 +250,17 @@ MainWindow::~MainWindow()
|
|||
{
|
||||
}
|
||||
|
||||
bool MainWindow::event(QEvent *event)
|
||||
{
|
||||
switch (event->type()) {
|
||||
case QEvent::PaletteChange:
|
||||
updateStyleSheet();
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
return QMainWindow::event(event);
|
||||
}
|
||||
|
||||
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (obj == m_undoView) {
|
||||
|
@ -332,6 +353,7 @@ void MainWindow::showSettingsDialog()
|
|||
if (m_qtSettings) {
|
||||
m_settingsDlg->setWindowTitle(tr("Qt settings"));
|
||||
m_settingsDlg->setSingleCategory(m_qtSettings->category());
|
||||
connect(m_settingsDlg, &SettingsDialog::applied, this, [this] { m_qtSettings->apply(); });
|
||||
}
|
||||
}
|
||||
if (m_settingsDlg->isHidden()) {
|
||||
|
@ -432,7 +454,8 @@ bool MainWindow::openFile(const QString &path, PasswordFileOpenFlags openFlags)
|
|||
}
|
||||
|
||||
// show error message
|
||||
const QString errmsg = tr("An IO error occured when opening the specified file \"%1\": %2").arg(path, QString::fromLocal8Bit(failure.what()));
|
||||
const QString errmsg
|
||||
= tr("An IO error occurred when opening the specified file \"%1\": %2").arg(path, QString::fromLocal8Bit(failure.what()));
|
||||
m_ui->statusBar->showMessage(errmsg, 5000);
|
||||
QMessageBox::critical(this, QApplication::applicationName(), errmsg);
|
||||
return false;
|
||||
|
@ -704,7 +727,7 @@ void MainWindow::insertFields(const QString &fieldsString)
|
|||
}();
|
||||
|
||||
auto row = selectedIndexes.front().row();
|
||||
m_fieldModel->insertRows(row, rowValues.size(), QModelIndex());
|
||||
m_fieldModel->insertRows(row, static_cast<int>(rowValues.size()), QModelIndex());
|
||||
|
||||
for (const auto &rowValue : rowValues) {
|
||||
int col = initCol;
|
||||
|
@ -827,7 +850,7 @@ bool MainWindow::saveFile()
|
|||
try {
|
||||
m_file.doBackup();
|
||||
} catch (const std::ios_base::failure &failure) {
|
||||
const QString message(tr("An IO error occured when making the backup file: %1").arg(QString::fromLocal8Bit(failure.what())));
|
||||
const QString message(tr("An IO error occurred when making the backup file: %1").arg(QString::fromLocal8Bit(failure.what())));
|
||||
QMessageBox::critical(this, QApplication::applicationName(), message);
|
||||
m_ui->statusBar->showMessage(message, 7000);
|
||||
return false;
|
||||
|
|
|
@ -50,6 +50,8 @@ public:
|
|||
explicit MainWindow(QSettings &settings, QtUtilities::QtSettings *qtSettings = nullptr, QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
QString selectedFieldsString() const;
|
||||
|
||||
public Q_SLOTS:
|
||||
// file management
|
||||
bool openFile(const QString &path);
|
||||
|
@ -72,6 +74,7 @@ public:
|
|||
bool openFile(const QString &path, Io::PasswordFileOpenFlags openFlags); // can not be a slot in Qt 6 without further effort, see QTBUG-86424
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
void timerEvent(QTimerEvent *event) override;
|
||||
|
@ -93,7 +96,6 @@ private Q_SLOTS:
|
|||
void markAsNormalField();
|
||||
void setFieldType(Io::FieldType fieldType);
|
||||
void setPasswordVisibility(QAction *selectedAction);
|
||||
QString selectedFieldsString() const;
|
||||
void insertFields(const QString &fieldsString);
|
||||
void copyFieldsForXMilliSeconds(int x = 5000);
|
||||
void copyFields();
|
||||
|
@ -107,6 +109,7 @@ private Q_SLOTS:
|
|||
void clearClipboard();
|
||||
void setSomethingChanged();
|
||||
void setSomethingChanged(bool somethingChanged);
|
||||
void updateStyleSheet();
|
||||
|
||||
private:
|
||||
// showing conditional messages/prompts
|
||||
|
|
|
@ -43,11 +43,8 @@ PasswordGeneratorDialog::PasswordGeneratorDialog(QWidget *parent)
|
|||
: QDialog(parent)
|
||||
, m_ui(new Ui::PasswordGeneratorDialog)
|
||||
{
|
||||
updateStyleSheet();
|
||||
m_ui->setupUi(this);
|
||||
#ifdef Q_OS_WIN32
|
||||
setStyleSheet(QStringLiteral("%1 QCommandLinkButton { font-size: 12pt; color: %2; font-weight: normal; }")
|
||||
.arg(dialogStyle(), instructionTextColor().name()));
|
||||
#endif
|
||||
setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
|
||||
|
||||
connect(m_ui->generatePassowordCommandLinkButton, &QCommandLinkButton::clicked, this, &PasswordGeneratorDialog::generateNewPassword);
|
||||
|
@ -75,6 +72,17 @@ PasswordGeneratorDialog::~PasswordGeneratorDialog()
|
|||
{
|
||||
}
|
||||
|
||||
bool PasswordGeneratorDialog::event(QEvent *event)
|
||||
{
|
||||
switch (event->type()) {
|
||||
case QEvent::PaletteChange:
|
||||
updateStyleSheet();
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
return QDialog::event(event);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Generates and shows a new password.
|
||||
*/
|
||||
|
@ -159,7 +167,18 @@ void PasswordGeneratorDialog::handleCheckedCategoriesChanged()
|
|||
*/
|
||||
void PasswordGeneratorDialog::handlePasswordChanged()
|
||||
{
|
||||
m_ui->copyPasswordCommandLinkButton->setEnabled(m_ui->passwordLineEdit->text().count() > 0);
|
||||
m_ui->copyPasswordCommandLinkButton->setEnabled(m_ui->passwordLineEdit->text().size() > 0);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Updates the style sheet.
|
||||
*/
|
||||
void PasswordGeneratorDialog::updateStyleSheet()
|
||||
{
|
||||
#ifdef Q_OS_WINDOWS
|
||||
setStyleSheet(QStringLiteral("%1 QCommandLinkButton { font-size: 12pt; color: %2; font-weight: normal; }")
|
||||
.arg(dialogStyleForPalette(palette()), instructionTextColor().name()));
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef QT_NO_CLIPBOARD
|
||||
|
|
|
@ -21,10 +21,14 @@ public:
|
|||
explicit PasswordGeneratorDialog(QWidget *parent = nullptr);
|
||||
~PasswordGeneratorDialog() override;
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void generateNewPassword();
|
||||
void handleCheckedCategoriesChanged();
|
||||
void handlePasswordChanged();
|
||||
void updateStyleSheet();
|
||||
#ifndef QT_NO_CLIPBOARD
|
||||
void copyPassword();
|
||||
#endif
|
||||
|
|
|
@ -281,22 +281,12 @@
|
|||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="closePushButton">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Segoe UI,Sans</family>
|
||||
<pointsize>9</pointsize>
|
||||
<weight>50</weight>
|
||||
<italic>false</italic>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="window-close">
|
||||
<normaloff/>
|
||||
</iconset>
|
||||
<normaloff>.</normaloff>.</iconset>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
|
@ -308,6 +298,17 @@
|
|||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>useSmallLettersCheckBox</tabstop>
|
||||
<tabstop>useCapitalLettersCheckBox</tabstop>
|
||||
<tabstop>useDigitsCheckBox</tabstop>
|
||||
<tabstop>otherCharsLineEdit</tabstop>
|
||||
<tabstop>LengthSpinBox</tabstop>
|
||||
<tabstop>passwordLineEdit</tabstop>
|
||||
<tabstop>generatePassowordCommandLinkButton</tabstop>
|
||||
<tabstop>copyPasswordCommandLinkButton</tabstop>
|
||||
<tabstop>closePushButton</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../resources/icons.qrc"/>
|
||||
</resources>
|
||||
|
|
|
@ -17,7 +17,7 @@ class StackSupport {
|
|||
friend class StackAbsorper;
|
||||
|
||||
public:
|
||||
StackSupport(QUndoStack *undoStack = nullptr);
|
||||
explicit StackSupport(QUndoStack *undoStack = nullptr);
|
||||
|
||||
protected:
|
||||
QUndoStack *undoStack();
|
||||
|
@ -68,7 +68,7 @@ inline void StackSupport::clearUndoStack()
|
|||
*/
|
||||
class StackAbsorper {
|
||||
public:
|
||||
StackAbsorper(StackSupport *supported);
|
||||
explicit StackAbsorper(StackSupport *supported);
|
||||
~StackAbsorper();
|
||||
QUndoStack *stack();
|
||||
|
||||
|
|
|
@ -62,6 +62,15 @@ void CustomUndoCommand::undo()
|
|||
* \brief Sets the value for the specified index and role in the specified field model.
|
||||
*/
|
||||
|
||||
/// \cond
|
||||
|
||||
static QString getFieldName(FieldModel *model, int row, const QModelIndex &index)
|
||||
{
|
||||
return model->index(row, 0, index.parent()).data().toString();
|
||||
}
|
||||
|
||||
/// \endcond
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new command.
|
||||
*/
|
||||
|
@ -75,7 +84,6 @@ FieldModelSetValueCommand::FieldModelSetValueCommand(FieldModel *model, const QM
|
|||
, m_oldValue(model->data(index, role))
|
||||
, m_role(role)
|
||||
{
|
||||
QString fieldName = model->index(m_row, 0, index.parent()).data().toString();
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
case Qt::EditRole:
|
||||
|
@ -88,16 +96,16 @@ FieldModelSetValueCommand::FieldModelSetValueCommand(FieldModel *model, const QM
|
|||
}
|
||||
break;
|
||||
case 1:
|
||||
if (fieldName.isEmpty()) {
|
||||
setText(QApplication::translate("undocommands", "setting value of empty field"));
|
||||
} else {
|
||||
if (const auto fieldName = getFieldName(model, m_row, index); !fieldName.isEmpty()) {
|
||||
setText(QApplication::translate("undocommands", "setting value of »%1« field").arg(fieldName));
|
||||
} else {
|
||||
setText(QApplication::translate("undocommands", "setting value of empty field"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case FieldTypeRole:
|
||||
setText(QApplication::translate("undocommands", "setting type of »%1« field").arg(fieldName));
|
||||
setText(QApplication::translate("undocommands", "setting type of »%1« field").arg(getFieldName(model, m_row, index)));
|
||||
break;
|
||||
default:
|
||||
setText(QApplication::translate("undocommands", "setting field property in row »%1«").arg(m_row + 1));
|
||||
|
@ -174,7 +182,7 @@ bool FieldModelRemoveRowsCommand::internalRedo()
|
|||
m_model->setAccountEntry(m_account);
|
||||
if (m_values.isEmpty()) {
|
||||
for (int row = m_row, end = m_row + m_count; row < end; ++row) {
|
||||
if (const Field *field = m_model->field(row)) {
|
||||
if (const Field *const field = m_model->field(static_cast<std::size_t>(row))) {
|
||||
m_values << Field(*field);
|
||||
}
|
||||
}
|
||||
|
@ -186,7 +194,7 @@ bool FieldModelRemoveRowsCommand::internalUndo()
|
|||
{
|
||||
m_model->setAccountEntry(m_account);
|
||||
bool res = m_model->insertRows(m_row, m_count, QModelIndex());
|
||||
for (int row = m_row, end = m_row + m_count, value = 0, values = m_values.size(); row < end && value < values; ++row, ++value) {
|
||||
for (int row = m_row, end = m_row + m_count, value = 0, values = static_cast<int>(m_values.size()); row < end && value < values; ++row, ++value) {
|
||||
m_model->setData(m_model->index(row, 0), QString::fromStdString(m_values.at(value).name()), Qt::EditRole);
|
||||
m_model->setData(m_model->index(row, 1), QString::fromStdString(m_values.at(value).value()), Qt::EditRole);
|
||||
m_model->setData(m_model->index(row, 0), static_cast<int>(m_values.at(value).type()), FieldTypeRole);
|
||||
|
@ -197,7 +205,7 @@ bool FieldModelRemoveRowsCommand::internalUndo()
|
|||
/*!
|
||||
* \brief Stores the entry path for the specified \a model and \a index in \a res.
|
||||
*/
|
||||
void indexToPath(EntryModel *model, const QModelIndex &index, list<string> &res)
|
||||
static void indexToPath(EntryModel *model, const QModelIndex &index, list<string> &res)
|
||||
{
|
||||
res.clear();
|
||||
if (Entry *entry = model->entry(index)) {
|
||||
|
@ -209,7 +217,7 @@ void indexToPath(EntryModel *model, const QModelIndex &index, list<string> &res)
|
|||
* \brief Fetches the entry for the specified \a model and \a path.
|
||||
* \remarks The \a path will be modified. To prevent this use entryFromPathCpy().
|
||||
*/
|
||||
Entry *entryFromPath(EntryModel *model, list<string> &path)
|
||||
static Entry *entryFromPath(EntryModel *model, list<string> &path)
|
||||
{
|
||||
if (NodeEntry *rootEntry = model->rootEntry()) {
|
||||
return rootEntry->entryByPath(path);
|
||||
|
@ -220,7 +228,7 @@ Entry *entryFromPath(EntryModel *model, list<string> &path)
|
|||
/*!
|
||||
* \brief Fetches the entry for the specified \a model and \a path.
|
||||
*/
|
||||
Entry *entryFromPathCpy(EntryModel *model, list<string> path)
|
||||
static Entry *entryFromPathCpy(EntryModel *model, list<string> path)
|
||||
{
|
||||
return entryFromPath(model, path);
|
||||
}
|
||||
|
@ -316,7 +324,7 @@ EntryModelModifyRowsCommand::~EntryModelModifyRowsCommand()
|
|||
*/
|
||||
bool EntryModelModifyRowsCommand::insert()
|
||||
{
|
||||
if (Entry *parentEntry = entryFromPathCpy(m_model, m_parentPath)) {
|
||||
if (Entry *const parentEntry = entryFromPathCpy(m_model, m_parentPath)) {
|
||||
if (m_model->insertEntries(m_row, m_model->index(parentEntry), m_values)) {
|
||||
m_values.clear();
|
||||
return true;
|
||||
|
@ -334,7 +342,7 @@ bool EntryModelModifyRowsCommand::insert()
|
|||
*/
|
||||
bool EntryModelModifyRowsCommand::remove()
|
||||
{
|
||||
if (Entry *parentEntry = entryFromPathCpy(m_model, m_parentPath)) {
|
||||
if (Entry *const parentEntry = entryFromPathCpy(m_model, m_parentPath)) {
|
||||
m_values = m_model->takeEntries(m_row, m_count, m_model->index(parentEntry));
|
||||
return !m_values.isEmpty();
|
||||
}
|
||||
|
@ -433,29 +441,28 @@ bool EntryModelMoveRowsCommand::internalRedo()
|
|||
|
||||
bool EntryModelMoveRowsCommand::internalUndo()
|
||||
{
|
||||
if (m_count) {
|
||||
Entry *sourceParentEntry = entryFromPathCpy(m_model, m_sourceParentPath);
|
||||
Entry *destParentEntry = entryFromPathCpy(m_model, m_destParentPath);
|
||||
if (sourceParentEntry && destParentEntry) {
|
||||
int sourceRow = m_destChild;
|
||||
int destChild = m_sourceRow;
|
||||
// moves whithin the same parent needs special consideration
|
||||
if (sourceParentEntry == destParentEntry) {
|
||||
// move entry down
|
||||
if (m_sourceRow < m_destChild) {
|
||||
sourceRow -= m_count;
|
||||
// move entry up
|
||||
} else if (m_sourceRow > m_destChild) {
|
||||
destChild += m_count;
|
||||
// keep entry were it is
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return m_model->moveRows(m_model->index(destParentEntry), sourceRow, m_count, m_model->index(sourceParentEntry), destChild);
|
||||
}
|
||||
return false;
|
||||
if (!m_count) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
Entry *const sourceParentEntry = entryFromPathCpy(m_model, m_sourceParentPath);
|
||||
Entry *const destParentEntry = entryFromPathCpy(m_model, m_destParentPath);
|
||||
if (sourceParentEntry && destParentEntry) {
|
||||
auto sourceRow = m_destChild, destChild = m_sourceRow;
|
||||
// moves within the same parent needs special consideration
|
||||
if (sourceParentEntry == destParentEntry) {
|
||||
// move entry down
|
||||
if (m_sourceRow < m_destChild) {
|
||||
sourceRow -= m_count;
|
||||
// move entry up
|
||||
} else if (m_sourceRow > m_destChild) {
|
||||
destChild += m_count;
|
||||
// keep entry were it is
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return m_model->moveRows(m_model->index(destParentEntry), sourceRow, m_count, m_model->index(sourceParentEntry), destChild);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace QtGui
|
||||
|
|
89
main.cpp
89
main.cpp
|
@ -1,4 +1,5 @@
|
|||
#include "./cli/cli.h"
|
||||
|
||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
||||
#include "./gui/initiategui.h"
|
||||
#endif
|
||||
|
@ -9,13 +10,12 @@
|
|||
#include "resources/config.h"
|
||||
#include "resources/qtconfig.h"
|
||||
|
||||
#include <passwordfile/util/openssl.h>
|
||||
|
||||
#include <c++utilities/application/argumentparser.h>
|
||||
#include <c++utilities/application/commandlineutils.h>
|
||||
#include <c++utilities/misc/parseerror.h>
|
||||
|
||||
#if defined(PASSWORD_MANAGER_GUI_QTWIDGETS) || defined(PASSWORD_MANAGER_GUI_QTQUICK)
|
||||
#define PASSWORD_MANAGER_GUI_QTWIDGETS_OR_QTQUICK
|
||||
#include <QCoreApplication>
|
||||
#include <QString>
|
||||
#include <qtutilities/resources/qtconfigarguments.h>
|
||||
|
@ -24,33 +24,39 @@ ENABLE_QT_RESOURCES_OF_STATIC_DEPENDENCIES
|
|||
#include <c++utilities/application/fakeqtconfigarguments.h>
|
||||
#endif
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
// force (preferably Qt Quick) GUI under Android
|
||||
#ifdef Q_OS_ANDROID
|
||||
#if defined(PASSWORD_MANAGER_GUI_QTWIDGETS) || defined(PASSWORD_MANAGER_GUI_QTQUICK)
|
||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS_OR_QTQUICK
|
||||
#define PASSWORD_MANAGER_FORCE_GUI
|
||||
#else
|
||||
#error "Must build at least one kind of GUI under Android."
|
||||
#error "Must configure building at least one kind of GUI under Android."
|
||||
#endif
|
||||
#endif
|
||||
|
||||
using namespace std;
|
||||
using namespace CppUtilities;
|
||||
using namespace Util;
|
||||
|
||||
#ifndef PASSWORD_MANAGER_FORCE_GUI
|
||||
static int fail(std::string_view error)
|
||||
{
|
||||
CMD_UTILS_START_CONSOLE;
|
||||
std::cerr << error << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
CMD_UTILS_CONVERT_ARGS_TO_UTF8;
|
||||
SET_APPLICATION_INFO;
|
||||
QT_CONFIG_ARGUMENTS qtConfigArgs;
|
||||
int returnCode = 0;
|
||||
|
||||
// parse CLI arguments
|
||||
auto qtConfigArgs = QT_CONFIG_ARGUMENTS();
|
||||
#ifndef PASSWORD_MANAGER_FORCE_GUI
|
||||
// setup argument parser
|
||||
ArgumentParser parser;
|
||||
// file argument
|
||||
Argument fileArg("file", 'f', "specifies the file to be opened (or created when using --modify)");
|
||||
auto parser = ArgumentParser();
|
||||
auto fileArg = Argument("file", 'f', "specifies the file to be opened (or created when using --modify)");
|
||||
fileArg.setValueNames({ "path" });
|
||||
fileArg.setRequiredValueCount(1);
|
||||
fileArg.setCombinable(true);
|
||||
|
@ -58,69 +64,44 @@ int main(int argc, char *argv[])
|
|||
fileArg.setImplicit(true);
|
||||
qtConfigArgs.qtWidgetsGuiArg().addSubArgument(&fileArg);
|
||||
qtConfigArgs.qtQuickGuiArg().addSubArgument(&fileArg);
|
||||
// cli argument
|
||||
Argument cliArg("interactive-cli", 'i', "starts the interactive command line interface");
|
||||
auto cliArg = Argument("interactive-cli", 'i', "starts the interactive command line interface");
|
||||
cliArg.setDenotesOperation(true);
|
||||
cliArg.setSubArguments({ &fileArg });
|
||||
// help argument
|
||||
HelpArgument helpArg(parser);
|
||||
auto helpArg = HelpArgument(parser);
|
||||
parser.setMainArguments({ &qtConfigArgs.qtWidgetsGuiArg(), &qtConfigArgs.qtQuickGuiArg(), &cliArg, &helpArg });
|
||||
// parse the specified arguments
|
||||
parser.parseArgs(argc, argv);
|
||||
#endif
|
||||
|
||||
#ifndef PASSWORD_MANAGER_FORCE_GUI
|
||||
// start either interactive CLI or GUI
|
||||
// run CLI if CLI-argument is present
|
||||
if (cliArg.isPresent()) {
|
||||
// init OpenSSL
|
||||
OpenSsl::init();
|
||||
return Cli::InteractiveCli().run(fileArg.isPresent() ? std::string(fileArg.firstValue()) : std::string());
|
||||
}
|
||||
|
||||
Cli::InteractiveCli cli;
|
||||
if (fileArg.isPresent()) {
|
||||
cli.run(fileArg.firstValue());
|
||||
} else {
|
||||
cli.run();
|
||||
}
|
||||
|
||||
// clean OpenSSL
|
||||
OpenSsl::clean();
|
||||
|
||||
} else if (qtConfigArgs.areQtGuiArgsPresent()) {
|
||||
#if defined(PASSWORD_MANAGER_GUI_QTWIDGETS) || defined(PASSWORD_MANAGER_GUI_QTQUICK)
|
||||
const auto file(fileArg.isPresent() ? QString::fromLocal8Bit(fileArg.firstValue()) : QString());
|
||||
// run GUI depending on which GUI-argument is present
|
||||
if (qtConfigArgs.areQtGuiArgsPresent()) {
|
||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS_OR_QTQUICK
|
||||
const auto file = fileArg.isPresent() ? QString::fromLocal8Bit(fileArg.firstValue()) : QString();
|
||||
#endif
|
||||
if (qtConfigArgs.qtWidgetsGuiArg().isPresent()) {
|
||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
||||
returnCode = QtGui::runWidgetsGui(argc, argv, qtConfigArgs, file);
|
||||
return QtGui::runWidgetsGui(argc, argv, qtConfigArgs, file);
|
||||
#else
|
||||
CMD_UTILS_START_CONSOLE;
|
||||
cerr << "The application has not been built with Qt widgets support." << endl;
|
||||
return fail("The application has not been built with Qt Widgets GUI support.");
|
||||
#endif
|
||||
} else if (qtConfigArgs.qtQuickGuiArg().isPresent()) {
|
||||
#ifdef PASSWORD_MANAGER_GUI_QTQUICK
|
||||
returnCode = QtGui::runQuickGui(argc, argv, qtConfigArgs, file);
|
||||
return QtGui::runQuickGui(argc, argv, qtConfigArgs, file);
|
||||
#else
|
||||
CMD_UTILS_START_CONSOLE;
|
||||
cerr << "The application has not been built with Qt quick support." << endl;
|
||||
#endif
|
||||
} else {
|
||||
#if defined(PASSWORD_MANAGER_GUI_QTQUICK)
|
||||
returnCode = QtGui::runQuickGui(argc, argv, qtConfigArgs, file);
|
||||
#elif defined(PASSWORD_MANAGER_GUI_QTWIDGETS)
|
||||
returnCode = QtGui::runWidgetsGui(argc, argv, qtConfigArgs, file);
|
||||
#else
|
||||
CMD_UTILS_START_CONSOLE;
|
||||
cerr << "See --help for usage." << endl;
|
||||
return fail("The application has not been built with Qt Quick GUI support.");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
return fail("See --help for usage.");
|
||||
|
||||
#else // PASSWORD_MANAGER_FORCE_GUI
|
||||
#ifdef PASSWORD_MANAGER_GUI_QTQUICK
|
||||
returnCode = QtGui::runQuickGui(argc, argv, qtConfigArgs, QString());
|
||||
return QtGui::runQuickGui(argc, argv, qtConfigArgs, QString());
|
||||
#else
|
||||
returnCode = QtGui::runWidgetsGui(argc, argv, qtConfigArgs, QString());
|
||||
return QtGui::runWidgetsGui(argc, argv, qtConfigArgs, QString());
|
||||
#endif
|
||||
#endif
|
||||
|
||||
return returnCode;
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ QList<Entry *> EntryModel::takeEntries(int row, int count, const QModelIndex &pa
|
|||
int lastIndex = row + count - 1;
|
||||
const vector<Entry *> &children = parentNodeEntry->children();
|
||||
if (lastIndex < 0 || static_cast<size_t>(lastIndex) >= children.size()) {
|
||||
lastIndex = children.size() - 1;
|
||||
lastIndex = static_cast<int>(children.size() - 1);
|
||||
}
|
||||
beginRemoveRows(parent, row, lastIndex);
|
||||
for (int index = lastIndex; index >= row; --index) {
|
||||
|
@ -125,9 +125,9 @@ bool EntryModel::insertEntries(int row, const QModelIndex &parent, const QList<E
|
|||
NodeEntry *const parentNodeEntry = static_cast<NodeEntry *>(parentEntry);
|
||||
const vector<Entry *> &children = parentNodeEntry->children();
|
||||
if (row < 0 || static_cast<size_t>(row) > children.size()) {
|
||||
row = children.size();
|
||||
row = static_cast<int>(children.size());
|
||||
}
|
||||
beginInsertRows(parent, row, row + entries.size() - 1);
|
||||
beginInsertRows(parent, row, row + static_cast<int>(entries.size()) - 1);
|
||||
for (Entry *const entry : entries) {
|
||||
entry->setParent(parentNodeEntry, row);
|
||||
++row;
|
||||
|
@ -243,7 +243,7 @@ QVariant EntryModel::data(const QModelIndex &index, int role) const
|
|||
entry->make(ss);
|
||||
// FIXME: make conversion to QByteArray more efficient
|
||||
const auto str(ss.str());
|
||||
return QByteArray(str.data(), str.size());
|
||||
return QByteArray(str.data(), static_cast<QByteArray::size_type>(str.size()));
|
||||
} catch (const std::ios_base::failure &) {
|
||||
return false;
|
||||
}
|
||||
|
@ -301,8 +301,11 @@ bool EntryModel::setData(const QModelIndex &index, const QVariant &value, int ro
|
|||
try {
|
||||
stringstream ss(stringstream::in | stringstream::out | stringstream::binary);
|
||||
ss.exceptions(std::stringstream::failbit | std::stringstream::badbit);
|
||||
#if defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION)
|
||||
ss.rdbuf()->pubsetbuf(array.data(), array.size());
|
||||
|
||||
#else
|
||||
ss.write(array.data(), array.size());
|
||||
#endif
|
||||
Entry *const newEntry = Entry::parse(ss);
|
||||
const int row = entry->index();
|
||||
beginRemoveRows(parentIndex, row, row);
|
||||
|
@ -375,7 +378,7 @@ int EntryModel::rowCount(const QModelIndex &parent) const
|
|||
if (Entry *parentEntry = static_cast<Entry *>(parent.internalPointer())) {
|
||||
switch (parentEntry->type()) {
|
||||
case EntryType::Node:
|
||||
return static_cast<NodeEntry *>(parentEntry)->children().size();
|
||||
return static_cast<int>(static_cast<NodeEntry *>(parentEntry)->children().size());
|
||||
case EntryType::Account:;
|
||||
}
|
||||
}
|
||||
|
@ -449,42 +452,56 @@ bool EntryModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co
|
|||
if (undoStack()) {
|
||||
return push(make_unique<EntryModelMoveRowsCommand>(this, sourceParent, sourceRow, count, destinationParent, destinationChild));
|
||||
}
|
||||
#endif
|
||||
#if CPP_UTILITIES_DEBUG_BUILD
|
||||
std::cout << "sourceRow: " << sourceRow << endl;
|
||||
std::cout << "destinationChild: " << destinationChild << endl;
|
||||
#endif
|
||||
// check validation of specified arguments: source and destination parent entries need to be node entries
|
||||
if (!sourceParent.isValid() || !destinationParent.isValid() || sourceRow < 0 || count <= 0 || entry(sourceParent)->type() != EntryType::Node //
|
||||
|| entry(destinationParent)->type() != EntryType::Node) {
|
||||
if (sourceRow < 0 || count <= 0) {
|
||||
return false;
|
||||
}
|
||||
const auto *const srcParentEntry = entry(sourceParent);
|
||||
const auto *const destParentEntry = entry(destinationParent);
|
||||
if (!srcParentEntry || !destParentEntry || srcParentEntry->type() != EntryType::Node || destParentEntry->type() != EntryType::Node) {
|
||||
return false;
|
||||
}
|
||||
// determine the source parent entry and dest parent entry as node entries
|
||||
auto *const srcParentEntry = static_cast<NodeEntry *>(sourceParent.internalPointer());
|
||||
auto *const destParentEntry = static_cast<NodeEntry *>(destinationParent.internalPointer());
|
||||
#if CPP_UTILITIES_DEBUG_BUILD
|
||||
cout << "destinationChild: " << destinationChild << endl;
|
||||
#endif
|
||||
auto *const srcParentNodeEntry = static_cast<NodeEntry *>(sourceParent.internalPointer());
|
||||
auto *const destParentNodeEntry = static_cast<NodeEntry *>(destinationParent.internalPointer());
|
||||
// source rows must be within the valid range
|
||||
if (static_cast<size_t>(sourceRow + count) > srcParentEntry->children().size()
|
||||
// if source and destination parent are the same the destination child mustn't be in the source range
|
||||
|| !(srcParentEntry != destParentEntry || (destinationChild < sourceRow || (sourceRow + count) < destinationChild))) {
|
||||
if (static_cast<std::size_t>(sourceRow + count) > srcParentNodeEntry->children().size()) {
|
||||
return false;
|
||||
}
|
||||
// if source and destination parent are the same the destination child mustn't be in the source range
|
||||
if (srcParentNodeEntry == destParentNodeEntry) {
|
||||
if (destinationChild == sourceRow) {
|
||||
return true;
|
||||
}
|
||||
if (!(destinationChild < sourceRow || (sourceRow + count) < destinationChild)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// do not move a row to one of its own children! -> check before
|
||||
for (int index = 0; index < count; ++index) {
|
||||
Entry *const toMove = srcParentEntry->children()[static_cast<size_t>(sourceRow + index)];
|
||||
Entry *const toMove = srcParentNodeEntry->children()[static_cast<std::size_t>(sourceRow + index)];
|
||||
if (toMove->type() != EntryType::Node) {
|
||||
continue;
|
||||
}
|
||||
if (toMove == destParentEntry || destParentEntry->isIndirectChildOf(static_cast<NodeEntry *>(toMove))) {
|
||||
if (toMove == destParentNodeEntry || destParentNodeEntry->isIndirectChildOf(static_cast<NodeEntry *>(toMove))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// actually perform the move operation
|
||||
beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild);
|
||||
if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild)) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < count; ++index) {
|
||||
Entry *toMove = srcParentEntry->children()[static_cast<size_t>(sourceRow + index)];
|
||||
if (srcParentEntry == destParentEntry && sourceRow < destinationChild) {
|
||||
toMove->setParent(destParentEntry, destinationChild + index - 1);
|
||||
Entry *toMove = srcParentNodeEntry->children()[static_cast<std::size_t>(sourceRow + index)];
|
||||
if (srcParentNodeEntry == destParentNodeEntry && sourceRow < destinationChild) {
|
||||
toMove->setParent(destParentNodeEntry, destinationChild + index - 1);
|
||||
} else {
|
||||
toMove->setParent(destParentEntry, destinationChild + index);
|
||||
toMove->setParent(destParentNodeEntry, destinationChild + index);
|
||||
}
|
||||
}
|
||||
endMoveRows();
|
||||
|
|
|
@ -89,7 +89,7 @@ inline QVariant FieldModel::passwordValue(const QModelIndex &index, int role) co
|
|||
return (m_passwordVisibility == PasswordVisibility::Always || role == Qt::EditRole
|
||||
|| (*m_fields)[static_cast<size_t>(index.row())].type() != FieldType::Password)
|
||||
? QString::fromStdString((*m_fields)[static_cast<size_t>(index.row())].value())
|
||||
: QString((*m_fields)[static_cast<size_t>(index.row())].value().size(), QChar(0x2022));
|
||||
: QString(static_cast<QString::size_type>((*m_fields)[static_cast<size_t>(index.row())].value().size()), QChar(0x2022));
|
||||
}
|
||||
|
||||
QVariant FieldModel::data(const QModelIndex &index, int role) const
|
||||
|
@ -155,14 +155,14 @@ QVariant FieldModel::data(const QModelIndex &index, int role) const
|
|||
QMap<int, QVariant> FieldModel::itemData(const QModelIndex &index) const
|
||||
{
|
||||
static const auto roleMap = [this, index] {
|
||||
QMap<int, QVariant> roleMap;
|
||||
auto res = QMap<int, QVariant>();
|
||||
for (const auto role : initializer_list<int>{ Qt::EditRole, FieldTypeRole }) {
|
||||
const auto variantData(data(index, role));
|
||||
if (variantData.isValid()) {
|
||||
roleMap.insert(role, variantData);
|
||||
res.insert(role, variantData);
|
||||
}
|
||||
}
|
||||
return roleMap;
|
||||
return res;
|
||||
}();
|
||||
return roleMap;
|
||||
}
|
||||
|
@ -233,17 +233,15 @@ bool FieldModel::setData(const QModelIndex &index, const QVariant &value, int ro
|
|||
switch (index.column()) {
|
||||
case 0:
|
||||
beginInsertRows(index.parent(), rowCount(), rowCount());
|
||||
m_fields->emplace_back(m_accountEntry);
|
||||
m_fields->back().setName(value.toString().toStdString());
|
||||
m_fields->emplace_back(m_accountEntry).setName(value.toString().toStdString());
|
||||
endInsertRows();
|
||||
roles << Qt::DisplayRole << Qt::EditRole << IsLastRow;
|
||||
roles << Qt::DisplayRole << Qt::EditRole << Key << IsLastRow;
|
||||
break;
|
||||
case 1:
|
||||
beginInsertRows(index.parent(), rowCount(), rowCount());
|
||||
m_fields->emplace_back(m_accountEntry);
|
||||
m_fields->back().setValue(value.toString().toStdString());
|
||||
m_fields->emplace_back(m_accountEntry).setValue(value.toString().toStdString());
|
||||
endInsertRows();
|
||||
roles << Qt::DisplayRole << Qt::EditRole << IsLastRow;
|
||||
roles << Qt::DisplayRole << Qt::EditRole << Value << IsLastRow;
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
|
@ -255,7 +253,7 @@ bool FieldModel::setData(const QModelIndex &index, const QVariant &value, int ro
|
|||
if (roles.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// emit data changed signal on sucess
|
||||
// emit data changed signal on success
|
||||
emit dataChanged(index, index, roles);
|
||||
return true;
|
||||
}
|
||||
|
@ -289,7 +287,7 @@ QVariant FieldModel::headerData(int section, Qt::Orientation orientation, int ro
|
|||
|
||||
int FieldModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return (!parent.isValid() && m_fields) ? m_fields->size() + 1 : 0;
|
||||
return (!parent.isValid() && m_fields) ? static_cast<int>(m_fields->size()) + 1 : 0;
|
||||
}
|
||||
|
||||
int FieldModel::columnCount(const QModelIndex &parent) const
|
||||
|
@ -335,25 +333,23 @@ bool FieldModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int co
|
|||
|
||||
// validate input parameter
|
||||
if (sourceParent.isValid() || destinationParent.isValid() || sourceRow < 0 || count <= 0 || destinationChild < 0
|
||||
|| static_cast<size_t>(sourceRow + count) > m_fields->size() || static_cast<size_t>(destinationChild) >= m_fields->size()
|
||||
|| static_cast<std::size_t>(sourceRow + count) > m_fields->size() || static_cast<std::size_t>(destinationChild) >= m_fields->size()
|
||||
|| (destinationChild >= sourceRow && destinationChild < (sourceRow + count))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// begin the move
|
||||
if (destinationChild > sourceRow) {
|
||||
// move rows down: the third param is still counted in the initial array!
|
||||
beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild + count);
|
||||
} else {
|
||||
// move rows up
|
||||
beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild);
|
||||
// note: When moving rows down (destinationChild > sourceRow) the third param is still counted in the initial array!
|
||||
if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent,
|
||||
destinationChild > sourceRow ? destinationChild + count : destinationChild)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// reserve space for temporary copies (FIXME: possible to avoid this?)
|
||||
m_fields->reserve(m_fields->size() + static_cast<size_t>(count));
|
||||
vector<Io::Field> tmp(static_cast<size_t>(count));
|
||||
m_fields->reserve(m_fields->size() + static_cast<std::size_t>(count));
|
||||
auto tmp = std::vector<Io::Field>(static_cast<std::size_t>(count));
|
||||
// move rows to temporary array
|
||||
move(m_fields->begin() + sourceRow, m_fields->begin() + sourceRow + count, tmp.begin());
|
||||
std::move(m_fields->begin() + sourceRow, m_fields->begin() + sourceRow + count, tmp.begin());
|
||||
// erase slots of rows to be moved
|
||||
m_fields->erase(m_fields->begin() + sourceRow, m_fields->begin() + sourceRow + count);
|
||||
// insert rows again at their new position
|
||||
|
@ -398,10 +394,7 @@ QMimeData *FieldModel::mimeData(const QModelIndexList &indices) const
|
|||
*/
|
||||
const Field *FieldModel::field(size_t row) const
|
||||
{
|
||||
if (m_fields && row < m_fields->size()) {
|
||||
return &(*m_fields)[row];
|
||||
}
|
||||
return nullptr;
|
||||
return m_fields && row < m_fields->size() ? &(*m_fields)[row] : nullptr;
|
||||
}
|
||||
|
||||
} // namespace QtGui
|
||||
|
|
|
@ -141,7 +141,7 @@ inline void FieldModel::setPasswordVisibility(PasswordVisibility passwordVisibil
|
|||
{
|
||||
m_passwordVisibility = passwordVisibility;
|
||||
if (m_fields) {
|
||||
emit dataChanged(index(0, 1), index(m_fields->size() - 1, 1), QVector<int>({ Qt::DisplayRole, Qt::EditRole }));
|
||||
emit dataChanged(index(0, 1), index(static_cast<int>(m_fields->size() - 1), 1), QVector<int>({ Qt::DisplayRole, Qt::EditRole }));
|
||||
}
|
||||
}
|
||||
} // namespace QtGui
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1 as Controls
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Dialogs 1.3
|
||||
import org.kde.kirigami 2.4 as Kirigami
|
||||
|
||||
BasicDialog {
|
||||
id: aboutDialog
|
||||
standardButtons: Controls.Dialog.Ok
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
ColumnLayout {
|
||||
contentItem: ColumnLayout {
|
||||
width: aboutDialog.availableWidth
|
||||
|
||||
Image {
|
||||
|
@ -44,16 +42,8 @@ BasicDialog {
|
|||
Layout.fillWidth: true
|
||||
text: "<a href=\"" + app.organizationDomain + "\">" + app.organizationDomain + "</a>"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
onLinkActivated: openWebsite()
|
||||
onLinkActivated: Qt.openUrlExternally(app.organizationDomain)
|
||||
wrapMode: Text.Wrap
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: parent.openWebsite()
|
||||
}
|
||||
function openWebsite() {
|
||||
Qt.openUrlExternally(app.organizationDomain)
|
||||
}
|
||||
}
|
||||
Controls.Label {
|
||||
Layout.fillWidth: true
|
||||
|
|
|
@ -6,9 +6,7 @@ Controls.Dialog {
|
|||
modal: true
|
||||
focus: true
|
||||
parent: applicationWindow().overlay
|
||||
//anchors.centerIn: parent // enable if requiring at least Qt 5.12 instead of setting x and y manually
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width, Kirigami.Units.gridUnit * 30)
|
||||
|
||||
function acceptOnReturn(event) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import QtQuick 2.4
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQml.Models 2.2
|
||||
import QtQuick.Controls 2.4 as Controls
|
||||
|
@ -15,17 +15,17 @@ Kirigami.ScrollablePage {
|
|||
var currentEntryName = entryModel.data(rootIndex)
|
||||
return currentEntryName ? currentEntryName : ""
|
||||
}
|
||||
actions {
|
||||
main: Kirigami.Action {
|
||||
iconName: "list-add"
|
||||
actions:[
|
||||
Kirigami.Action {
|
||||
icon.name: "list-add"
|
||||
text: qsTr("Add account")
|
||||
visible: !nativeInterface.hasEntryFilter
|
||||
enabled: !nativeInterface.hasEntryFilter
|
||||
onTriggered: insertEntry("Account")
|
||||
shortcut: "Ctrl+A"
|
||||
}
|
||||
left: Kirigami.Action {
|
||||
iconName: "edit-paste"
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "edit-paste"
|
||||
text: qsTr("Paste account")
|
||||
visible: !nativeInterface.hasEntryFilter
|
||||
enabled: nativeInterface.canPaste && !nativeInterface.hasEntryFilter
|
||||
|
@ -40,19 +40,16 @@ Kirigami.ScrollablePage {
|
|||
pastedEntries.join(", ")))
|
||||
}
|
||||
shortcut: StandardKey.Paste
|
||||
}
|
||||
right: Kirigami.Action {
|
||||
iconName: "folder-add"
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "folder-add"
|
||||
text: qsTr("Add category")
|
||||
visible: !nativeInterface.hasEntryFilter
|
||||
enabled: !nativeInterface.hasEntryFilter
|
||||
onTriggered: insertEntry("Node")
|
||||
shortcut: "Ctrl+Shift+A"
|
||||
}
|
||||
}
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
]
|
||||
|
||||
// dialog to confirm deletion of an entry
|
||||
BasicDialog {
|
||||
|
@ -64,8 +61,7 @@ Kirigami.ScrollablePage {
|
|||
standardButtons: Controls.Dialog.Ok | Controls.Dialog.Cancel
|
||||
title: qsTr("Delete %1?").arg(entryDesc)
|
||||
onAccepted: entryModel.removeRows(this.entryIndex, 1, rootIndex)
|
||||
|
||||
ColumnLayout {
|
||||
contentItem: ColumnLayout {
|
||||
Controls.Label {
|
||||
text: " "
|
||||
}
|
||||
|
@ -109,13 +105,12 @@ Kirigami.ScrollablePage {
|
|||
entryModel.removeRows(this.entryIndex, 1, rootIndex)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
contentItem: ColumnLayout {
|
||||
Controls.TextField {
|
||||
id: entryNameTextField
|
||||
Layout.preferredWidth: renameDialog.availableWidth
|
||||
placeholderText: qsTr("enter new name here")
|
||||
Keys.onPressed: renameDialog.acceptOnReturn(event)
|
||||
Keys.onPressed: (event) => renameDialog.acceptOnReturn(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,115 +133,6 @@ Kirigami.ScrollablePage {
|
|||
}
|
||||
}
|
||||
|
||||
// component representing an entry
|
||||
Component {
|
||||
id: listDelegateComponent
|
||||
|
||||
Kirigami.SwipeListItem {
|
||||
id: listItem
|
||||
contentItem: RowLayout {
|
||||
Kirigami.ListItemDragHandle {
|
||||
listItem: listItem
|
||||
listView: entriesListView
|
||||
enabled: !nativeInterface.hasEntryFilter
|
||||
// FIXME: not sure why newIndex + 1 is required to be able to move a row at the end
|
||||
onMoveRequested: entryModel.moveRows(
|
||||
rootIndex, oldIndex, 1, rootIndex,
|
||||
oldIndex < newIndex ? newIndex + 1 : newIndex)
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
Kirigami.Icon {
|
||||
width: Kirigami.Units.iconSizes.smallMedium
|
||||
height: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.fillHeight: true
|
||||
source: delegateModel.isNode(
|
||||
index) ? "folder-symbolic" : "story-editor"
|
||||
}
|
||||
Controls.Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
text: model.name
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
entryContextMenu.popup()
|
||||
return
|
||||
}
|
||||
delegateModel.handleEntryClicked(index, model.name)
|
||||
}
|
||||
onPressAndHold: entryContextMenu.popup()
|
||||
}
|
||||
Controls.Menu {
|
||||
id: entryContextMenu
|
||||
Controls.MenuItem {
|
||||
icon.name: "edit-cut"
|
||||
text: qsTr("Cut")
|
||||
enabled: !nativeInterface.hasEntryFilter
|
||||
onTriggered: {
|
||||
nativeInterface.cutEntry(
|
||||
entryModel.index(index, 0,
|
||||
rootIndex))
|
||||
showPassiveNotification(qsTr("Cut %1").arg(
|
||||
model.name))
|
||||
}
|
||||
}
|
||||
Controls.MenuItem {
|
||||
icon.name: "edit-delete"
|
||||
text: qsTr("Delete")
|
||||
enabled: !nativeInterface.hasEntryFilter
|
||||
onTriggered: confirmDeletionDialog.confirmDeletion(
|
||||
model.name, index)
|
||||
}
|
||||
Controls.MenuItem {
|
||||
icon.name: "edit-rename"
|
||||
text: qsTr("Rename")
|
||||
enabled: !nativeInterface.hasEntryFilter
|
||||
onTriggered: renameDialog.renameEntry(model.name,
|
||||
index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
iconName: "edit-cut"
|
||||
text: qsTr("Cut")
|
||||
enabled: !nativeInterface.hasEntryFilter
|
||||
onTriggered: {
|
||||
nativeInterface.cutEntry(entryModel.index(index, 0,
|
||||
rootIndex))
|
||||
showPassiveNotification(text + " " + model.name)
|
||||
}
|
||||
shortcut: StandardKey.Cut
|
||||
},
|
||||
Kirigami.Action {
|
||||
iconName: "edit-delete"
|
||||
text: qsTr("Delete")
|
||||
enabled: !nativeInterface.hasEntryFilter
|
||||
onTriggered: confirmDeletionDialog.confirmDeletion(
|
||||
model.name, index)
|
||||
shortcut: StandardKey.Delete
|
||||
},
|
||||
Kirigami.Action {
|
||||
iconName: "edit-rename"
|
||||
text: qsTr("Rename")
|
||||
enabled: !nativeInterface.hasEntryFilter
|
||||
onTriggered: renameDialog.renameEntry(model.name, index)
|
||||
shortcut: "F2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// list view to display one hierarchy level of entry model
|
||||
ListView {
|
||||
id: entriesListView
|
||||
|
@ -257,12 +143,16 @@ Kirigami.ScrollablePage {
|
|||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
reuseItems: true
|
||||
model: DelegateModel {
|
||||
id: delegateModel
|
||||
|
||||
delegate: Kirigami.DelegateRecycler {
|
||||
width: parent ? parent.width : implicitWidth
|
||||
sourceComponent: listDelegateComponent
|
||||
delegate: EntryDelegate {
|
||||
width: entriesListView.width
|
||||
view: entriesListView
|
||||
onMoveRequested:
|
||||
(oldIndex, newIndex) => {
|
||||
entryModel.moveRows(rootIndex, oldIndex, 1, rootIndex, oldIndex < newIndex ? newIndex + 1 : newIndex)
|
||||
}
|
||||
}
|
||||
|
||||
function isNode(rowNumber) {
|
||||
|
|
|
@ -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 QtQml.Models 2.2
|
||||
import QtQuick.Controls 2.4 as Controls
|
||||
|
@ -10,9 +10,21 @@ Kirigami.ScrollablePage {
|
|||
|
||||
Layout.fillWidth: true
|
||||
title: nativeInterface.currentAccountName
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
actions:[
|
||||
Kirigami.Action {
|
||||
icon.name: "list-add"
|
||||
text: qsTr("Add field")
|
||||
visible: !nativeInterface.hasEntryFilter
|
||||
enabled: !nativeInterface.hasEntryFilter
|
||||
onTriggered: {
|
||||
const delegateModel = fieldsListView.model
|
||||
const row = delegateModel.rowCount() - 1
|
||||
fieldDialog.init(delegateModel, row)
|
||||
fieldDialog.open()
|
||||
}
|
||||
shortcut: "Ctrl+Shift+A"
|
||||
}
|
||||
]
|
||||
|
||||
// dialog to edit certain field
|
||||
BasicDialog {
|
||||
|
@ -32,8 +44,7 @@ Kirigami.ScrollablePage {
|
|||
fieldsListView.model.setData(column0, isPassword ? 1 : 0,
|
||||
0x0100 + 1)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
contentItem: ColumnLayout {
|
||||
GridLayout {
|
||||
Layout.preferredWidth: fieldDialog.availableWidth
|
||||
columns: 2
|
||||
|
@ -43,7 +54,7 @@ Kirigami.ScrollablePage {
|
|||
id: fieldNameEdit
|
||||
Layout.fillWidth: true
|
||||
text: fieldDialog.fieldName
|
||||
Keys.onPressed: fieldDialog.acceptOnReturn(event)
|
||||
Keys.onPressed: (event) => fieldDialog.acceptOnReturn(event)
|
||||
}
|
||||
Controls.RoundButton {
|
||||
flat: true
|
||||
|
@ -69,7 +80,7 @@ Kirigami.ScrollablePage {
|
|||
// fix ugly bullet points under Android
|
||||
font.pointSize: hideCharacters ? fieldNameEdit.font.pointSize
|
||||
* 0.5 : fieldNameEdit.font.pointSize
|
||||
Keys.onPressed: fieldDialog.acceptOnReturn(event)
|
||||
Keys.onPressed: (event) => fieldDialog.acceptOnReturn(event)
|
||||
}
|
||||
Controls.RoundButton {
|
||||
flat: true
|
||||
|
@ -107,169 +118,27 @@ Kirigami.ScrollablePage {
|
|||
}
|
||||
}
|
||||
|
||||
// component representing a field
|
||||
Component {
|
||||
id: fieldsListDelegateComponent
|
||||
|
||||
Kirigami.SwipeListItem {
|
||||
id: fieldsListItem
|
||||
contentItem: RowLayout {
|
||||
id: fieldRow
|
||||
readonly property bool isLast: model.isLastRow
|
||||
|
||||
Kirigami.ListItemDragHandle {
|
||||
listItem: fieldsListItem
|
||||
listView: fieldsListView
|
||||
onMoveRequested: fieldsListView.model.moveRows(
|
||||
fieldsListView.model.index(-1, 0),
|
||||
oldIndex, 1,
|
||||
fieldsListView.model.index(-1, 0),
|
||||
newIndex)
|
||||
visible: !fieldRow.isLast
|
||||
}
|
||||
Kirigami.Icon {
|
||||
width: Kirigami.Units.iconSizes.smallMedium
|
||||
height: width
|
||||
source: "list-add"
|
||||
opacity: 0.6
|
||||
visible: fieldRow.isLast
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
fieldDialog.init(model, index)
|
||||
fieldDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
Controls.Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
text: {
|
||||
if (fieldRow.isLast) {
|
||||
return qsTr("click to append new field")
|
||||
}
|
||||
|
||||
var pieces = []
|
||||
if (model.key) {
|
||||
pieces.push(model.key)
|
||||
}
|
||||
if (model.value) {
|
||||
pieces.push(model.value)
|
||||
}
|
||||
return pieces.join(": ")
|
||||
}
|
||||
color: fieldRow.isLast ? "gray" : palette.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (!fieldRow.isLast) {
|
||||
fieldContextMenu.popup()
|
||||
}
|
||||
return
|
||||
}
|
||||
fieldDialog.init(model, index)
|
||||
fieldDialog.open()
|
||||
}
|
||||
onPressAndHold: fieldContextMenu.popup()
|
||||
}
|
||||
Controls.Menu {
|
||||
id: fieldContextMenu
|
||||
Controls.MenuItem {
|
||||
icon.name: !model.isPassword ? "password-show-off" : "password-show-on"
|
||||
text: model.isPassword ? qsTr("Mark as normal field") : qsTr(
|
||||
"Mark as password field")
|
||||
onClicked: fieldsListView.model.setData(
|
||||
fieldsListView.model.index(index,
|
||||
0),
|
||||
model.isPassword ? 0 : 1, 0x0100 + 1)
|
||||
}
|
||||
Controls.MenuItem {
|
||||
icon.name: "edit-copy"
|
||||
text: model.isPassword ? qsTr("Copy password") : qsTr(
|
||||
"Copy value")
|
||||
onClicked: showPassiveNotification(
|
||||
nativeInterface.copyToClipboard(
|
||||
model.actualValue) ? qsTr("Copied") : qsTr(
|
||||
"Unable to access clipboard"))
|
||||
}
|
||||
Controls.MenuItem {
|
||||
icon.name: "edit-delete"
|
||||
text: qsTr("Delete field")
|
||||
onClicked: fieldsListView.model.removeRows(index, 1)
|
||||
}
|
||||
Controls.MenuItem {
|
||||
icon.name: "list-add"
|
||||
text: qsTr("Insert empty field after this")
|
||||
onClicked: fieldsListView.model.insertRows(
|
||||
index + 1, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
iconName: !model.isPassword ? "password-show-off" : "password-show-on"
|
||||
text: model.isPassword ? qsTr(
|
||||
"Mark as normal field") : qsTr(
|
||||
"Mark as password field")
|
||||
onTriggered: fieldsListView.model.setData(
|
||||
fieldsListView.model.index(index, 0),
|
||||
model.isPassword ? 0 : 1, 0x0100 + 1)
|
||||
visible: !fieldRow.isLast
|
||||
},
|
||||
Kirigami.Action {
|
||||
iconName: "edit-copy"
|
||||
text: model.isPassword ? qsTr("Copy password") : qsTr(
|
||||
"Copy value")
|
||||
onTriggered: showPassiveNotification(
|
||||
nativeInterface.copyToClipboard(
|
||||
model.actualValue) ? qsTr("Copied") : qsTr(
|
||||
"Unable to access clipboard"))
|
||||
shortcut: StandardKey.Cut
|
||||
visible: !fieldRow.isLast
|
||||
},
|
||||
Kirigami.Action {
|
||||
iconName: "edit-delete"
|
||||
text: qsTr("Delete field")
|
||||
onTriggered: fieldsListView.model.removeRows(index, 1)
|
||||
shortcut: StandardKey.Delete
|
||||
visible: !fieldRow.isLast
|
||||
},
|
||||
Kirigami.Action {
|
||||
iconName: "list-add"
|
||||
text: qsTr("Insert empty field after this")
|
||||
enabled: !nativeInterface.hasEntryFilter
|
||||
onTriggered: fieldsListView.model.insertRows(index + 1, 1)
|
||||
visible: !fieldRow.isLast
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// list view to edit the currently selected account
|
||||
ListView {
|
||||
id: fieldsListView
|
||||
implicitWidth: Kirigami.Units.gridUnit * 30
|
||||
model: nativeInterface.fieldModel
|
||||
reuseItems: true
|
||||
moveDisplaced: Transition {
|
||||
YAnimator {
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
delegate: Kirigami.DelegateRecycler {
|
||||
width: parent ? parent.width : implicitWidth
|
||||
sourceComponent: fieldsListDelegateComponent
|
||||
delegate: FieldsDelegate {
|
||||
width: fieldsListView.width
|
||||
view: fieldsListView
|
||||
onMoveRequested:
|
||||
(oldIndex, newIndex) => {
|
||||
const model = fieldsListView.model
|
||||
const invalidIndex = model.index(-1, 0)
|
||||
model.moveRows(invalidIndex, oldIndex, 1, invalidIndex, newIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import QtQuick.Dialogs 1.3
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
property bool createNewFile: false
|
||||
title: selectExisting ? qsTr("Select an existing file") : (saveAs ? qsTr("Select path to save file") : qsTr("Select path for new file"))
|
||||
onAccepted: {
|
||||
if (fileUrls.length < 1) {
|
||||
return
|
||||
}
|
||||
nativeInterface.handleFileSelectionAccepted(fileUrls[0], "",
|
||||
this.selectExisting,
|
||||
this.createNewFile)
|
||||
}
|
||||
onRejected: nativeInterface.handleFileSelectionCanceled()
|
||||
|
||||
function show() {
|
||||
if (nativeInterface.showNativeFileDialog(this.selectExisting, this.createNewFile)) {
|
||||
return
|
||||
}
|
||||
// fallback to the Qt Quick file dialog if a native implementation is not available
|
||||
this.open()
|
||||
}
|
||||
function openExisting() {
|
||||
this.selectExisting = true
|
||||
this.createNewFile = false
|
||||
this.show()
|
||||
}
|
||||
function createNew() {
|
||||
this.selectExisting = false
|
||||
this.createNewFile = true
|
||||
this.show()
|
||||
}
|
||||
function saveAs() {
|
||||
this.selectExisting = false
|
||||
this.createNewFile = false
|
||||
this.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import QtQuick.Dialogs 6.2 as Dialogs
|
||||
|
||||
Dialogs.FileDialog {
|
||||
id: fileDialog
|
||||
property bool createNewFile: false
|
||||
property bool selectExisting: fileMode !== Dialogs.FileDialog.SaveFile
|
||||
title: selectExisting ? qsTr("Select an existing file") : (!createNewFile ? qsTr("Select path to save file") : qsTr("Select path for new file"))
|
||||
onAccepted: {
|
||||
if (selectedFiles.length < 1) {
|
||||
return
|
||||
}
|
||||
nativeInterface.handleFileSelectionAccepted(selectedFiles[0], "",
|
||||
this.selectExisting,
|
||||
this.createNewFile)
|
||||
}
|
||||
onRejected: nativeInterface.handleFileSelectionCanceled()
|
||||
|
||||
function show() {
|
||||
if (nativeInterface.showNativeFileDialog(this.selectExisting, this.createNewFile)) {
|
||||
return
|
||||
}
|
||||
// fallback to the Qt Quick file dialog if a native implementation is not available
|
||||
this.open()
|
||||
}
|
||||
function openExisting() {
|
||||
this.fileMode = Dialogs.FileDialog.OpenFile
|
||||
this.createNewFile = false
|
||||
this.show()
|
||||
}
|
||||
function createNew() {
|
||||
this.fileMode = Dialogs.FileDialog.SaveFile
|
||||
this.createNewFile = true
|
||||
this.show()
|
||||
}
|
||||
function saveAs() {
|
||||
this.fileMode = Dialogs.FileDialog.SaveFile
|
||||
this.createNewFile = false
|
||||
this.show()
|
||||
}
|
||||
}
|
|
@ -30,8 +30,7 @@ BasicDialog {
|
|||
qsTr("You aborted. The password has not been altered."))
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
contentItem: ColumnLayout {
|
||||
Controls.Label {
|
||||
id: instructionLabel
|
||||
Layout.preferredWidth: passwordDialog.availableWidth
|
||||
|
@ -42,26 +41,18 @@ BasicDialog {
|
|||
id: passwordTextField
|
||||
Layout.preferredWidth: passwordDialog.availableWidth
|
||||
echoMode: showCharactersCheckBox.checked ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: qsTr("enter password here, leave empty for no encryption")
|
||||
color: "#101010"
|
||||
placeholderTextColor: "#505050"
|
||||
background: Rectangle {
|
||||
border.color: "#5d5e6d"
|
||||
}
|
||||
Keys.onPressed: passwordDialog.acceptOnReturn(event)
|
||||
placeholderText: newPassword
|
||||
? qsTr("enter password here, leave empty for no encryption")
|
||||
: qsTr("enter password here")
|
||||
Keys.onPressed: (event) => passwordDialog.acceptOnReturn(event)
|
||||
}
|
||||
Controls.TextField {
|
||||
id: repeatPasswordTextField
|
||||
Layout.preferredWidth: passwordDialog.availableWidth
|
||||
visible: passwordDialog.newPassword
|
||||
&& !showCharactersCheckBox.checked
|
||||
enabled: visible && !showCharactersCheckBox.checked
|
||||
echoMode: TextInput.Password
|
||||
placeholderText: qsTr("repeat password")
|
||||
color: "#101010"
|
||||
placeholderTextColor: "#505050"
|
||||
background: Rectangle {
|
||||
border.color: passwordDialog.canAccept ? "#089900" : "#ff0000"
|
||||
}
|
||||
Keys.onPressed: passwordDialog.acceptOnReturn(event)
|
||||
}
|
||||
Controls.CheckBox {
|
||||
|
|
120
qml/main.qml
120
qml/main.qml
|
@ -1,8 +1,9 @@
|
|||
import QtQuick 2.7
|
||||
import QtQuick.Templates 2.0 as T2
|
||||
import QtQuick.Controls 2.1 as Controls
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Controls.Universal
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Dialogs 1.3
|
||||
import org.kde.kirigami 2.4 as Kirigami
|
||||
|
||||
Kirigami.ApplicationWindow {
|
||||
|
@ -10,13 +11,14 @@ Kirigami.ApplicationWindow {
|
|||
property var fieldsPage: undefined
|
||||
property var lastEntriesPage: undefined
|
||||
|
||||
Material.theme: nativeInterface.darkModeEnabled ? Material.Dark : Material.Light
|
||||
Universal.theme: nativeInterface.darkModeEnabled ? Universal.Dark : Universal.Light
|
||||
|
||||
globalDrawer: Kirigami.GlobalDrawer {
|
||||
id: leftMenu
|
||||
property bool showNoPasswordWarning: nativeInterface.fileOpen
|
||||
&& !nativeInterface.passwordSet
|
||||
|
||||
title: app.applicationName
|
||||
titleIcon: "qrc://icons/hicolor/scalable/apps/passwordmanager.svg"
|
||||
visible: true
|
||||
resetMenuOnTriggered: false
|
||||
topContent: ColumnLayout {
|
||||
|
@ -102,20 +104,20 @@ Kirigami.ApplicationWindow {
|
|||
actions: [
|
||||
Kirigami.Action {
|
||||
text: qsTr("Create new file")
|
||||
iconName: "document-new"
|
||||
icon.name: "document-new"
|
||||
onTriggered: fileDialog.createNew()
|
||||
shortcut: StandardKey.New
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: qsTr("Open existing file")
|
||||
iconName: "document-open"
|
||||
icon.name: "document-open"
|
||||
onTriggered: fileDialog.openExisting()
|
||||
shortcut: StandardKey.Open
|
||||
},
|
||||
Kirigami.Action {
|
||||
id: recentlyOpenedAction
|
||||
text: qsTr("Recently opened ...")
|
||||
iconName: "document-open-recent"
|
||||
icon.name: "document-open-recent"
|
||||
children: createRecentlyOpenedActions(
|
||||
nativeInterface.recentFiles)
|
||||
visible: nativeInterface.recentFiles.length > 0
|
||||
|
@ -124,14 +126,14 @@ Kirigami.ApplicationWindow {
|
|||
Kirigami.Action {
|
||||
text: qsTr("Save modifications")
|
||||
enabled: nativeInterface.fileOpen
|
||||
iconName: "document-save"
|
||||
icon.name: "document-save"
|
||||
onTriggered: nativeInterface.save()
|
||||
shortcut: StandardKey.Save
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: qsTr("Save as")
|
||||
enabled: nativeInterface.fileOpen
|
||||
iconName: "document-save-as"
|
||||
icon.name: "document-save-as"
|
||||
onTriggered: fileDialog.saveAs()
|
||||
shortcut: StandardKey.SaveAs
|
||||
},
|
||||
|
@ -139,7 +141,7 @@ Kirigami.ApplicationWindow {
|
|||
text: nativeInterface.passwordSet ? qsTr("Change password") : qsTr(
|
||||
"Add password")
|
||||
enabled: nativeInterface.fileOpen
|
||||
iconName: "document-encrypt"
|
||||
icon.name: "document-encrypt"
|
||||
onTriggered: enterPasswordDialog.askForNewPassword(
|
||||
qsTr("Change password for %1").arg(
|
||||
nativeInterface.filePath))
|
||||
|
@ -148,7 +150,7 @@ Kirigami.ApplicationWindow {
|
|||
Kirigami.Action {
|
||||
text: qsTr("Details")
|
||||
enabled: nativeInterface.fileOpen
|
||||
iconName: "document-properties"
|
||||
icon.name: "document-properties"
|
||||
onTriggered: {
|
||||
leftMenu.resetMenu()
|
||||
fileSummaryDialog.show()
|
||||
|
@ -160,7 +162,7 @@ Kirigami.ApplicationWindow {
|
|||
"Adjust search")
|
||||
enabled: nativeInterface.fileOpen
|
||||
visible: nativeInterface.filterAsDialog
|
||||
iconName: "search"
|
||||
icon.name: "search"
|
||||
onTriggered: {
|
||||
leftMenu.resetMenu()
|
||||
filterDialog.open()
|
||||
|
@ -172,7 +174,7 @@ Kirigami.ApplicationWindow {
|
|||
enabled: nativeInterface.fileOpen
|
||||
visible: nativeInterface.filterAsDialog
|
||||
&& nativeInterface.entryFilter.length > 0
|
||||
iconName: "edit-clear"
|
||||
icon.name: "edit-clear"
|
||||
onTriggered: {
|
||||
leftMenu.resetMenu()
|
||||
nativeInterface.entryFilter = ""
|
||||
|
@ -184,7 +186,7 @@ Kirigami.ApplicationWindow {
|
|||
visible: nativeInterface.undoText.length !== 0
|
||||
&& nativeInterface.entryFilter.length === 0
|
||||
enabled: visible
|
||||
iconName: "edit-undo"
|
||||
icon.name: "edit-undo"
|
||||
shortcut: StandardKey.Undo
|
||||
onTriggered: nativeInterface.undo()
|
||||
},
|
||||
|
@ -193,29 +195,30 @@ Kirigami.ApplicationWindow {
|
|||
visible: nativeInterface.redoText.length !== 0
|
||||
&& nativeInterface.entryFilter.length === 0
|
||||
enabled: visible
|
||||
iconName: "edit-redo"
|
||||
icon.name: "edit-redo"
|
||||
shortcut: StandardKey.Redo
|
||||
onTriggered: nativeInterface.redo()
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: qsTr("Close file")
|
||||
enabled: nativeInterface.fileOpen
|
||||
iconName: "document-close"
|
||||
icon.name: "document-close"
|
||||
shortcut: StandardKey.Close
|
||||
onTriggered: nativeInterface.close()
|
||||
},
|
||||
Kirigami.Action {
|
||||
separator: true
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: qsTr("About")
|
||||
icon.name: "help-about"
|
||||
shortcut: "Ctrl+?"
|
||||
onTriggered: {
|
||||
leftMenu.resetMenu()
|
||||
aboutDialog.open()
|
||||
}
|
||||
}
|
||||
]
|
||||
onBannerClicked: {
|
||||
leftMenu.resetMenu()
|
||||
aboutDialog.open()
|
||||
}
|
||||
|
||||
Controls.Switch {
|
||||
text: qsTr("Use native file dialog")
|
||||
checked: nativeInterface.useNativeFileDialog
|
||||
visible: nativeInterface.supportsNativeFileDialog
|
||||
onCheckedChanged: nativeInterface.useNativeFileDialog = checked
|
||||
}
|
||||
}
|
||||
contextDrawer: Kirigami.ContextDrawer {
|
||||
id: contextDrawer
|
||||
|
@ -240,9 +243,9 @@ Kirigami.ApplicationWindow {
|
|||
id: fileSummaryDialog
|
||||
standardButtons: Controls.Dialog.Ok
|
||||
title: qsTr("File details")
|
||||
|
||||
Controls.Label {
|
||||
contentItem: Controls.TextArea {
|
||||
id: fileSummaryLabel
|
||||
readOnly: true
|
||||
text: "No file summary available"
|
||||
textFormat: Text.RichText
|
||||
wrapMode: Text.Wrap
|
||||
|
@ -257,41 +260,6 @@ Kirigami.ApplicationWindow {
|
|||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
property bool createNewFile: false
|
||||
title: selectExisting ? qsTr("Select an existing file") : (saveAs ? qsTr("Select path to save file") : qsTr("Select path for new file"))
|
||||
onAccepted: {
|
||||
if (fileUrls.length < 1) {
|
||||
return
|
||||
}
|
||||
nativeInterface.handleFileSelectionAccepted(fileUrls[0], "",
|
||||
this.selectExisting,
|
||||
this.createNewFile)
|
||||
}
|
||||
onRejected: nativeInterface.handleFileSelectionCanceled()
|
||||
|
||||
function show() {
|
||||
if (nativeInterface.showNativeFileDialog(this.selectExisting,
|
||||
this.createNewFile)) {
|
||||
return
|
||||
}
|
||||
// fallback to the Qt Quick file dialog if a native implementation is not available
|
||||
this.open()
|
||||
}
|
||||
function openExisting() {
|
||||
this.selectExisting = true
|
||||
this.createNewFile = false
|
||||
this.show()
|
||||
}
|
||||
function createNew() {
|
||||
this.selectExisting = false
|
||||
this.createNewFile = true
|
||||
this.show()
|
||||
}
|
||||
function saveAs() {
|
||||
this.selectExisting = false
|
||||
this.createNewFile = false
|
||||
this.show()
|
||||
}
|
||||
}
|
||||
|
||||
BasicDialog {
|
||||
|
@ -323,8 +291,7 @@ Kirigami.ApplicationWindow {
|
|||
Controls.DialogButtonBox.buttonRole: Controls.DialogButtonBox.RejectRole
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
contentItem: ColumnLayout {
|
||||
Controls.TextField {
|
||||
id: filterDialogTextField
|
||||
Layout.preferredWidth: filterDialog.availableWidth
|
||||
|
@ -335,12 +302,12 @@ Kirigami.ApplicationWindow {
|
|||
|
||||
Connections {
|
||||
target: nativeInterface
|
||||
onEntryFilterChanged: {
|
||||
function onEntryFilterChanged(newFilter) {
|
||||
if (filterTextField.text !== newFilter) {
|
||||
filterTextField.text = newFilter
|
||||
}
|
||||
}
|
||||
onFileError: {
|
||||
function onFileError(errorMessage, retryAction) {
|
||||
var retryMethod = null
|
||||
if (retryAction === "load" || retryAction === "save") {
|
||||
retryMethod = retryAction
|
||||
|
@ -354,13 +321,16 @@ Kirigami.ApplicationWindow {
|
|||
})
|
||||
}
|
||||
}
|
||||
onPasswordRequired: {
|
||||
function onSettingsError(errorMessage) {
|
||||
showPassiveNotification(errorMessage)
|
||||
}
|
||||
function onPasswordRequired(filePath) {
|
||||
enterPasswordDialog.askForExistingPassword(
|
||||
qsTr("Password required to open %1").arg(
|
||||
nativeInterface.filePath))
|
||||
leftMenu.resetMenu()
|
||||
}
|
||||
onFileOpenChanged: {
|
||||
function onFileOpenChanged(fileOpen) {
|
||||
clearStack()
|
||||
if (!nativeInterface.fileOpen) {
|
||||
showPassiveNotification(qsTr("%1 closed").arg(
|
||||
|
@ -372,20 +342,20 @@ Kirigami.ApplicationWindow {
|
|||
nativeInterface.fileName))
|
||||
leftMenu.close()
|
||||
}
|
||||
onFileSaved: {
|
||||
function onFileSaved() {
|
||||
showPassiveNotification(qsTr("%1 saved").arg(
|
||||
nativeInterface.fileName))
|
||||
}
|
||||
onNewNotification: {
|
||||
function onNewNotification(message) {
|
||||
showPassiveNotification(message)
|
||||
}
|
||||
onCurrentAccountChanged: {
|
||||
function onCurrentAccountChanged() {
|
||||
// remove the fields page if the current account has been removed
|
||||
if (!nativeInterface.hasCurrentAccount) {
|
||||
pageStack.pop(lastEntriesPage)
|
||||
}
|
||||
}
|
||||
onEntryAboutToBeRemoved: {
|
||||
function onEntryAboutToBeRemoved(removedIndex) {
|
||||
// get the filter entry index
|
||||
if (nativeInterface.hasEntryFilter) {
|
||||
removedIndex = nativeInterface.filterEntryIndex(removedIndex)
|
||||
|
@ -403,7 +373,7 @@ Kirigami.ApplicationWindow {
|
|||
}
|
||||
}
|
||||
}
|
||||
onHasEntryFilterChanged: {
|
||||
function onHasEntryFilterChanged(hasEntryFilter) {
|
||||
if (nativeInterface.fileOpen) {
|
||||
pageStack.clear()
|
||||
initStack()
|
||||
|
@ -428,7 +398,7 @@ Kirigami.ApplicationWindow {
|
|||
id: clearRecentFilesActionComponent
|
||||
Kirigami.Action {
|
||||
text: qsTr("Clear recently opened files")
|
||||
iconName: "edit-clear"
|
||||
icon.name: "edit-clear"
|
||||
onTriggered: {
|
||||
nativeInterface.clearRecentFiles()
|
||||
leftMenu.resetMenu()
|
||||
|
|
|
@ -5,12 +5,11 @@
|
|||
|
||||
#include <c++utilities/conversion/stringbuilder.h>
|
||||
|
||||
#include <QAndroidJniObject>
|
||||
#include <QColor>
|
||||
#include <QCoreApplication>
|
||||
#include <QJniObject>
|
||||
#include <QMessageLogContext>
|
||||
#include <QMetaObject>
|
||||
#include <QtAndroid>
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
|
@ -35,9 +34,9 @@ static Controller *controllerForAndroid = nullptr;
|
|||
|
||||
void applyThemingForAndroid()
|
||||
{
|
||||
QtAndroid::runOnAndroidThread([=]() {
|
||||
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() {
|
||||
const auto color = QColor(QLatin1String("#2c714a")).rgba();
|
||||
QAndroidJniObject window = QtAndroid::androidActivity().callObjectMethod("getWindow", "()Landroid/view/Window;");
|
||||
QJniObject window = QJniObject(QNativeInterface::QAndroidApplication::context()).callObjectMethod("getWindow", "()Landroid/view/Window;");
|
||||
window.callMethod<void>("addFlags", "(I)V", Android::WindowManager::LayoutParams::DrawsSystemBarBackgrounds);
|
||||
window.callMethod<void>("clearFlags", "(I)V", Android::WindowManager::LayoutParams::TranslucentStatus);
|
||||
window.callMethod<void>("setStatusBarColor", "(I)V", color);
|
||||
|
@ -52,13 +51,14 @@ void registerControllerForAndroid(Controller *controller)
|
|||
|
||||
bool showAndroidFileDialog(bool existing, bool createNew)
|
||||
{
|
||||
return QtAndroid::androidActivity().callMethod<jboolean>("showAndroidFileDialog", "(ZZ)Z", existing, createNew);
|
||||
return QJniObject(QNativeInterface::QAndroidApplication::context()).callMethod<jboolean>("showAndroidFileDialog", "(ZZ)Z", existing, createNew);
|
||||
}
|
||||
|
||||
int openFileDescriptorFromAndroidContentUrl(const QString &url, const QString &mode)
|
||||
{
|
||||
return QtAndroid::androidActivity().callMethod<jint>("openFileDescriptorFromAndroidContentUri", "(Ljava/lang/String;Ljava/lang/String;)I",
|
||||
QAndroidJniObject::fromString(url).object<jstring>(), QAndroidJniObject::fromString(mode).object<jstring>());
|
||||
return QJniObject(QNativeInterface::QAndroidApplication::context())
|
||||
.callMethod<jint>("openFileDescriptorFromAndroidContentUri", "(Ljava/lang/String;Ljava/lang/String;)I",
|
||||
QJniObject::fromString(url).object<jstring>(), QJniObject::fromString(mode).object<jstring>());
|
||||
}
|
||||
|
||||
void writeToAndroidLog(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
|
@ -102,19 +102,20 @@ void setupAndroidSpecifics()
|
|||
static void onAndroidError(JNIEnv *, jobject, jstring message)
|
||||
{
|
||||
QMetaObject::invokeMethod(
|
||||
QtGui::controllerForAndroid, "newNotification", Qt::QueuedConnection, Q_ARG(QString, QAndroidJniObject::fromLocalRef(message).toString()));
|
||||
QtGui::controllerForAndroid, "newNotification", Qt::QueuedConnection, Q_ARG(QString, QJniObject::fromLocalRef(message).toString()));
|
||||
}
|
||||
|
||||
static void onAndroidFileDialogAccepted(JNIEnv *, jobject, jstring fileName, jboolean existing, jboolean createNew)
|
||||
{
|
||||
QMetaObject::invokeMethod(QtGui::controllerForAndroid, "handleFileSelectionAccepted", Qt::QueuedConnection,
|
||||
Q_ARG(QString, QAndroidJniObject::fromLocalRef(fileName).toString()), Q_ARG(bool, existing), Q_ARG(bool, createNew));
|
||||
Q_ARG(QString, QJniObject::fromLocalRef(fileName).toString()), Q_ARG(bool, existing), Q_ARG(bool, createNew));
|
||||
}
|
||||
|
||||
static void onAndroidFileDialogAcceptedDescriptor(JNIEnv *, jobject, jstring nativeUrl, jstring fileName, jint fileHandle, jboolean existing, jboolean createNew)
|
||||
static void onAndroidFileDialogAcceptedDescriptor(
|
||||
JNIEnv *, jobject, jstring nativeUrl, jstring fileName, jint fileHandle, jboolean existing, jboolean createNew)
|
||||
{
|
||||
QMetaObject::invokeMethod(QtGui::controllerForAndroid, "handleFileSelectionAcceptedDescriptor", Qt::QueuedConnection,
|
||||
Q_ARG(QString, QAndroidJniObject::fromLocalRef(nativeUrl).toString()), Q_ARG(QString, QAndroidJniObject::fromLocalRef(fileName).toString()),
|
||||
Q_ARG(QString, QJniObject::fromLocalRef(nativeUrl).toString()), Q_ARG(QString, QJniObject::fromLocalRef(fileName).toString()),
|
||||
Q_ARG(int, fileHandle), Q_ARG(bool, existing), Q_ARG(bool, createNew));
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
#include <passwordfile/io/cryptoexception.h>
|
||||
#include <passwordfile/io/parsingexception.h>
|
||||
|
||||
#include <qtutilities/misc/compat.h>
|
||||
#include <qtutilities/misc/dialogutils.h>
|
||||
#include <qtutilities/resources/resources.h>
|
||||
|
||||
#include <c++utilities/io/nativefilestream.h>
|
||||
#include <c++utilities/io/path.h>
|
||||
|
@ -41,14 +43,15 @@ Controller::Controller(QSettings &settings, const QString &filePath, QObject *pa
|
|||
#endif
|
||||
, m_fileOpen(false)
|
||||
, m_fileModified(false)
|
||||
, m_useNativeFileDialog(false)
|
||||
, m_useNativeFileDialog(supportsNativeFileDialog())
|
||||
, m_filterAsDialog(
|
||||
#ifdef Q_OS_ANDROID
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
)
|
||||
)
|
||||
, m_darkModeEnabled(false)
|
||||
{
|
||||
m_fieldModel.setPasswordVisibility(PasswordVisibility::Never);
|
||||
m_entryFilterModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
|
@ -73,9 +76,9 @@ Controller::Controller(QSettings &settings, const QString &filePath, QObject *pa
|
|||
void Controller::setFilePath(const QString &filePath)
|
||||
{
|
||||
// get rid of file:// prefix
|
||||
QStringRef actualFilePath(&filePath);
|
||||
auto actualFilePath = makeStringView(filePath);
|
||||
if (filePath.startsWith(QLatin1String("file:"))) {
|
||||
actualFilePath = filePath.midRef(5);
|
||||
actualFilePath = midRef(filePath, 5);
|
||||
}
|
||||
while (actualFilePath.startsWith(QLatin1String("//"))) {
|
||||
actualFilePath = actualFilePath.mid(1);
|
||||
|
@ -121,6 +124,9 @@ void Controller::init()
|
|||
if (!m_filePath.isEmpty()) {
|
||||
load();
|
||||
}
|
||||
if (const auto error = QtUtilities::errorMessageForSettings(m_settings); !error.isEmpty()) {
|
||||
emit settingsError(error);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::load()
|
||||
|
@ -129,7 +135,7 @@ void Controller::load()
|
|||
m_file.load();
|
||||
m_entryModel.setRootEntry(m_file.rootEntry());
|
||||
if (!m_entryModel.rootEntry()) {
|
||||
emit fileError(tr("An error occured when opening the file: root element missing"), QStringLiteral("load"));
|
||||
emit fileError(tr("An error occurred when opening the file: root element missing"), QStringLiteral("load"));
|
||||
return;
|
||||
}
|
||||
setFileOpen(true);
|
||||
|
@ -140,7 +146,7 @@ void Controller::load()
|
|||
} else {
|
||||
// clear password since the password which has been provided likely wasn't correct
|
||||
clearPassword();
|
||||
emit fileError(tr("A crypto error occured when opening the file: ") + QString::fromLocal8Bit(e.what()), QStringLiteral("load"));
|
||||
emit fileError(tr("A crypto error occurred when opening the file: ") + QString::fromLocal8Bit(e.what()), QStringLiteral("load"));
|
||||
}
|
||||
} catch (...) {
|
||||
if ((m_file.saveOptions() & PasswordFileSaveFlags::Encryption) && m_password.isEmpty()) {
|
||||
|
@ -230,7 +236,7 @@ void Controller::save()
|
|||
#endif
|
||||
emit fileSaved();
|
||||
} catch (const CryptoException &e) {
|
||||
emit fileError(tr("A crypto error occured when saving the file: ") + QString::fromLocal8Bit(e.what()), QStringLiteral("save"));
|
||||
emit fileError(tr("A crypto error occurred when saving the file: ") + QString::fromLocal8Bit(e.what()), QStringLiteral("save"));
|
||||
} catch (...) {
|
||||
emitFileError(tr("saving"));
|
||||
}
|
||||
|
@ -452,14 +458,14 @@ void Controller::emitFileError(const QString &when)
|
|||
throw;
|
||||
} catch (const std::ios_base::failure &failure) {
|
||||
emit fileError(
|
||||
tr("An IO error occured when %1 the file %2: ").arg(when, m_filePath) + QString::fromLocal8Bit(failure.what()), QStringLiteral("load"));
|
||||
tr("An IO error occurred when %1 the file %2: ").arg(when, m_filePath) + QString::fromLocal8Bit(failure.what()), QStringLiteral("load"));
|
||||
} catch (const runtime_error &e) {
|
||||
emit fileError(tr("An error occured when %1 the file: ").arg(when) + QString::fromLocal8Bit(e.what()), QStringLiteral("save"));
|
||||
emit fileError(tr("An error occurred when %1 the file: ").arg(when) + QString::fromLocal8Bit(e.what()), QStringLiteral("save"));
|
||||
} catch (const exception &e) {
|
||||
emit fileError(tr("An unknown exception occured when %1 the file %2: ").arg(when, m_filePath) + QString::fromLocal8Bit(e.what()),
|
||||
emit fileError(tr("An unknown exception occurred when %1 the file %2: ").arg(when, m_filePath) + QString::fromLocal8Bit(e.what()),
|
||||
QStringLiteral("load"));
|
||||
} catch (...) {
|
||||
emit fileError(tr("An unknown error occured when %1 the file %2.").arg(when, m_filePath), QStringLiteral("load"));
|
||||
emit fileError(tr("An unknown error occurred when %1 the file %2.").arg(when, m_filePath), QStringLiteral("load"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -483,11 +489,19 @@ void Controller::setUseNativeFileDialog(bool useNativeFileDialog)
|
|||
|
||||
void Controller::setEntryFilter(const QString &filter)
|
||||
{
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
|
||||
const auto previousFilter(m_entryFilterModel.filterRegularExpression().pattern());
|
||||
#else
|
||||
const auto previousFilter(m_entryFilterModel.filterRegExp().pattern());
|
||||
#endif
|
||||
if (filter == previousFilter) {
|
||||
return;
|
||||
}
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
|
||||
m_entryFilterModel.setFilterRegularExpression(filter);
|
||||
#else
|
||||
m_entryFilterModel.setFilterRegExp(filter);
|
||||
#endif
|
||||
emit entryFilterChanged(filter);
|
||||
if (previousFilter.isEmpty() != filter.isEmpty()) {
|
||||
emit hasEntryFilterChanged(!filter.isEmpty());
|
||||
|
|
|
@ -44,6 +44,7 @@ class Controller : public QObject {
|
|||
Q_PROPERTY(QUndoStack *undoStack READ undoStack NOTIFY undoStackChanged)
|
||||
Q_PROPERTY(QString undoText READ undoText NOTIFY undoTextChanged)
|
||||
Q_PROPERTY(QString redoText READ redoText NOTIFY redoTextChanged)
|
||||
Q_PROPERTY(bool darkModeEnabled READ isDarkModeEnabled WRITE setDarkModeEnabled NOTIFY darkModeEnabledChanged)
|
||||
|
||||
public:
|
||||
explicit Controller(QSettings &settings, const QString &filePath = QString(), QObject *parent = nullptr);
|
||||
|
@ -85,6 +86,9 @@ public:
|
|||
QUndoStack *undoStack();
|
||||
QString undoText() const;
|
||||
QString redoText() const;
|
||||
Io::PasswordFileSaveFlags prepareSaving();
|
||||
bool isDarkModeEnabled() const;
|
||||
void setDarkModeEnabled(bool darkModeEnabled);
|
||||
|
||||
public Q_SLOTS:
|
||||
void init();
|
||||
|
@ -101,7 +105,6 @@ public Q_SLOTS:
|
|||
void handleFileSelectionCanceled();
|
||||
void undo();
|
||||
void redo();
|
||||
Io::PasswordFileSaveFlags prepareSaving();
|
||||
QString computeFileSummary();
|
||||
|
||||
Q_SIGNALS:
|
||||
|
@ -128,6 +131,8 @@ Q_SIGNALS:
|
|||
void undoStackChanged(QUndoStack *undoStack);
|
||||
void undoTextChanged(const QString &undoText);
|
||||
void redoTextChanged(const QString &redoText);
|
||||
void settingsError(const QString &errorMessage);
|
||||
void darkModeEnabledChanged(bool darkModeEnabled);
|
||||
|
||||
private Q_SLOTS:
|
||||
void handleEntriesRemoved(const QModelIndex &parentIndex, int first, int last);
|
||||
|
@ -159,6 +164,7 @@ private:
|
|||
bool m_fileModified;
|
||||
bool m_useNativeFileDialog;
|
||||
bool m_filterAsDialog;
|
||||
bool m_darkModeEnabled;
|
||||
};
|
||||
|
||||
inline QModelIndex Controller::ensureSourceEntryIndex(const QModelIndex &entryIndexMaybeFromFilterModel) const
|
||||
|
@ -296,12 +302,20 @@ inline QModelIndex Controller::filterEntryIndex(const QModelIndex &entryIndex) c
|
|||
|
||||
inline QString Controller::entryFilter() const
|
||||
{
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
|
||||
return m_entryFilterModel.filterRegularExpression().pattern();
|
||||
#else
|
||||
return m_entryFilterModel.filterRegExp().pattern();
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool Controller::hasEntryFilter() const
|
||||
{
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
|
||||
return !m_entryFilterModel.filterRegularExpression().pattern().isEmpty();
|
||||
#else
|
||||
return !m_entryFilterModel.filterRegExp().isEmpty();
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool Controller::filterAsDialog() const
|
||||
|
@ -336,6 +350,18 @@ inline QString Controller::redoText() const
|
|||
#endif
|
||||
}
|
||||
|
||||
inline bool Controller::isDarkModeEnabled() const
|
||||
{
|
||||
return m_darkModeEnabled;
|
||||
}
|
||||
|
||||
inline void Controller::setDarkModeEnabled(bool darkModeEnabled)
|
||||
{
|
||||
if (darkModeEnabled != m_darkModeEnabled) {
|
||||
emit darkModeEnabledChanged(m_darkModeEnabled = darkModeEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
inline void Controller::undo()
|
||||
{
|
||||
#ifdef PASSWORD_MANAGER_UNDO_SUPPORT
|
||||
|
|
|
@ -10,26 +10,29 @@
|
|||
// enable inline helper functions for Qt Quick provided by qtutilities
|
||||
#define QT_UTILITIES_GUI_QTQUICK
|
||||
|
||||
// ensure QGuiApplication is defined before resources.h for desktop file name
|
||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
||||
#include <QApplication>
|
||||
using App = QApplication;
|
||||
#else
|
||||
#include <QGuiApplication>
|
||||
using App = QGuiApplication;
|
||||
#endif
|
||||
|
||||
#include <qtutilities/misc/desktoputils.h>
|
||||
#include <qtutilities/resources/qtconfigarguments.h>
|
||||
#include <qtutilities/resources/resources.h>
|
||||
#include <qtutilities/settingsdialog/qtsettings.h>
|
||||
|
||||
#include <passwordfile/util/openssl.h>
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QDebug>
|
||||
#include <QIcon>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QSettings>
|
||||
#include <QTextCodec>
|
||||
#include <QtQml>
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QDebug>
|
||||
#include <QDirIterator>
|
||||
#endif
|
||||
|
||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
||||
#include <QApplication>
|
||||
#endif
|
||||
#include <cstdlib>
|
||||
|
||||
using namespace CppUtilities;
|
||||
using namespace Util;
|
||||
|
@ -43,64 +46,75 @@ int runQuickGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, c
|
|||
setupAndroidSpecifics();
|
||||
#endif
|
||||
|
||||
// work around kirigami plugin trying to be clever
|
||||
if (!qEnvironmentVariableIsSet("XDG_CURRENT_DESKTOP")) {
|
||||
qputenv("XDG_CURRENT_DESKTOP", QByteArray("please don't override my settings"));
|
||||
}
|
||||
|
||||
// init OpenSSL
|
||||
OpenSsl::init();
|
||||
|
||||
// init application
|
||||
SET_QT_APPLICATION_INFO;
|
||||
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
||||
QApplication application(argc, argv);
|
||||
auto application = App(argc, argv);
|
||||
QObject::connect(&application, &QCoreApplication::aboutToQuit, &OpenSsl::clean);
|
||||
|
||||
// restore Qt settings
|
||||
auto qtSettings = QtUtilities::QtSettings();
|
||||
auto settings = QtUtilities::getSettings(QStringLiteral(PROJECT_NAME));
|
||||
if (auto settingsError = QtUtilities::errorMessageForSettings(*settings); !settingsError.isEmpty()) {
|
||||
qDebug() << settingsError;
|
||||
}
|
||||
qtSettings.restore(*settings);
|
||||
qtSettings.apply();
|
||||
|
||||
// create controller and handle dark mode
|
||||
// note: Not handling changes of the dark mode setting dynamically yet because it does not work with Kirigami.
|
||||
// It looks like Kirigami does not follow the QCC2 theme (the Material.theme/Material.theme settings) but
|
||||
// instead uses colors based on the initial palette. Not sure how to toggle Kirigami's palette in accordance
|
||||
// with the QCC2 theme. Hence this code is disabled via APPLY_COLOR_SCHEME_DYNAMICALLY for now.
|
||||
auto controller = Controller(*settings, file);
|
||||
#ifdef APPLY_COLOR_SCHEME_DYNAMICALLY
|
||||
QtUtilities::onDarkModeChanged(
|
||||
[&qtSettings, &controller](bool isDarkModeEnabled) {
|
||||
qtSettings.reapplyDefaultIconTheme(isDarkModeEnabled);
|
||||
controller.setDarkModeEnabled(isDarkModeEnabled);
|
||||
},
|
||||
&controller);
|
||||
#else
|
||||
QGuiApplication application(argc, argv);
|
||||
const auto isDarkModeEnabled = QtUtilities::isDarkModeEnabled().value_or(false);
|
||||
qtSettings.reapplyDefaultIconTheme(isDarkModeEnabled);
|
||||
controller.setDarkModeEnabled(isDarkModeEnabled);
|
||||
#endif
|
||||
|
||||
// apply settings specified via command line args
|
||||
qtConfigArgs.applySettings();
|
||||
qtConfigArgs.applySettings(qtSettings.hasCustomFont());
|
||||
qtConfigArgs.applySettingsForQuickGui();
|
||||
|
||||
// assume we're bundling breeze icons
|
||||
if (QIcon::themeName().isEmpty()) {
|
||||
QIcon::setThemeName(QStringLiteral("breeze"));
|
||||
}
|
||||
|
||||
// log resource information
|
||||
#if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_DEBUG_BUILD)
|
||||
qDebug() << "Using icon theme" << QIcon::themeName();
|
||||
qDebug() << "Icon theme search paths" << QIcon::themeSearchPaths();
|
||||
qDebug() << "Resources:";
|
||||
QDirIterator it(QStringLiteral(":/"), QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
qDebug() << it.next();
|
||||
}
|
||||
#endif
|
||||
|
||||
// load settings from configuration file
|
||||
QSettings settings(QSettings::IniFormat, QSettings::UserScope, QStringLiteral(PROJECT_NAME));
|
||||
|
||||
// load translations
|
||||
LOAD_QT_TRANSLATIONS;
|
||||
|
||||
// init Quick GUI
|
||||
QQmlApplicationEngine engine;
|
||||
Controller controller(settings, file);
|
||||
// init QML engine
|
||||
auto engine = QQmlApplicationEngine();
|
||||
#ifdef Q_OS_ANDROID
|
||||
registerControllerForAndroid(&controller);
|
||||
#endif
|
||||
auto *const context(engine.rootContext());
|
||||
auto *const context = engine.rootContext();
|
||||
context->setContextProperty(QStringLiteral("nativeInterface"), &controller);
|
||||
context->setContextProperty(QStringLiteral("app"), &application);
|
||||
context->setContextProperty(QStringLiteral("description"), QStringLiteral(APP_DESCRIPTION));
|
||||
context->setContextProperty(QStringLiteral("dependencyVersions"), QStringList(DEPENCENCY_VERSIONS));
|
||||
engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml")));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
const auto importPaths = qEnvironmentVariable(PROJECT_VARNAME_UPPER "_QML_IMPORT_PATHS").split(QChar(':'));
|
||||
for (const auto &path : importPaths) {
|
||||
engine.addImportPath(path);
|
||||
}
|
||||
#endif
|
||||
|
||||
// run event loop
|
||||
QObject::connect(&application, &QCoreApplication::aboutToQuit, &OpenSsl::clean);
|
||||
// load main QML file; run event loop or exit if it cannot be loaded
|
||||
const auto mainUrl = QUrl(QStringLiteral("qrc:/qml/main.qml"));
|
||||
QObject::connect(
|
||||
&engine, &QQmlApplicationEngine::objectCreated, &application,
|
||||
[&mainUrl](QObject *obj, const QUrl &objUrl) {
|
||||
if (!obj && objUrl == mainUrl) {
|
||||
QCoreApplication::exit(EXIT_FAILURE);
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
engine.load(mainUrl);
|
||||
return application.exec();
|
||||
}
|
||||
} // namespace QtGui
|
||||
|
|
|
@ -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="PasswordDialog.qml">../qml/PasswordDialog.qml</file>
|
||||
<file alias="EntriesPage.qml">../qml/EntriesPage.qml</file>
|
||||
<file alias="EntryDelegate.qml">../qml/EntryDelegate.qml</file>
|
||||
<file alias="FieldsPage.qml">../qml/FieldsPage.qml</file>
|
||||
<file alias="FieldsDelegate.qml">../qml/FieldsDelegate.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<RCC>
|
||||
<qresource prefix="/qml">
|
||||
<file alias="FileDialog.qml">../qml/FileDialog5.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
|
@ -0,0 +1,5 @@
|
|||
<RCC>
|
||||
<qresource prefix="/qml">
|
||||
<file alias="FileDialog.qml">../qml/FileDialog6.qml</file>
|
||||
</qresource>
|
||||
</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