]> OCCT Git - occt-copy.git/commitdiff
0032267: Samples - convert JNIViewer sample from Java to Kotlin CR32267
authorkgv <kgv@opencascade.com>
Tue, 30 Mar 2021 11:33:36 +0000 (14:33 +0300)
committerkgv <kgv@opencascade.com>
Mon, 5 Apr 2021 07:14:08 +0000 (10:14 +0300)
41 files changed:
samples/kotlin/jniviewer/.gitattributes [new file with mode: 0644]
samples/kotlin/jniviewer/.gitignore [new file with mode: 0644]
samples/kotlin/jniviewer/ReadMe.md [new file with mode: 0644]
samples/kotlin/jniviewer/app/build.gradle [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/AndroidManifest.xml [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniActivity.kt [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniFileDialog.kt [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniLogger.kt [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniRenderer.kt [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniView.kt [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/jni/CMakeLists.txt [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.cxx [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.hxx [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.cxx [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.hxx [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_l.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_p.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/fit.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/ic_launcher.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/info.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/info_image.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/message.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_l.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_p.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_back.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_bottom.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_front.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_left.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_right.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_top.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/view.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-mdpi/ic_launcher.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-xhdpi/ic_launcher.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/drawable-xxhdpi/ic_launcher.png [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/layout/activity_main.xml [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/values/id.xml [new file with mode: 0644]
samples/kotlin/jniviewer/app/src/main/res/values/strings.xml [new file with mode: 0644]
samples/kotlin/jniviewer/build.gradle [new file with mode: 0644]
samples/kotlin/jniviewer/gradle.properties.template [new file with mode: 0644]
samples/kotlin/jniviewer/settings.gradle [new file with mode: 0644]

diff --git a/samples/kotlin/jniviewer/.gitattributes b/samples/kotlin/jniviewer/.gitattributes
new file mode 100644 (file)
index 0000000..c275dc2
--- /dev/null
@@ -0,0 +1 @@
+*.gradle eol=lf
diff --git a/samples/kotlin/jniviewer/.gitignore b/samples/kotlin/jniviewer/.gitignore
new file mode 100644 (file)
index 0000000..3328b0f
--- /dev/null
@@ -0,0 +1,10 @@
+/.gradle
+/.idea
+/build
+/gradle
+gradlew
+gradlew.bat
+/app/.cxx
+/app/build
+gradle.properties
+local.properties
diff --git a/samples/kotlin/jniviewer/ReadMe.md b/samples/kotlin/jniviewer/ReadMe.md
new file mode 100644 (file)
index 0000000..ceeb123
--- /dev/null
@@ -0,0 +1,38 @@
+OCCT JniViewer Kotlin sample for Android {#samples_kotlin_android_occt}
+================== 
+
+This sample demonstrates simple way of using OCCT libraries in Android application written using Kotlin.
+
+The connection between Kotlin and OCCT (C++) level is provided by proxy library, libTKJniSample.so, written in C++ with exported JNI methods of Kotlin class OcctJniRenderer.
+The proxy library contains single C++ class OcctJni_Viewer encapsulating OCCT viewer and providing functionality to manipulate this viewer
+and to import OCCT shapes from several supported formats of CAD files (IGES, STEP, BREP).
+
+This sample demonstrates indirect method of wrapping C++ to Kotlin using manually created proxy library.
+
+Install Android Studio 4.0+ and building tools (check Tools -> SDK Manager):
+- Android SDK (API level 21 or higher).
+- Android SDK build tools.
+- Android NDK r16 or higher (coming with CMake toolchain).
+  Using NDK r18 or newer will require changing ANDROID_STL in project settings.
+- CMake 3.10+.
+
+Specify this folder location in Android Studio for opening project.
+You might need re-entering Android SDK explicitly in File -> Project Structure -> SDK Location settings (SDK, NDK, JDK locations).
+
+This sample expects OCCT to be already build - please refer to appropriate CMake building instructions in OCCT documentation.
+The following variables should be added into file gradle.properties (see gradle.properties.template as template):
+- `OCCT_ROOT` - path to OCCT installation folder.
+- `FREETYPE_ROOT` - path to FreeType installation folder.
+
+FreeImage is optional and does not required for this sample, however you should include all extra libraries used for OCCT building
+and load the explicitly from Java code within OcctJniActivity::loadNatives() method, including toolkits from OCCT itself in proper order:
+~~~~
+    if (!loadLibVerbose ("TKernel", aLoaded, aFailed)
+     || !loadLibVerbose ("TKMath",  aLoaded, aFailed)
+     || !loadLibVerbose ("TKG2d",   aLoaded, aFailed)
+~~~~
+Note that C++ STL library is not part of Android system, and application must package this library as well as extra component ("gnustl_shared" by default - see also `ANDROID_STL`).
+
+After successful build via Build -> Rebuild Project, the application can be packaged to Android:
+- Deploy and run application on connected device or emulator directly from Android Studio using adb interface by menu items "Run" and "Debug". This would sign package with debug certificate.
+- Prepare signed end-user package using wizard Build -> Generate signed APK.
diff --git a/samples/kotlin/jniviewer/app/build.gradle b/samples/kotlin/jniviewer/app/build.gradle
new file mode 100644 (file)
index 0000000..277b8b4
--- /dev/null
@@ -0,0 +1,53 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+    compileSdkVersion 21
+    buildToolsVersion "30.0.0"
+
+    defaultConfig {
+        applicationId "com.opencascade.jnisample"
+        minSdkVersion 21
+        targetSdkVersion 26
+
+        ndk {
+            abiFilters "arm64-v8a"
+        }
+
+        externalNativeBuild {
+            cmake {
+                arguments "-DOCCT_ROOT=" + OCCT_ROOT,
+                        "-DFREETYPE_ROOT=" + FREETYPE_ROOT,
+                        "-DANDROID_STL=gnustl_shared"
+            }
+        }
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+        }
+    }
+
+    sourceSets {
+        main {
+            manifest.srcFile 'src/main/AndroidManifest.xml'
+            assets.srcDirs = [OCCT_ROOT + "/src"]
+        }
+    }
+
+    externalNativeBuild {
+        cmake {
+            path "src/main/jni/CMakeLists.txt"
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: 'java/com/opencascade/jnisample', include: ['*.jar'])
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
+repositories {
+    mavenCentral()
+}
diff --git a/samples/kotlin/jniviewer/app/src/main/AndroidManifest.xml b/samples/kotlin/jniviewer/app/src/main/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..c25a6d6
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.opencascade.jnisample">
+    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+        <activity android:name="OcctJniActivity"
+                  android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
+                  android:launchMode="singleTask"
+                  android:configChanges="orientation|keyboardHidden|screenSize">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+
+                <data android:scheme="" />
+                <data android:scheme="file" />
+                <data android:scheme="content" />
+                <data android:host="*" />
+
+                <data android:pathPattern=".*\\.brep" />
+                <data android:pathPattern=".*\\.rle" />
+                <data android:pathPattern=".*\\.step" />
+                <data android:pathPattern=".*\\.stp" />
+                <data android:pathPattern=".*\\.iges" />
+                <data android:pathPattern=".*\\.igs" />
+                <data android:pathPattern=".*\\.stl" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+
+                <data android:mimeType="model/iges"/>
+                <data android:mimeType="application/vnd.ms-pki.stl"/>
+            </intent-filter>
+        </activity>
+    </application>
+    <uses-feature android:glEsVersion="0x00020000"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+</manifest>
diff --git a/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniActivity.kt b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniActivity.kt
new file mode 100644 (file)
index 0000000..714c187
--- /dev/null
@@ -0,0 +1,692 @@
+// Copyright (c) 2014-2021 OPEN CASCADE SAS
+//
+// This file is part of the examples of the Open CASCADE Technology software library.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+package com.opencascade.jnisample
+
+import android.Manifest
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.AssetManager
+import android.content.res.Configuration
+import android.graphics.Point
+import android.os.Bundle
+import android.os.Environment
+import android.text.Html
+import android.text.Html.ImageGetter
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.LinearLayout
+import android.widget.TextView
+import android.widget.Toast
+import java.io.*
+import java.lang.reflect.InvocationTargetException
+import java.lang.reflect.Method
+import java.util.*
+import kotlin.jvm.Throws
+
+//! Main activity
+class OcctJniActivity : Activity(), View.OnClickListener {
+    //! Auxiliary method to load native libraries
+    fun loadNatives(): Boolean {
+        if (wasNativesLoadCalled) {
+            return areNativeLoaded
+        }
+        wasNativesLoadCalled = true
+        val aLoaded = StringBuilder()
+        val aFailed = StringBuilder()
+
+        // copy OCCT resources
+        val aResFolder = filesDir.absolutePath
+        copyAssetFolder(assets, "src/SHMessage", "$aResFolder/SHMessage")
+        copyAssetFolder(assets, "src/XSMessage", "$aResFolder/XSMessage")
+
+        // C++ runtime
+        loadLibVerbose("gnustl_shared", aLoaded, aFailed)
+
+        // 3rd-parties
+        loadLibVerbose("freetype", aLoaded, aFailed)
+        loadLibVerbose("freeimage", aLoaded, aFailed)
+        if ( // OCCT modeling
+                !loadLibVerbose("TKernel", aLoaded, aFailed)
+                || !loadLibVerbose("TKMath", aLoaded, aFailed)
+                || !loadLibVerbose("TKG2d", aLoaded, aFailed)
+                || !loadLibVerbose("TKG3d", aLoaded, aFailed)
+                || !loadLibVerbose("TKGeomBase", aLoaded, aFailed)
+                || !loadLibVerbose("TKBRep", aLoaded, aFailed)
+                || !loadLibVerbose("TKGeomAlgo", aLoaded, aFailed)
+                || !loadLibVerbose("TKTopAlgo", aLoaded, aFailed)
+                || !loadLibVerbose("TKShHealing", aLoaded, aFailed)
+                || !loadLibVerbose("TKMesh", aLoaded, aFailed) // exchange
+                || !loadLibVerbose("TKPrim", aLoaded, aFailed)
+                || !loadLibVerbose("TKBO", aLoaded, aFailed)
+                || !loadLibVerbose("TKBool", aLoaded, aFailed)
+                || !loadLibVerbose("TKFillet", aLoaded, aFailed)
+                || !loadLibVerbose("TKOffset", aLoaded, aFailed)
+                || !loadLibVerbose("TKXSBase", aLoaded, aFailed)
+                || !loadLibVerbose("TKSTL",  aLoaded, aFailed)
+                || !loadLibVerbose("TKIGES", aLoaded, aFailed)
+                || !loadLibVerbose("TKSTEPBase", aLoaded, aFailed)
+                || !loadLibVerbose("TKSTEPAttr", aLoaded, aFailed)
+                || !loadLibVerbose("TKSTEP209", aLoaded, aFailed)
+                || !loadLibVerbose("TKSTEP", aLoaded, aFailed) // OCCT Visualization
+                || !loadLibVerbose("TKService", aLoaded, aFailed)
+                || !loadLibVerbose("TKHLR", aLoaded, aFailed)
+                || !loadLibVerbose("TKV3d", aLoaded, aFailed)
+                || !loadLibVerbose("TKOpenGles", aLoaded, aFailed) // application code
+                || !loadLibVerbose("TKJniSample", aLoaded, aFailed)) {
+            nativeLoaded = aLoaded.toString()
+            nativeFailed = aFailed.toString()
+            areNativeLoaded = false
+            //exitWithError (theActivity, "Broken apk?\n" + theFailedInfo);
+            return false
+        }
+        nativeLoaded = aLoaded.toString()
+        areNativeLoaded = true
+        return true
+    }
+
+    //! Create activity
+    override fun onCreate(theBundle: Bundle?) {
+        super.onCreate(theBundle)
+        val isLoaded = loadNatives()
+        if (!isLoaded) {
+            printShortInfo(this, nativeFailed)
+            OcctJniLogger.postMessage("""
+    $nativeLoaded
+    $nativeFailed
+    """.trimIndent())
+        }
+        setContentView(R.layout.activity_main)
+        myOcctView = findViewById(R.id.custom_view) as OcctJniView
+        myMessageTextView = findViewById(R.id.message_view) as TextView
+        OcctJniLogger.setTextView(myMessageTextView)
+        createViewAndButtons(Configuration.ORIENTATION_LANDSCAPE)
+        myButtonPreferSize = defineButtonSize(findViewById(R.id.panel_menu) as LinearLayout)
+        val aScrollBtn = findViewById(R.id.scroll_btn) as ImageButton
+        aScrollBtn.y = myButtonPreferSize.toFloat()
+        aScrollBtn.setOnTouchListener { theView, theEvent -> onScrollBtnTouch(theView, theEvent) }
+        onConfigurationChanged(resources.configuration)
+        val anIntent = intent
+        val aDataUrl = anIntent?.data
+        val aDataPath = if (aDataUrl != null) aDataUrl.path else ""
+        myOcctView!!.open(aDataPath)
+        myLastPath = aDataPath
+        myContext = ContextWrapper(this)
+        myContext!!.getExternalFilesDir(null)
+    }
+
+    //! Handle scroll events
+    private fun onScrollBtnTouch(theView: View,
+                                 theEvent: MotionEvent): Boolean {
+        when (theEvent.action) {
+            MotionEvent.ACTION_DOWN -> {
+                val aPanelMenu = findViewById(R.id.panel_menu) as LinearLayout
+                val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+                if (aPanelMenu.visibility == View.VISIBLE) {
+                    aPanelMenu.visibility = View.GONE
+                    if (!isLandscape) {
+                        (theView as ImageButton).setImageResource(R.drawable.open_p)
+                        theView.setY(0f)
+                    } else {
+                        (theView as ImageButton).setImageResource(R.drawable.open_l)
+                        theView.setX(0f)
+                    }
+                } else {
+                    aPanelMenu.visibility = View.VISIBLE
+                    if (!isLandscape) {
+                        (theView as ImageButton).setImageResource(R.drawable.close_p)
+                        theView.setY(myButtonPreferSize.toFloat())
+                    } else {
+                        (theView as ImageButton).setImageResource(R.drawable.close_l)
+                        theView.setX(myButtonPreferSize.toFloat())
+                    }
+                }
+            }
+        }
+        return false
+    }
+
+    //! Initialize views and buttons
+    @Suppress("UNUSED_PARAMETER")
+    private fun createViewAndButtons(theOrientation: Int) {
+        // open button
+        val anOpenButton = findViewById(R.id.open) as ImageButton
+        anOpenButton.setOnClickListener(this)
+
+        // fit all
+        val aFitAllButton = findViewById(R.id.fit) as ImageButton
+        aFitAllButton.setOnClickListener(this)
+        aFitAllButton.setOnTouchListener { theView, theEvent -> onTouchButton(theView, theEvent) }
+
+        // message
+        val aMessageButton = findViewById(R.id.message) as ImageButton
+        aMessageButton.setOnClickListener(this)
+
+        // info
+        val anInfoButton = findViewById(R.id.info) as ImageButton
+        anInfoButton.setOnClickListener(this)
+
+        // font for text view
+        val anInfoView = findViewById(R.id.info_view) as TextView
+        anInfoView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
+
+        // add submenu buttons
+        createSubmenuBtn(R.id.view, R.id.view_group,
+                Arrays.asList(R.id.proj_front, R.id.proj_top, R.id.proj_left,
+                        R.id.proj_back, R.id.proj_bottom, R.id.proj_right),
+                Arrays.asList(R.drawable.proj_front, R.drawable.proj_top, R.drawable.proj_left,
+                        R.drawable.proj_back, R.drawable.proj_bottom, R.drawable.proj_right),
+                4)
+    }
+
+    override fun onNewIntent(theIntent: Intent) {
+        super.onNewIntent(theIntent)
+        intent = theIntent
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        OcctJniLogger.setTextView(null)
+    }
+
+    override fun onPause() {
+        super.onPause()
+        myOcctView!!.onPause()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        myOcctView!!.onResume()
+        val anIntent = intent
+        val aDataUrl = anIntent?.data
+        val aDataPath = if (aDataUrl != null) aDataUrl.path else ""
+        if (aDataPath != myLastPath) {
+            myOcctView!!.open(aDataPath)
+            myLastPath = aDataPath
+        }
+    }
+
+    //! Copy folder from assets
+    private fun copyAssetFolder(theAssetMgr: AssetManager,
+                                theAssetFolder: String,
+                                theFolderPathTo: String): Boolean {
+        return try {
+            val aFiles = theAssetMgr.list(theAssetFolder)
+            val aFolder = File(theFolderPathTo)
+            aFolder.mkdirs()
+            var isOk = true
+            for (aFileIter in aFiles) {
+                isOk = if (aFileIter.contains(".")) {
+                    isOk and copyAsset(theAssetMgr,
+                            "$theAssetFolder/$aFileIter",
+                            "$theFolderPathTo/$aFileIter")
+                } else {
+                    isOk and copyAssetFolder(theAssetMgr,
+                            "$theAssetFolder/$aFileIter",
+                            "$theFolderPathTo/$aFileIter")
+                }
+            }
+            isOk
+        } catch (theError: Exception) {
+            theError.printStackTrace()
+            false
+        }
+    }
+
+    //! Copy single file from assets
+    private fun copyAsset(theAssetMgr: AssetManager,
+                          thePathFrom: String,
+                          thePathTo: String): Boolean {
+        return try {
+            val aStreamIn = theAssetMgr.open(thePathFrom)
+            val aFileTo = File(thePathTo)
+            aFileTo.createNewFile()
+            val aStreamOut: OutputStream? = FileOutputStream(thePathTo)
+            copyStreamContent(aStreamIn, aStreamOut)
+            aStreamIn.close()
+            aStreamOut!!.flush()
+            aStreamOut.close()
+            true
+        } catch (theError: Exception) {
+            theError.printStackTrace()
+            false
+        }
+    }
+
+    //! Show/hide text view
+    private fun switchTextView(theTextView: TextView?,
+                               theClickedBtn: ImageButton,
+                               theToSwitchOn: Boolean) {
+        if (theTextView != null && theTextView.visibility == View.GONE && theToSwitchOn) {
+            theTextView.visibility = View.VISIBLE
+            theClickedBtn.setBackgroundColor(resources.getColor(R.color.pressedBtnColor))
+            setTextViewPosition(theTextView)
+        } else {
+            theTextView!!.visibility = View.GONE
+            theClickedBtn.setBackgroundColor(resources.getColor(R.color.btnColor))
+        }
+    }
+
+    //! Setup text view position
+    private fun setTextViewPosition(theTextView: TextView?) {
+        if (theTextView!!.visibility != View.VISIBLE) {
+            return
+        }
+        if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+            theTextView.x = myButtonPreferSize.toFloat()
+            theTextView.y = 0f
+        } else {
+            theTextView.x = 0f
+            theTextView.y = myButtonPreferSize.toFloat()
+        }
+    }
+
+    override fun onClick(theButton: View) {
+        val aClickedBtn = theButton as ImageButton
+        when (aClickedBtn.id) {
+            R.id.message -> {
+                switchTextView(findViewById(R.id.info_view) as TextView,
+                        findViewById(R.id.info) as ImageButton, false)
+                switchTextView(myMessageTextView, aClickedBtn, true)
+                return
+            }
+            R.id.info -> {
+                var aText = getString(R.string.info_html)
+                aText = String.format(aText, cppOcctMajorVersion(), cppOcctMinorVersion(), cppOcctMicroVersion())
+                val aSpanned = Html.fromHtml(aText, ImageGetter { theSource ->
+                    val aResources = resources
+                    val anId = aResources.getIdentifier(theSource, "drawable", packageName)
+                    val aRes = aResources.getDrawable(anId)
+                    aRes.setBounds(0, 0, aRes.intrinsicWidth, aRes.intrinsicHeight)
+                    aRes
+                }, null)
+                val anInfoView = findViewById(R.id.info_view) as TextView
+                anInfoView.text = aSpanned
+                switchTextView(myMessageTextView, findViewById(R.id.message) as ImageButton, false)
+                switchTextView(anInfoView, aClickedBtn, true)
+                return
+            }
+            R.id.fit -> {
+                myOcctView!!.fitAll()
+                return
+            }
+            R.id.proj_front -> {
+                myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Xpos)
+                return
+            }
+            R.id.proj_left -> {
+                myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Yneg)
+                return
+            }
+            R.id.proj_top -> {
+                myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Zpos)
+                return
+            }
+            R.id.proj_back -> {
+                myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Xneg)
+                return
+            }
+            R.id.proj_right -> {
+                myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Ypos)
+                return
+            }
+            R.id.proj_bottom -> {
+                myOcctView!!.setProj(OcctJniRenderer.TypeOfOrientation.Zneg)
+                return
+            }
+            R.id.open -> {
+                val aPath = Environment.getExternalStorageDirectory()
+                aClickedBtn.setBackgroundColor(resources.getColor(R.color.pressedBtnColor))
+                if (myFileOpenDialog == null) {
+                    // should be requested on runtime since API level 26 (Android 8)
+                    askUserPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, null) // for accessing SD card
+                    myFileOpenDialog = OcctJniFileDialog(this, aPath)
+                    myFileOpenDialog!!.setFileEndsWith(".brep")
+                    myFileOpenDialog!!.setFileEndsWith(".rle")
+                    myFileOpenDialog!!.setFileEndsWith(".iges")
+                    myFileOpenDialog!!.setFileEndsWith(".igs")
+                    myFileOpenDialog!!.setFileEndsWith(".step")
+                    myFileOpenDialog!!.setFileEndsWith(".stp")
+                    myFileOpenDialog!!.setFileEndsWith(".stl")
+
+                    myFileOpenDialog!!.addFileListener (object : OcctJniFileDialog.FileSelectedListener {
+                        override fun fileSelected(theFile: File?) {
+                            if (theFile != null && myOcctView != null) {
+                                myOcctView!!.open(theFile.getPath())
+                            }
+                        }
+                    })
+
+                    myFileOpenDialog!!.addDialogDismissedListener (object : OcctJniFileDialog.DialogDismissedListener {
+                        override fun dialogDismissed() {
+                            val openButton = findViewById(R.id.open) as ImageButton
+                            openButton.setBackgroundColor(resources.getColor(R.color.btnColor))
+                        }
+                    })
+                }
+                myFileOpenDialog!!.showDialog()
+                return
+            }
+        }
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    private fun createSubmenuBtn(theParentBtnId: Int,
+                                 theParentLayoutId: Int,
+                                 theNewButtonIds: List<Int>,
+                                 theNewButtonImageIds: List<Int>,
+                                 thePosition: Int) {
+        var aPosInList = 0
+        val aParentBtn = findViewById(theParentBtnId) as? ImageButton
+        val aParams: ViewGroup.LayoutParams? = null
+        val parentLayout = findViewById(theParentLayoutId) as LinearLayout
+        for (newButtonId in theNewButtonIds) {
+            var aNewButton = findViewById(newButtonId) as? ImageButton
+            if (aNewButton == null) {
+                aNewButton = ImageButton(this)
+                aNewButton.id = newButtonId
+                aNewButton.setImageResource(theNewButtonImageIds[aPosInList])
+                aNewButton.layoutParams = aParams
+                parentLayout.addView(aNewButton)
+            }
+            aNewButton.setOnClickListener(this)
+            aNewButton.visibility = View.GONE
+            aNewButton.setOnTouchListener { theView, theEvent -> onTouchButton(theView, theEvent) }
+            ++aPosInList
+        }
+        if (aParentBtn != null) {
+            aParentBtn.setOnTouchListener(null)
+            aParentBtn.setOnTouchListener { theView, theEvent ->
+                if (theView == null) {} // dummy
+                if (theEvent.action == MotionEvent.ACTION_DOWN) {
+                    var isVisible = false
+                    for (aNewButtonId in theNewButtonIds) {
+                        val anBtn = findViewById(aNewButtonId) as? ImageButton
+                        if (anBtn != null) {
+                            if (anBtn.visibility == View.GONE) {
+                                anBtn.visibility = View.VISIBLE
+                                isVisible = true
+                            } else {
+                                anBtn.visibility = View.GONE
+                            }
+                        }
+                    }
+                    aParentBtn.setBackgroundColor(if (!isVisible) resources.getColor(R.color.btnColor) else resources.getColor(R.color.pressedBtnColor))
+                }
+                false
+            }
+        }
+    }
+
+    //! Implements onTouch functionality
+    private fun onTouchButton(theView: View,
+                              theEvent: MotionEvent): Boolean {
+        when (theEvent.action) {
+            MotionEvent.ACTION_DOWN -> (theView as ImageButton).setBackgroundColor(resources.getColor(R.color.pressedBtnColor))
+            MotionEvent.ACTION_UP -> (theView as ImageButton).setBackgroundColor(resources.getColor(R.color.btnColor))
+        }
+        return false
+    }
+
+    //! Handle configuration change event
+    override fun onConfigurationChanged(theNewConfig: Configuration) {
+        super.onConfigurationChanged(theNewConfig)
+        val aLayoutPanelMenu = findViewById(R.id.panel_menu) as LinearLayout
+        val aPanelMenuLayoutParams = aLayoutPanelMenu.layoutParams
+        val aLayoutViewGroup = findViewById(R.id.view_group) as LinearLayout
+        val aViewGroupLayoutParams = aLayoutViewGroup.layoutParams
+        val aScrollBtn = findViewById(R.id.scroll_btn) as ImageButton
+        val aScrollBtnLayoutParams = aScrollBtn.layoutParams
+        myButtonPreferSize = defineButtonSize(findViewById(R.id.panel_menu) as LinearLayout)
+        defineButtonSize(findViewById(R.id.view_group) as LinearLayout)
+        when (theNewConfig.orientation) {
+            Configuration.ORIENTATION_PORTRAIT -> {
+                setHorizontal(aLayoutPanelMenu, aPanelMenuLayoutParams)
+                setHorizontal(aLayoutViewGroup, aViewGroupLayoutParams)
+                aLayoutViewGroup.setGravity(Gravity.BOTTOM)
+                aScrollBtnLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
+                aScrollBtnLayoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
+                aScrollBtn.layoutParams = aScrollBtnLayoutParams
+                if (aLayoutPanelMenu.visibility == View.VISIBLE) {
+                    aScrollBtn.setImageResource(R.drawable.close_p)
+                    aScrollBtn.y = myButtonPreferSize.toFloat()
+                    aScrollBtn.x = 0f
+                } else {
+                    aScrollBtn.setImageResource(R.drawable.open_p)
+                    aScrollBtn.y = 0f
+                    aScrollBtn.x = 0f
+                }
+            }
+            Configuration.ORIENTATION_LANDSCAPE -> {
+                setVertical(aLayoutPanelMenu, aPanelMenuLayoutParams)
+                setVertical(aLayoutViewGroup, aViewGroupLayoutParams)
+                aLayoutViewGroup.setGravity(Gravity.RIGHT)
+                aScrollBtnLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+                aScrollBtnLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT
+                aScrollBtn.layoutParams = aScrollBtnLayoutParams
+                if (aLayoutPanelMenu.visibility == View.VISIBLE) {
+                    aScrollBtn.setImageResource(R.drawable.close_l)
+                    aScrollBtn.x = myButtonPreferSize.toFloat()
+                    aScrollBtn.y = 0f
+                } else {
+                    aScrollBtn.setImageResource(R.drawable.open_l)
+                    aScrollBtn.y = 0f
+                    aScrollBtn.x = 0f
+                }
+            }
+        }
+        setTextViewPosition(myMessageTextView)
+        setTextViewPosition(findViewById(R.id.info_view) as TextView)
+    }
+
+    private fun setHorizontal(theLayout: LinearLayout,
+                              theLayoutParams: ViewGroup.LayoutParams) {
+        theLayout.orientation = LinearLayout.HORIZONTAL
+        theLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
+        theLayoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
+        theLayout.layoutParams = theLayoutParams
+    }
+
+    private fun setVertical(theLayout: LinearLayout,
+                            theLayoutParams: ViewGroup.LayoutParams) {
+        theLayout.orientation = LinearLayout.VERTICAL
+        theLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+        theLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT
+        theLayout.layoutParams = theLayoutParams
+    }
+
+    //! Define button size
+    private fun defineButtonSize(theLayout: LinearLayout): Int {
+        val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+        val aDisplay = windowManager.defaultDisplay
+        val aDispPnt = Point()
+        aDisplay.getSize(aDispPnt)
+        val aNbChildren = theLayout.childCount
+        val aHeight = aDispPnt.y / aNbChildren
+        val aWidth = aDispPnt.x / aNbChildren
+
+        for (aChildIter in 0 until aNbChildren) {
+            val aView = theLayout.getChildAt(aChildIter)
+            if (aView is ImageButton) {
+                val aButton = aView
+                if (isLandscape) {
+                    aButton.minimumWidth = aHeight
+                } else {
+                    aButton.minimumHeight = aWidth
+                }
+            }
+        }
+        return if (isLandscape) { aHeight } else { aWidth }
+    }
+
+    //! Request user permission.
+    private fun askUserPermission(thePermission: String, theRationale: String?) {
+        // Dynamically load methods introduced by API level 23.
+        // On older system this permission is granted by user during application installation.
+        val aMetPtrCheckSelfPermission: Method
+        val aMetPtrRequestPermissions: Method
+        val aMetPtrShouldShowRequestPermissionRationale: Method
+        try {
+            aMetPtrCheckSelfPermission = myContext!!.javaClass.getMethod("checkSelfPermission", String::class.java)
+            aMetPtrRequestPermissions = javaClass.getMethod("requestPermissions", Array<String>::class.java, Int::class.javaPrimitiveType)
+            aMetPtrShouldShowRequestPermissionRationale = javaClass.getMethod("shouldShowRequestPermissionRationale", String::class.java)
+        } catch (theError: SecurityException) {
+            postMessage("""
+    Unable to find permission methods:
+    ${theError.message}
+    """.trimIndent(), Message_Trace)
+            return
+        } catch (theError: NoSuchMethodException) {
+            postMessage("""
+    Unable to find permission methods:
+    ${theError.message}
+    """.trimIndent(), Message_Trace)
+            return
+        }
+        try {
+            val isAlreadyGranted = aMetPtrCheckSelfPermission.invoke(myContext, thePermission) as Int
+            if (isAlreadyGranted == PackageManager.PERMISSION_GRANTED) {
+                return
+            }
+            val toShowInfo = theRationale != null && aMetPtrShouldShowRequestPermissionRationale.invoke(this, thePermission) as Boolean
+            if (toShowInfo) {
+                postMessage(theRationale, Message_Info)
+            }
+
+            // show dialog to user
+            aMetPtrRequestPermissions.invoke(this, arrayOf(thePermission), 0)
+        } catch (theError: IllegalArgumentException) {
+            postMessage("""
+    Internal error: Unable to call permission method:
+    ${theError.message}
+    """.trimIndent(), Message_Fail)
+            return
+        } catch (theError: IllegalAccessException) {
+            postMessage("""
+    Internal error: Unable to call permission method:
+    ${theError.message}
+    """.trimIndent(), Message_Fail)
+            return
+        } catch (theError: InvocationTargetException) {
+            postMessage("""
+    Internal error: Unable to call permission method:
+    ${theError.message}
+    """.trimIndent(), Message_Fail)
+            return
+        }
+    }
+
+    //! Auxiliary method to show info message.
+    fun postMessage(theMessage: String?, theGravity: Int) {
+        if (theGravity == Message_Trace) {
+            return
+        }
+        val aCtx: Context = this
+        runOnUiThread {
+            val aBuilder = AlertDialog.Builder(aCtx)
+            aBuilder.setMessage(theMessage).setNegativeButton("OK", null)
+            val aDialog = aBuilder.create()
+            aDialog.show()
+        }
+    }
+
+    //! OCCT major version
+    private external fun cppOcctMajorVersion(): Long
+
+    //! OCCT minor version
+    private external fun cppOcctMinorVersion(): Long
+
+    //! OCCT micro version
+    private external fun cppOcctMicroVersion(): Long
+    private var myOcctView: OcctJniView? = null
+    private var myMessageTextView: TextView? = null
+    private var myLastPath: String? = null
+    private var myContext: ContextWrapper? = null
+    private var myFileOpenDialog: OcctJniFileDialog? = null
+    private var myButtonPreferSize = 65
+
+    companion object {
+        //! Auxiliary method to print temporary info messages
+        fun printShortInfo(theActivity: Activity,
+                           theInfo: CharSequence?) {
+            val aCtx = theActivity.applicationContext
+            val aToast = Toast.makeText(aCtx, theInfo, Toast.LENGTH_LONG)
+            aToast.show()
+        }
+
+        //! Load single native library
+        private fun loadLibVerbose(theLibName: String,
+                                   theLoadedInfo: StringBuilder,
+                                   theFailedInfo: StringBuilder): Boolean {
+            return try {
+                System.loadLibrary(theLibName)
+                theLoadedInfo.append("Info:  native library \"")
+                theLoadedInfo.append(theLibName)
+                theLoadedInfo.append("\" has been loaded\n")
+                true
+            } catch (theError: UnsatisfiedLinkError) {
+                theFailedInfo.append("Error: native library \"")
+                theFailedInfo.append(theLibName)
+                theFailedInfo.append("""" is unavailable:
+  ${theError.message}""")
+                false
+            } catch (theError: SecurityException) {
+                theFailedInfo.append("Error: native library \"")
+                theFailedInfo.append(theLibName)
+                theFailedInfo.append("""" can not be loaded for security reasons:
+  ${theError.message}""")
+                false
+            }
+        }
+
+        var wasNativesLoadCalled = false
+        @JvmField
+        var areNativeLoaded = false
+        var nativeLoaded = ""
+        var nativeFailed = ""
+
+        //! Copy single file
+        @Throws(IOException::class)
+        private fun copyStreamContent(theIn: InputStream?,
+                                      theOut: OutputStream?) {
+            val aBuffer = ByteArray(1024)
+            var aNbReadBytes: Int
+            while (theIn!!.read(aBuffer).also { aNbReadBytes = it } != -1) {
+                theOut!!.write(aBuffer, 0, aNbReadBytes)
+            }
+        }
+
+        //! Message gravity.
+        private const val Message_Trace = 0
+        private const val Message_Info = 1
+        private const val Message_Warning = 2
+        private const val Message_Alarm = 3
+        private const val Message_Fail = 4
+    }
+}
diff --git a/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniFileDialog.kt b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniFileDialog.kt
new file mode 100644 (file)
index 0000000..bbbc119
--- /dev/null
@@ -0,0 +1,213 @@
+// Copyright (c) 2014-2021 OPEN CASCADE SAS
+//
+// This file is part of the examples of the Open CASCADE Technology software library.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+package com.opencascade.jnisample
+
+import android.app.Activity
+import android.app.AlertDialog
+import android.app.Dialog
+import android.content.DialogInterface
+import android.graphics.Color
+import android.os.Environment
+import android.view.ViewGroup
+import android.widget.*
+import android.widget.AdapterView.OnItemClickListener
+import com.opencascade.jnisample.ListenerList.FireHandler
+import java.io.File
+import java.io.FilenameFilter
+import java.util.*
+
+//! Simple open file dialog
+class OcctJniFileDialog(theActivity: Activity,
+                        thePath: File) {
+
+    private var myFileList: Array<String>? = emptyArray()
+    private var myCurrentPath: File? = null
+    private val myFileListenerList = ListenerList<FileSelectedListener>()
+    private val myDialogDismissedList = ListenerList<DialogDismissedListener>()
+    private val myActivity: Activity
+    private var myFileEndsWith: MutableList<String>? = null
+
+    //! Main constructor.
+    init {
+        var aPath = thePath
+        myActivity = theActivity
+        if (!aPath.exists()) {
+            aPath = Environment.getExternalStorageDirectory()
+        }
+        loadFileList(aPath)
+    }
+
+    interface FileSelectedListener {
+        fun fileSelected(theFile: File?)
+    }
+
+    interface DialogDismissedListener {
+        fun dialogDismissed()
+    }
+
+    //! Create new dialog
+    fun createFileDialog(): Dialog? {
+        val anObjWrapper = arrayOfNulls<Any>(1)
+        val aBuilder = AlertDialog.Builder(myActivity)
+        aBuilder.setTitle(myCurrentPath!!.path)
+        val aTitleLayout = LinearLayout(myActivity)
+        aTitleLayout.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+        aTitleLayout.orientation = LinearLayout.VERTICAL
+        val list = ListView(myActivity)
+        list.isScrollingCacheEnabled = false
+        list.setBackgroundColor(Color.parseColor("#33B5E5"))
+        list.adapter = ArrayAdapter(myActivity, android.R.layout.select_dialog_item, myFileList)
+        list.onItemClickListener = OnItemClickListener { arg0, view, pos, id ->
+            if (arg0 == null || view == null || id == 0L) {} // dummy
+            val fileChosen = myFileList!![pos]
+            val aChosenFile = getChosenFile(fileChosen)
+            if (aChosenFile.isDirectory) {
+                loadFileList(aChosenFile)
+                (anObjWrapper[0] as Dialog?)!!.cancel()
+                (anObjWrapper[0] as Dialog?)!!.dismiss()
+                showDialog()
+            } else {
+                (anObjWrapper[0] as Dialog?)!!.cancel()
+                (anObjWrapper[0] as Dialog?)!!.dismiss()
+                fireFileSelectedEvent(aChosenFile)
+            }
+        }
+        list.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0.6f)
+        aTitleLayout.addView(list)
+        aBuilder.setNegativeButton("Cancel", null)
+        aBuilder.setView(aTitleLayout)
+
+        val aDialog = aBuilder.show()
+        aDialog.setOnDismissListener(DialogInterface.OnDismissListener { fireDialogDismissedEvent() })
+        anObjWrapper[0] = aDialog
+        return aDialog
+    }
+
+    fun addFileListener(theListener: FileSelectedListener) {
+        myFileListenerList.add(theListener)
+    }
+
+    fun addDialogDismissedListener(theListener: DialogDismissedListener) {
+        myDialogDismissedList.add(theListener)
+    }
+
+    //! Show file dialog
+    fun showDialog() {
+        createFileDialog()!!.show()
+    }
+
+    private fun fireFileSelectedEvent(theFile: File) {
+        myFileListenerList.fireEvent(object : FireHandler<FileSelectedListener> {
+            override fun fireEvent(theListener: FileSelectedListener) {
+                theListener.fileSelected(theFile)
+            }
+        })
+    }
+
+    private fun fireDialogDismissedEvent() {
+        myDialogDismissedList.fireEvent(object : FireHandler<DialogDismissedListener> {
+            override fun fireEvent(theListener: DialogDismissedListener) {
+                theListener.dialogDismissed()
+            }
+        })
+    }
+
+    private fun loadFileList(thePath: File?) {
+        myCurrentPath = thePath
+        val aList: MutableList<String> = ArrayList()
+        if (thePath!!.exists()) {
+            if (thePath.parentFile != null) {
+                aList.add(PARENT_DIR)
+            }
+            val aFilter = FilenameFilter { theDir, theFilename ->
+                val aSel = File(theDir, theFilename)
+                if (!aSel.canRead()) {
+                    return@FilenameFilter false
+                }
+                var isEndWith = false
+                if (myFileEndsWith != null) {
+                    for (aFileExtIter in myFileEndsWith!!) {
+                        if (theFilename.toLowerCase().endsWith(aFileExtIter)) {
+                            isEndWith = true
+                            break
+                        }
+                    }
+                }
+                isEndWith || aSel.isDirectory
+            }
+            val aFileList1 = thePath.list(aFilter)
+            if (aFileList1 != null) {
+                for (aFileIter in aFileList1) {
+                    aList.add(aFileIter)
+                }
+            }
+        }
+        myFileList = aList.toTypedArray()
+    }
+
+    private fun getChosenFile(theFileChosen: String): File {
+        return if (theFileChosen == PARENT_DIR) myCurrentPath!!.parentFile else File(myCurrentPath, theFileChosen)
+    }
+
+    fun setFileEndsWith(fileEndsWith: String) {
+        if (myFileEndsWith == null) {
+            myFileEndsWith = ArrayList()
+        }
+        if (myFileEndsWith!!.indexOf(fileEndsWith) == -1) {
+            myFileEndsWith!!.add(fileEndsWith)
+        }
+    }
+
+    fun setFileEndsWith(theFileEndsWith: MutableList<String>?) {
+        myFileEndsWith = theFileEndsWith
+    }
+
+    companion object {
+        private const val PARENT_DIR = ".."
+    }
+}
+
+internal class ListenerList<L> {
+    private val myListenerList: MutableList<L> = ArrayList()
+
+    interface FireHandler<L> {
+        fun fireEvent(theListener: L)
+    }
+
+    fun add(theListener: L) {
+        myListenerList.add(theListener)
+    }
+
+    fun fireEvent(theFireHandler: FireHandler<L>) {
+        val aCopy: List<L> = ArrayList(myListenerList)
+        for (anIter in aCopy) {
+            theFireHandler.fireEvent(anIter)
+        }
+    }
+
+    fun remove(theListener: L) {
+        myListenerList.remove(theListener)
+    }
+
+    val listenerList: List<L>
+        get() = myListenerList
+}
diff --git a/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniLogger.kt b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniLogger.kt
new file mode 100644 (file)
index 0000000..f1bd685
--- /dev/null
@@ -0,0 +1,68 @@
+// Copyright (c) 2014-2021 OPEN CASCADE SAS
+//
+// This file is part of the examples of the Open CASCADE Technology software library.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+package com.opencascade.jnisample
+
+import android.util.Log
+import android.widget.TextView
+import java.util.concurrent.locks.ReentrantLock
+
+//! Auxiliary class for logging messages
+object OcctJniLogger {
+    //! Setup text view
+    fun setTextView(theTextView: TextView?) {
+        if (myTextView != null) {
+            myLog = myTextView!!.text.toString()
+        }
+        myTextView = theTextView
+        if (myTextView != null) {
+            myTextView!!.text = myLog
+            myLog = ""
+        }
+    }
+
+    //! Interface implementation
+    @JvmStatic
+    fun postMessage(theText: String?) {
+        var aCopy = String()
+        aCopy += theText
+        Log.e(myTag, theText)
+        myMutex.lock()
+        val aView = myTextView
+        if (aView == null) {
+            myLog += aCopy
+            myMutex.unlock()
+            return
+        }
+        aView.post(Runnable {
+            aView.text = """
+     ${aView.text}$aCopy
+
+     """.trimIndent()
+        })
+        myMutex.unlock()
+    }
+
+    private const val myTag = "occtJniViewer"
+    private val myMutex = ReentrantLock(true)
+    private var myTextView: TextView? = null
+    private var myLog = ""
+}
diff --git a/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniRenderer.kt b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniRenderer.kt
new file mode 100644 (file)
index 0000000..3eeb7fa
--- /dev/null
@@ -0,0 +1,186 @@
+// Copyright (c) 2014-2021 OPEN CASCADE SAS
+//
+// This file is part of the examples of the Open CASCADE Technology software library.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+package com.opencascade.jnisample
+
+import android.opengl.GLSurfaceView
+import javax.microedition.khronos.egl.EGLConfig
+import javax.microedition.khronos.opengles.GL10
+
+//! Wrapper for C++ OCCT viewer.
+class OcctJniRenderer internal constructor(theView: GLSurfaceView?,
+                                           theScreenDensity: Float) : GLSurfaceView.Renderer {
+    //! Wrapper for V3d_TypeOfOrientation
+    enum class TypeOfOrientation {
+        Xpos,  // front
+        Ypos,  // left
+        Zpos,  // top
+        Xneg,  // back
+        Yneg,  // right
+        Zneg // bottom
+    }
+
+    //! Open file.
+    fun open(thePath: String) {
+        if (myCppViewer != 0L) {
+            cppOpen(myCppViewer, thePath)
+        }
+    }
+
+    //! Update viewer.
+    override fun onDrawFrame(theGl: GL10) {
+        if (myCppViewer != 0L) {
+            if (cppRedraw(myCppViewer)) {
+                myView!!.requestRender() // this method is allowed from any thread
+            }
+        }
+    }
+
+    //! (re)initialize viewer.
+    override fun onSurfaceChanged(theGl: GL10, theWidth: Int, theHeight: Int) {
+        if (myCppViewer != 0L) {
+            cppResize(myCppViewer, theWidth, theHeight)
+        }
+    }
+
+    override fun onSurfaceCreated(theGl: GL10, theEglConfig: EGLConfig) {
+        if (myCppViewer != 0L) {
+            cppInit(myCppViewer)
+        }
+    }
+
+    //! Add touch point.
+    fun onAddTouchPoint(theId: Int, theX: Float, theY: Float) {
+        if (myCppViewer != 0L) {
+            cppAddTouchPoint(myCppViewer, theId, theX, theY)
+        }
+    }
+
+    //! Update touch point.
+    fun onUpdateTouchPoint(theId: Int, theX: Float, theY: Float) {
+        if (myCppViewer != 0L) {
+            cppUpdateTouchPoint(myCppViewer, theId, theX, theY)
+        }
+    }
+
+    //! Remove touch point.
+    fun onRemoveTouchPoint(theId: Int) {
+        if (myCppViewer != 0L) {
+            cppRemoveTouchPoint(myCppViewer, theId)
+        }
+    }
+
+    //! Select in 3D Viewer.
+    fun onSelectInViewer(theX: Float, theY: Float) {
+        if (myCppViewer != 0L) {
+            cppSelectInViewer(myCppViewer, theX, theY)
+        }
+    }
+
+    //! Fit All
+    fun fitAll() {
+        if (myCppViewer != 0L) {
+            cppFitAll(myCppViewer)
+        }
+    }
+
+    //! Move camera
+    fun setProj(theProj: TypeOfOrientation?) {
+        if (myCppViewer == 0L) {
+            return
+        }
+        when (theProj) {
+            TypeOfOrientation.Xpos -> cppSetXposProj(myCppViewer)
+            TypeOfOrientation.Ypos -> cppSetYposProj(myCppViewer)
+            TypeOfOrientation.Zpos -> cppSetZposProj(myCppViewer)
+            TypeOfOrientation.Xneg -> cppSetXnegProj(myCppViewer)
+            TypeOfOrientation.Yneg -> cppSetYnegProj(myCppViewer)
+            TypeOfOrientation.Zneg -> cppSetZnegProj(myCppViewer)
+        }
+    }
+
+    //! Post message to the text view.
+    fun postMessage(theText: String?) {
+        OcctJniLogger.postMessage(theText)
+    }
+
+    //! Create instance of C++ class
+    private external fun cppCreate(theDispDensity: Float): Long
+
+    //! Destroy instance of C++ class
+    private external fun cppDestroy(theCppPtr: Long)
+
+    //! Initialize OCCT viewer (steal OpenGL ES context bound to this thread)
+    private external fun cppInit(theCppPtr: Long)
+
+    //! Resize OCCT viewer
+    private external fun cppResize(theCppPtr: Long, theWidth: Int, theHeight: Int)
+
+    //! Open CAD file
+    private external fun cppOpen(theCppPtr: Long, thePath: String)
+
+    //! Add touch point
+    private external fun cppAddTouchPoint(theCppPtr: Long, theId: Int, theX: Float, theY: Float)
+
+    //! Update touch point
+    private external fun cppUpdateTouchPoint(theCppPtr: Long, theId: Int, theX: Float, theY: Float)
+
+    //! Remove touch point
+    private external fun cppRemoveTouchPoint(theCppPtr: Long, theId: Int)
+
+    //! Select in 3D Viewer.
+    private external fun cppSelectInViewer(theCppPtr: Long, theX: Float, theY: Float)
+
+    //! Redraw OCCT viewer
+    //! Returns TRUE if more frames are requested.
+    private external fun cppRedraw(theCppPtr: Long): Boolean
+
+    //! Fit All
+    private external fun cppFitAll(theCppPtr: Long)
+
+    //! Move camera
+    private external fun cppSetXposProj(theCppPtr: Long)
+
+    //! Move camera
+    private external fun cppSetYposProj(theCppPtr: Long)
+
+    //! Move camera
+    private external fun cppSetZposProj(theCppPtr: Long)
+
+    //! Move camera
+    private external fun cppSetXnegProj(theCppPtr: Long)
+
+    //! Move camera
+    private external fun cppSetYnegProj(theCppPtr: Long)
+
+    //! Move camera
+    private external fun cppSetZnegProj(theCppPtr: Long)
+    private var myView: GLSurfaceView? = null //!< back reference to the View
+    private var myCppViewer: Long = 0 //!< pointer to c++ class instance
+
+    //! Empty constructor.
+    init {
+        myView = theView // this makes cyclic dependency, but it is OK for JVM
+        if (OcctJniActivity.areNativeLoaded) {
+            myCppViewer = cppCreate(theScreenDensity)
+        }
+    }
+}
diff --git a/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniView.kt b/samples/kotlin/jniviewer/app/src/main/java/com/opencascade/jnisample/OcctJniView.kt
new file mode 100644 (file)
index 0000000..971aa19
--- /dev/null
@@ -0,0 +1,250 @@
+// Copyright (c) 2014-2021 OPEN CASCADE SAS
+//
+// This file is part of the examples of the Open CASCADE Technology software library.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+package com.opencascade.jnisample
+
+import android.app.ActionBar
+import android.content.Context
+import android.graphics.PointF
+import android.opengl.GLSurfaceView
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.widget.RelativeLayout
+import com.opencascade.jnisample.OcctJniLogger.postMessage
+import com.opencascade.jnisample.OcctJniRenderer.TypeOfOrientation
+import javax.microedition.khronos.egl.EGL10
+import javax.microedition.khronos.egl.EGLConfig
+import javax.microedition.khronos.egl.EGLContext
+import javax.microedition.khronos.egl.EGLDisplay
+
+//! OpenGL ES 2.0+ view.
+//! Performs rendering in parallel thread.
+internal class OcctJniView(theContext: Context,
+                           theAttrs: AttributeSet?) : GLSurfaceView(theContext, theAttrs) {
+    //! Open file.
+    fun open(thePath: String) {
+        queueEvent { myRenderer!!.open(thePath) }
+        requestRender()
+    }
+
+    //! Create OpenGL ES 2.0+ context
+    private class ContextFactory : EGLContextFactory {
+        override fun createContext(theEgl: EGL10,
+                                   theEglDisplay: EGLDisplay,
+                                   theEglConfig: EGLConfig): EGLContext {
+            // reset EGL errors stack
+            while (theEgl.eglGetError() != EGL10.EGL_SUCCESS) {}
+            val anAttribs = intArrayOf(EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE)
+            val aEglContext = theEgl.eglCreateContext(theEglDisplay, theEglConfig, EGL10.EGL_NO_CONTEXT, anAttribs)
+            var anError = theEgl.eglGetError()
+            while (anError != EGL10.EGL_SUCCESS) {
+                postMessage("Error: eglCreateContext() " + String.format("0x%x", anError))
+                anError = theEgl.eglGetError()
+            }
+            return aEglContext
+        }
+
+        override fun destroyContext(theEgl: EGL10,
+                                    theEglDisplay: EGLDisplay,
+                                    theEglContext: EGLContext) {
+            theEgl.eglDestroyContext(theEglDisplay, theEglContext)
+        }
+
+        companion object {
+            private const val EGL_CONTEXT_CLIENT_VERSION = 0x3098
+        }
+    }
+
+    //! Search for RGB24 config with depth and stencil buffers
+    private class ConfigChooser : EGLConfigChooser {
+        //! Reset EGL errors stack
+        private fun popEglErrors(theEgl: EGL10) {
+            var anError = theEgl.eglGetError()
+            while (anError != EGL10.EGL_SUCCESS) {
+                postMessage("EGL Error: " + String.format("0x%x", anError))
+                anError = theEgl.eglGetError()
+            }
+        }
+
+        //! Auxiliary method to dump EGL configuration - for debugging purposes
+        private fun printConfig(theEgl: EGL10,
+                                theEglDisplay: EGLDisplay,
+                                theEglConfig: EGLConfig) {
+            val THE_ATTRIBS = intArrayOf(
+                    EGL10.EGL_BUFFER_SIZE, EGL10.EGL_ALPHA_SIZE, EGL10.EGL_BLUE_SIZE, EGL10.EGL_GREEN_SIZE, EGL10.EGL_RED_SIZE, EGL10.EGL_DEPTH_SIZE, EGL10.EGL_STENCIL_SIZE,
+                    EGL10.EGL_CONFIG_CAVEAT,
+                    EGL10.EGL_CONFIG_ID,
+                    EGL10.EGL_LEVEL,
+                    EGL10.EGL_MAX_PBUFFER_HEIGHT, EGL10.EGL_MAX_PBUFFER_PIXELS, EGL10.EGL_MAX_PBUFFER_WIDTH,
+                    EGL10.EGL_NATIVE_RENDERABLE, EGL10.EGL_NATIVE_VISUAL_ID, EGL10.EGL_NATIVE_VISUAL_TYPE,
+                    0x3030,  // EGL10.EGL_PRESERVED_RESOURCES,
+                    EGL10.EGL_SAMPLES, EGL10.EGL_SAMPLE_BUFFERS,
+                    EGL10.EGL_SURFACE_TYPE,
+                    EGL10.EGL_TRANSPARENT_TYPE, EGL10.EGL_TRANSPARENT_RED_VALUE, EGL10.EGL_TRANSPARENT_GREEN_VALUE, EGL10.EGL_TRANSPARENT_BLUE_VALUE,
+                    0x3039, 0x303A,  // EGL10.EGL_BIND_TO_TEXTURE_RGB, EGL10.EGL_BIND_TO_TEXTURE_RGBA,
+                    0x303B, 0x303C,  // EGL10.EGL_MIN_SWAP_INTERVAL, EGL10.EGL_MAX_SWAP_INTERVAL
+                    EGL10.EGL_LUMINANCE_SIZE, EGL10.EGL_ALPHA_MASK_SIZE,
+                    EGL10.EGL_COLOR_BUFFER_TYPE, EGL10.EGL_RENDERABLE_TYPE,
+                    0x3042 // EGL10.EGL_CONFORMANT
+            )
+            val THE_NAMES = arrayOf(
+                    "EGL_BUFFER_SIZE", "EGL_ALPHA_SIZE", "EGL_BLUE_SIZE", "EGL_GREEN_SIZE", "EGL_RED_SIZE", "EGL_DEPTH_SIZE", "EGL_STENCIL_SIZE",
+                    "EGL_CONFIG_CAVEAT",
+                    "EGL_CONFIG_ID",
+                    "EGL_LEVEL",
+                    "EGL_MAX_PBUFFER_HEIGHT", "EGL_MAX_PBUFFER_PIXELS", "EGL_MAX_PBUFFER_WIDTH",
+                    "EGL_NATIVE_RENDERABLE", "EGL_NATIVE_VISUAL_ID", "EGL_NATIVE_VISUAL_TYPE",
+                    "EGL_PRESERVED_RESOURCES",
+                    "EGL_SAMPLES", "EGL_SAMPLE_BUFFERS",
+                    "EGL_SURFACE_TYPE",
+                    "EGL_TRANSPARENT_TYPE", "EGL_TRANSPARENT_RED_VALUE", "EGL_TRANSPARENT_GREEN_VALUE", "EGL_TRANSPARENT_BLUE_VALUE",
+                    "EGL_BIND_TO_TEXTURE_RGB", "EGL_BIND_TO_TEXTURE_RGBA",
+                    "EGL_MIN_SWAP_INTERVAL", "EGL_MAX_SWAP_INTERVAL",
+                    "EGL_LUMINANCE_SIZE", "EGL_ALPHA_MASK_SIZE",
+                    "EGL_COLOR_BUFFER_TYPE", "EGL_RENDERABLE_TYPE",
+                    "EGL_CONFORMANT"
+            )
+            val aValue = IntArray(1)
+            for (anAttrIter in THE_ATTRIBS.indices) {
+                val anAttr = THE_ATTRIBS[anAttrIter]
+                val aName = THE_NAMES[anAttrIter]
+                if (theEgl.eglGetConfigAttrib(theEglDisplay, theEglConfig, anAttr, aValue)) {
+                    postMessage(String.format("  %s: %d\n", aName, aValue[0]))
+                } else {
+                    popEglErrors(theEgl)
+                }
+            }
+        }
+
+        //! Interface implementation
+        override fun chooseConfig(theEgl: EGL10,
+                                  theEglDisplay: EGLDisplay): EGLConfig {
+            val EGL_OPENGL_ES2_BIT = 4
+            val aCfgAttribs = intArrayOf(
+                    EGL10.EGL_RED_SIZE, 8,
+                    EGL10.EGL_GREEN_SIZE, 8,
+                    EGL10.EGL_BLUE_SIZE, 8,
+                    EGL10.EGL_ALPHA_SIZE, 0,
+                    EGL10.EGL_DEPTH_SIZE, 24,
+                    EGL10.EGL_STENCIL_SIZE, 8,
+                    EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+                    EGL10.EGL_NONE
+            )
+            val aConfigs = arrayOfNulls<EGLConfig>(1)
+            val aNbConfigs = IntArray(1)
+            if (!theEgl.eglChooseConfig(theEglDisplay, aCfgAttribs, aConfigs, 1, aNbConfigs)
+                    || aConfigs[0] == null) {
+                aCfgAttribs[4 * 2 + 1] = 16 // try config with smaller depth buffer
+                popEglErrors(theEgl)
+                if (!theEgl.eglChooseConfig(theEglDisplay, aCfgAttribs, aConfigs, 1, aNbConfigs)
+                        || aConfigs[0] == null) {
+                    postMessage("Error: eglChooseConfig() has failed!")
+                    ///return null
+                }
+            }
+
+            //printConfig (theEgl, theEglDisplay, aConfigs[0]);
+            return aConfigs[0]!!
+        }
+    }
+
+    //! Callback to handle touch events
+    override fun onTouchEvent(theEvent: MotionEvent): Boolean {
+        val aMaskedAction = theEvent.actionMasked
+        when (aMaskedAction) {
+            MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
+                val aPointerIndex = theEvent.actionIndex
+                val aPointerId = theEvent.getPointerId(aPointerIndex)
+                val aPnt = PointF(theEvent.getX(aPointerIndex), theEvent.getY(aPointerIndex))
+                mySelectPoint = if (theEvent.pointerCount == 1) {
+                    aPnt
+                } else {
+                    null
+                }
+                queueEvent { myRenderer!!.onAddTouchPoint(aPointerId, aPnt.x, aPnt.y) }
+            }
+            MotionEvent.ACTION_MOVE -> {
+                val aNbPointers = theEvent.pointerCount
+                var aPntIter = 0
+                while (aPntIter < aNbPointers) {
+                    val aPointerId = theEvent.getPointerId(aPntIter)
+                    val aPnt = PointF(theEvent.getX(aPntIter), theEvent.getY(aPntIter))
+                    queueEvent { myRenderer!!.onUpdateTouchPoint(aPointerId, aPnt.x, aPnt.y) }
+                    ++aPntIter
+                }
+                if (mySelectPoint != null) {
+                    val aTouchThreshold = 5.0f * myScreenDensity
+                    val aPointerIndex = theEvent.actionIndex
+                    val aDelta = PointF(theEvent.getX(aPointerIndex) - mySelectPoint!!.x, theEvent.getY(aPointerIndex) - mySelectPoint!!.y)
+                    if (Math.abs(aDelta.x) > aTouchThreshold || Math.abs(aDelta.y) > aTouchThreshold) {
+                        mySelectPoint = null
+                    }
+                }
+            }
+            MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> {
+                if (mySelectPoint != null) {
+                    val aSelX = mySelectPoint!!.x
+                    val aSelY = mySelectPoint!!.y
+                    queueEvent { myRenderer!!.onSelectInViewer(aSelX, aSelY) }
+                    mySelectPoint = null
+                }
+                val aPointerIndex = theEvent.actionIndex
+                val aPointerId = theEvent.getPointerId(aPointerIndex)
+                //val aPnt = PointF(theEvent.getX(aPointerIndex), theEvent.getY(aPointerIndex))
+                queueEvent { myRenderer!!.onRemoveTouchPoint(aPointerId) }
+            }
+        }
+        requestRender()
+        return true
+    }
+
+    //! Fit All
+    fun fitAll() {
+        queueEvent { myRenderer!!.fitAll() }
+        requestRender()
+    }
+
+    //! Move camera
+    fun setProj(theProj: TypeOfOrientation?) {
+        queueEvent { myRenderer!!.setProj(theProj) }
+        requestRender()
+    }
+
+    //! OCCT viewer
+    private var myRenderer: OcctJniRenderer? = null
+    private val mySelectId = -1
+    private var mySelectPoint: PointF? = null
+    private var myScreenDensity = 1.0f
+
+    // ! Default constructor.
+    init {
+        val aDispInfo = theContext.resources.displayMetrics
+        myScreenDensity = aDispInfo.density
+        preserveEGLContextOnPause = true
+        setEGLContextFactory(ContextFactory())
+        setEGLConfigChooser(ConfigChooser())
+        val aLParams = RelativeLayout.LayoutParams(ActionBar.LayoutParams.WRAP_CONTENT, ActionBar.LayoutParams.WRAP_CONTENT)
+        aLParams.addRule(RelativeLayout.ALIGN_TOP)
+        myRenderer = OcctJniRenderer(this, myScreenDensity)
+        setRenderer(myRenderer)
+        renderMode = RENDERMODE_WHEN_DIRTY // render on request to spare battery
+    }
+}
diff --git a/samples/kotlin/jniviewer/app/src/main/jni/CMakeLists.txt b/samples/kotlin/jniviewer/app/src/main/jni/CMakeLists.txt
new file mode 100644 (file)
index 0000000..59e41e6
--- /dev/null
@@ -0,0 +1,44 @@
+cmake_minimum_required(VERSION 3.4.1)
+
+set(HEADER_FILES OcctJni_MsgPrinter.hxx OcctJni_Viewer.hxx)
+set(SOURCE_FILES OcctJni_MsgPrinter.cxx OcctJni_Viewer.cxx)
+
+set (anOcctLibs
+  TKernel TKMath TKG2d TKG3d TKGeomBase TKBRep TKGeomAlgo TKTopAlgo TKShHealing TKMesh
+  # exchange
+  TKPrim TKBO TKBool TKFillet TKOffset
+  TKXSBase
+  TKSTL
+  TKIGES
+  TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP
+  # OCCT Visualization
+  TKService TKHLR TKV3d TKOpenGles
+)
+
+set(aLibDeps "")
+
+# OCCT libraries
+include_directories(${OCCT_ROOT}/inc)
+foreach(anOcctLib ${anOcctLibs})
+  add_library(lib_${anOcctLib} SHARED IMPORTED)
+  set_target_properties(lib_${anOcctLib} PROPERTIES IMPORTED_LOCATION ${OCCT_ROOT}/libs/${ANDROID_ABI}/lib${anOcctLib}.so)
+  list(APPEND aLibDeps lib_${anOcctLib})
+endforeach()
+
+# FreeType
+add_library(lib_FreeType SHARED IMPORTED)
+set_target_properties(lib_FreeType PROPERTIES IMPORTED_LOCATION ${FREETYPE_ROOT}/libs/${ANDROID_ABI}/libfreetype.so)
+list(APPEND aLibDeps lib_FreeType)
+
+# FreeImage - uncomment, if OCCT was built with FreeImage
+#add_library(lib_FreeImage SHARED IMPORTED)
+#set_target_properties(lib_FreeImage PROPERTIES IMPORTED_LOCATION ${FREETYPE_ROOT}/libs/${ANDROID_ABI}/libfreeimage.so)
+#list(APPEND aLibDeps lib_FreeImage)
+
+# system libraries
+list(APPEND aLibDeps EGL GLESv2 log android)
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -frtti -fexceptions -fpermissive")
+
+add_library(TKJniSample SHARED ${SOURCE_FILES})
+target_link_libraries(TKJniSample ${aLibDeps})
diff --git a/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.cxx b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.cxx
new file mode 100644 (file)
index 0000000..2934c97
--- /dev/null
@@ -0,0 +1,79 @@
+// Copyright (c) 2014-2021 OPEN CASCADE SAS
+//
+// This file is part of the examples of the Open CASCADE Technology software library.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+#include "OcctJni_MsgPrinter.hxx"
+
+#include <TCollection_AsciiString.hxx>
+#include <TCollection_ExtendedString.hxx>
+
+#include <android/log.h>
+
+IMPLEMENT_STANDARD_RTTIEXT(OcctJni_MsgPrinter, Message_Printer)
+
+// =======================================================================
+// function : OcctJni_MsgPrinter
+// purpose  :
+// =======================================================================
+OcctJni_MsgPrinter::OcctJni_MsgPrinter (JNIEnv* theJEnv,
+                                        jobject theJObj)
+: myJEnv (theJEnv),
+  myJObj (theJEnv->NewGlobalRef (theJObj)),
+  myJMet (NULL)
+{
+  jclass aJClass = theJEnv->GetObjectClass (theJObj);
+  myJMet = theJEnv->GetMethodID (aJClass, "postMessage", "(Ljava/lang/String;)V");
+  if (myJMet == NULL)
+  {
+    __android_log_write (ANDROID_LOG_FATAL, "jniSample", "Broken initialization of OcctJni_MsgPrinter!");
+  }
+}
+
+// =======================================================================
+// function : ~OcctJni_MsgPrinter
+// purpose  :
+// =======================================================================
+OcctJni_MsgPrinter::~OcctJni_MsgPrinter()
+{
+  //myJEnv->DeleteGlobalRef (myJObj);
+}
+
+// =======================================================================
+// function : send
+// purpose  :
+// =======================================================================
+void OcctJni_MsgPrinter::send (const TCollection_AsciiString& theString,
+                               const Message_Gravity theGravity) const
+{
+  if (theGravity < myTraceLevel)
+  {
+    return;
+  }
+
+  ///__android_log_write (ANDROID_LOG_DEBUG, "OcctJni_MsgPrinter", (TCollection_AsciiString(" @@ ") + theString).ToCString());
+  if (myJMet == NULL)
+  {
+    return;
+  }
+
+  jstring aJStr = myJEnv->NewStringUTF ((theString + "\n").ToCString());
+  myJEnv->CallVoidMethod (myJObj, myJMet, aJStr);
+  myJEnv->DeleteLocalRef (aJStr);
+}
diff --git a/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.hxx b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_MsgPrinter.hxx
new file mode 100644 (file)
index 0000000..a52b03b
--- /dev/null
@@ -0,0 +1,56 @@
+// Copyright (c) 2014-2021 OPEN CASCADE SAS
+//
+// This file is part of the examples of the Open CASCADE Technology software library.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+#ifndef OcctJni_MsgPrinter_H
+#define OcctJni_MsgPrinter_H
+
+#include <Message_Printer.hxx>
+
+#include <jni.h>
+
+// Class providing connection between messenger interfaces in C++ and Java layers.
+class OcctJni_MsgPrinter : public Message_Printer
+{
+  DEFINE_STANDARD_RTTIEXT(OcctJni_MsgPrinter, Message_Printer)
+public:
+
+  //! Default constructor
+  OcctJni_MsgPrinter (JNIEnv* theJEnv,
+                      jobject theJObj);
+
+  //! Destructor.
+  ~OcctJni_MsgPrinter();
+
+protected:
+
+  //! Main printing method
+  virtual void send (const TCollection_AsciiString& theString,
+                     const Message_Gravity theGravity) const override;
+
+private:
+
+  JNIEnv*   myJEnv;
+  jobject   myJObj;
+  jmethodID myJMet;
+
+};
+
+#endif // OcctJni_MsgPrinter_H
diff --git a/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.cxx b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.cxx
new file mode 100644 (file)
index 0000000..59ec8e9
--- /dev/null
@@ -0,0 +1,718 @@
+// Copyright (c) 2014-2021 OPEN CASCADE SAS
+//
+// This file is part of the examples of the Open CASCADE Technology software library.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+#include "OcctJni_Viewer.hxx"
+#include "OcctJni_MsgPrinter.hxx"
+
+#include <AIS_ViewCube.hxx>
+#include <AIS_Shape.hxx>
+#include <Aspect_NeutralWindow.hxx>
+#include <Image_AlienPixMap.hxx>
+#include <BRepTools.hxx>
+#include <Message_Messenger.hxx>
+#include <Message_PrinterSystemLog.hxx>
+#include <OpenGl_GraphicDriver.hxx>
+#include <OSD_Timer.hxx>
+#include <Prs3d_DatumAspect.hxx>
+#include <Standard_Version.hxx>
+
+#include <BRepPrimAPI_MakeBox.hxx>
+
+#include <RWStl.hxx>
+#include <IGESControl_Reader.hxx>
+#include <STEPControl_Reader.hxx>
+#include <XSControl_WorkSession.hxx>
+
+#include <EGL/egl.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <jni.h>
+
+// =======================================================================
+// function : OcctJni_Viewer
+// purpose  :
+// =======================================================================
+OcctJni_Viewer::OcctJni_Viewer (float theDispDensity)
+: myDevicePixelRatio (theDispDensity),
+  myIsJniMoreFrames (false)
+{
+  SetTouchToleranceScale (theDispDensity);
+#ifndef NDEBUG
+  // Register printer for logging messages into global Android log.
+  // Should never be used in production (or specify higher gravity for logging only failures).
+  Handle(Message_Messenger) aMsgMgr = Message::DefaultMessenger();
+  aMsgMgr->RemovePrinters (STANDARD_TYPE (Message_PrinterSystemLog));
+  aMsgMgr->AddPrinter (new Message_PrinterSystemLog ("OcctJni_Viewer"));
+#endif
+}
+
+// ================================================================
+// Function : dumpGlInfo
+// Purpose  :
+// ================================================================
+void OcctJni_Viewer::dumpGlInfo (bool theIsBasic)
+{
+  TColStd_IndexedDataMapOfStringString aGlCapsDict;
+  myView->DiagnosticInformation (aGlCapsDict, Graphic3d_DiagnosticInfo_Basic); //theIsBasic ? Graphic3d_DiagnosticInfo_Basic : Graphic3d_DiagnosticInfo_Complete);
+  if (theIsBasic)
+  {
+    TCollection_AsciiString aViewport;
+    aGlCapsDict.FindFromKey ("Viewport", aViewport);
+    aGlCapsDict.Clear();
+    aGlCapsDict.Add ("Viewport", aViewport);
+  }
+  aGlCapsDict.Add ("Display scale", TCollection_AsciiString(myDevicePixelRatio));
+
+  // beautify output
+  {
+    TCollection_AsciiString* aGlVer   = aGlCapsDict.ChangeSeek ("GLversion");
+    TCollection_AsciiString* aGlslVer = aGlCapsDict.ChangeSeek ("GLSLversion");
+    if (aGlVer   != NULL
+     && aGlslVer != NULL)
+    {
+      *aGlVer = *aGlVer + " [GLSL: " + *aGlslVer + "]";
+      aGlslVer->Clear();
+    }
+  }
+
+  TCollection_AsciiString anInfo;
+  for (TColStd_IndexedDataMapOfStringString::Iterator aValueIter (aGlCapsDict); aValueIter.More(); aValueIter.Next())
+  {
+    if (!aValueIter.Value().IsEmpty())
+    {
+      if (!anInfo.IsEmpty())
+      {
+        anInfo += "\n";
+      }
+      anInfo += aValueIter.Key() + ": " + aValueIter.Value();
+    }
+  }
+
+  Message::SendWarning (anInfo);
+}
+
+// =======================================================================
+// function : init
+// purpose  :
+// =======================================================================
+bool OcctJni_Viewer::init()
+{
+  EGLint aCfgId = 0;
+  int aWidth = 0, aHeight = 0;
+  EGLDisplay anEglDisplay = eglGetCurrentDisplay();
+  EGLContext anEglContext = eglGetCurrentContext();
+  EGLSurface anEglSurf    = eglGetCurrentSurface (EGL_DRAW);
+  if (anEglDisplay == EGL_NO_DISPLAY
+   || anEglContext == EGL_NO_CONTEXT
+   || anEglSurf    == EGL_NO_SURFACE)
+  {
+    Message::SendFail ("Error: No active EGL context!");
+    release();
+    return false;
+  }
+
+  eglQuerySurface (anEglDisplay, anEglSurf, EGL_WIDTH,     &aWidth);
+  eglQuerySurface (anEglDisplay, anEglSurf, EGL_HEIGHT,    &aHeight);
+  eglQuerySurface (anEglDisplay, anEglSurf, EGL_CONFIG_ID, &aCfgId);
+  const EGLint aConfigAttribs[] = { EGL_CONFIG_ID, aCfgId, EGL_NONE };
+  EGLint       aNbConfigs = 0;
+  void*        anEglConfig = NULL;
+  if (eglChooseConfig (anEglDisplay, aConfigAttribs, &anEglConfig, 1, &aNbConfigs) != EGL_TRUE)
+  {
+    Message::SendFail ("Error: EGL does not provide compatible configurations!");
+    release();
+    return false;
+  }
+
+  if (!myViewer.IsNull())
+  {
+    Handle(OpenGl_GraphicDriver) aDriver = Handle(OpenGl_GraphicDriver)::DownCast (myViewer->Driver());
+    Handle(Aspect_NeutralWindow) aWindow = Handle(Aspect_NeutralWindow)::DownCast (myView->Window());
+    if (!aDriver->InitEglContext (anEglDisplay, anEglContext, anEglConfig))
+    {
+      Message::SendFail ("Error: OpenGl_GraphicDriver can not be initialized!");
+      release();
+      return false;
+    }
+
+    aWindow->SetSize (aWidth, aHeight);
+    myView->SetWindow (aWindow, (Aspect_RenderingContext )anEglContext);
+    dumpGlInfo (true);
+    return true;
+  }
+
+  Handle(OpenGl_GraphicDriver) aDriver = new OpenGl_GraphicDriver (NULL, Standard_False);
+  aDriver->ChangeOptions().buffersNoSwap = true;
+  aDriver->ChangeOptions().buffersOpaqueAlpha = true;
+  aDriver->ChangeOptions().useSystemBuffer = false;
+  if (!aDriver->InitEglContext (anEglDisplay, anEglContext, anEglConfig))
+  {
+    Message::SendFail ("Error: OpenGl_GraphicDriver can not be initialized!");
+    release();
+    return false;
+  }
+
+  myTextStyle = new Prs3d_TextAspect();
+  myTextStyle->SetFont (Font_NOF_ASCII_MONO);
+  myTextStyle->SetHeight (12);
+  myTextStyle->Aspect()->SetColor (Quantity_NOC_GRAY95);
+  myTextStyle->Aspect()->SetColorSubTitle (Quantity_NOC_BLACK);
+  myTextStyle->Aspect()->SetDisplayType (Aspect_TODT_SHADOW);
+  myTextStyle->Aspect()->SetTextFontAspect (Font_FA_Bold);
+  myTextStyle->Aspect()->SetTextZoomable (false);
+  myTextStyle->SetHorizontalJustification (Graphic3d_HTA_LEFT);
+  myTextStyle->SetVerticalJustification (Graphic3d_VTA_BOTTOM);
+
+  // create viewer
+  myViewer = new V3d_Viewer (aDriver);
+  myViewer->SetDefaultBackgroundColor (Quantity_NOC_BLACK);
+  myViewer->SetDefaultLights();
+  myViewer->SetLightOn();
+
+  // create AIS context
+  myContext = new AIS_InteractiveContext (myViewer);
+  myContext->SetPixelTolerance (int(myDevicePixelRatio * 6.0)); // increase tolerance and adjust to hi-dpi screens
+  myContext->SetDisplayMode (AIS_Shaded, false);
+
+  Handle(Aspect_NeutralWindow) aWindow = new Aspect_NeutralWindow();
+  aWindow->SetSize (aWidth, aHeight);
+  myView = myViewer->CreateView();
+  myView->SetImmediateUpdate (false);
+  myView->ChangeRenderingParams().Resolution = (unsigned int )(96.0 * myDevicePixelRatio + 0.5);
+  myView->ChangeRenderingParams().ToShowStats = true;
+  myView->ChangeRenderingParams().CollectedStats = (Graphic3d_RenderingParams::PerfCounters ) (Graphic3d_RenderingParams::PerfCounters_FrameRate | Graphic3d_RenderingParams::PerfCounters_Triangles);
+  myView->ChangeRenderingParams().StatsTextAspect = myTextStyle->Aspect();
+  myView->ChangeRenderingParams().StatsTextHeight = (int )myTextStyle->Height();
+
+  myView->SetWindow (aWindow, (Aspect_RenderingContext )anEglContext);
+  dumpGlInfo (false);
+  //myView->TriedronDisplay (Aspect_TOTP_RIGHT_LOWER, Quantity_NOC_WHITE, 0.08 * myDevicePixelRatio, V3d_ZBUFFER);
+
+  initContent();
+  return true;
+}
+
+// =======================================================================
+// function : release
+// purpose  :
+// =======================================================================
+void OcctJni_Viewer::release()
+{
+  myContext.Nullify();
+  myView.Nullify();
+  myViewer.Nullify();
+}
+
+// =======================================================================
+// function : resize
+// purpose  :
+// =======================================================================
+void OcctJni_Viewer::resize (int theWidth,
+                             int theHeight)
+{
+  if (myContext.IsNull())
+  {
+    Message::SendFail ("Resize failed - view is unavailable");
+    return;
+  }
+
+  Handle(OpenGl_GraphicDriver) aDriver = Handle(OpenGl_GraphicDriver)::DownCast (myViewer->Driver());
+  Handle(Aspect_NeutralWindow) aWindow = Handle(Aspect_NeutralWindow)::DownCast (myView->Window());
+  aWindow->SetSize (theWidth, theHeight);
+  //myView->MustBeResized(); // can be used instead of SetWindow() when EGLsurface has not been changed
+
+  EGLContext anEglContext = eglGetCurrentContext();
+  myView->SetWindow (aWindow, (Aspect_RenderingContext )anEglContext);
+  dumpGlInfo (true);
+}
+
+// =======================================================================
+// function : initContent
+// purpose  :
+// =======================================================================
+void OcctJni_Viewer::initContent()
+{
+  myContext->RemoveAll (Standard_False);
+
+  if (myViewCube.IsNull())
+  {
+    myViewCube = new AIS_ViewCube();
+    {
+      // setup view cube size
+      static const double THE_CUBE_SIZE = 60.0;
+      myViewCube->SetSize (myDevicePixelRatio * THE_CUBE_SIZE, false);
+      myViewCube->SetBoxFacetExtension (myViewCube->Size() * 0.15);
+      myViewCube->SetAxesPadding (myViewCube->Size() * 0.10);
+      myViewCube->SetFontHeight  (THE_CUBE_SIZE * 0.16);
+    }
+    // presentation parameters
+    myViewCube->SetTransformPersistence (new Graphic3d_TransformPers (Graphic3d_TMF_TriedronPers, Aspect_TOTP_RIGHT_LOWER, Graphic3d_Vec2i (200, 200)));
+    myViewCube->Attributes()->SetDatumAspect (new Prs3d_DatumAspect());
+    myViewCube->Attributes()->DatumAspect()->SetTextAspect (myTextStyle);
+    // animation parameters
+    myViewCube->SetViewAnimation (myViewAnimation);
+    myViewCube->SetFixedAnimationLoop (false);
+    myViewCube->SetAutoStartAnimation (true);
+  }
+  myContext->Display (myViewCube, false);
+
+  OSD_Timer aTimer;
+  aTimer.Start();
+  if (!myShape.IsNull())
+  {
+    Handle(AIS_Shape) aShapePrs = new AIS_Shape (myShape);
+    myContext->Display (aShapePrs, Standard_False);
+  }
+  else
+  {
+    BRepPrimAPI_MakeBox aBuilder (1.0, 2.0, 3.0);
+    Handle(AIS_Shape) aShapePrs = new AIS_Shape (aBuilder.Shape());
+    myContext->Display (aShapePrs, Standard_False);
+  }
+  myView->FitAll();
+
+  aTimer.Stop();
+  Message::SendInfo (TCollection_AsciiString() + "Presentation computed in " + aTimer.ElapsedTime() + " seconds");
+}
+
+//! Load shape from IGES file
+static TopoDS_Shape loadIGES (const TCollection_AsciiString& thePath)
+{
+  TopoDS_Shape          aShape;
+  IGESControl_Reader    aReader;
+  IFSelect_ReturnStatus aReadStatus = IFSelect_RetFail;
+  try
+  {
+    aReadStatus = aReader.ReadFile (thePath.ToCString());
+  }
+  catch (Standard_Failure)
+  {
+    Message::SendFail ("Error: IGES reader, computation error");
+    return aShape;
+  }
+
+  if (aReadStatus != IFSelect_RetDone)
+  {
+    Message::SendFail ("Error: IGES reader, bad file format");
+    return aShape;
+  }
+
+  // now perform the translation
+  aReader.TransferRoots();
+  if (aReader.NbShapes() <= 0)
+  {
+    Handle(XSControl_WorkSession) aWorkSession = new XSControl_WorkSession();
+    aWorkSession->SelectNorm ("IGES");
+    aReader.SetWS (aWorkSession, Standard_True);
+    aReader.SetReadVisible (Standard_False);
+    aReader.TransferRoots();
+  }
+  if (aReader.NbShapes() <= 0)
+  {
+    Message::SendFail ("Error: IGES reader, no shapes has been found");
+    return aShape;
+  }
+  return aReader.OneShape();
+}
+
+//! Load shape from STEP file
+static TopoDS_Shape loadSTEP (const TCollection_AsciiString& thePath)
+{
+  STEPControl_Reader    aReader;
+  IFSelect_ReturnStatus aReadStatus = IFSelect_RetFail;
+  try
+  {
+    aReadStatus = aReader.ReadFile (thePath.ToCString());
+  }
+  catch (Standard_Failure)
+  {
+    Message::SendFail ("Error: STEP reader, computation error");
+    return TopoDS_Shape();
+  }
+
+  if (aReadStatus != IFSelect_RetDone)
+  {
+    Message::SendFail ("Error: STEP reader, bad file format");
+    return TopoDS_Shape();
+  }
+  else if (aReader.NbRootsForTransfer() <= 0)
+  {
+    Message::SendFail ("Error: STEP reader, shape is empty");
+    return TopoDS_Shape();
+  }
+
+  // now perform the translation
+  aReader.TransferRoots();
+  return aReader.OneShape();
+}
+
+//! Load shape from STL file
+static TopoDS_Shape loadSTL (const TCollection_AsciiString& thePath)
+{
+  Handle(Poly_Triangulation) aTri = RWStl::ReadFile (thePath.ToCString());
+  TopoDS_Face aFace;
+  BRep_Builder().MakeFace (aFace, aTri);
+  return aFace;
+}
+
+// =======================================================================
+// function : open
+// purpose  :
+// =======================================================================
+bool OcctJni_Viewer::open (const TCollection_AsciiString& thePath)
+{
+  myShape.Nullify();
+  if (!myContext.IsNull())
+  {
+    myContext->RemoveAll (Standard_False);
+    if (!myViewCube.IsNull())
+    {
+      myContext->Display (myViewCube, false);
+    }
+  }
+  if (thePath.IsEmpty())
+  {
+    return false;
+  }
+
+  OSD_Timer aTimer;
+  aTimer.Start();
+  TCollection_AsciiString aFileName, aFormatStr;
+  OSD_Path::FileNameAndExtension (thePath, aFileName, aFormatStr);
+    aFormatStr.LowerCase();
+
+  TopoDS_Shape aShape;
+  if (aFormatStr == "stp"
+   || aFormatStr == "step")
+  {
+    aShape = loadSTEP (thePath);
+  }
+  else if (aFormatStr == "igs"
+        || aFormatStr == "iges")
+  {
+    aShape = loadIGES (thePath);
+  }
+  else if (aFormatStr == "stl")
+  {
+      aShape = loadSTL (thePath);
+  }
+  else
+      // if (aFormatStr == "brep"
+      //  || aFormatStr == "rle")
+  {
+    BRep_Builder aBuilder;
+    if (!BRepTools::Read (aShape, thePath.ToCString(), aBuilder))
+    {
+      Message::SendInfo (TCollection_AsciiString() + "Error: file '" + thePath + "' can not be opened");
+      return false;
+    }
+  }
+  if (aShape.IsNull())
+  {
+    return false;
+  }
+  aTimer.Stop();
+  Message::SendInfo (TCollection_AsciiString() + "File '" + thePath + "' loaded in " + aTimer.ElapsedTime() + " seconds");
+
+  myShape = aShape;
+  if (myContext.IsNull())
+  {
+    return true;
+  }
+
+  aTimer.Reset();
+  aTimer.Start();
+
+  Handle(AIS_Shape) aShapePrs = new AIS_Shape (aShape);
+  myContext->Display (aShapePrs, Standard_False);
+  myView->FitAll();
+
+  aTimer.Stop();
+  Message::SendInfo (TCollection_AsciiString() + "Presentation computed in " + aTimer.ElapsedTime() + " seconds");
+  return true;
+}
+
+// =======================================================================
+// function : saveSnapshot
+// purpose  :
+// =======================================================================
+bool OcctJni_Viewer::saveSnapshot (const TCollection_AsciiString& thePath,
+                                   int theWidth,
+                                   int theHeight)
+{
+  if (myContext.IsNull()
+   || thePath.IsEmpty())
+  {
+    Message::SendFail ("Image dump failed - view is unavailable");
+    return false;
+  }
+
+  if (theWidth  < 1
+   || theHeight < 1)
+  {
+    myView->Window()->Size (theWidth, theHeight);
+  }
+  if (theWidth  < 1
+   || theHeight < 1)
+  {
+    Message::SendFail ("Image dump failed - view is unavailable");
+    return false;
+  }
+
+  Image_AlienPixMap anImage;
+  if (!anImage.InitTrash (Image_Format_BGRA, theWidth, theHeight))
+  {
+    Message::SendFail (TCollection_AsciiString() + "RGBA image " + theWidth + "x" + theHeight + " allocation failed");
+    return false;
+  }
+
+  if (!myView->ToPixMap (anImage, theWidth, theHeight, Graphic3d_BT_RGBA))
+  {
+    Message::SendFail (TCollection_AsciiString() + "View dump to the image " + theWidth + "x" + theHeight + " failed");
+  }
+  if (!anImage.Save (thePath))
+  {
+    Message::SendFail (TCollection_AsciiString() + "Image saving to path '" + thePath + "' failed");
+    return false;
+  }
+  Message::SendInfo (TCollection_AsciiString() + "View " + theWidth + "x" + theHeight + " dumped to image '" + thePath + "'");
+  return true;
+}
+
+// ================================================================
+// Function : handleViewRedraw
+// Purpose  :
+// ================================================================
+void OcctJni_Viewer::handleViewRedraw (const Handle(AIS_InteractiveContext)& theCtx,
+                                       const Handle(V3d_View)& theView)
+{
+  AIS_ViewController::handleViewRedraw (theCtx, theView);
+  myIsJniMoreFrames = myToAskNextFrame;
+}
+
+// =======================================================================
+// function : redraw
+// purpose  :
+// =======================================================================
+bool OcctJni_Viewer::redraw()
+{
+  if (myView.IsNull())
+  {
+    return false;
+  }
+
+  // handle user input
+  myIsJniMoreFrames = false;
+  myView->InvalidateImmediate();
+  FlushViewEvents (myContext, myView, true);
+  return myIsJniMoreFrames;
+}
+
+// =======================================================================
+// function : fitAll
+// purpose  :
+// =======================================================================
+void OcctJni_Viewer::fitAll()
+{
+  if (myView.IsNull())
+  {
+    return;
+  }
+
+  myView->FitAll (0.01, Standard_False);
+  myView->Invalidate();
+}
+
+#define jexp extern "C" JNIEXPORT
+
+jexp jlong JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppCreate (JNIEnv* theEnv,
+                                                                             jobject theObj,
+                                                                             jfloat  theDispDensity)
+{
+  return jlong(new OcctJni_Viewer (theDispDensity));
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppDestroy (JNIEnv* theEnv,
+                                                                             jobject theObj,
+                                                                             jlong   theCppPtr)
+{
+  delete (OcctJni_Viewer* )theCppPtr;
+
+  Handle(Message_Messenger) aMsgMgr = Message::DefaultMessenger();
+  aMsgMgr->RemovePrinters (STANDARD_TYPE (OcctJni_MsgPrinter));
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppRelease (JNIEnv* theEnv,
+                                                                             jobject theObj,
+                                                                             jlong   theCppPtr)
+{
+  ((OcctJni_Viewer* )theCppPtr)->release();
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppInit (JNIEnv* theEnv,
+                                                                          jobject theObj,
+                                                                          jlong   theCppPtr)
+{
+  Handle(Message_Messenger) aMsgMgr = Message::DefaultMessenger();
+  aMsgMgr->RemovePrinters (STANDARD_TYPE (OcctJni_MsgPrinter));
+  aMsgMgr->AddPrinter (new OcctJni_MsgPrinter (theEnv, theObj));
+  ((OcctJni_Viewer* )theCppPtr)->init();
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppResize (JNIEnv* theEnv,
+                                                                            jobject theObj,
+                                                                            jlong   theCppPtr,
+                                                                            jint    theWidth,
+                                                                            jint    theHeight)
+{
+  ((OcctJni_Viewer* )theCppPtr)->resize (theWidth, theHeight);
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppOpen (JNIEnv* theEnv,
+                                                                          jobject theObj,
+                                                                          jlong   theCppPtr,
+                                                                          jstring thePath)
+{
+  const char* aPathPtr = theEnv->GetStringUTFChars (thePath, 0);
+  const TCollection_AsciiString aPath (aPathPtr);
+  theEnv->ReleaseStringUTFChars (thePath, aPathPtr);
+  ((OcctJni_Viewer* )theCppPtr)->open (aPath);
+}
+
+jexp jboolean JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppRedraw (JNIEnv* theEnv,
+                                                                                jobject theObj,
+                                                                                jlong   theCppPtr)
+{
+  return ((OcctJni_Viewer* )theCppPtr)->redraw() ? JNI_TRUE : JNI_FALSE;
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetAxoProj (JNIEnv* theEnv,
+                                                                                jobject theObj,
+                                                                                jlong   theCppPtr)
+{
+  ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_XposYnegZpos);
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetXposProj (JNIEnv* theEnv,
+                                                                                 jobject theObj,
+                                                                                 jlong   theCppPtr)
+{
+  ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Xpos);
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetYposProj (JNIEnv* theEnv,
+                                                                                 jobject theObj,
+                                                                                 jlong   theCppPtr)
+{
+  ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Ypos);
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetZposProj (JNIEnv* theEnv,
+                                                                                 jobject theObj,
+                                                                                 jlong   theCppPtr)
+{
+  ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Zpos);
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetXnegProj (JNIEnv* theEnv,
+                                                                                 jobject theObj,
+                                                                                 jlong   theCppPtr)
+{
+  ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Xneg);
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetYnegProj (JNIEnv* theEnv,
+                                                                                 jobject theObj,
+                                                                                 jlong   theCppPtr)
+{
+  ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Yneg);
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSetZnegProj (JNIEnv* theEnv,
+                                                                                 jobject theObj,
+                                                                                 jlong   theCppPtr)
+{
+  ((OcctJni_Viewer* )theCppPtr)->setProj (V3d_Zneg);
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppFitAll (JNIEnv* theEnv,
+                                                                            jobject theObj,
+                                                                            jlong   theCppPtr)
+{
+  ((OcctJni_Viewer* )theCppPtr)->fitAll();
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppAddTouchPoint (JNIEnv* theEnv,
+                                                                                   jobject theObj,
+                                                                                   jlong   theCppPtr,
+                                                                                   jint    theId,
+                                                                                   jfloat  theX,
+                                                                                   jfloat  theY)
+{
+  ((OcctJni_Viewer* )theCppPtr)->AddTouchPoint (theId, Graphic3d_Vec2d (theX, theY));
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppUpdateTouchPoint (JNIEnv* theEnv,
+                                                                                   jobject theObj,
+                                                                                   jlong   theCppPtr,
+                                                                                   jint    theId,
+                                                                                   jfloat  theX,
+                                                                                   jfloat  theY)
+{
+  ((OcctJni_Viewer* )theCppPtr)->UpdateTouchPoint (theId, Graphic3d_Vec2d (theX, theY));
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppRemoveTouchPoint (JNIEnv* theEnv,
+                                                                                   jobject theObj,
+                                                                                   jlong   theCppPtr,
+                                                                                   jint    theId)
+{
+  ((OcctJni_Viewer* )theCppPtr)->RemoveTouchPoint (theId);
+}
+
+jexp void JNICALL Java_com_opencascade_jnisample_OcctJniRenderer_cppSelectInViewer (JNIEnv* theEnv,
+                                                                                    jobject theObj,
+                                                                                    jlong   theCppPtr,
+                                                                                    jfloat  theX,
+                                                                                    jfloat  theY)
+{
+  ((OcctJni_Viewer* )theCppPtr)->SelectInViewer (Graphic3d_Vec2i ((int )theX, (int )theY));
+}
+
+jexp jlong JNICALL Java_com_opencascade_jnisample_OcctJniActivity_cppOcctMajorVersion (JNIEnv* theEnv,
+                                                                                       jobject theObj)
+{
+  return OCC_VERSION_MAJOR;
+}
+
+jexp jlong JNICALL Java_com_opencascade_jnisample_OcctJniActivity_cppOcctMinorVersion (JNIEnv* theEnv,
+                                                                                       jobject theObj)
+{
+  return OCC_VERSION_MINOR;
+}
+
+jexp jlong JNICALL Java_com_opencascade_jnisample_OcctJniActivity_cppOcctMicroVersion (JNIEnv* theEnv,
+                                                                                       jobject theObj)
+{
+  return OCC_VERSION_MAINTENANCE;
+}
diff --git a/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.hxx b/samples/kotlin/jniviewer/app/src/main/jni/OcctJni_Viewer.hxx
new file mode 100644 (file)
index 0000000..f56e86e
--- /dev/null
@@ -0,0 +1,99 @@
+// Copyright (c) 2014-2021 OPEN CASCADE SAS
+//
+// This file is part of the examples of the Open CASCADE Technology software library.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+#include <AIS_InteractiveContext.hxx>
+#include <AIS_ViewController.hxx>
+#include <TopoDS_Shape.hxx>
+#include <V3d_Viewer.hxx>
+#include <V3d_View.hxx>
+
+class AIS_ViewCube;
+
+//! Main C++ back-end for activity.
+class OcctJni_Viewer : public AIS_ViewController
+{
+
+public:
+
+  //! Empty constructor
+  OcctJni_Viewer (float theDispDensity);
+
+  //! Initialize the viewer
+  bool init();
+
+  //! Release the viewer
+  void release();
+
+  //! Resize the viewer
+  void resize (int theWidth,
+               int theHeight);
+
+  //! Open CAD file
+  bool open (const TCollection_AsciiString& thePath);
+
+  //! Take snapshot
+  bool saveSnapshot (const TCollection_AsciiString& thePath,
+                     int theWidth  = 0,
+                     int theHeight = 0);
+
+  //! Viewer update.
+  //! Returns TRUE if more frames should be requested.
+  bool redraw();
+
+  //! Move camera
+  void setProj (V3d_TypeOfOrientation theProj)
+  {
+    if (myView.IsNull())
+    {
+      return;
+    }
+
+    myView->SetProj (theProj);
+    myView->Invalidate();
+  }
+
+  //! Fit All.
+  void fitAll();
+
+protected:
+
+  //! Reset viewer content.
+  void initContent();
+
+  //! Print information about OpenGL ES context.
+  void dumpGlInfo (bool theIsBasic);
+
+  //! Handle redraw.
+  virtual void handleViewRedraw (const Handle(AIS_InteractiveContext)& theCtx,
+                                 const Handle(V3d_View)& theView) override;
+
+protected:
+
+  Handle(V3d_Viewer)             myViewer;
+  Handle(V3d_View)               myView;
+  Handle(AIS_InteractiveContext) myContext;
+  Handle(Prs3d_TextAspect)       myTextStyle; //!< text style for OSD elements
+  Handle(AIS_ViewCube)           myViewCube;  //!< view cube object
+  TopoDS_Shape                   myShape;
+  float                          myDevicePixelRatio; //!< device pixel ratio for handling high DPI displays
+  bool                           myIsJniMoreFrames;  //!< need more frame flag
+
+};
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_l.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_l.png
new file mode 100644 (file)
index 0000000..0125c4a
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_l.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_p.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_p.png
new file mode 100644 (file)
index 0000000..b5ce8bd
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/close_p.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/fit.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/fit.png
new file mode 100644 (file)
index 0000000..70daee1
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/fit.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/ic_launcher.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..d27ba82
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/ic_launcher.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/info.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/info.png
new file mode 100644 (file)
index 0000000..88d27c8
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/info.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/info_image.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/info_image.png
new file mode 100644 (file)
index 0000000..bac9bea
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/info_image.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/message.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/message.png
new file mode 100644 (file)
index 0000000..a3dc8cc
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/message.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open.png
new file mode 100644 (file)
index 0000000..68e5265
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_l.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_l.png
new file mode 100644 (file)
index 0000000..6069ce3
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_l.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_p.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_p.png
new file mode 100644 (file)
index 0000000..c0898e6
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/open_p.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_back.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_back.png
new file mode 100644 (file)
index 0000000..77ab199
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_back.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_bottom.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_bottom.png
new file mode 100644 (file)
index 0000000..b0bb012
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_bottom.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_front.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_front.png
new file mode 100644 (file)
index 0000000..b93d4d3
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_front.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_left.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_left.png
new file mode 100644 (file)
index 0000000..bce95df
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_left.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_right.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_right.png
new file mode 100644 (file)
index 0000000..8cdb338
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_right.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_top.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_top.png
new file mode 100644 (file)
index 0000000..4ad098f
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/proj_top.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/view.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/view.png
new file mode 100644 (file)
index 0000000..76ce879
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-hdpi/view.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-mdpi/ic_launcher.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..4b86dbf
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-mdpi/ic_launcher.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-xhdpi/ic_launcher.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..cd79bea
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/samples/kotlin/jniviewer/app/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..a34301f
Binary files /dev/null and b/samples/kotlin/jniviewer/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/samples/kotlin/jniviewer/app/src/main/res/layout/activity_main.xml b/samples/kotlin/jniviewer/app/src/main/res/layout/activity_main.xml
new file mode 100644 (file)
index 0000000..e7f3ebf
--- /dev/null
@@ -0,0 +1,166 @@
+<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"\r
+android:id="@+id/panel_main"\r
+android:layout_width="fill_parent"\r
+android:layout_height="fill_parent"\r
+android:stretchColumns="1">\r
+\r
+    <LinearLayout android:layout_height="fill_parent"\r
+    android:layout_width="fill_parent"\r
+    android:id="@+id/linearLayout2">\r
+\r
+        <FrameLayout\r
+        android:id="@+id/submenu_group"\r
+        android:layout_width="fill_parent"\r
+        android:layout_height="fill_parent" >\r
+\r
+            <com.opencascade.jnisample.OcctJniView\r
+            android:id="@+id/custom_view"\r
+            android:layout_width="fill_parent"\r
+            android:layout_height="fill_parent"\r
+            android:layout_gravity="bottom|end" >\r
+            </com.opencascade.jnisample.OcctJniView>\r
+\r
+            <ImageButton\r
+            android:id="@+id/scroll_btn"\r
+            style="?android:borderlessButtonStyle"\r
+            android:layout_width="match_parent"\r
+            android:layout_height="wrap_content"\r
+            android:background="@null"\r
+            android:src="@drawable/close_p" />\r
+\r
+            <LinearLayout\r
+            android:id="@+id/panel_menu"\r
+            android:layout_width="fill_parent"\r
+            android:layout_height="wrap_content"\r
+            android:orientation="horizontal" >\r
+                <ImageButton\r
+                android:id="@+id/open"\r
+                style="?android:borderlessButtonStyle"\r
+                android:layout_width="wrap_content"\r
+                android:layout_height="wrap_content"\r
+                android:layout_weight=".2"\r
+                android:background="@color/btnColor"\r
+                android:src="@drawable/open" />\r
+\r
+                <ImageButton\r
+                android:id="@+id/fit"\r
+                style="?android:borderlessButtonStyle"\r
+                android:layout_width="wrap_content"\r
+                android:layout_height="wrap_content"\r
+                android:layout_weight=".2"\r
+                android:background="@color/btnColor"\r
+                android:src="@drawable/fit" />\r
+\r
+                <ImageButton\r
+                android:id="@+id/view"\r
+                style="?android:borderlessButtonStyle"\r
+                android:layout_width="wrap_content"\r
+                android:layout_height="wrap_content"\r
+                android:layout_weight=".2"\r
+                android:background="@color/btnColor"\r
+                android:src="@drawable/view" />\r
+\r
+                <ImageButton\r
+                android:id="@+id/info"\r
+                style="?android:borderlessButtonStyle"\r
+                android:layout_width="wrap_content"\r
+                android:layout_height="wrap_content"\r
+                android:layout_weight=".2"\r
+                android:background="@color/btnColor"\r
+                android:src="@drawable/info" />\r
+\r
+                <ImageButton\r
+                android:id="@+id/message"\r
+                style="?android:borderlessButtonStyle"\r
+                android:layout_width="wrap_content"\r
+                android:layout_height="wrap_content"\r
+                android:layout_weight=".2"\r
+                android:background="@color/btnColor"\r
+                android:src="@drawable/message" />\r
+            </LinearLayout>\r
+\r
+                <TextView\r
+                android:id="@+id/message_view"\r
+                android:background="@color/viewColor"\r
+                android:text="Message Log"\r
+                android:textSize="16px"\r
+                android:textStyle="bold"\r
+                android:visibility="gone"\r
+                android:layout_width="wrap_content"\r
+                android:layout_height="wrap_content"/>\r
+\r
+            <TextView\r
+            android:id="@+id/info_view"\r
+            android:background="@color/viewColor"\r
+            android:gravity="center"\r
+            android:text="info Log"\r
+            android:textSize="16px"\r
+            android:textStyle="bold"\r
+            android:visibility="gone"\r
+            android:layout_width="fill_parent"\r
+            android:layout_height="wrap_content"/>\r
+\r
+            <LinearLayout\r
+            android:id="@+id/view_group"\r
+            android:layout_width="fill_parent"\r
+            android:layout_height="wrap_content"\r
+            android:layout_gravity="bottom|end"\r
+            android:orientation="horizontal" >\r
+\r
+                <ImageButton \r
+                style="?android:borderlessButtonStyle"\r
+                android:background="@color/btnColor" \r
+                android:id="@+id/proj_front"\r
+                android:layout_height="wrap_content" \r
+                android:layout_width="fill_parent"\r
+                android:src="@drawable/proj_front"\r
+                android:layout_weight=".16"/>\r
+\r
+                <ImageButton \r
+                style="?android:borderlessButtonStyle"\r
+                android:background="@color/btnColor" \r
+                android:id="@+id/proj_top"\r
+                android:layout_height="wrap_content" \r
+                android:layout_width="fill_parent"\r
+                android:src="@drawable/proj_top"\r
+                android:layout_weight=".16"/>\r
+\r
+                <ImageButton \r
+                style="?android:borderlessButtonStyle"\r
+                android:background="@color/btnColor" \r
+                android:id="@+id/proj_left"\r
+                android:layout_height="wrap_content" \r
+                android:layout_width="fill_parent"\r
+                android:src="@drawable/proj_left"\r
+                android:layout_weight=".16"/>\r
+\r
+                <ImageButton \r
+                style="?android:borderlessButtonStyle"\r
+                android:background="@color/btnColor" \r
+                android:id="@+id/proj_back"\r
+                android:layout_height="wrap_content" \r
+                android:layout_width="fill_parent"\r
+                android:src="@drawable/proj_back"\r
+                android:layout_weight=".16"/>\r
+\r
+                <ImageButton \r
+                style="?android:borderlessButtonStyle"\r
+                android:background="@color/btnColor" \r
+                android:id="@+id/proj_bottom"\r
+                android:layout_height="wrap_content" \r
+                android:layout_width="fill_parent"\r
+                android:src="@drawable/proj_bottom"\r
+                android:layout_weight=".16"/>\r
+\r
+                <ImageButton \r
+                style="?android:borderlessButtonStyle"\r
+                android:background="@color/btnColor" \r
+                android:id="@+id/proj_right"\r
+                android:layout_height="wrap_content" \r
+                android:layout_width="fill_parent"\r
+                android:src="@drawable/proj_right"\r
+                android:layout_weight=".16"/>\r
+            </LinearLayout>\r
+        </FrameLayout>\r
+    </LinearLayout>\r
+</TableLayout>\r
diff --git a/samples/kotlin/jniviewer/app/src/main/res/values/id.xml b/samples/kotlin/jniviewer/app/src/main/res/values/id.xml
new file mode 100644 (file)
index 0000000..56bde94
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+   <item name="open" type="id"/>
+   <item name="save" type="id"/>
+   <item name="screenshot" type="id"/>
+   <item name="mesh_none" type="id"/>
+   <item name="mesh_low_q" type="id"/>
+   <item name="mesh_norm_q" type="id"/>
+   <item name="mesh_high_q" type="id"/>
+   <item name="view1" type="id"/>
+   <item name="view2" type="id"/>
+   <item name="view3" type="id"/>
+   <item name="settings" type="id"/>
+   <item name="message" type="id"/>
+   <item name="lock" type="id"/>
+   <item name="info" type="id"/>
+</resources>
diff --git a/samples/kotlin/jniviewer/app/src/main/res/values/strings.xml b/samples/kotlin/jniviewer/app/src/main/res/values/strings.xml
new file mode 100644 (file)
index 0000000..039a88f
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">OpenCASCADE JNI Kotlin Sample</string>
+    <color name="btnColor">#484848</color>
+    <color name="pressedBtnColor">#0099CC</color>
+    <color name="viewColor">#66252525</color>
+        <string-array name="ext_to_save">
+        <item>.png</item>
+        <item>.jpg</item>
+    </string-array>
+    <string-array name="ext_to_exp">
+        <item>.brep</item>
+        <item>.rle</item>
+        <item>.iges</item>
+        <item>.igs</item>
+        <item>.step</item>
+        <item>.stp</item>
+    </string-array>
+    <string name="wireframe_shading_title">wireframe/shading</string>
+    <string name="color_title">color</string>
+    <string name="material_title">material</string>
+    <string name="transparency_title">transparency</string>
+    <string name="hidden_lines_title">show/hide hidden lines</string>
+    <string name="info_html" formatted="false">
+        <![CDATA[
+        <p>OpenCASCADE JNI Kotlin Sample</p>
+        <p>Simple viewer for BREP, STEP and IGES files.</p>
+        <p>Driven by Open CASCADE Technology %d.%d.%d.</p>
+        <p>Copyright 2014-2021 OPEN CASCADE SAS.</p>
+        <p><img src="info_image"></p>
+        <p>https://dev.opencascade.org</p>
+        ]]>
+    </string>
+</resources>
diff --git a/samples/kotlin/jniviewer/build.gradle b/samples/kotlin/jniviewer/build.gradle
new file mode 100644 (file)
index 0000000..7252192
--- /dev/null
@@ -0,0 +1,19 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+    ext.kotlin_version = '1.4.32'
+    repositories {
+        jcenter()
+        google()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:4.0.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+        google()
+    }
+}
diff --git a/samples/kotlin/jniviewer/gradle.properties.template b/samples/kotlin/jniviewer/gradle.properties.template
new file mode 100644 (file)
index 0000000..2cf7323
--- /dev/null
@@ -0,0 +1,5 @@
+# customized paths
+OCCT_ROOT=c\:/android/occt-dev-android
+FREETYPE_ROOT=c\:/android/freetype-2.7.1-android
+# in case if OCCT was built with FreeImage
+#FREEIMAGE_ROOT=c\:/android/freeimage-3.17-android
diff --git a/samples/kotlin/jniviewer/settings.gradle b/samples/kotlin/jniviewer/settings.gradle
new file mode 100644 (file)
index 0000000..e7b4def
--- /dev/null
@@ -0,0 +1 @@
+include ':app'