diff --git a/android/src/org/martchus/passwordmanager/Activity.java b/android/src/org/martchus/passwordmanager/Activity.java index bb28ac5..6311af4 100644 --- a/android/src/org/martchus/passwordmanager/Activity.java +++ b/android/src/org/martchus/passwordmanager/Activity.java @@ -12,16 +12,16 @@ import java.io.FileNotFoundException; import org.qtproject.qt5.android.bindings.QtActivity; public class Activity extends QtActivity { - private final int REQUEST_CODE_PICK_DIR = 1; - private final int REQUEST_CODE_PICK_EXISTING_FILE = 2; - private final int REQUEST_CODE_PICK_NEW_FILE = 3; + private final int REQUEST_CODE_OPEN_EXISTING_FILE = 1; + private final int REQUEST_CODE_CREATE_NEW_FILE = 2; + private final int REQUEST_CODE_SAVE_FILE_AS = 3; /*! * \brief Shows the native Android file dialog. Results are handled in onActivityResult(). */ - public boolean showAndroidFileDialog(boolean existing) { + public boolean showAndroidFileDialog(boolean existing, boolean createNew) { String action = existing ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_CREATE_DOCUMENT; - int requestCode = existing ? REQUEST_CODE_PICK_EXISTING_FILE : REQUEST_CODE_PICK_NEW_FILE; + int requestCode = existing ? REQUEST_CODE_OPEN_EXISTING_FILE : (createNew ? REQUEST_CODE_CREATE_NEW_FILE : REQUEST_CODE_SAVE_FILE_AS); Intent intent = new Intent(action); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); @@ -51,9 +51,12 @@ public class Activity extends QtActivity { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { - case REQUEST_CODE_PICK_EXISTING_FILE: - case REQUEST_CODE_PICK_NEW_FILE: - boolean existingFile = requestCode == REQUEST_CODE_PICK_EXISTING_FILE; + case REQUEST_CODE_OPEN_EXISTING_FILE: + case REQUEST_CODE_CREATE_NEW_FILE: + case REQUEST_CODE_SAVE_FILE_AS: + boolean createNew = requestCode == REQUEST_CODE_CREATE_NEW_FILE; + boolean existingFile = requestCode == REQUEST_CODE_OPEN_EXISTING_FILE; + boolean saveAs = requestCode == REQUEST_CODE_SAVE_FILE_AS; if (resultCode != RESULT_OK) { onAndroidFileDialogRejected(); @@ -65,7 +68,7 @@ public class Activity extends QtActivity { try { DocumentFile file = DocumentFile.fromSingleUri(this, uri); ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(file.getUri(), existingFile ? "r" : "wt"); - onAndroidFileDialogAcceptedDescriptor(file.getUri().toString(), file.getName(), fd.detachFd(), existingFile); + onAndroidFileDialogAcceptedDescriptor(file.getUri().toString(), file.getName(), fd.detachFd(), existingFile, createNew); } catch (FileNotFoundException e) { onAndroidError("Failed to find selected file."); } @@ -74,7 +77,7 @@ public class Activity extends QtActivity { String fileName = data.getDataString(); if (fileName != null) { - onAndroidFileDialogAccepted(fileName, existingFile); + onAndroidFileDialogAccepted(fileName, existingFile, createNew); return; } onAndroidError("Failed to read result from Android's file dialog."); @@ -85,7 +88,7 @@ public class Activity extends QtActivity { } public static native void onAndroidError(String message); - public static native void onAndroidFileDialogAccepted(String fileName, boolean existing); - public static native void onAndroidFileDialogAcceptedDescriptor(String nativeUrl, String fileName, int fileDescriptor, boolean existing); + public static native void onAndroidFileDialogAccepted(String fileName, boolean existing, boolean createNew); + public static native void onAndroidFileDialogAcceptedDescriptor(String nativeUrl, String fileName, int fileDescriptor, boolean existing, boolean createNew); public static native void onAndroidFileDialogRejected(); } diff --git a/qml/main.qml b/qml/main.qml index 5636d50..665aae4 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -128,6 +128,13 @@ Kirigami.ApplicationWindow { onTriggered: nativeInterface.save() shortcut: StandardKey.Save }, + Kirigami.Action { + text: qsTr("Save as") + enabled: nativeInterface.fileOpen + iconName: "document-save-as" + onTriggered: fileDialog.saveAs() + shortcut: StandardKey.SaveAs + }, Kirigami.Action { text: nativeInterface.passwordSet ? qsTr("Change password") : qsTr( "Add password") @@ -250,19 +257,21 @@ Kirigami.ApplicationWindow { FileDialog { id: fileDialog - title: selectExisting ? qsTr("Select an existing file") : qsTr( - "Select path for new file") + 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) + nativeInterface.handleFileSelectionAccepted(fileUrls[0], "", + this.selectExisting, + this.createNewFile) } onRejected: nativeInterface.handleFileSelectionCanceled() function show() { - if (nativeInterface.showNativeFileDialog(this.selectExisting)) { + if (nativeInterface.showNativeFileDialog(this.selectExisting, + this.createNewFile)) { return } // fallback to the Qt Quick file dialog if a native implementation is not available @@ -270,10 +279,17 @@ Kirigami.ApplicationWindow { } 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() } } @@ -325,12 +341,16 @@ Kirigami.ApplicationWindow { } } onFileError: { - if (retryAction.length === 0) { + var retryMethod = null + if (retryAction === "load" || retryAction === "save") { + retryMethod = retryAction + } + if (retryMethod) { showPassiveNotification(errorMessage) } else { showPassiveNotification(errorMessage, 2500, qsTr("Retry"), function () { - nativeInterface[retryAction]() + nativeInterface[retryMethod]() }) } } @@ -396,7 +416,11 @@ Kirigami.ApplicationWindow { Kirigami.Action { property string filePath text: filePath.substring(filePath.lastIndexOf('/') + 1) - onTriggered: nativeInterface.load(filePath) + onTriggered: { + nativeInterface.clear() + nativeInterface.filePath = filePath + nativeInterface.load() + } } } diff --git a/quickgui/android.cpp b/quickgui/android.cpp index b6030c2..726fc99 100644 --- a/quickgui/android.cpp +++ b/quickgui/android.cpp @@ -50,9 +50,9 @@ void registerControllerForAndroid(Controller *controller) controllerForAndroid = controller; } -bool showAndroidFileDialog(bool existing) +bool showAndroidFileDialog(bool existing, bool createNew) { - return QtAndroid::androidActivity().callMethod("showAndroidFileDialog", "(Z)Z", existing); + return QtAndroid::androidActivity().callMethod("showAndroidFileDialog", "(ZZ)Z", existing, createNew); } int openFileDescriptorFromAndroidContentUrl(const QString &url, const QString &mode) @@ -105,17 +105,17 @@ static void onAndroidError(JNIEnv *, jobject, jstring message) QtGui::controllerForAndroid, "newNotification", Qt::QueuedConnection, Q_ARG(QString, QAndroidJniObject::fromLocalRef(message).toString())); } -static void onAndroidFileDialogAccepted(JNIEnv *, jobject, jstring fileName, jboolean existing) +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(QString, QAndroidJniObject::fromLocalRef(fileName).toString()), Q_ARG(bool, existing), Q_ARG(bool, createNew)); } -static void onAndroidFileDialogAcceptedDescriptor(JNIEnv *, jobject, jstring nativeUrl, jstring fileName, jint fileHandle, jboolean existing) +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(int, fileHandle), Q_ARG(bool, existing)); + Q_ARG(int, fileHandle), Q_ARG(bool, existing), Q_ARG(bool, createNew)); } static void onAndroidFileDialogRejected(JNIEnv *, jobject) @@ -144,8 +144,8 @@ JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) // register native methods static const JNINativeMethod methods[] = { { "onAndroidError", "(Ljava/lang/String;)V", reinterpret_cast(onAndroidError) }, - { "onAndroidFileDialogAccepted", "(Ljava/lang/String;Z)V", reinterpret_cast(onAndroidFileDialogAccepted) }, - { "onAndroidFileDialogAcceptedDescriptor", "(Ljava/lang/String;Ljava/lang/String;IZ)V", + { "onAndroidFileDialogAccepted", "(Ljava/lang/String;ZZ)V", reinterpret_cast(onAndroidFileDialogAccepted) }, + { "onAndroidFileDialogAcceptedDescriptor", "(Ljava/lang/String;Ljava/lang/String;IZZ)V", reinterpret_cast(onAndroidFileDialogAcceptedDescriptor) }, { "onAndroidFileDialogRejected", "()V", reinterpret_cast(onAndroidFileDialogRejected) }, }; diff --git a/quickgui/android.h b/quickgui/android.h index d978cab..d0a6edd 100644 --- a/quickgui/android.h +++ b/quickgui/android.h @@ -12,7 +12,7 @@ class Controller; void applyThemingForAndroid(); void registerControllerForAndroid(Controller *controller); -bool showAndroidFileDialog(bool existing); +bool showAndroidFileDialog(bool existing, bool createNew); int openFileDescriptorFromAndroidContentUrl(const QString &url, const QString &mode); void writeToAndroidLog(QtMsgType type, const QMessageLogContext &context, const QString &msg); void setupAndroidSpecifics(); diff --git a/quickgui/controller.cpp b/quickgui/controller.cpp index d325890..62cf748 100644 --- a/quickgui/controller.cpp +++ b/quickgui/controller.cpp @@ -77,25 +77,21 @@ void Controller::setFilePath(const QString &filePath) if (filePath.startsWith(QLatin1String("file:"))) { actualFilePath = filePath.midRef(5); } - while (filePath.startsWith(QLatin1String("//"))) { + while (actualFilePath.startsWith(QLatin1String("//"))) { actualFilePath = actualFilePath.mid(1); } - // skip if this path is already set - if (m_filePath == actualFilePath) { - return; - } - // assign full file path and file name - m_file.clear(); - m_file.setPath(filePath.toLocal8Bit().data()); - m_fileName = QString::fromLocal8Bit(CppUtilities::fileName(m_file.path()).data()); - emit filePathChanged(m_filePath = filePath); - - // clear password so we don't use the password from the previous file - m_password.clear(); + m_filePath = actualFilePath.toString(); + m_file.setPath(m_filePath.toLocal8Bit().toStdString()); + const auto fileName = CppUtilities::fileName(m_file.path()); + m_fileName = QString::fromLocal8Bit(fileName.data(), static_cast(fileName.size())); + emit filePathChanged(m_filePath); // handle recent files + if (m_filePath.isEmpty()) { + return; + } const auto index = m_recentFiles.indexOf(m_filePath); if (!index) { return; @@ -127,13 +123,8 @@ void Controller::init() } } -void Controller::load(const QString &filePath) +void Controller::load() { - if (!filePath.isEmpty()) { - setFilePath(filePath); - } - - resetFileStatus(); try { m_file.load(); m_entryModel.setRootEntry(m_file.rootEntry()); @@ -158,17 +149,9 @@ void Controller::load(const QString &filePath) } } -void Controller::create(const QString &filePath) +void Controller::create() { - if (!filePath.isEmpty()) { - setFilePath(filePath); - } - - resetFileStatus(); try { - if (filePath.isEmpty()) { - m_file.clear(); - } m_file.create(); } catch (...) { emitFileError(tr("creating")); @@ -190,9 +173,20 @@ void Controller::close() } } +void Controller::clear() +{ + try { + m_file.close(); + } catch (...) { + emitFileError(tr("closing")); + } + m_file.clear(); + resetFileStatus(); +} + PasswordFileSaveFlags Controller::prepareSaving() { - auto flags = PasswordFileSaveFlags::Compression | PasswordFileSaveFlags::PasswordHashing; + auto flags = PasswordFileSaveFlags::Compression | PasswordFileSaveFlags::PasswordHashing | PasswordFileSaveFlags::AllowToCreateNewFile; if (!m_password.isEmpty()) { flags |= PasswordFileSaveFlags::Encryption; const auto passwordUtf8(m_password.toUtf8()); @@ -216,7 +210,7 @@ void Controller::save() qDebug() << "Opening new fd for saving, native url: " << m_nativeUrl; const auto newFileDescriptor = openFileDescriptorFromAndroidContentUrl(m_nativeUrl, QStringLiteral("wt")); if (newFileDescriptor < 0) { - emit fileError(tr("Unable to open file descriptor for saving the file.")); + emit fileError(tr("Unable to open file descriptor for saving the file."), QStringLiteral("save")); return; } @@ -243,43 +237,69 @@ void Controller::save() * \brief Shows a native file dialog if supported; otherwise returns false. * \remarks If supported, this method will load/create the selected file (according to \a existing). */ -bool Controller::showNativeFileDialog(bool existing) +bool Controller::showNativeFileDialog(bool existing, bool createNew) { #if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_USE_NATIVE_FILE_BUFFER) if (!m_useNativeFileDialog) { return false; } - return showAndroidFileDialog(existing); + return showAndroidFileDialog(existing, createNew); #else Q_UNUSED(existing) + Q_UNUSED(createNew) return false; #endif } -void Controller::handleFileSelectionAccepted(const QString &filePath, bool existing) +void Controller::handleFileSelectionAccepted(const QString &filePath, const QString &nativeUrl, bool existing, bool createNew) { - m_nativeUrl.clear(); + m_nativeUrl = nativeUrl; + + // assign the "ordinary" file path if one has been passed; otherwise the caller is responsible for handling this + const auto saveAs = !existing && !createNew; + if (!filePath.isEmpty()) { + // clear leftovers from possibly previously opened file unless we want to save the current file under a different location + if (!saveAs) { + m_file.clear(); + } + setFilePath(filePath); + } + cout << "path is still " << m_file.path() << " (2)" << endl; + + if (!saveAs) { + resetFileStatus(); + } + if (existing) { - load(filePath); - } else { - create(filePath); + load(); + } else if (createNew) { + create(); + } else if (saveAs) { + save(); } } #if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_USE_NATIVE_FILE_BUFFER) -void Controller::handleFileSelectionAcceptedDescriptor(const QString &nativeUrl, const QString &fileName, int fileDescriptor, bool existing) +void Controller::handleFileSelectionAcceptedDescriptor( + const QString &nativeUrl, const QString &fileName, int fileDescriptor, bool existing, bool createNew) { + qDebug() << "Opening file descriptor for native url: " << nativeUrl; + qDebug() << "(existing: " << existing << ", create new: " << createNew << ")"; + try { - qDebug() << "Opening fd for native url: " << nativeUrl; + // clear leftovers from possibly previously opened file unless we want to save the current file under a different location + if (existing || createNew) { + m_file.clear(); + } m_file.setPath(fileName.toStdString()); m_file.fileStream().open(fileDescriptor, ios_base::in | ios_base::binary); m_file.opened(); } catch (...) { - emitFileError(tr("opening from native file descriptor")); + emitFileError(existing ? QStringLiteral("load") : (createNew ? QStringLiteral("create") : QStringLiteral("save"))); } + emit filePathChanged(m_filePath = m_fileName = fileName); - handleFileSelectionAccepted(QString(), existing); - m_nativeUrl = nativeUrl; + handleFileSelectionAccepted(QString(), nativeUrl, existing, createNew); } #endif diff --git a/quickgui/controller.h b/quickgui/controller.h index f75085a..b51e6ee 100644 --- a/quickgui/controller.h +++ b/quickgui/controller.h @@ -29,11 +29,11 @@ class Controller : public QObject { Q_PROPERTY(EntryModel *entryModel READ entryModel NOTIFY entryModelChanged) Q_PROPERTY(EntryFilterModel *entryFilterModel READ entryFilterModel NOTIFY entryFilterModelChanged) Q_PROPERTY(FieldModel *fieldModel READ fieldModel NOTIFY fieldModelChanged) - Q_PROPERTY(Io::AccountEntry *currentAccount READ currentAccount WRITE setCurrentAccount NOTIFY currentAccountChanged) + //Q_PROPERTY(Io::AccountEntry *currentAccount READ currentAccount WRITE setCurrentAccount NOTIFY currentAccountChanged) Q_PROPERTY(QModelIndex currentAccountIndex READ currentAccountIndex WRITE setCurrentAccountIndex NOTIFY currentAccountChanged) Q_PROPERTY(QString currentAccountName READ currentAccountName NOTIFY currentAccountChanged) Q_PROPERTY(bool hasCurrentAccount READ hasCurrentAccount NOTIFY currentAccountChanged) - Q_PROPERTY(QList cutEntries READ cutEntries WRITE setCutEntries NOTIFY cutEntriesChanged) + //Q_PROPERTY(QList cutEntries READ cutEntries WRITE setCutEntries NOTIFY cutEntriesChanged) Q_PROPERTY(bool canPaste READ canPaste NOTIFY cutEntriesChanged) Q_PROPERTY(QStringList recentFiles READ recentFiles RESET clearRecentFiles NOTIFY recentFilesChanged) Q_PROPERTY(bool useNativeFileDialog READ useNativeFileDialog WRITE setUseNativeFileDialog NOTIFY useNativeFileDialogChanged) @@ -88,14 +88,15 @@ public: public slots: void init(); - void load(const QString &filePath = QString()); - void create(const QString &filePath = QString()); + void load(); + void create(); void close(); + void clear(); void save(); - bool showNativeFileDialog(bool existing); - void handleFileSelectionAccepted(const QString &filePath, bool existing); + bool showNativeFileDialog(bool existing, bool createNew); + void handleFileSelectionAccepted(const QString &filePath, const QString &nativeUrl, bool existing, bool createNew); #if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_USE_NATIVE_FILE_BUFFER) - void handleFileSelectionAcceptedDescriptor(const QString &nativeUrl, const QString &fileName, int fileDescriptor, bool existing); + void handleFileSelectionAcceptedDescriptor(const QString &nativeUrl, const QString &fileName, int fileDescriptor, bool existing, bool createNew); #endif void handleFileSelectionCanceled(); void undo(); @@ -109,7 +110,7 @@ signals: void passwordRequired(const QString &filePath); void windowTitleChanged(const QString &windowTitle); void fileOpenChanged(bool fileOpen); - void fileError(const QString &errorMessage, const QString &retryAction = QString()); + void fileError(const QString &errorMessage, const QString &retryAction); void fileSaved(); void entryModelChanged(); void entryFilterModelChanged();