├── LICENSE
├── README.md
├── data
├── AndroidImagePicker.jpg
├── DesktopPicker.png
├── IOSImagePicker.jpg
├── QtImagePicker.jpg
└── logo.png
├── qpm.json
├── quicknative.pri
└── source
├── cpp
├── nativefiledialogandroid.cpp
├── nativefiledialogandroid.h
├── nativefiledialogext.cpp
└── nativefiledialogext.h
├── qml
└── nativefiledialog.qml
├── qmldir
└── quicknative.qrc
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Paulo Nogueira
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | The goal of quick native is to provide native user experience in components that require a fine integration in both Android and IOS. Also, this library is inspired by how [RectNative](http://www.reactnative.com) extensions work which briefly means to provide an abstracted interface that's platform independent.
4 |
5 | One of the main reasons to use QML/Qt is the fact that apps don't need to run in their target platform all the time (through emulators or physical devices), instead they can be executed locally which makes the process of writing mobile apps very convenient, therefore components in this library fallback to Qt's standard ones when the running platform does not have the equivalent native one.
6 |
7 | ### Native Components
8 | Component | Native Android | Native IOS | Desktop |
9 | --- | --- | --- | --- |
10 | NativeFileDialog | Provided By QuickNative | Native component provided by Qt | Provided By Qt |
11 | NativeTextInput | *coming soon* | *coming soon* | *coming soon* |
12 |
13 | ### Examples
14 |
15 |
16 | #### Image Picker (NativeFileDialog)
17 | * This component has been successfully tested accross different android versions and devices
18 | * Follows the same api as [FileDialog](http://doc.qt.io/qt-5/qml-qtquick-dialogs-filedialog.html):
19 |
20 | ```javascript
21 |
22 | import QuickNative 0.1
23 |
24 | ...
25 |
26 | NativeFileDialog {
27 | selectMultiple: true
28 | folder: shortcuts.pictures
29 |
30 | Component.onCompleted: {
31 | open()
32 | }
33 |
34 | onFileUrlsChanged: {
35 | console.log(fileUrls)
36 | }
37 | }
38 |
39 | ...
40 |
41 | ```
42 |
43 | #### Result
44 |
45 | Android | IOS | Desktop
46 | --- | --- | --- |
47 |
|
|
48 |
49 |
50 |
51 | ### Installation
52 |
53 | This library is available through [qpm](https://www.qpm.io):
54 |
55 | qpm install com.paulon.quicknative
56 |
57 |
58 | ### Licensing
59 | QuickNative is free software; you can redistribute it and/or modify it under the terms of the MIT License
60 |
61 |
--------------------------------------------------------------------------------
/data/AndroidImagePicker.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paulondc/quicknative/f1e3fa64810fe4658ae9fad4aa49a979e6c763ed/data/AndroidImagePicker.jpg
--------------------------------------------------------------------------------
/data/DesktopPicker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paulondc/quicknative/f1e3fa64810fe4658ae9fad4aa49a979e6c763ed/data/DesktopPicker.png
--------------------------------------------------------------------------------
/data/IOSImagePicker.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paulondc/quicknative/f1e3fa64810fe4658ae9fad4aa49a979e6c763ed/data/IOSImagePicker.jpg
--------------------------------------------------------------------------------
/data/QtImagePicker.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paulondc/quicknative/f1e3fa64810fe4658ae9fad4aa49a979e6c763ed/data/QtImagePicker.jpg
--------------------------------------------------------------------------------
/data/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paulondc/quicknative/f1e3fa64810fe4658ae9fad4aa49a979e6c763ed/data/logo.png
--------------------------------------------------------------------------------
/qpm.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "com.paulon.quicknative",
3 | "description": "Native components for QML",
4 | "author": {
5 | "name": "Paulo Nogueira",
6 | "email": "paulondc@gmail.com"
7 | },
8 | "repository": {
9 | "type": "GITHUB",
10 | "url": "https://github.com/paulondc/quicknative.git"
11 | },
12 | "version": {
13 | "label": "0.2.0"
14 | },
15 | "pri_filename": "quicknative.pri",
16 | "license": "MIT"
17 | }
18 |
--------------------------------------------------------------------------------
/quicknative.pri:
--------------------------------------------------------------------------------
1 | RESOURCES += $$PWD/source/quicknative.qrc
2 |
3 | android {
4 | QT += androidextras
5 |
6 | HEADERS += $$PWD/source/cpp/nativefiledialogext.h \
7 | $$PWD/source/cpp/nativefiledialogandroid.h
8 |
9 | SOURCES += $$PWD/source/cpp/nativefiledialogext.cpp \
10 | $$PWD/source/cpp/nativefiledialogandroid.cpp
11 | }
12 |
--------------------------------------------------------------------------------
/source/cpp/nativefiledialogandroid.cpp:
--------------------------------------------------------------------------------
1 | #include "nativefiledialogandroid.h"
2 | #include
3 | #include
4 |
5 |
6 | void NativeFileDialogAndroid::open(QUrl initialFolder=QUrl()) {
7 |
8 | QAndroidJniObject pickAction = QAndroidJniObject::fromString("android.intent.action.PICK");
9 |
10 | // making sure the pick action is available
11 | if (pickAction.isValid()) {
12 |
13 | QAndroidJniObject intent("android/content/Intent");
14 | QString contentType;
15 |
16 | // querying the android version to make sure the multiple selection feature is available
17 | if (m_selectMultiple && QtAndroid::androidSdkVersion() >= 19) {
18 | intent.callObjectMethod("putExtra","(Ljava/lang/String;Z)Landroid/content/Intent;",
19 | QAndroidJniObject::getStaticObjectField("android/content/Intent", "EXTRA_ALLOW_MULTIPLE", "Ljava/lang/String;").object(), true);
20 | }
21 |
22 | intent.callObjectMethod("setAction", "(Ljava/lang/String;)Landroid/content/Intent;", pickAction.object());
23 |
24 | // pictures
25 | if (initialFolder == QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation))) {
26 | contentType = "image/*";
27 | }
28 |
29 | // pictures & videos
30 | // Using MoviesLocation to also let the user to pick images, the reason for this is
31 | // the fact that the current available shortcuts in Qt are too desktop centric
32 | else if (initialFolder == QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::MoviesLocation))) {
33 | contentType = "image/*,video/*";
34 | }
35 |
36 | // ...
37 | else {
38 | contentType = "*/*";
39 | }
40 |
41 | intent.callObjectMethod("setType", "(Ljava/lang/String;)Landroid/content/Intent;", QAndroidJniObject::fromString(contentType).object());
42 |
43 | // starting the action
44 | QtAndroid::startActivity(intent.object(), PICK_FILE, this);
45 | }
46 | else {
47 | throw "Could not load pick action!";
48 | }
49 | }
50 |
51 | void NativeFileDialogAndroid::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) {
52 |
53 | QStringList result;
54 |
55 | jint RESULT_OK = QAndroidJniObject::getStaticField("android/app/Activity", "RESULT_OK");
56 | if (receiverRequestCode == PICK_FILE && resultCode == RESULT_OK) {
57 |
58 | bool singleFile = true;
59 |
60 | // multiple selection mode
61 | if (m_selectMultiple) {
62 | QAndroidJniObject clipData = data.callObjectMethod("getClipData","()Landroid/content/ClipData;");
63 |
64 | // making sure the multiple selection is really available on the current android,
65 | // this also depends on the gallery app that is used to pick the files, otherwise
66 | // it fallbacks to single file
67 | if (clipData.isValid()) {
68 | singleFile = false;
69 |
70 | int clipDataTotal = clipData.callMethod("getItemCount");
71 |
72 | // querying the selected uris
73 | for (int i=0; i < clipDataTotal; i++){
74 | QAndroidJniObject itemSelected = clipData.callObjectMethod(
75 | "getItemAt",
76 | "(I)Landroid/content/ClipData$Item;", i);
77 |
78 | QAndroidJniObject pathSelected = itemSelected.callObjectMethod("getUri", "()Landroid/net/Uri;");
79 |
80 | QString path = NativeFileDialogAndroid::filePath(pathSelected);
81 |
82 | // making sure the path is not empty (invalid)
83 | if (!path.isEmpty()) {
84 | result.append(path);
85 | }
86 | }
87 | }
88 | }
89 |
90 | // single selection mode
91 | if (singleFile) {
92 |
93 | QAndroidJniObject uri = data.callObjectMethod("getData", "()Landroid/net/Uri;");
94 |
95 | QString path = NativeFileDialogAndroid::filePath(uri);
96 |
97 | // making sure the path is not empty (invalid)
98 | if (!path.isEmpty()){
99 | result.append(path);
100 | }
101 | }
102 | }
103 |
104 | emit selectedFilePaths(result);
105 | }
106 |
107 | QString NativeFileDialogAndroid::filePath(QAndroidJniObject & uri) {
108 |
109 | // in case the uri is already resolved
110 | if (uri.toString().startsWith("file:")) {
111 |
112 | return uri.callObjectMethod("getPath", "()Ljava/lang/String;").toString();
113 | }
114 |
115 | // otherwise query the file path through content resolver
116 | else {
117 |
118 | QAndroidJniEnvironment env;
119 |
120 | QAndroidJniObject mediaStore = QAndroidJniObject::getStaticObjectField("android/provider/MediaStore$MediaColumns", "DATA", "Ljava/lang/String;");
121 |
122 | jobjectArray projection = (jobjectArray)env->NewObjectArray(1, env->FindClass("java/lang/String"), env->NewStringUTF(""));
123 |
124 | env->SetObjectArrayElement(projection, 0, mediaStore.object());
125 |
126 | QAndroidJniObject contentResolver = QtAndroid::androidActivity().callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
127 |
128 | QAndroidJniObject cursor = contentResolver.callObjectMethod("query",
129 | "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;",
130 | uri.object(), projection, NULL, NULL, NULL);
131 |
132 | if (cursor.isValid()) {
133 |
134 | cursor.callMethod("moveToFirst");
135 |
136 | jint columnIndex = cursor.callMethod("getColumnIndexOrThrow","(Ljava/lang/String;)I", mediaStore.object());
137 |
138 | QAndroidJniObject path = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", columnIndex);
139 |
140 | if (!cursor.callMethod("isClosed")) {
141 |
142 | cursor.callObjectMethod("close", "()Z");
143 | }
144 |
145 | // making sure the path has been resolved
146 | if (path.isValid()) {
147 | QString filePath = path.toString();
148 |
149 | if (!filePath.isEmpty()) {
150 | filePath = "file://" + filePath;
151 | return filePath;
152 | }
153 | }
154 | }
155 |
156 | return QString();
157 | }
158 | }
159 |
160 | NativeFileDialogAndroid::NativeFileDialogAndroid(bool selectMultiple, QObject *parent): QObject(parent) {
161 |
162 | m_selectMultiple = selectMultiple;
163 | }
164 |
--------------------------------------------------------------------------------
/source/cpp/nativefiledialogandroid.h:
--------------------------------------------------------------------------------
1 | #ifndef NATIVEFILEDIALOGANDROID_H
2 | #define NATIVEFILEDIALOGANDROID_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 |
12 | // Android's file picker action through JNI
13 | class NativeFileDialogAndroid: public QObject, QAndroidActivityResultReceiver {
14 |
15 | Q_OBJECT
16 | static const int PICK_FILE = 0xFFF;
17 |
18 | private:
19 | static QString filePath(QAndroidJniObject & uri);
20 | void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data);
21 | bool m_selectMultiple;
22 |
23 | public:
24 | NativeFileDialogAndroid(bool selectMultiple, QObject *parent = 0);
25 | void open(QUrl initialFolder);
26 |
27 | signals:
28 | void selectedFilePaths(QStringList result);
29 | };
30 |
31 | #endif // NATIVEFILEDIALOGANDROID_H
32 |
--------------------------------------------------------------------------------
/source/cpp/nativefiledialogext.cpp:
--------------------------------------------------------------------------------
1 | #include "nativefiledialogext.h"
2 | #include
3 |
4 | #ifdef Q_OS_ANDROID
5 | #include "nativefiledialogandroid.h"
6 | #endif
7 |
8 |
9 | NativeFileDialogExt::NativeFileDialogExt(QQuickItem * parent): m_selectMultiple(false), QQuickItem(parent) {
10 |
11 | // file dialog is not visible by default
12 | setVisible(false);
13 |
14 | // also, providing the same shortcuts found in the file dialog (for convenience)
15 | m_shortcuts.insert("desktop", QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)));
16 | m_shortcuts.insert("documents", QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)));
17 | m_shortcuts.insert("home", QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)));
18 | m_shortcuts.insert("music", QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::MusicLocation)));
19 | m_shortcuts.insert("movies", QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::MoviesLocation)));
20 | m_shortcuts.insert("pictures", QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)));
21 | }
22 |
23 | void NativeFileDialogExt::componentComplete() {
24 |
25 | // in case the component is set to visible by default
26 | onVisibleChanged();
27 |
28 | // there are two ways to trigger the picker, either by calling .open() or making it visible,
29 | // for this reason hooking the visibleChanged signal
30 | connect(this, SIGNAL(visibleChanged()), this, SLOT(onVisibleChanged()));
31 | }
32 |
33 | void NativeFileDialogExt::onVisibleChanged() {
34 |
35 | if (isVisible())
36 | open();
37 | }
38 |
39 | void NativeFileDialogExt::open() {
40 |
41 | #ifdef Q_OS_ANDROID
42 | NativeFileDialogAndroid * picker = new NativeFileDialogAndroid(m_selectMultiple);
43 | #endif
44 |
45 | connect(picker, SIGNAL(selectedFilePaths(QStringList)), this, SLOT(setFileUrls(QStringList)));
46 |
47 | // launching the native picker dialog
48 | try {
49 | picker->open(m_folder);
50 | }
51 | catch (const char* exceptionMessage) {
52 | qDebug() << exceptionMessage;
53 | }
54 | }
55 |
56 | QString NativeFileDialogExt::fileUrl() {
57 |
58 | return m_fileUrls.size() ? m_fileUrls[0] : QString();
59 | }
60 |
61 | QStringList NativeFileDialogExt::fileUrls() {
62 |
63 | return m_fileUrls;
64 | }
65 |
66 | bool NativeFileDialogExt::selectMultiple() {
67 |
68 | return m_selectMultiple;
69 | }
70 |
71 | QUrl NativeFileDialogExt::folder() {
72 |
73 | return m_folder;
74 | }
75 |
76 | QVariantMap NativeFileDialogExt::shortcuts() {
77 |
78 | return m_shortcuts;
79 | }
80 |
81 | void NativeFileDialogExt::setFileUrl(QString url) {
82 |
83 | if (url.isEmpty()) {
84 | setFileUrls(QStringList());
85 | }
86 | else{
87 | setFileUrls(QStringList(url));
88 | }
89 | }
90 |
91 | void NativeFileDialogExt::setFileUrls(QStringList urls) {
92 |
93 | if (m_selectMultiple) {
94 | m_fileUrls = urls;
95 | emit fileUrlsChanged();
96 | }
97 | else{
98 | m_fileUrls = urls.size() ? QStringList(urls[0]) : QStringList();
99 | }
100 |
101 | emit fileUrlChanged();
102 | }
103 |
104 | void NativeFileDialogExt::setSelectMultiple(bool multiple) {
105 |
106 | m_selectMultiple = multiple;
107 |
108 | emit selectMultipleChanged();
109 | }
110 |
111 | void NativeFileDialogExt::setFolder(QUrl folder) {
112 |
113 | m_folder = folder;
114 |
115 | emit folderChanged();
116 | }
117 |
118 | NativeFileDialogExt* NativeFileDialogExt::qmlAttachedProperties(QObject * object) {
119 |
120 | return new NativeFileDialogExt();
121 | }
122 |
123 | static void registerNativeFileDialogExt() {
124 | // overriding the qml version when this component is avaiable
125 | qmlRegisterType("QuickNative", 0, 1, "NativeFileDialog");
126 | }
127 |
128 | Q_COREAPP_STARTUP_FUNCTION(registerNativeFileDialogExt)
129 |
--------------------------------------------------------------------------------
/source/cpp/nativefiledialogext.h:
--------------------------------------------------------------------------------
1 | #ifndef NATIVEFILEDIALOGEXT_H
2 | #define NATIVEFILEDIALOGEXT_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 |
12 | // Component that works as wrapper to call the native file dialogue. This class intentionally
13 | // mimics the basic interface of FileDialog, such as support for multiple selection,
14 | // folder, shortcuts, fileUrls and how the dialog is triggered
15 | class NativeFileDialogExt : public QQuickItem {
16 |
17 | Q_OBJECT
18 |
19 | Q_PROPERTY(QString fileUrl READ fileUrl NOTIFY fileUrlChanged)
20 | Q_PROPERTY(QStringList fileUrls READ fileUrls NOTIFY fileUrlsChanged)
21 | Q_PROPERTY(bool selectMultiple READ selectMultiple WRITE setSelectMultiple NOTIFY selectMultipleChanged)
22 | Q_PROPERTY(QUrl folder READ folder WRITE setFolder NOTIFY folderChanged)
23 | Q_PROPERTY(QVariantMap shortcuts READ shortcuts CONSTANT FINAL)
24 |
25 | public:
26 | explicit NativeFileDialogExt(QQuickItem * parent = 0);
27 | static NativeFileDialogExt * qmlAttachedProperties(QObject *object);
28 | Q_INVOKABLE void open();
29 | QString fileUrl();
30 | QStringList fileUrls();
31 | bool selectMultiple();
32 | QUrl folder();
33 | QVariantMap shortcuts();
34 | void componentComplete();
35 |
36 | public slots:
37 | void setFileUrl(QString url);
38 | void setFileUrls(QStringList urls);
39 | void setSelectMultiple(bool multiple);
40 | void setFolder(QUrl folder);
41 |
42 | private slots:
43 | void onVisibleChanged();
44 |
45 | private:
46 | QStringList m_fileUrls;
47 | bool m_selectMultiple;
48 | QUrl m_folder;
49 | QVariantMap m_shortcuts;
50 |
51 | signals:
52 | void fileUrlChanged();
53 | void fileUrlsChanged();
54 | void folderChanged();
55 | void selectMultipleChanged();
56 | };
57 |
58 | QML_DECLARE_TYPEINFO(NativeFileDialogExt, QML_HAS_ATTACHED_PROPERTIES)
59 |
60 | #endif // NATIVEFILEDIALOGEXT_H
61 |
--------------------------------------------------------------------------------
/source/qml/nativefiledialog.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.2
2 | import QtQuick.Dialogs 1.2
3 |
4 |
5 | // this component is available when the platform does not have a spefic
6 | // native file dialog implementation through NativeFileDialogExt
7 | FileDialog {
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/source/qmldir:
--------------------------------------------------------------------------------
1 | module QuickNative
2 | NativeFileDialog 0.1 qml/nativefiledialog.qml
3 |
--------------------------------------------------------------------------------
/source/quicknative.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | qmldir
4 | qml/nativefiledialog.qml
5 |
6 |
7 |
--------------------------------------------------------------------------------