diff --git a/examples/mvvmquick/SampleQuick/SampleQuick.pro b/examples/mvvmquick/SampleQuick/SampleQuick.pro index 0a93ca8..b145764 100644 --- a/examples/mvvmquick/SampleQuick/SampleQuick.pro +++ b/examples/mvvmquick/SampleQuick/SampleQuick.pro @@ -13,7 +13,7 @@ target.path = $$[QT_INSTALL_EXAMPLES]/mvvmquick/$$TARGET INSTALLS += target #not found by linker? -unix:!mac { +linux:!android { LIBS += -L$$OUT_PWD/../../../lib #required to make this the first place to search LIBS += -L$$[QT_INSTALL_LIBS] -licudata LIBS += -L$$[QT_INSTALL_LIBS] -licui18n diff --git a/examples/mvvmquick/SampleQuick/SampleView.qml b/examples/mvvmquick/SampleQuick/SampleView.qml index c401142..b73d697 100644 --- a/examples/mvvmquick/SampleQuick/SampleView.qml +++ b/examples/mvvmquick/SampleQuick/SampleView.qml @@ -15,19 +15,19 @@ Page { title: qsTr("Sample") moreMenu: Menu { - Action { - text: qsTr("Another &Input") + MenuItem { + text: qsTr("Another Input") onTriggered: viewModel.getInput() } - Action { - text: qsTr("Add &Files") + MenuItem { + text: qsTr("Add Files") onTriggered: viewModel.getFiles() } MenuSeparator {} - Action { - text: qsTr("&About") + MenuItem { + text: qsTr("About") onTriggered: viewModel.about() } } diff --git a/examples/mvvmquick/SampleQuick/main.cpp b/examples/mvvmquick/SampleQuick/main.cpp index 3faba02..efa1b84 100644 --- a/examples/mvvmquick/SampleQuick/main.cpp +++ b/examples/mvvmquick/SampleQuick/main.cpp @@ -1,7 +1,9 @@ -#include +#include #include #include #include +#include +#include #include @@ -23,6 +25,7 @@ int main(int argc, char *argv[]) QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication app(argc, argv); + qDebug() << QQuickStyle::availableStyles() << QQuickStyle::name(); qmlRegisterUncreatableType("de.skycoder42.QtMvvm.Sample", 1, 0, "SampleViewModel", QStringLiteral("ViewModels cannot be created")); qmlRegisterUncreatableType("de.skycoder42.QtMvvm.Sample", 1, 0, "ResultViewModel", QStringLiteral("ViewModels cannot be created")); diff --git a/examples/mvvmwidgets/SampleWidgets/SampleWidgets.pro b/examples/mvvmwidgets/SampleWidgets/SampleWidgets.pro index 4ca11af..a2646ae 100644 --- a/examples/mvvmwidgets/SampleWidgets/SampleWidgets.pro +++ b/examples/mvvmwidgets/SampleWidgets/SampleWidgets.pro @@ -6,24 +6,24 @@ TARGET = SampleWidgets HEADERS += \ widgetseventservice.h \ - sampleview.h \ - resultdialog.h + sampleview.h \ + resultdialog.h SOURCES += \ main.cpp \ widgetseventservice.cpp \ - sampleview.cpp \ - resultdialog.cpp + sampleview.cpp \ + resultdialog.cpp FORMS += \ - sampleview.ui \ - resultdialog.ui + sampleview.ui \ + resultdialog.ui target.path = $$[QT_INSTALL_EXAMPLES]/mvvmwidgets/$$TARGET INSTALLS += target #not found by linker? -unix:!mac { +linux:!android { LIBS += -L$$OUT_PWD/../../../lib #required to make this the first place to search LIBS += -L$$[QT_INSTALL_LIBS] -licudata LIBS += -L$$[QT_INSTALL_LIBS] -licui18n diff --git a/src/imports/mvvmquick/AndroidFileDialog.qml b/src/imports/mvvmquick/AndroidFileDialog.qml index 9c36e13..84ac553 100644 --- a/src/imports/mvvmquick/AndroidFileDialog.qml +++ b/src/imports/mvvmquick/AndroidFileDialog.qml @@ -1,5 +1,44 @@ -import QtQuick 2.0 +import QtQuick 2.10 +import de.skycoder42.QtMvvm.Core 1.0 +import de.skycoder42.QtMvvm.Quick 1.0 -Item { +FileChooser { + id: _fileChooser + property var msgConfig + property MessageResult msgResult + + signal closed() + + title: msgConfig.title + folderUrl: msgConfig.defaultValue + type: { + if(msgConfig.subType == "open") + return FileChooser.OpenDocument; + else if(msgConfig.subType == "files") + return FileChooser.OpenMultipleDocuments; + else if(msgConfig.subType == "save") + return FileChooser.CreateDocument; + else if(msgConfig.subType == "get") //special value for android only + return FileChooser.GetContent; + else { + return FileChooser.OpenDocument; + } + } + + onAccepted: { + if(msgResult) { + msgResult.complete(MessageConfig.Ok, result); + msgResult = null; + } + closed(); + } + + onRejected: { + if(msgResult) { + msgResult.complete(MessageConfig.Cancel); + msgResult = null; + } + closed(); + } } diff --git a/src/imports/mvvmquick/AndroidFolderDialog.qml b/src/imports/mvvmquick/AndroidFolderDialog.qml index 9c36e13..3a9810b 100644 --- a/src/imports/mvvmquick/AndroidFolderDialog.qml +++ b/src/imports/mvvmquick/AndroidFolderDialog.qml @@ -1,5 +1,32 @@ -import QtQuick 2.0 +import QtQuick 2.10 +import de.skycoder42.QtMvvm.Core 1.0 +import de.skycoder42.QtMvvm.Quick 1.0 -Item { +FileChooser { + id: _folderChooser + property var msgConfig + property MessageResult msgResult + + signal closed() + + title: msgConfig.title + folderUrl: msgConfig.defaultValue + type: FileChooser.OpenDocumentTree + + onAccepted: { + if(msgResult) { + msgResult.complete(MessageConfig.Ok, result); + msgResult = null; + } + closed(); + } + + onRejected: { + if(msgResult) { + msgResult.complete(MessageConfig.Cancel); + msgResult = null; + } + closed(); + } } diff --git a/src/imports/mvvmquick/FileDialog.qml b/src/imports/mvvmquick/FileDialog.qml index 7a6951f..86fd046 100644 --- a/src/imports/mvvmquick/FileDialog.qml +++ b/src/imports/mvvmquick/FileDialog.qml @@ -9,6 +9,7 @@ Labs.FileDialog { property var msgConfig property MessageResult msgResult + property var mimeTypes: [] signal closed() @@ -26,7 +27,7 @@ Labs.FileDialog { return Labs.FileDialog.OpenFile; //fallback } } - nameFilters: QuickPresenter.mimeTypeFilters(msgConfig.viewProperties["mimeTypes"]) + nameFilters: QuickPresenter.mimeTypeFilters(mimeTypes) Component.onCompleted: { if(msgResult) diff --git a/src/imports/mvvmquick/androidfilechooser.cpp b/src/imports/mvvmquick/androidfilechooser.cpp new file mode 100644 index 0000000..cfe08fe --- /dev/null +++ b/src/imports/mvvmquick/androidfilechooser.cpp @@ -0,0 +1,304 @@ +#include "androidfilechooser.h" +#include +#include +using namespace QtMvvm; + +AndroidFileChooser::AndroidFileChooser(QObject *parent) : + QObject(parent), + _title(), + _folderUrl(), + _type(OpenDocument), + _mimeTypes(QStringLiteral("*/*")), + _flags(OpenableFlag | AlwaysGrantWriteFlag), + _active(false), + _result() +{} + +AndroidFileChooser::~AndroidFileChooser() +{ + qt_noop(); +} + +QString AndroidFileChooser::title() const +{ + return _title; +} + +QUrl AndroidFileChooser::folderUrl() const +{ + return _folderUrl; +} + +AndroidFileChooser::ChooserType AndroidFileChooser::type() const +{ + return _type; +} + +QStringList AndroidFileChooser::mimeTypes() const +{ + return _mimeTypes; +} + +AndroidFileChooser::ChooserFlags AndroidFileChooser::chooserFlags() const +{ + return _flags; +} + +QVariant AndroidFileChooser::result() const +{ + return _result; +} + +void AndroidFileChooser::open() +{ + if(_active) + return; + + QAndroidJniObject intent; + switch (_type) { + case GetContent: + intent = createGetIntent(); + break; + case OpenDocument: + intent = createOpenIntent(); + break; + case OpenMultipleDocuments: + intent = createOpenMultipleIntent(); + break; + case CreateDocument: + intent = createSaveIntent(); + break; + case OpenDocumentTree: + intent = createOpenTreeIntent(); + break; + default: + Q_UNREACHABLE(); + break; + } + + auto chooserIntent = QAndroidJniObject::callStaticObjectMethod("android/content/Intent", "createChooser", + "(Landroid/content/Intent;Ljava/lang/CharSequence;)Landroid/content/Intent;", + intent.object(), + QAndroidJniObject::fromString(_title).object()); + _active = true; + QtAndroid::startActivity(chooserIntent, + _flags.testFlag(PersistPermissionsFlag) ? RequestCodePersist : RequestCodeNormal, + this); +} + +void AndroidFileChooser::setTitle(const QString &title) +{ + if (_title == title) + return; + + _title = title; + emit titleChanged(title); +} + +void AndroidFileChooser::setFolderUrl(const QUrl &contentUrl) +{ + if (_folderUrl == contentUrl) + return; + + _folderUrl = contentUrl; + emit folderUrlChanged(contentUrl); +} + +void AndroidFileChooser::setType(AndroidFileChooser::ChooserType type) +{ + if (_type == type) + return; + + _type = type; + emit typeChanged(type); +} + +void AndroidFileChooser::setMimeTypes(const QStringList &mimeType) +{ + if (_mimeTypes == mimeType) + return; + + _mimeTypes = mimeType; + emit mimeTypesChanged(mimeType); +} + +void AndroidFileChooser::setChooserFlags(ChooserFlags chooserFlags) +{ + if (_flags == chooserFlags) + return; + + _flags = chooserFlags; + emit chooserFlagsChanged(chooserFlags); +} + +void AndroidFileChooser::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) +{ + if(receiverRequestCode == RequestCodeNormal || receiverRequestCode == RequestCodePersist) { + static const auto RESULT_OK = QAndroidJniObject::getStaticField("android/app/Activity", "RESULT_OK"); + static const auto FLAG_GRANT_READ_URI_PERMISSION = QAndroidJniObject::getStaticField("android/content/Intent", "FLAG_GRANT_READ_URI_PERMISSION"); + static const auto FLAG_GRANT_WRITE_URI_PERMISSION = QAndroidJniObject::getStaticField("android/content/Intent", "FLAG_GRANT_WRITE_URI_PERMISSION"); + + if(resultCode == RESULT_OK) { + if(receiverRequestCode == RequestCodePersist) { + auto flags = data.callMethod("getFlags"); + flags &= (FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION); + + auto resolver = QtAndroid::androidContext().callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); + resolver.callMethod("takePersistableUriPermission", "(Landroid/net/Uri;I)V", + data.callObjectMethod("getData", "()Landroid/net/Uri;").object(), + flags); + } + + _result.clear(); + if(_type == OpenMultipleDocuments) { + auto clipData = data.callObjectMethod("getClipData", "()Landroid/content/ClipData;"); + if(clipData.isValid()) { + QList urls; + const auto cnt = clipData.callMethod("getItemCount"); + for(auto i = 0; i < cnt; i++) { + auto item = clipData.callObjectMethod("getItemAt", "(I)Landroid/content/ClipData$Item;", i); + urls.append(item.callObjectMethod("getUri", "()Landroid/net/Uri;").toString()); + } + _result = QVariant::fromValue(urls); + } + } + + if(!_result.isValid()) + _result = QUrl(data.callObjectMethod("getDataString", "()Ljava/lang/String;").toString()); + emit resultChanged(_result); + _active = false; + emit accepted(); + } else { + _active = false; + emit rejected(); + } + } +} + +QAndroidJniObject AndroidFileChooser::createGetIntent() +{ + static const auto ACTION_GET_CONTENT = QAndroidJniObject::getStaticObjectField("android/content/Intent", "ACTION_GET_CONTENT"); + static const auto EXTRA_LOCAL_ONLY = QAndroidJniObject::getStaticObjectField("android/content/Intent", "EXTRA_LOCAL_ONLY"); + + QAndroidJniObject intent("android/content/Intent", "(Ljava/lang/String;)V", + ACTION_GET_CONTENT.object()); + setupBasic(intent); + + if(_flags.testFlag(LocalOnlyFlag)) { + intent.callObjectMethod("putExtra", "(Ljava/lang/String;Z)Landroid/content/Intent;", + EXTRA_LOCAL_ONLY.object(), true); + } + + return intent; +} + +QAndroidJniObject AndroidFileChooser::createOpenIntent() +{ + static const auto ACTION_OPEN_DOCUMENT = QAndroidJniObject::getStaticObjectField("android/content/Intent", "ACTION_OPEN_DOCUMENT"); + + QAndroidJniObject intent("android/content/Intent", "(Ljava/lang/String;)V", + ACTION_OPEN_DOCUMENT.object()); + setupBasic(intent); + + return intent; +} + +QAndroidJniObject AndroidFileChooser::createOpenMultipleIntent() +{ + static const auto EXTRA_ALLOW_MULTIPLE = QAndroidJniObject::getStaticObjectField("android/content/Intent", "EXTRA_ALLOW_MULTIPLE"); + + auto intent = createOpenIntent(); + intent.callObjectMethod("putExtra", "(Ljava/lang/String;Z)Landroid/content/Intent;", + EXTRA_ALLOW_MULTIPLE.object(), true); + + return intent; +} + +QAndroidJniObject AndroidFileChooser::createSaveIntent() +{ + static const auto ACTION_CREATE_DOCUMENT = QAndroidJniObject::getStaticObjectField("android/content/Intent", "ACTION_CREATE_DOCUMENT"); + + QAndroidJniObject intent("android/content/Intent", "(Ljava/lang/String;)V", + ACTION_CREATE_DOCUMENT.object()); + setupBasic(intent); + + return intent; +} + +QAndroidJniObject AndroidFileChooser::createOpenTreeIntent() +{ + static const auto ACTION_OPEN_DOCUMENT_TREE = QAndroidJniObject::getStaticObjectField("android/content/Intent", "ACTION_OPEN_DOCUMENT_TREE"); + + QAndroidJniObject intent("android/content/Intent", "(Ljava/lang/String;)V", + ACTION_OPEN_DOCUMENT_TREE.object()); + setupBasic(intent, true); + + return intent; +} + +void AndroidFileChooser::setupBasic(QAndroidJniObject &intent, bool asTree) +{ + static const auto CATEGORY_OPENABLE = QAndroidJniObject::getStaticObjectField("android/content/Intent", "CATEGORY_OPENABLE"); + + static const auto FLAG_GRANT_PERSISTABLE_URI_PERMISSION = QAndroidJniObject::getStaticField("android/content/Intent", "FLAG_GRANT_PERSISTABLE_URI_PERMISSION"); + static const auto FLAG_GRANT_READ_URI_PERMISSION = QAndroidJniObject::getStaticField("android/content/Intent", "FLAG_GRANT_READ_URI_PERMISSION"); + static const auto FLAG_GRANT_WRITE_URI_PERMISSION = QAndroidJniObject::getStaticField("android/content/Intent", "FLAG_GRANT_WRITE_URI_PERMISSION"); + + static const auto EXTRA_MIME_TYPES = QAndroidJniObject::getStaticObjectField("android/content/Intent", "EXTRA_MIME_TYPES"); + static const auto EXTRA_INITIAL_URI = [](){ + if(QtAndroid::androidSdkVersion() >= 26) //Android Oreo + return QAndroidJniObject::getStaticObjectField("android/content/Intent", "EXTRA_INITIAL_URI"); + else + return QAndroidJniObject(); + }(); + + //set the acceptable mimetypes + if(!asTree) { + if(_mimeTypes.size() == 1) { + intent.callObjectMethod("setTypeAndNormalize", "(Ljava/lang/String;)Landroid/content/Intent;", + QAndroidJniObject::fromString(_mimeTypes.first()).object()); + } else { + intent.callObjectMethod("setType", "(Ljava/lang/String;)Landroid/content/Intent;", + QAndroidJniObject::fromString(QStringLiteral("*/*")).object()); + + if(!_mimeTypes.isEmpty()) { + QAndroidJniEnvironment env; + auto strClass = env->FindClass("java/lang/String"); + QAndroidJniObject strArray(env->NewObjectArray(_mimeTypes.size(), strClass, nullptr)); + for(auto i = 0; i < _mimeTypes.size(); i++) { + auto mimeStr = QAndroidJniObject::callStaticObjectMethod("android/content/Intent", "normalizeMimeType", + "(Ljava/lang/String;)Ljava/lang/String;", + QAndroidJniObject::fromString(_mimeTypes[i]).object()); + env->SetObjectArrayElement(strArray.object(), i, mimeStr.object()); + } + + intent.callObjectMethod("putExtra", "(Ljava/lang/String;[Ljava/lang/String;)Landroid/content/Intent;", + EXTRA_MIME_TYPES.object(), strArray.object()); + } + } + } + + //Set the intent flags + auto aFlags = FLAG_GRANT_READ_URI_PERMISSION; + if(_type != GetContent) + aFlags |= FLAG_GRANT_PERSISTABLE_URI_PERMISSION; + if(_type == CreateDocument || _flags.testFlag(AlwaysGrantWriteFlag)) + aFlags |= FLAG_GRANT_WRITE_URI_PERMISSION; + intent.callObjectMethod("addFlags", "(I)Landroid/content/Intent;", + aFlags); + + //set openable + if(_flags.testFlag(OpenableFlag) && !asTree) { + intent.callObjectMethod("addCategory", "(Ljava/lang/String;)Landroid/content/Intent;", + CATEGORY_OPENABLE.object()); + } + + + if(EXTRA_INITIAL_URI.isValid()) { + auto uri = QAndroidJniObject::callStaticObjectMethod("android/net/Uri", "parse", + "(Ljava/lang/String;)Landroid/net/Uri;", + QAndroidJniObject::fromString(_folderUrl.toString()).object()); + intent.callObjectMethod("putExtra", "(Ljava/lang/String;Z)Landroid/content/Intent;", + EXTRA_INITIAL_URI.object(), uri.object()); + } +} diff --git a/src/imports/mvvmquick/androidfilechooser.h b/src/imports/mvvmquick/androidfilechooser.h new file mode 100644 index 0000000..125d71d --- /dev/null +++ b/src/imports/mvvmquick/androidfilechooser.h @@ -0,0 +1,106 @@ +#ifndef QTMVVM_ANDROIDFILECHOOSER_H +#define QTMVVM_ANDROIDFILECHOOSER_H + +#include +#include +#include + +#include +#include + +namespace QtMvvm { + +class AndroidFileChooser : public QObject, public QAndroidActivityResultReceiver +{ + Q_OBJECT + + Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) + + Q_PROPERTY(QUrl folderUrl READ folderUrl WRITE setFolderUrl NOTIFY folderUrlChanged) + Q_PROPERTY(ChooserType type READ type WRITE setType NOTIFY typeChanged) + Q_PROPERTY(QStringList mimeTypes READ mimeTypes WRITE setMimeTypes NOTIFY mimeTypesChanged) + Q_PROPERTY(ChooserFlags chooserFlags READ chooserFlags WRITE setChooserFlags NOTIFY chooserFlagsChanged) + + Q_PROPERTY(QVariant result READ result NOTIFY resultChanged) + +public: + enum ChooserType { + GetContent = 0, + OpenDocument = 1, + OpenMultipleDocuments = 2, + CreateDocument = 3, + OpenDocumentTree = 4 + }; + Q_ENUM(ChooserType) + + enum ChooserFlag { + OpenableFlag = 0x01, + LocalOnlyFlag = 0x02, + AlwaysGrantWriteFlag = 0x04, + PersistPermissionsFlag = 0x08 + }; + Q_DECLARE_FLAGS(ChooserFlags, ChooserFlag) + Q_FLAG(ChooserFlags) + + explicit AndroidFileChooser(QObject *parent = nullptr); + ~AndroidFileChooser(); + + QString title() const; + QUrl folderUrl() const; + ChooserType type() const; + QStringList mimeTypes() const; + ChooserFlags chooserFlags() const; + + QVariant result() const; + +public Q_SLOTS: + void open(); + + void setTitle(const QString &title); + void setFolderUrl(const QUrl &folderUrl); + void setType(ChooserType type); + void setMimeTypes(const QStringList &mimeTypes); + void setChooserFlags(ChooserFlags chooserFlags); + +Q_SIGNALS: + void accepted(); + void rejected(); + + void titleChanged(const QString &title); + void folderUrlChanged(const QUrl &folderUrl); + void typeChanged(ChooserType type); + void mimeTypesChanged(const QStringList &mimeTypes); + void chooserFlagsChanged(ChooserFlags chooserFlags); + + void resultChanged(QVariant result); + +protected: + void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override; + +private: + const static int RequestCodeNormal = 0x1091c657; + const static int RequestCodePersist = 0x1091c658; + + QString _title; + QUrl _folderUrl; + ChooserType _type; + QStringList _mimeTypes; + ChooserFlags _flags; + + bool _active; + QVariant _result; + + QAndroidJniObject createGetIntent(); + QAndroidJniObject createOpenIntent(); + QAndroidJniObject createOpenMultipleIntent(); + QAndroidJniObject createSaveIntent(); + QAndroidJniObject createOpenTreeIntent(); + + void setupBasic(QAndroidJniObject &intent, bool asTree = false); +}; + +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(QtMvvm::AndroidFileChooser::ChooserFlags) + +#endif // QTMVVM_ANDROIDFILECHOOSER_H diff --git a/src/imports/mvvmquick/mvvmquick.pro b/src/imports/mvvmquick/mvvmquick.pro index eaed5d5..391b409 100644 --- a/src/imports/mvvmquick/mvvmquick.pro +++ b/src/imports/mvvmquick/mvvmquick.pro @@ -29,6 +29,12 @@ QML_FILES += \ OTHER_FILES += qmldir +android { + QT += androidextras + HEADERS += androidfilechooser.h + SOURCES += androidfilechooser.cpp +} + generate_qmltypes { typeextra1.target = qmltypes typeextra1.depends += export LD_LIBRARY_PATH := "$$shadowed($$dirname(_QMAKE_CONF_))/lib/:$$[QT_INSTALL_LIBS]:$(LD_LIBRARY_PATH)" diff --git a/src/imports/mvvmquick/qtmvvmquick_plugin.cpp b/src/imports/mvvmquick/qtmvvmquick_plugin.cpp index 38d5937..26c28f7 100644 --- a/src/imports/mvvmquick/qtmvvmquick_plugin.cpp +++ b/src/imports/mvvmquick/qtmvvmquick_plugin.cpp @@ -6,6 +6,9 @@ #include "qqmlquickpresenter.h" #include "svgimageprovider.h" +#ifdef Q_OS_ANDROID +#include "androidfilechooser.h" +#endif static void initResources() { @@ -39,6 +42,10 @@ void QtMvvmQuickDeclarativeModule::registerTypes(const char *uri) qmlRegisterType(QUrl(QStringLiteral("qrc:/de/skycoder42/qtmvvm/quick/qml/FileDialog.qml")), uri, 1, 0, "FileDialog"); qmlRegisterType(QUrl(QStringLiteral("qrc:/de/skycoder42/qtmvvm/quick/qml/FolderDialog.qml")), uri, 1, 0, "FolderDialog"); +#ifdef Q_OS_ANDROID + qmlRegisterType(uri, 1, 0, "FileChooser"); +#endif + // Check to make shure no module update is forgotten static_assert(VERSION_MAJOR == 1 && VERSION_MINOR == 0, "QML module version needs to be updated"); } diff --git a/src/src.pro b/src/src.pro index b8a0c46..e4a4ccb 100644 --- a/src/src.pro +++ b/src/src.pro @@ -4,7 +4,7 @@ CONFIG += ordered SUBDIRS += mvvmcore \ mvvmwidgets \ mvvmquick \ - imports + imports prepareRecursiveTarget(lrelease) QMAKE_EXTRA_TARGETS += lrelease