diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8e801b3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: [ '**' ] + pull_request: + branches: [ '**' ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install tools + run: | + python -m pip install --upgrade pip + pip install -e ".[ci]" + + - name: Lint (Ruff) + run: | + ruff check . + + - name: Format check (Black) + run: | + black --check src examples tests + + - name: Type check (MyPy) + run: | + mypy --install-types --non-interactive + + - name: Install package + run: | + pip install . + + - name: Run tests + run: | + pytest -q diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..ddce20b --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,30 @@ +name: Docs + +on: + push: + branches: [ main ] + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install docs deps + run: | + python -m pip install --upgrade pip + pip install -e ".[docs]" + - name: Build site + run: | + python -m mkdocs build --strict + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site + cname: docs.pythonnative.com diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..07450cc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: Release to PyPI + +on: + push: + tags: + - 'v*.*.*' + +jobs: + build-and-publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9' + + - name: Build sdist and wheel + run: | + python -m pip install -U pip build + python -m build + + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true diff --git a/.gitignore b/.gitignore index d5dd7b9..c5de671 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ cover/ local_settings.py db.sqlite3 db.sqlite3-journal +**/staticfiles/ # Flask stuff: instance/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cedf103 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,261 @@ +### Contributing to PythonNative + +Thanks for your interest in contributing. This repository contains the PythonNative library, CLI, templates, a demo app, and a Django site used for docs/demo hosting and E2E. Contributions should keep the code reliable, cross‑platform, and easy to use. + +## Quick start + +Development uses Python ≥ 3.9. + +```bash +# create and activate a venv (recommended) +python3 -m venv .venv && source .venv/bin/activate + +# install dev tools (lint/format/test) +pip install -e ".[dev]" + +# install library (editable) and CLI +pip install -e . + +# run tests +pytest -q + +# format and lint +black src examples tests || true +ruff check . +``` + +Common library and CLI entry points: + +```bash +# CLI help +pn --help + +# create a new sample app (template fetch is remote) +pn init my_app + +# run the Hello World example +cd examples/hello-world && pn run android +``` + +## Project layout (high‑level) + +- `src/pythonnative/` – installable library and CLI + - `pythonnative/` – core cross‑platform UI components and utilities + - `cli/` – `pn` command +- `tests/` – unit tests for the library +- `templates/` – Android/iOS project templates and zips +- `examples/` – runnable example apps + - `hello-world/` – minimal demo app using the library +- `experiments/` – platform experiments (Android/iOS/Briefcase) +- `README.md`, `pyproject.toml` – repo docs and packaging + +## Coding guidelines + +- Style: Black; lint: Ruff; typing where useful. Keep APIs stable. +- Prefer explicit, descriptive names; keep platform abstractions clean. +- Add/extend tests under `tests/` for new behavior. +- Do not commit generated artifacts or large binaries; templates live under `templates/`. + +Common commands: + +```bash +pytest -q # run tests +ruff check . # lint +black src examples tests # format +``` + +## Conventional Commits + +This project uses Conventional Commits. Use the form: + +``` +(): + +[optional body] + +[optional footer(s)] +``` + +### Commit message character set + +- Encoding: UTF‑8 is allowed and preferred across subjects and bodies. +- Keep the subject ≤ 72 chars; avoid emoji. + +Accepted types (standard): + +- `build` – build system or external dependencies (e.g., requirements, packaging) +- `chore` – maintenance (no library behavior change) +- `ci` – continuous integration configuration (workflows, pipelines) +- `docs` – documentation only +- `feat` – user‑facing feature or capability +- `fix` – bug fix +- `perf` – performance improvements +- `refactor` – code change that neither fixes a bug nor adds a feature +- `revert` – revert of a previous commit +- `style` – formatting/whitespace (no code behavior) +- `test` – add/adjust tests only + +Recommended scopes (match the smallest accurate directory/module): + +- Library/CLI scopes: + - `cli` – `src/pythonnative/cli/` (the `pn` command) + - `core` – `src/pythonnative/pythonnative/` package internals + - `components` – UI view modules under `src/pythonnative/pythonnative/` (e.g., `button.py`, `label.py`) + - `utils` – utilities like `utils.py` + - `tests` – `tests/` + +- Templates and examples: + - `templates` – `templates/` (Android/iOS templates, zips) + - `examples` – `examples/` (e.g., `hello-world/`) + - `experiments` – `experiments/` + + + +- Repo‑level and ops: + - `deps` – dependency updates and version pins + - `docker` – containerization files (e.g., `Dockerfile`) + - `repo` – top‑level files (`README.md`, `CONTRIBUTING.md`, `.gitignore`, licenses) + - `mkdocs` – documentation site (MkDocs/Material) configuration and content under `docs/` + - `workflows` – CI pipelines (e.g., `.github/workflows/`) + +Note: Avoid redundant type==scope pairs (e.g., `docs(docs)`). Prefer a module scope (e.g., `docs(core)`) or `docs(repo)` for top‑level updates. + +Examples: + +```text +build(deps): refresh pinned versions +chore(repo): add contributing guidelines +ci(workflows): add publish job +docs(core): clarify ListView data contract +feat(components): add MaterialSearchBar +fix(cli): handle missing Android SDK gracefully +perf(core): reduce allocations in list diffing +refactor(utils): extract path helpers +test(tests): cover ios template copy flow +``` + +Examples (no scope): + +```text +build: update packaging metadata +chore: update .gitignore patterns +docs: add project overview +``` + +Breaking changes: + +- Use `!` after the type/scope or a `BREAKING CHANGE:` footer. + +```text +feat(core)!: rename Page.set_root_view to set_root + +BREAKING CHANGE: API renamed; update app code and templates. +``` + +### Multiple scopes (optional) + +- Comma‑separate scopes without spaces: `type(scope1,scope2): ...` +- Prefer a single scope when possible; use multiple only when the change genuinely spans tightly related areas. + +Scope ordering (house style): + +- Put the most impacted scope first (e.g., `repo`), then any secondary scopes. +- For extra consistency, alphabetize the remaining scopes after the primary. +- Keep it to 1–3 scopes max. + +Example: + +```text +feat(templates,cli): add ios template and wire pn init +``` + +## Pull requests and squash merges + +- PR title: use Conventional Commit format. + - Example: `feat(cli): add init subcommand` + - Imperative mood; no trailing period; ≤ 72 chars; `!` for breaking changes. +- PR description: include brief sections: What, Why, How (brief), Testing, Risks/Impact, Docs/Follow‑ups. + - Link issues with keywords (e.g., `Closes #123`). +- Merging: prefer “Squash and merge” with “Pull request title and description”. +- Keep PRs focused; avoid unrelated changes in the same PR. + +Recommended PR template: + +```text +What +- Short summary of the change + +Why +- Motivation/user value + +How (brief) +- Key implementation notes or decisions + +Testing +- Local/CI coverage; links to tests if relevant + +Risks/Impact +- Compat, rollout, perf, security; mitigations + +Docs/Follow-ups +- Docs updated or TODO next steps + +Closes #123 +BREAKING CHANGE:
+Co-authored-by: Name +``` + +## Pull request checklist + +- Tests: added/updated; `pytest` passes. +- Lint/format: `ruff check .`, `black` pass. +- Docs: update `README.md` and any Django docs pages if behavior changes. +- Templates: update `templates/` if generator output changes. +- No generated artifacts committed. + +## Versioning and releases + +- The library version is tracked in `pyproject.toml` (`project.version`). Use SemVer. +- Workflow: + - Contributors: branch off `main` (or `dev` if used) and open PRs. + - Maintainer (release): bump version, tag, and publish to PyPI. + - Tag on `main`: `git tag -a vX.Y.Z -m "Release vX.Y.Z" && git push --tags`. + +### Branch naming (suggested) + +- Use lowercase kebab‑case; concise (≤ 40 chars). +- Prefix conventions: + - `feature/-` + - `fix/-` + - `chore/` + - `docs/` + - `ci/` + - `refactor/-` + - `test/` + - `perf/` + - `build/` + - `release/vX.Y.Z` + - `hotfix/` + +Examples: + +```text +feature/cli-init +fix/core-threading-deadlock-123 +docs/contributing +ci/publish-pypi +build/lock-versions +refactor/utils-paths +test/templates-android +release/v0.2.0 +hotfix/cli-regression +``` + +## Security and provenance + +- Avoid bundling secrets or credentials in templates or code. +- Prefer runtime configuration via environment variables for Django and CI. + +## License + +By contributing, you agree that your contributions are licensed under the repository’s MIT License. diff --git a/libs/pythonnative/LICENSE b/LICENSE similarity index 95% rename from libs/pythonnative/LICENSE rename to LICENSE index 783dd5e..18fff31 100644 --- a/libs/pythonnative/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 PythonNative +Copyright (c) 2025 PythonNative Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ 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. \ No newline at end of file +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4418c1b --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# PythonNative + +**PythonNative** is a cross-platform toolkit that allows you to create native Android and iOS apps using Python. Inspired by frameworks like React Native and NativeScript, PythonNative provides a Pythonic interface for building native UI elements, handling lifecycle events, and accessing platform-specific APIs. + +## Features + +- **Native UI Components**: Create and manage native buttons, labels, lists, and more, all from Python. +- **Cross-Platform**: Write once, run on both Android and iOS. +- **Lifecycle Management**: Handle app lifecycle events with ease. +- **Native API Access**: Access device features like Camera, Geolocation, and Notifications. +- **Powered by Proven Tools**: PythonNative integrates seamlessly with [Rubicon](https://beeware.org/project/projects/bridges/rubicon/) for iOS and [Chaquopy](https://chaquo.com/chaquopy/) for Android, ensuring robust native performance. + +## Quick Start + +### Installation + +First, install PythonNative via pip: + +```bash +pip install pythonnative +``` + +### Create Your First App + +Initialize a new PythonNative app: + +```bash +pn init my_app +``` + +Your app directory will look like this: + +```text +my_app/ +├── README.md +├── app +│ ├── __init__.py +│ ├── main_page.py +│ └── resources +├── pythonnative.json +├── requirements.txt +└── tests +``` + +### Writing Views + +In PythonNative, everything is a view. Here's a simple example of how to create a main page with a list view: + +```python +import pythonnative as pn + +class MainPage(pn.Page): + def __init__(self, native_instance): + super().__init__(native_instance) + + def on_create(self): + super().on_create() + stack_view = pn.StackView(self.native_instance) + list_data = ["item_{}".format(i) for i in range(100)] + list_view = pn.ListView(self.native_instance, list_data) + stack_view.add_view(list_view) + self.set_root_view(stack_view) +``` + +### Run the app + +```bash +pn run android +pn run ios +``` + +## Documentation + +For detailed guides and API references, visit the [PythonNative documentation](https://docs.pythonnative.com/). diff --git a/apps/android_pythonnative/app/build.gradle b/apps/android_pythonnative/app/build.gradle deleted file mode 100644 index c540b35..0000000 --- a/apps/android_pythonnative/app/build.gradle +++ /dev/null @@ -1,77 +0,0 @@ -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' - id 'com.chaquo.python' -} - -android { - namespace 'com.pythonnative.pythonnative' - compileSdk 33 - - defaultConfig { - applicationId "com.pythonnative.pythonnative" - minSdk 24 - targetSdk 33 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary true - } - - ndk { - abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" - } - python { - pip { - install "matplotlib" - } - } - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } - buildFeatures { - compose true - } - composeOptions { - kotlinCompilerExtensionVersion '1.3.2' - } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } -} - -dependencies { - - implementation 'androidx.core:core-ktx:1.8.0' - implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' - implementation 'androidx.activity:activity-compose:1.5.1' - implementation platform('androidx.compose:compose-bom:2022.10.00') - implementation 'androidx.compose.ui:ui' - implementation 'androidx.compose.ui:ui-graphics' - implementation 'androidx.compose.ui:ui-tooling-preview' - implementation 'androidx.compose.material3:material3' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') - androidTestImplementation 'androidx.compose.ui:ui-test-junit4' - debugImplementation 'androidx.compose.ui:ui-tooling' - debugImplementation 'androidx.compose.ui:ui-test-manifest' -} \ No newline at end of file diff --git a/apps/android_pythonnative/app/src/main/AndroidManifest.xml b/apps/android_pythonnative/app/src/main/AndroidManifest.xml deleted file mode 100644 index 353b451..0000000 --- a/apps/android_pythonnative/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt b/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt deleted file mode 100644 index 25332e8..0000000 --- a/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.pythonnative.pythonnative - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.pythonnative.pythonnative.ui.theme.PythonnativeTheme - -import android.graphics.BitmapFactory -import android.widget.Toast -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.OutlinedTextField -import androidx.compose.runtime.* -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import com.chaquo.python.PyException -import com.chaquo.python.Python -import com.chaquo.python.android.AndroidPlatform -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.platform.LocalSoftwareKeyboardController - - - - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - PythonnativeTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - MainScreen() - } - } - } - } -} - - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - PythonnativeTheme { - Greeting("Android") - } -} - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) -@Composable -fun MainScreen() { - val context = LocalContext.current - var bitmap by remember { mutableStateOf(null) } - val coroutineScope = rememberCoroutineScope() - - // Initialize Chaquopy - if (!Python.isStarted()) { - Python.start(AndroidPlatform(context)) - } - val py = Python.getInstance() - val plotModule = py.getModule("plot") - - // Variables to keep the user's input - var xInput by remember { mutableStateOf("") } - var yInput by remember { mutableStateOf("") } - - val keyboardController = LocalSoftwareKeyboardController.current - - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - OutlinedTextField( - value = xInput, - onValueChange = { xInput = it }, - label = { Text("Input X") } - ) - OutlinedTextField( - value = yInput, - onValueChange = { yInput = it }, - label = { Text("Input Y") } - ) - Button(onClick = { - coroutineScope.launch { - try { - val bytes = plotModule.callAttr( - "plot", - xInput, - yInput - ).toJava(ByteArray::class.java) - withContext(Dispatchers.IO) { - bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - } - } catch (e: PyException) { - Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() - } - keyboardController?.hide() - } - }) { - Text(text = "Generate Plot") - } - bitmap?.let { - Image( - bitmap = it.asImageBitmap(), - contentDescription = "Generated plot", - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - contentScale = ContentScale.FillWidth - ) - } - } -} diff --git a/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/ui/theme/Color.kt b/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/ui/theme/Color.kt deleted file mode 100644 index 4c935ff..0000000 --- a/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.pythonnative.pythonnative.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/ui/theme/Theme.kt b/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/ui/theme/Theme.kt deleted file mode 100644 index 19b8b7f..0000000 --- a/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/ui/theme/Theme.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.pythonnative.pythonnative.ui.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun PythonnativeTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme - } - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} \ No newline at end of file diff --git a/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/ui/theme/Type.kt b/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/ui/theme/Type.kt deleted file mode 100644 index a4f1ca3..0000000 --- a/apps/android_pythonnative/app/src/main/java/com/pythonnative/pythonnative/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.pythonnative.pythonnative.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/apps/android_pythonnative/app/src/main/res/values/colors.xml b/apps/android_pythonnative/app/src/main/res/values/colors.xml deleted file mode 100644 index f8c6127..0000000 --- a/apps/android_pythonnative/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/apps/android_pythonnative/app/src/main/res/values/themes.xml b/apps/android_pythonnative/app/src/main/res/values/themes.xml deleted file mode 100644 index 3d9a4dd..0000000 --- a/apps/android_pythonnative/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/android_pythonnative_2/app/src/main/res/values/colors.xml b/apps/android_pythonnative_2/app/src/main/res/values/colors.xml deleted file mode 100644 index f8c6127..0000000 --- a/apps/android_pythonnative_2/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/apps/android_pythonnative_2/app/src/main/res/values/dimens.xml b/apps/android_pythonnative_2/app/src/main/res/values/dimens.xml deleted file mode 100644 index e00c2dd..0000000 --- a/apps/android_pythonnative_2/app/src/main/res/values/dimens.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 16dp - 16dp - \ No newline at end of file diff --git a/apps/android_pythonnative_2/app/src/main/res/values/strings.xml b/apps/android_pythonnative_2/app/src/main/res/values/strings.xml deleted file mode 100644 index 93b11db..0000000 --- a/apps/android_pythonnative_2/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - pythonnative - Home - Dashboard - Notifications - \ No newline at end of file diff --git a/apps/android_pythonnative_2/app/src/main/res/values/themes.xml b/apps/android_pythonnative_2/app/src/main/res/values/themes.xml deleted file mode 100644 index 7566594..0000000 --- a/apps/android_pythonnative_2/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/apps/android_pythonnative_2/app/src/test/java/com/pythonnative/pythonnative/ExampleUnitTest.kt b/apps/android_pythonnative_2/app/src/test/java/com/pythonnative/pythonnative/ExampleUnitTest.kt deleted file mode 100644 index 1bb2d2b..0000000 --- a/apps/android_pythonnative_2/app/src/test/java/com/pythonnative/pythonnative/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.pythonnative.pythonnative - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/apps/android_pythonnative_2/gradle/wrapper/gradle-wrapper.properties b/apps/android_pythonnative_2/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ec5b26f..0000000 --- a/apps/android_pythonnative_2/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Sat May 27 19:46:05 PDT 2023 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/apps/android_pythonnative_2/settings.gradle b/apps/android_pythonnative_2/settings.gradle deleted file mode 100644 index 8d1d42f..0000000 --- a/apps/android_pythonnative_2/settings.gradle +++ /dev/null @@ -1,16 +0,0 @@ -pluginManagement { - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} -rootProject.name = "pythonnative" -include ':app' diff --git a/apps/android_pythonnative_3/.gitignore b/apps/android_pythonnative_3/.gitignore deleted file mode 100644 index aa724b7..0000000 --- a/apps/android_pythonnative_3/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -.DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties diff --git a/apps/android_pythonnative_3/app/.gitignore b/apps/android_pythonnative_3/app/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/apps/android_pythonnative_3/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/apps/android_pythonnative_3/app/proguard-rules.pro b/apps/android_pythonnative_3/app/proguard-rules.pro deleted file mode 100644 index 481bb43..0000000 --- a/apps/android_pythonnative_3/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/apps/android_pythonnative_3/app/src/main/python/plot.py b/apps/android_pythonnative_3/app/src/main/python/plot.py deleted file mode 100644 index fdfdf94..0000000 --- a/apps/android_pythonnative_3/app/src/main/python/plot.py +++ /dev/null @@ -1,14 +0,0 @@ -import io -import matplotlib.pyplot as plt - - -def plot(x, y): - xa = [float(word) for word in x.split()] - ya = [float(word) for word in y.split()] - - fig, ax = plt.subplots() - ax.plot(xa, ya) - - f = io.BytesIO() - plt.savefig(f, format="png") - return f.getvalue() diff --git a/apps/android_pythonnative_3/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/apps/android_pythonnative_3/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/apps/android_pythonnative_3/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/apps/android_pythonnative_3/app/src/main/res/drawable/ic_launcher_background.xml b/apps/android_pythonnative_3/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/apps/android_pythonnative_3/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 6f3b755..0000000 --- a/apps/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/apps/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 6f3b755..0000000 --- a/apps/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/apps/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/apps/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/apps/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/apps/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/apps/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/apps/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/apps/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/apps/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/apps/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/apps/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/apps/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/apps/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/apps/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/apps/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/apps/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/apps/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/apps/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/apps/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/apps/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/apps/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/apps/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/apps/android_pythonnative_3/app/src/main/res/values/strings.xml b/apps/android_pythonnative_3/app/src/main/res/values/strings.xml deleted file mode 100644 index 115bda4..0000000 --- a/apps/android_pythonnative_3/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - pythonnative - \ No newline at end of file diff --git a/apps/android_pythonnative_3/app/src/main/res/xml/backup_rules.xml b/apps/android_pythonnative_3/app/src/main/res/xml/backup_rules.xml deleted file mode 100644 index fa0f996..0000000 --- a/apps/android_pythonnative_3/app/src/main/res/xml/backup_rules.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - \ No newline at end of file diff --git a/apps/android_pythonnative_3/app/src/main/res/xml/data_extraction_rules.xml b/apps/android_pythonnative_3/app/src/main/res/xml/data_extraction_rules.xml deleted file mode 100644 index 9ee9997..0000000 --- a/apps/android_pythonnative_3/app/src/main/res/xml/data_extraction_rules.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/apps/android_pythonnative_3/gradle.properties b/apps/android_pythonnative_3/gradle.properties deleted file mode 100644 index 3c5031e..0000000 --- a/apps/android_pythonnative_3/gradle.properties +++ /dev/null @@ -1,23 +0,0 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file diff --git a/apps/android_pythonnative_3/gradle/wrapper/gradle-wrapper.jar b/apps/android_pythonnative_3/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c..0000000 Binary files a/apps/android_pythonnative_3/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/apps/android_pythonnative_3/gradlew b/apps/android_pythonnative_3/gradlew deleted file mode 100755 index 4f906e0..0000000 --- a/apps/android_pythonnative_3/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/apps/beeware_pythonnative/.gitignore b/apps/beeware_pythonnative/.gitignore deleted file mode 100644 index f6c30e2..0000000 --- a/apps/beeware_pythonnative/.gitignore +++ /dev/null @@ -1,62 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# OSX useful to ignore -*.DS_Store -.AppleDouble -.LSOverride - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.dist-info/ -*.egg-info/ -.installed.cfg -*.egg - -# IntelliJ Idea family of suites -.idea -*.iml -## File-based project format: -*.ipr -*.iws -## mpeltonen/sbt-idea plugin -.idea_modules/ - -# Briefcase log files -logs/ diff --git a/apps/beeware_pythonnative/CHANGELOG b/apps/beeware_pythonnative/CHANGELOG deleted file mode 100644 index 80b32e7..0000000 --- a/apps/beeware_pythonnative/CHANGELOG +++ /dev/null @@ -1,5 +0,0 @@ -# Hello World Release Notes - -## 0.0.1 (27 May 2023) - -* Initial release diff --git a/apps/beeware_pythonnative/LICENSE b/apps/beeware_pythonnative/LICENSE deleted file mode 100644 index e7f170b..0000000 --- a/apps/beeware_pythonnative/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ - -Copyright (c) 2023, Owen Carey -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE -OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED -OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/apps/beeware_pythonnative/README.rst b/apps/beeware_pythonnative/README.rst deleted file mode 100644 index 1b2bad8..0000000 --- a/apps/beeware_pythonnative/README.rst +++ /dev/null @@ -1,12 +0,0 @@ -Hello World -=========== - -**This cross-platform app was generated by** `Briefcase`_ **- part of** -`The BeeWare Project`_. **If you want to see more tools like Briefcase, please -consider** `becoming a financial member of BeeWare`_. - -My first application - -.. _`Briefcase`: https://briefcase.readthedocs.io/ -.. _`The BeeWare Project`: https://beeware.org/ -.. _`becoming a financial member of BeeWare`: https://beeware.org/contributing/membership diff --git a/apps/beeware_pythonnative/pyproject.toml b/apps/beeware_pythonnative/pyproject.toml deleted file mode 100644 index 61fe56b..0000000 --- a/apps/beeware_pythonnative/pyproject.toml +++ /dev/null @@ -1,153 +0,0 @@ -# This project was generated with Unknown using template: https://github.com/beeware/briefcase-template@v0.3.14 -[tool.briefcase] -project_name = "Hello World" -bundle = "com.pythonnative" -version = "0.0.1" -url = "https://pythonnative.com/beeware_pythonnative" -license = "BSD license" -author = "Owen Carey" -author_email = "pythonnative@gmail.com" - -[tool.briefcase.app.beeware_pythonnative] -formal_name = "Hello World" -description = "My first application" -long_description = """More details about the app should go here. -""" -icon = "src/beeware_pythonnative/resources/beeware_pythonnative" -sources = [ - "src/beeware_pythonnative", -] -test_sources = [ - "tests", -] - -requires = [ -] -test_requires = [ - "pytest", -] - -[tool.briefcase.app.beeware_pythonnative.macOS] -requires = [ - "toga-cocoa~=0.3.1", - "std-nslog~=1.0.0" -] - -[tool.briefcase.app.beeware_pythonnative.linux] -requires = [ - "toga-gtk~=0.3.1", -] - -[tool.briefcase.app.beeware_pythonnative.linux.system.debian] -system_requires = [ - # Needed to compile pycairo wheel - "libcairo2-dev", - # Needed to compile PyGObject wheel - "libgirepository1.0-dev", -] - -system_runtime_requires = [ - # Needed to provide GTK - "libgtk-3-0", - # Needed to provide GI bindings to GTK - "libgirepository-1.0-1", - "gir1.2-gtk-3.0", - # Needed to provide WebKit2 at runtime - # "libwebkit2gtk-4.0-37", - # "gir1.2-webkit2-4.0", -] - -[tool.briefcase.app.beeware_pythonnative.linux.system.rhel] -system_requires = [ - # Needed to compile pycairo wheel - "cairo-gobject-devel", - # Needed to compile PyGObject wheel - "gobject-introspection-devel", -] - -system_runtime_requires = [ - # Needed to support Python bindings to GTK - "gobject-introspection", - # Needed to provide GTK - "gtk3", - # Needed to provide WebKit2 at runtime - # "webkit2gtk3", -] - -[tool.briefcase.app.beeware_pythonnative.linux.system.arch] -system_requires = [ - # Needed to compile pycairo wheel - "cairo", - # Needed to compile PyGObject wheel - "gobject-introspection", - # Runtime dependencies that need to exist so that the - # Arch package passes final validation. - # Needed to provide GTK - "gtk3", - # Dependencies that GTK looks for at runtime - "libcanberra", - # Needed to provide WebKit2 - # "webkit2gtk", -] - -system_runtime_requires = [ - # Needed to provide GTK - "gtk3", - # Needed to provide PyGObject bindings - "gobject-introspection-runtime", - # Dependencies that GTK looks for at runtime - "libcanberra", - # Needed to provide WebKit2 at runtime - # "webkit2gtk", -] - -[tool.briefcase.app.beeware_pythonnative.linux.appimage] -manylinux = "manylinux2014" - -system_requires = [ - # Needed to compile pycairo wheel - "cairo-gobject-devel", - # Needed to compile PyGObject wheel - "gobject-introspection-devel", - # Needed to provide GTK - "gtk3-devel", - # Dependencies that GTK looks for at runtime, that need to be - # in the build environment to be picked up by linuxdeploy - "libcanberra-gtk3", - "PackageKit-gtk3-module", - "gvfs-client", - # Needed to provide WebKit2 at runtime - # "webkit2gtk3", -] -linuxdeploy_plugins = [ - "DEPLOY_GTK_VERSION=3 gtk", -] - -[tool.briefcase.app.beeware_pythonnative.linux.flatpak] -flatpak_runtime = "org.gnome.Platform" -flatpak_runtime_version = "44" -flatpak_sdk = "org.gnome.Sdk" - -[tool.briefcase.app.beeware_pythonnative.windows] -requires = [ - "toga-winforms~=0.3.1", -] - -# Mobile deployments -[tool.briefcase.app.beeware_pythonnative.iOS] -requires = [ - "toga-iOS~=0.3.1", - "std-nslog~=1.0.0" -] - -[tool.briefcase.app.beeware_pythonnative.android] -requires = [ - "toga-android~=0.3.1" -] - -# Web deployments -[tool.briefcase.app.beeware_pythonnative.web] -requires = [ - "toga-web~=0.3.1", -] -style_framework = "Shoelace v2.3" diff --git a/apps/beeware_pythonnative/src/beeware_pythonnative/__main__.py b/apps/beeware_pythonnative/src/beeware_pythonnative/__main__.py deleted file mode 100644 index 673d874..0000000 --- a/apps/beeware_pythonnative/src/beeware_pythonnative/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -from beeware_pythonnative.app import main - -if __name__ == "__main__": - main().main_loop() diff --git a/apps/beeware_pythonnative/src/beeware_pythonnative/app.py b/apps/beeware_pythonnative/src/beeware_pythonnative/app.py deleted file mode 100644 index 6a5096c..0000000 --- a/apps/beeware_pythonnative/src/beeware_pythonnative/app.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -My first application -""" -import toga -from toga.style import Pack -from toga.style.pack import CENTER, COLUMN, ROW - - -class HelloWorld(toga.App): - def startup(self): - """ - Construct and show the Toga application. - - Usually, you would add your application to a main content box. - We then create a main window (with a name matching the app), and - show the main window. - """ - self.main_window = toga.MainWindow(title=self.name) - - self.webview = toga.WebView( - on_webview_load=self.on_webview_loaded, style=Pack(flex=1) - ) - self.url_input = toga.TextInput( - value="https://beeware.org/", style=Pack(flex=1) - ) - - main_box = toga.Box( - children=[ - toga.Box( - children=[ - self.url_input, - toga.Button( - "Go", - on_press=self.load_page, - style=Pack(width=50, padding_left=5), - ), - ], - style=Pack( - direction=ROW, - alignment=CENTER, - padding=5, - ), - ), - self.webview, - ], - style=Pack(direction=COLUMN), - ) - - self.main_window.content = main_box - self.webview.url = self.url_input.value - - # Show the main window - self.main_window.show() - - def load_page(self, widget): - self.webview.url = self.url_input.value - - def on_webview_loaded(self, widget): - self.url_input.value = self.webview.url - - -def main(): - return HelloWorld() diff --git a/apps/beeware_pythonnative/src/beeware_pythonnative/resources/beeware_pythonnative.icns b/apps/beeware_pythonnative/src/beeware_pythonnative/resources/beeware_pythonnative.icns deleted file mode 100644 index 27b43d3..0000000 Binary files a/apps/beeware_pythonnative/src/beeware_pythonnative/resources/beeware_pythonnative.icns and /dev/null differ diff --git a/apps/beeware_pythonnative/src/beeware_pythonnative/resources/beeware_pythonnative.ico b/apps/beeware_pythonnative/src/beeware_pythonnative/resources/beeware_pythonnative.ico deleted file mode 100644 index 357ada2..0000000 Binary files a/apps/beeware_pythonnative/src/beeware_pythonnative/resources/beeware_pythonnative.ico and /dev/null differ diff --git a/apps/beeware_pythonnative/src/beeware_pythonnative/resources/beeware_pythonnative.png b/apps/beeware_pythonnative/src/beeware_pythonnative/resources/beeware_pythonnative.png deleted file mode 100644 index 0ab2cb6..0000000 Binary files a/apps/beeware_pythonnative/src/beeware_pythonnative/resources/beeware_pythonnative.png and /dev/null differ diff --git a/apps/beeware_pythonnative/tests/beeware_pythonnative.py b/apps/beeware_pythonnative/tests/beeware_pythonnative.py deleted file mode 100644 index e1324c6..0000000 --- a/apps/beeware_pythonnative/tests/beeware_pythonnative.py +++ /dev/null @@ -1,36 +0,0 @@ -import os -import sys -import tempfile -from pathlib import Path - -import pytest - - -def run_tests(): - project_path = Path(__file__).parent.parent - os.chdir(project_path) - - # Determine any args to pass to pytest. If there aren't any, - # default to running the whole test suite. - args = sys.argv[1:] - if len(args) == 0: - args = ["tests"] - - returncode = pytest.main( - [ - # Turn up verbosity - "-vv", - # Disable color - "--color=no", - # Overwrite the cache directory to somewhere writable - "-o", - f"cache_dir={tempfile.gettempdir()}/.pytest_cache", - ] - + args - ) - - print(f">>>>>>>>>> EXIT {returncode} <<<<<<<<<<") - - -if __name__ == "__main__": - run_tests() diff --git a/apps/beeware_pythonnative/tests/test_app.py b/apps/beeware_pythonnative/tests/test_app.py deleted file mode 100644 index 896e9a8..0000000 --- a/apps/beeware_pythonnative/tests/test_app.py +++ /dev/null @@ -1,3 +0,0 @@ -def test_first(): - "An initial test for the app" - assert 1 + 1 == 2 diff --git a/apps/pythonnative_demo/app/main.py b/apps/pythonnative_demo/app/main.py deleted file mode 100644 index 17b1e6a..0000000 --- a/apps/pythonnative_demo/app/main.py +++ /dev/null @@ -1,27 +0,0 @@ -import pythonnative as pn - - -def main(): - # Create a screen - screen = pn.Screen() - - # Create a layout - layout = pn.LinearLayout() - - # Create a button and add it to layout - button = pn.Button("Click Me") - layout.add_view(button) - - # Create a label and add it to layout - label = pn.Label("Hello, World!") - layout.add_view(label) - - # Set layout to screen - screen.set_layout(layout) - - # Display the screen - screen.show() - - -if __name__ == "__main__": - main() diff --git a/apps/pythonnative_demo/tests/.gitkeep b/apps/pythonnative_demo/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/api/pythonnative.md b/docs/api/pythonnative.md new file mode 100644 index 0000000..744d7a4 --- /dev/null +++ b/docs/api/pythonnative.md @@ -0,0 +1,9 @@ +# pythonnative package + +API reference will be generated here via mkdocstrings. + +Key flags and helpers (0.2.0): + +- `pythonnative.utils.IS_ANDROID`: platform flag with robust detection for Chaquopy/Android. +- `pythonnative.utils.get_android_context()`: returns the current Android Activity/Context when running on Android. +- `pythonnative.utils.set_android_context(ctx)`: set by `pythonnative.Page` on Android; you generally don’t call this directly. diff --git a/docs/concepts/components.md b/docs/concepts/components.md new file mode 100644 index 0000000..e1e33aa --- /dev/null +++ b/docs/concepts/components.md @@ -0,0 +1,51 @@ +# Components + +High-level overview of PythonNative components and how they map to native UI. + +## Constructor pattern (0.2.0) + +- All core components share a consistent, contextless constructor on both platforms. +- On Android, a `Context` is acquired implicitly from the current `Activity` set by `pn.Page`. +- On iOS, UIKit classes are allocated/initialized directly. + +Examples: + +```python +import pythonnative as pn + +class MainPage(pn.Page): + def __init__(self, native_instance): + super().__init__(native_instance) + + def on_create(self): + super().on_create() + stack = pn.StackView() + stack.add_view(pn.Label("Hello")) + stack.add_view(pn.Button("Tap me")) + stack.add_view(pn.TextField("initial")) + self.set_root_view(stack) +``` + +Notes: +- `pn.Page` stores the Android `Activity` so components like `pn.Button()` and `pn.Label()` can construct their native counterparts. +- If you construct views before the `Page` is created on Android, a runtime error will be raised because no `Context` is available. + +## Core components (0.2.0) + +Stabilized with contextless constructors on both platforms: + +- `Page` +- `StackView` +- `Label`, `Button` +- `ImageView` +- `TextField`, `TextView` +- `Switch` +- `ProgressView`, `ActivityIndicatorView` +- `WebView` + +APIs are intentionally small and grow progressively in later releases. Properties and setters are kept consistent where supported by both platforms. + +## Platform detection and Android context + +- Use `pythonnative.utils.IS_ANDROID` for platform checks when needed. +- On Android, `Page` records the current `Activity` so child views can acquire a `Context` implicitly. Constructing views before `Page` initialization will raise. diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..821ee4a --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,3 @@ +# Examples + +A collection of simple examples showing PythonNative components and patterns. diff --git a/docs/examples/hello-world.md b/docs/examples/hello-world.md new file mode 100644 index 0000000..b939317 --- /dev/null +++ b/docs/examples/hello-world.md @@ -0,0 +1,30 @@ +# Hello World + +Create a simple page with a label and a button. + +```python +import pythonnative as pn + + +class MainPage(pn.Page): + def __init__(self, native_instance): + super().__init__(native_instance) + + def on_create(self): + super().on_create() + stack = pn.StackView() + label = pn.Label("Hello, world!") + button = pn.Button("Tap me") + button.set_on_click(lambda: print("Hello tapped")) + stack.add_view(label) + stack.add_view(button) + self.set_root_view(stack) +``` + +Run it: + +```bash +pn run android +# or +pn run ios +``` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..bd5fb67 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,74 @@ +# Getting Started + +```bash +pip install pythonnative +pn --help +``` + +## Create a project + +```bash +pn init MyApp +``` + +This scaffolds: + +- `app/` with a minimal `main_page.py` +- `pythonnative.json` project config +- `requirements.txt` +- `.gitignore` + +A minimal `app/main_page.py` looks like: + +```python +import pythonnative as pn + + +class MainPage(pn.Page): + def __init__(self, native_instance): + super().__init__(native_instance) + + def on_create(self): + super().on_create() + stack = pn.StackView() + stack.add_view(pn.Label("Hello from PythonNative!")) + button = pn.Button("Tap me") + button.set_on_click(lambda: print("Button clicked")) + stack.add_view(button) + self.set_root_view(stack) + + +def bootstrap(native_instance): + """Entry point called by the host app (Activity or ViewController).""" + page = MainPage(native_instance) + page.on_create() + return page +``` + +## Run on a platform + +```bash +pn run android +# or +pn run ios +``` + +- Uses bundled templates (no network required for scaffolding) +- Copies your `app/` into the generated project + +If you just want to scaffold the platform project without building, use: + +```bash +pn run android --prepare-only +pn run ios --prepare-only +``` + +This stages files under `build/` so you can open them in Android Studio or Xcode. + +## Clean + +Remove the build artifacts safely: + +```bash +pn clean +``` diff --git a/docs/guides/android.md b/docs/guides/android.md new file mode 100644 index 0000000..5e939dd --- /dev/null +++ b/docs/guides/android.md @@ -0,0 +1,36 @@ +# Android Guide + +Basic steps to build and run an Android project generated by `pn`. + +## What gets generated + +`pn run android` unpacks the bundled Android template (Kotlin + Chaquopy) into `build/android/android_template` and copies your `app/` into the template's `app/src/main/python/app/`. + +No network is required for the template itself; the template zip is bundled with the package. + +## Run + +```bash +pn run android +``` + +Or to only prepare the project without building: + +```bash +pn run android --prepare-only +``` + +This will stage files under `build/android/android_template` so you can open it in Android Studio if you prefer. + +## Clean + +Remove the build directory safely: + +```bash +pn clean +``` + +## Troubleshooting + +- If `gradlew` fails due to JDK path on macOS, ensure `JAVA_HOME` is set (the CLI attempts to detect Homebrew `openjdk@17`). +- Ensure an Android emulator or device is available for `installDebug`. diff --git a/docs/guides/ios.md b/docs/guides/ios.md new file mode 100644 index 0000000..a0a400f --- /dev/null +++ b/docs/guides/ios.md @@ -0,0 +1,35 @@ +# iOS Guide + +Basic steps to build and run an iOS project generated by `pn`. + +## What gets generated + +`pn run ios` unpacks the bundled iOS template (Swift + PythonKit, with optional Rubicon-ObjC) into `build/ios/ios_template` and copies your `app/` under `build/ios/app/` for later integration steps. The template zip is bundled with the package, so no network is required to scaffold. + +The default `ViewController.swift` initializes PythonKit, prints the Python version, and attempts to import `rubicon.objc` if present. + +## Run / Prepare + +```bash +pn run ios +``` + +Or prepare without building: + +```bash +pn run ios --prepare-only +``` + +You can then open `build/ios/ios_template/ios_template.xcodeproj` in Xcode. + +## Clean + +Remove the build directory safely: + +```bash +pn clean +``` + +## Notes + +- Building and running for Simulator via the CLI is best-effort. Opening the generated project in Xcode is recommended for iterative development. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..97b6b2b --- /dev/null +++ b/docs/index.md @@ -0,0 +1,3 @@ +# PythonNative + +Build native Android and iOS apps with Python. PythonNative provides a Pythonic API for native UI components and a simple CLI to scaffold and run projects. diff --git a/docs/meta/contributing.md b/docs/meta/contributing.md new file mode 100644 index 0000000..1189adb --- /dev/null +++ b/docs/meta/contributing.md @@ -0,0 +1,3 @@ +# Contributing + +See repository CONTRIBUTING.md for full guidelines. diff --git a/docs/meta/roadmap.md b/docs/meta/roadmap.md new file mode 100644 index 0000000..5cb3873 --- /dev/null +++ b/docs/meta/roadmap.md @@ -0,0 +1,153 @@ +--- +title: Roadmap +--- + +# PythonNative Roadmap (v0.2.0 → v0.10.0) + +This roadmap focuses on transforming PythonNative into a workable, React Native / Expo-like framework from a developer-experience and simplicity standpoint. Releases are incremental and designed to be shippable, with DX-first improvements balanced with platform capability. + +Assumptions +- Scope: Android (Chaquopy/Java bridge) and iOS (Rubicon-ObjC), Python 3.9–3.12 +- Goals: Zero-config templates, one CLI, fast iteration loop, portable component API, and a curated subset of native capabilities with room to expand. + +Guiding Principles +- Single CLI for init/run/build/clean. +- Convention over configuration: opinionated project layout (`app/`, `pythonnative.json`, `requirements.txt`). +- Hot reload (where feasible) and rapid feedback. +- Stable component API; platform shims kept internal. +- Progressive enhancement: start with a minimal but complete loop, add breadth and depth over time. + +Milestones + +0.2.0 — Foundations: DX Baseline and Templates +- CLI + - pn init: generate project with `app/`, `pythonnative.json`, `requirements.txt`, `.gitignore`. + - pn run android|ios: scaffold template apps (from bundled zips), copy `app/`, install requirements, build+install/run. + - pn clean: remove `build/` safely. +- Templates + - Bundle `templates/android_template.zip` and `templates/ios_template.zip` into package to avoid network. + - Ensure Android template uses Kotlin+Chaquopy; iOS template uses Swift+PythonKit+Rubicon. +- Core APIs + - Stabilize `Page`, `StackView`, `Label`, `Button`, `ImageView`, `TextField`, `TextView`, `Switch`, `ProgressView`, `ActivityIndicatorView`, `WebView` with consistent ctor patterns. + - Add `utils.IS_ANDROID` fallback detection improvements. +- Docs + - Getting Started (one page), Hello World, Concepts: Components, Guides: Android/iOS quickstart. + - Roadmap (this page). Contributing. + +Success Criteria +- New user can: pn init → pn run android → sees Hello World UI; same for iOS. + +0.3.0 — Navigation and Lifecycle +- API + - Page navigation abstraction with push/pop (Android: Activity/Fragment shim, iOS: UINavigationController). + - Lifecycle events stabilized and wired from host to Python (on_create/start/resume/pause/stop/destroy). +- Templates + - Two-screen sample demonstrating navigation and parameter passing. +- Docs + - Navigation guide with examples. + +Success Criteria +- Sample app navigates between two pages on both platforms using the same Python API. + +0.4.0 — Layout and Styling Pass +- API + - Improve `StackView` configuration: axis, spacing, alignment; add `ScrollView` wrapping helpers. + - Add lightweight style API (padding/margin where supported, background color, text color/size for text components). +- DX + - Component property setters return self for fluent configuration where ergonomic. +- Docs + - Styling guide and component property reference. + +Success Criteria +- Build complex vertical forms and simple horizontal layouts with predictable results on both platforms. + +0.5.0 — Developer Experience: Live Reload Loop +- DX + - pn dev android|ios: dev server watching `app/` with file-sync into running app. + - Implement soft-reload: trigger Python module reload and page re-render without full app restart where possible. + - Fallback to fast reinstall when soft-reload not possible. +- Templates + - Integrate dev menu gesture (e.g., triple-tap or shake) to trigger reload. +- Docs + - Dev workflow: live reload expectations and caveats. + +Success Criteria +- Edit Python in `app/`, trigger near-instant UI update on device/emulator. + +0.6.0 — Forms and Lists +- API + - `ListView` cross-platform wrapper with simple adapter API (Python callback to render rows, handle click). + - Input controls: `DatePicker`, `TimePicker`, basic validation utilities. + - Add `PickerView` parity or mark as experimental if iOS-first. +- Performance + - Ensure cell reuse on Android/iOS to handle 1k-row lists smoothly. +- Docs + - Lists guide, forms guide with validation patterns. + +Success Criteria +- Build a basic todo app with a scrollable list and an add-item form. + +0.7.0 — Networking, Storage, and Permissions Primitives +- API + - Simple `fetch`-like helper (thin wrapper over requests/URLSession with threading off main UI thread). + - Key-value storage abstraction (Android SharedPreferences / iOS UserDefaults). + - Permission prompts helper (camera, location, notifications) with consistent API returning futures/promises. +- DX + - Background threading utilities for long-running tasks with callback to main thread. +- Docs + - Data fetching, local storage, permissions cookbook. + +Success Criteria +- Build a data-driven screen that fetches remote JSON, caches a token, and requests permission. + +0.8.0 — Theming and Material Components (Android parity), iOS polish +- API + - Theme object for colors/typography; propagate defaults to components. + - Material variants: MaterialButton, MaterialProgress, MaterialSearchBar, MaterialSwitch stabilized. + - iOS polishing: ensure UIKit equivalents’ look-and-feel is sensible by default. +- DX + - Dark/light theme toggling hook. +- Docs + - Theming guide with examples. + +Success Criteria +- Switch between light/dark themes and see consistent component styling across screens. + +0.9.0 — Packaging, Testing, and CI +- CLI + - pn build android|ios: produce signed (debug) APK/IPA or x archive guidance; integrate keystore setup helper for Android. + - pn test: run Python unit tests; document UI test strategy (manual/host-level instrumentation later). +- Tooling + - Add ruff/black/mypy default config and `pn fmt`, `pn lint` wrappers. +- Docs + - Release checklist; testing guide. + +Success Criteria +- Produce installable builds via pn build; run unit tests with a single command. + +0.10.0 — Plugin System (Early) and Project Orchestration +- Plugins + - Define `pythonnative.plugins` entry point allowing add-ons (e.g., Camera, Filesystem) to register platform shims. + - pn plugin add : scaffold plugin structure and install dependency. +- Orchestration + - Config-driven `pythonnative.json`: targets, app id/name, icons/splash, permissions, minSDK/iOS version. + - Asset pipeline: copy assets to correct platform locations. +- Docs + - Plugin authoring guide; configuration reference. + +Success Criteria +- Install a community plugin and use it from Python without touching native code. + +Backlog and Stretch (post-0.10) +- Cross-platform navigation stack parity (Fragments vs Activities, or single-activity multi-fragment on Android). +- Advanced layout (ConstraintLayout/AutoLayout helpers) with declarative constraints. +- Gesture/touch handling unification, animations/transitions. +- Expo-like over-the-air updates pipeline. +- Desktop/web exploration via PyObjC/Qt bridges (research). + +Breaking Changes Policy +- Pre-1.0: Minor versions may include breaking changes; provide migration notes and deprecation warnings one release ahead when possible. + +Tracking and Releases +- Each milestone will have a GitHub project board and labeled issues. +- Changelogs maintained per release; upgrade guides in docs. diff --git a/docs/styles/brand.css b/docs/styles/brand.css new file mode 100644 index 0000000..e9ca30e --- /dev/null +++ b/docs/styles/brand.css @@ -0,0 +1,11 @@ +:root{ + --md-primary-fg-color:#f9d253; + --md-primary-fg-color--light:#fbe18c; + --md-primary-fg-color--dark:#e6be3b; + --md-accent-fg-color:#85c0e0; + --md-accent-fg-color--transparent:rgba(133,192,224,.1); +} + +/* Hide the bottom footer bar entirely */ +.md-footer{display:none} +.md-main__inner{margin-bottom:2rem} diff --git a/apps/pythonnative_demo/.gitignore b/examples/hello-world/.gitignore similarity index 100% rename from apps/pythonnative_demo/.gitignore rename to examples/hello-world/.gitignore diff --git a/apps/pythonnative_demo/README.md b/examples/hello-world/README.md similarity index 100% rename from apps/pythonnative_demo/README.md rename to examples/hello-world/README.md diff --git a/apps/beeware_pythonnative/src/beeware_pythonnative/__init__.py b/examples/hello-world/app/__init__.py similarity index 100% rename from apps/beeware_pythonnative/src/beeware_pythonnative/__init__.py rename to examples/hello-world/app/__init__.py diff --git a/examples/hello-world/app/main_page.py b/examples/hello-world/app/main_page.py new file mode 100644 index 0000000..8a36892 --- /dev/null +++ b/examples/hello-world/app/main_page.py @@ -0,0 +1,65 @@ +import pythonnative as pn + +try: + # Optional: used for styling below; safe if rubicon isn't available + from rubicon.objc import ObjCClass + + UIColor = ObjCClass("UIColor") +except Exception: # pragma: no cover + UIColor = None + + +class MainPage(pn.Page): + def __init__(self, native_instance): + super().__init__(native_instance) + + def on_create(self): + super().on_create() + stack = pn.StackView() + # Ensure vertical stacking + try: + stack.native_instance.setAxis_(1) # 1 = vertical + except Exception: + pass + stack.add_view(pn.Label("Hello from PythonNative Demo!")) + button = pn.Button("Tap me") + button.set_on_click(lambda: print("Demo button clicked")) + # Make the button visually obvious + try: + if UIColor is not None: + button.native_instance.setBackgroundColor_(UIColor.systemBlueColor()) + button.native_instance.setTitleColor_forState_(UIColor.whiteColor(), 0) + except Exception: + pass + stack.add_view(button) + self.set_root_view(stack) + + def on_start(self): + super().on_start() + + def on_resume(self): + super().on_resume() + + def on_pause(self): + super().on_pause() + + def on_stop(self): + super().on_stop() + + def on_destroy(self): + super().on_destroy() + + def on_restart(self): + super().on_restart() + + def on_save_instance_state(self): + super().on_save_instance_state() + + def on_restore_instance_state(self): + super().on_restore_instance_state() + + +def bootstrap(native_instance): + page = MainPage(native_instance) + page.on_create() + return page diff --git a/apps/.gitkeep b/examples/hello-world/app/resources/.gitkeep similarity index 100% rename from apps/.gitkeep rename to examples/hello-world/app/resources/.gitkeep diff --git a/examples/hello-world/app/second_page.py b/examples/hello-world/app/second_page.py new file mode 100644 index 0000000..9be9495 --- /dev/null +++ b/examples/hello-world/app/second_page.py @@ -0,0 +1,37 @@ +import pythonnative as pn + + +class SecondPage(pn.Page): + def __init__(self, native_instance): + super().__init__(native_instance) + + def on_create(self): + super().on_create() + stack_view = pn.StackView() + label = pn.Label("Second page!") + stack_view.add_view(label) + self.set_root_view(stack_view) + + def on_start(self): + super().on_start() + + def on_resume(self): + super().on_resume() + + def on_pause(self): + super().on_pause() + + def on_stop(self): + super().on_stop() + + def on_destroy(self): + super().on_destroy() + + def on_restart(self): + super().on_restart() + + def on_save_instance_state(self): + super().on_save_instance_state() + + def on_restore_instance_state(self): + super().on_restore_instance_state() diff --git a/examples/hello-world/pythonnative.json b/examples/hello-world/pythonnative.json new file mode 100644 index 0000000..04dc187 --- /dev/null +++ b/examples/hello-world/pythonnative.json @@ -0,0 +1,7 @@ +{ + "name": "PythonNative Demo", + "appId": "com.pythonnative.demo", + "entryPoint": "app/main_page.py", + "ios": {}, + "android": {} +} diff --git a/examples/hello-world/requirements.txt b/examples/hello-world/requirements.txt new file mode 100644 index 0000000..7e4d11b --- /dev/null +++ b/examples/hello-world/requirements.txt @@ -0,0 +1 @@ +pythonnative diff --git a/apps/pythonnative_demo/app/resources/.gitkeep b/examples/hello-world/tests/.gitkeep similarity index 100% rename from apps/pythonnative_demo/app/resources/.gitkeep rename to examples/hello-world/tests/.gitkeep diff --git a/apps/android_pythonnative/.gitignore b/experiments/android_pythonnative_3/.gitignore similarity index 100% rename from apps/android_pythonnative/.gitignore rename to experiments/android_pythonnative_3/.gitignore diff --git a/apps/android_pythonnative/app/.gitignore b/experiments/android_pythonnative_3/app/.gitignore similarity index 100% rename from apps/android_pythonnative/app/.gitignore rename to experiments/android_pythonnative_3/app/.gitignore diff --git a/apps/android_pythonnative_3/app/build.gradle b/experiments/android_pythonnative_3/app/build.gradle similarity index 77% rename from apps/android_pythonnative_3/app/build.gradle rename to experiments/android_pythonnative_3/app/build.gradle index eb8cfa8..50bbbfe 100644 --- a/apps/android_pythonnative_3/app/build.gradle +++ b/experiments/android_pythonnative_3/app/build.gradle @@ -21,7 +21,13 @@ android { } python { pip { - install "matplotlib" + // https://chaquo.com/chaquopy/doc/current/android.html#android-requirements +// install "matplotlib" +// install "pythonnative" + + // A directory containing a setup.py, relative to the project + // directory (must contain at least one slash): + install "/Users/owenthcarey/Documents/pythonnative-workspace/libs/pythonnative" } } } diff --git a/apps/android_pythonnative/app/proguard-rules.pro b/experiments/android_pythonnative_3/app/proguard-rules.pro similarity index 100% rename from apps/android_pythonnative/app/proguard-rules.pro rename to experiments/android_pythonnative_3/app/proguard-rules.pro diff --git a/apps/android_pythonnative/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt b/experiments/android_pythonnative_3/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt similarity index 100% rename from apps/android_pythonnative/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt rename to experiments/android_pythonnative_3/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt diff --git a/apps/android_pythonnative_2/app/src/main/AndroidManifest.xml b/experiments/android_pythonnative_3/app/src/main/AndroidManifest.xml similarity index 81% rename from apps/android_pythonnative_2/app/src/main/AndroidManifest.xml rename to experiments/android_pythonnative_3/app/src/main/AndroidManifest.xml index 816e709..411d3d1 100644 --- a/apps/android_pythonnative_2/app/src/main/AndroidManifest.xml +++ b/experiments/android_pythonnative_3/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + + android:exported="true"> diff --git a/apps/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt b/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt similarity index 52% rename from apps/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt rename to experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt index e8156f8..b29e93e 100644 --- a/apps/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt +++ b/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt @@ -8,10 +8,12 @@ import android.widget.Button import android.widget.ImageView import android.widget.TextView import android.graphics.Color +import android.view.View import android.widget.LinearLayout import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.RecyclerView import com.chaquo.python.PyException +import com.chaquo.python.PyObject import com.chaquo.python.Python import com.chaquo.python.android.AndroidPlatform import kotlinx.coroutines.CoroutineScope @@ -21,10 +23,15 @@ import kotlinx.coroutines.withContext import org.json.JSONObject class MainActivity : AppCompatActivity() { + private val TAG = javaClass.simpleName + private lateinit var page: PyObject + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - val layoutMain = findViewById(R.id.layout_main) + Log.d(TAG, "onCreate() called") + +// setContentView(R.layout.activity_main) +// val layoutMain = findViewById(R.id.layout_main) // Initialize Chaquopy if (!Python.isStarted()) { @@ -32,6 +39,21 @@ class MainActivity : AppCompatActivity() { } val py = Python.getInstance() + // Create an instance of the Page class +// val pyModule = py.getModule("app/main_2") +// page = pyModule.callAttr("Page", this) +// val pyLayout = page.callAttr("on_create").toJava(View::class.java) +// setContentView(pyLayout) + + // Create an instance of the Page class + val pyModule = py.getModule("app/main_3") + page = pyModule.callAttr("MainPage", this) + page.callAttr("on_create") + +// val pyModule = py.getModule("app/main") +// val pyLayout = pyModule.callAttr("on_create", this).toJava(View::class.java) +// setContentView(pyLayout) + // val createButtonModule = py.getModule("create_button") // val pyButton = createButtonModule.callAttr("create_button", this).toJava(Button::class.java) // layoutMain.addView(pyButton) @@ -44,9 +66,9 @@ class MainActivity : AppCompatActivity() { // val pyLayout = createConstraintLayoutModule.callAttr("create_constraint_layout", this).toJava(ConstraintLayout::class.java) // layoutMain.addView(pyLayout) - val createRecyclerViewModule = py.getModule("create_recycler_view") - val pyRecyclerView = createRecyclerViewModule.callAttr("create_recycler_view", this).toJava(RecyclerView::class.java) - layoutMain.addView(pyRecyclerView) +// val createRecyclerViewModule = py.getModule("create_recycler_view") +// val pyRecyclerView = createRecyclerViewModule.callAttr("create_recycler_view", this).toJava(RecyclerView::class.java) +// layoutMain.addView(pyRecyclerView) // Existing code for displaying plot // val imageView = findViewById(R.id.image_home) @@ -71,4 +93,52 @@ class MainActivity : AppCompatActivity() { // } // } } + + override fun onStart() { + super.onStart() + Log.d(TAG, "onStart() called") + page.callAttr("on_start") + } + + override fun onResume() { + super.onResume() + Log.d(TAG, "onResume() called") + page.callAttr("on_resume") + } + + override fun onPause() { + super.onPause() + Log.d(TAG, "onPause() called") + page.callAttr("on_pause") + } + + override fun onStop() { + super.onStop() + Log.d(TAG, "onStop() called") + page.callAttr("on_stop") + } + + override fun onDestroy() { + super.onDestroy() + Log.d(TAG, "onDestroy() called") + page.callAttr("on_destroy") + } + + override fun onRestart() { + super.onRestart() + Log.d(TAG, "onRestart() called") + page.callAttr("on_restart") + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + Log.d(TAG, "onSaveInstanceState() called") + page.callAttr("on_save_instance_state") + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + Log.d(TAG, "onRestoreInstanceState() called") + page.callAttr("on_restore_instance_state") + } } diff --git a/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/SecondActivity.kt b/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/SecondActivity.kt new file mode 100644 index 0000000..e377e5e --- /dev/null +++ b/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/SecondActivity.kt @@ -0,0 +1,73 @@ +package com.pythonnative.pythonnative + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.util.Log +import com.chaquo.python.PyObject +import com.chaquo.python.Python +import com.chaquo.python.android.AndroidPlatform + +class SecondActivity : AppCompatActivity() { + private val TAG = javaClass.simpleName + private lateinit var page: PyObject + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.d(TAG, "onCreate() called") + if (!Python.isStarted()) { + Python.start(AndroidPlatform(this)) + } + val py = Python.getInstance() + val pyModule = py.getModule("app/second_page") + page = pyModule.callAttr("SecondPage", this) + page.callAttr("on_create") + } + + override fun onStart() { + super.onStart() + Log.d(TAG, "onStart() called") + page.callAttr("on_start") + } + + override fun onResume() { + super.onResume() + Log.d(TAG, "onResume() called") + page.callAttr("on_resume") + } + + override fun onPause() { + super.onPause() + Log.d(TAG, "onPause() called") + page.callAttr("on_pause") + } + + override fun onStop() { + super.onStop() + Log.d(TAG, "onStop() called") + page.callAttr("on_stop") + } + + override fun onDestroy() { + super.onDestroy() + Log.d(TAG, "onDestroy() called") + page.callAttr("on_destroy") + } + + override fun onRestart() { + super.onRestart() + Log.d(TAG, "onRestart() called") + page.callAttr("on_restart") + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + Log.d(TAG, "onSaveInstanceState() called") + page.callAttr("on_save_instance_state") + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + Log.d(TAG, "onRestoreInstanceState() called") + page.callAttr("on_restore_instance_state") + } +} \ No newline at end of file diff --git a/apps/beeware_pythonnative/src/beeware_pythonnative/resources/__init__.py b/experiments/android_pythonnative_3/app/src/main/python/app/__init__.py similarity index 100% rename from apps/beeware_pythonnative/src/beeware_pythonnative/resources/__init__.py rename to experiments/android_pythonnative_3/app/src/main/python/app/__init__.py diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/main.py b/experiments/android_pythonnative_3/app/src/main/python/app/main.py new file mode 100644 index 0000000..2eab767 --- /dev/null +++ b/experiments/android_pythonnative_3/app/src/main/python/app/main.py @@ -0,0 +1,109 @@ +import pythonnative as pn + + +def on_create(context): + stack_view = pn.StackView(context) + + # label = pn.Label(context, "This is a PythonNative label") + # stack_view.add_view(label) + # + # switch = pn.Switch(context) + # stack_view.add_view(switch) + # + # text_field = pn.TextField(context) + # stack_view.add_view(text_field) + # + # text_view = pn.TextView(context) + # stack_view.add_view(text_view) + + activity_indicator_view = pn.ActivityIndicatorView(context) + activity_indicator_view.start_animating() + stack_view.add_view(activity_indicator_view) + + material_activity_indicator_view = pn.MaterialActivityIndicatorView(context) + material_activity_indicator_view.start_animating() + stack_view.add_view(material_activity_indicator_view) + + progress_view = pn.ProgressView(context) + progress_view.set_progress(0.5) + stack_view.add_view(progress_view) + + material_progress_view = pn.MaterialProgressView(context) + material_progress_view.set_progress(0.5) + stack_view.add_view(material_progress_view) + + material_button = pn.MaterialButton(context, "MaterialButton") + stack_view.add_view(material_button) + + search_bar = pn.SearchBar(context) + stack_view.add_view(search_bar) + + image_view = pn.ImageView(context) + stack_view.add_view(image_view) + + picker_view = pn.PickerView(context) + stack_view.add_view(picker_view) + + # date_picker = pn.DatePicker(context) + # stack_view.add_view(date_picker) + + # time_picker = pn.TimePicker(context) + # stack_view.add_view(time_picker) + + # TODO: fix + # material_time_picker = pn.MaterialTimePicker(context) + # stack_view.add_view(material_time_picker) + + # TODO: fix + # material_date_picker = pn.MaterialDatePicker(context) + # stack_view.add_view(material_date_picker) + + # TODO: fix + # material_switch = pn.MaterialSwitch(context) + # stack_view.add_view(material_switch) + + # TODO: fix + # material_search_bar = pn.MaterialSearchBar(context) + # stack_view.add_view(material_search_bar) + + # web_view = pn.WebView(context) + # web_view.load_url("https://www.djangoproject.com/") + # stack_view.add_view(web_view) + # + # for i in range(100): + # button = pn.Button(context, "Click me") + # stack_view.add_view(button) + + return stack_view.native_instance + + +def on_start(): + print("on_start() called") + + +def on_resume(): + print("on_resume() called") + + +def on_pause(): + print("on_pause() called") + + +def on_stop(): + print("on_stop() called") + + +def on_destroy(): + print("on_destroy() called") + + +def on_restart(): + print("on_restart() called") + + +def on_save_instance_state(): + print("on_save_instance_state() called") + + +def on_restore_instance_state(): + print("on_restore_instance_state() called") diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/main_2.py b/experiments/android_pythonnative_3/app/src/main/python/app/main_2.py new file mode 100644 index 0000000..1dba4ee --- /dev/null +++ b/experiments/android_pythonnative_3/app/src/main/python/app/main_2.py @@ -0,0 +1,38 @@ +import pythonnative as pn + + +class Page: + def __init__(self, context): + self.context = context + + def on_create(self): + print("on_create() called") + stack_view = pn.StackView(self.context) + material_button = pn.MaterialButton(self.context, "MaterialButton") + stack_view.add_view(material_button) + # Create and add other views to the stack_view here + return stack_view.native_instance + + def on_start(self): + print("on_start() called") + + def on_resume(self): + print("on_resume() called") + + def on_pause(self): + print("on_pause() called") + + def on_stop(self): + print("on_stop() called") + + def on_destroy(self): + print("on_destroy() called") + + def on_restart(self): + print("on_restart() called") + + def on_save_instance_state(self): + print("on_save_instance_state() called") + + def on_restore_instance_state(self): + print("on_restore_instance_state() called") diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/main_3.py b/experiments/android_pythonnative_3/app/src/main/python/app/main_3.py new file mode 100644 index 0000000..6c24d43 --- /dev/null +++ b/experiments/android_pythonnative_3/app/src/main/python/app/main_3.py @@ -0,0 +1,42 @@ +import pythonnative as pn + + +class MainPage(pn.Page): + def __init__(self, native_instance): + super().__init__(native_instance) + + def on_create(self): + super().on_create() + stack_view = pn.StackView(self.native_instance) + # list_data = ["item_{}".format(i) for i in range(100)] + # list_view = pn.ListView(self.native_instance, list_data) + # stack_view.add_view(list_view) + button = pn.Button(self.native_instance, "Button") + button.set_on_click(lambda: self.navigate_to("")) + # button.set_on_click(lambda: print("Button was clicked!")) + stack_view.add_view(button) + self.set_root_view(stack_view) + + def on_start(self): + super().on_start() + + def on_resume(self): + super().on_resume() + + def on_pause(self): + super().on_pause() + + def on_stop(self): + super().on_stop() + + def on_destroy(self): + super().on_destroy() + + def on_restart(self): + super().on_restart() + + def on_save_instance_state(self): + super().on_save_instance_state() + + def on_restore_instance_state(self): + super().on_restore_instance_state() diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/second_page.py b/experiments/android_pythonnative_3/app/src/main/python/app/second_page.py new file mode 100644 index 0000000..442fb53 --- /dev/null +++ b/experiments/android_pythonnative_3/app/src/main/python/app/second_page.py @@ -0,0 +1,37 @@ +import pythonnative as pn + + +class SecondPage(pn.Page): + def __init__(self, native_instance): + super().__init__(native_instance) + + def on_create(self): + super().on_create() + stack_view = pn.StackView(self.native_instance) + label = pn.Label(self.native_instance, "Second page!") + stack_view.add_view(label) + self.set_root_view(stack_view) + + def on_start(self): + super().on_start() + + def on_resume(self): + super().on_resume() + + def on_pause(self): + super().on_pause() + + def on_stop(self): + super().on_stop() + + def on_destroy(self): + super().on_destroy() + + def on_restart(self): + super().on_restart() + + def on_save_instance_state(self): + super().on_save_instance_state() + + def on_restore_instance_state(self): + super().on_restore_instance_state() diff --git a/apps/android_pythonnative_3/app/src/main/python/create_button.py b/experiments/android_pythonnative_3/app/src/main/python/create_button.py similarity index 100% rename from apps/android_pythonnative_3/app/src/main/python/create_button.py rename to experiments/android_pythonnative_3/app/src/main/python/create_button.py diff --git a/apps/android_pythonnative_3/app/src/main/python/create_constraint_layout.py b/experiments/android_pythonnative_3/app/src/main/python/create_constraint_layout.py similarity index 88% rename from apps/android_pythonnative_3/app/src/main/python/create_constraint_layout.py rename to experiments/android_pythonnative_3/app/src/main/python/create_constraint_layout.py index 8b1f828..9d295bd 100644 --- a/apps/android_pythonnative_3/app/src/main/python/create_constraint_layout.py +++ b/experiments/android_pythonnative_3/app/src/main/python/create_constraint_layout.py @@ -19,7 +19,9 @@ def create_constraint_layout(context): # Create BottomNavigationView bottom_nav = BottomNavigationView(context) - bottom_nav.setId(View.generateViewId()) # Add this line to generate unique id for the view + bottom_nav.setId( + View.generateViewId() + ) # Add this line to generate unique id for the view # Create Menu for BottomNavigationView menu = bottom_nav.getMenu() @@ -34,7 +36,7 @@ def create_constraint_layout(context): # Add BottomNavigationView to ConstraintLayout nav_layout_params = ConstraintLayout.LayoutParams( ConstraintLayout.LayoutParams.MATCH_PARENT, - ConstraintLayout.LayoutParams.WRAP_CONTENT + ConstraintLayout.LayoutParams.WRAP_CONTENT, ) # Set the constraints here nav_layout_params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID diff --git a/experiments/android_pythonnative_3/app/src/main/python/create_pn_layout.py b/experiments/android_pythonnative_3/app/src/main/python/create_pn_layout.py new file mode 100644 index 0000000..b4a7fde --- /dev/null +++ b/experiments/android_pythonnative_3/app/src/main/python/create_pn_layout.py @@ -0,0 +1,13 @@ +import pythonnative as pn + + +def create_pn_layout(context): + layout = pn.StackView(context) + + label = pn.Label(context, "This is a PythonNative label") + layout.add_view(label) + + button = pn.Button(context, "Click me") + layout.add_view(button) + + return layout.native_instance diff --git a/apps/android_pythonnative_3/app/src/main/python/create_recycler_view.py b/experiments/android_pythonnative_3/app/src/main/python/create_recycler_view.py similarity index 100% rename from apps/android_pythonnative_3/app/src/main/python/create_recycler_view.py rename to experiments/android_pythonnative_3/app/src/main/python/create_recycler_view.py diff --git a/apps/android_pythonnative_3/app/src/main/python/create_widgets.py b/experiments/android_pythonnative_3/app/src/main/python/create_widgets.py similarity index 86% rename from apps/android_pythonnative_3/app/src/main/python/create_widgets.py rename to experiments/android_pythonnative_3/app/src/main/python/create_widgets.py index b752683..22668b3 100644 --- a/apps/android_pythonnative_3/app/src/main/python/create_widgets.py +++ b/experiments/android_pythonnative_3/app/src/main/python/create_widgets.py @@ -1,4 +1,26 @@ -from java import jclass +from java import dynamic_proxy, jclass, static_proxy +import random + +# Import View class which contains OnClickListener +View = jclass('android.view.View') +Color = jclass('android.graphics.Color') + + +class ButtonClickListener(dynamic_proxy(View.OnClickListener)): + def __init__(self, button): + super().__init__() + self.button = button + + def onClick(self, view): + # Generate a random hex color. + color = "#" + "".join( + [random.choice("0123456789ABCDEF") for _ in range(6)]) + + # Set the button's background color. + self.button.setBackgroundColor(Color.parseColor(color)) + + # Print something to the console. + print("Button clicked! New color is " + color) def create_widgets(context): @@ -62,6 +84,7 @@ def create_widgets(context): # Create Button button = Button(context) button.setText("Button created in Python") + button.setOnClickListener(ButtonClickListener(button)) layout.addView(button) # Create TextView diff --git a/apps/android_pythonnative/app/src/main/python/plot.py b/experiments/android_pythonnative_3/app/src/main/python/plot.py similarity index 100% rename from apps/android_pythonnative/app/src/main/python/plot.py rename to experiments/android_pythonnative_3/app/src/main/python/plot.py diff --git a/apps/android_pythonnative_3/app/src/main/python/ui_layout.py b/experiments/android_pythonnative_3/app/src/main/python/ui_layout.py similarity index 100% rename from apps/android_pythonnative_3/app/src/main/python/ui_layout.py rename to experiments/android_pythonnative_3/app/src/main/python/ui_layout.py diff --git a/apps/android_pythonnative/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/experiments/android_pythonnative_3/app/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from apps/android_pythonnative/app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to experiments/android_pythonnative_3/app/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/apps/android_pythonnative/app/src/main/res/drawable/ic_launcher_background.xml b/experiments/android_pythonnative_3/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from apps/android_pythonnative/app/src/main/res/drawable/ic_launcher_background.xml rename to experiments/android_pythonnative_3/app/src/main/res/drawable/ic_launcher_background.xml diff --git a/apps/android_pythonnative_3/app/src/main/res/layout/activity_main.xml b/experiments/android_pythonnative_3/app/src/main/res/layout/activity_main.xml similarity index 100% rename from apps/android_pythonnative_3/app/src/main/res/layout/activity_main.xml rename to experiments/android_pythonnative_3/app/src/main/res/layout/activity_main.xml diff --git a/experiments/android_pythonnative_3/app/src/main/res/layout/activity_second.xml b/experiments/android_pythonnative_3/app/src/main/res/layout/activity_second.xml new file mode 100644 index 0000000..9569f77 --- /dev/null +++ b/experiments/android_pythonnative_3/app/src/main/res/layout/activity_second.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-hdpi/ic_launcher.webp rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-mdpi/ic_launcher.webp rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/apps/android_pythonnative/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from apps/android_pythonnative/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/apps/android_pythonnative_3/app/src/main/res/values-night/themes.xml b/experiments/android_pythonnative_3/app/src/main/res/values-night/themes.xml similarity index 100% rename from apps/android_pythonnative_3/app/src/main/res/values-night/themes.xml rename to experiments/android_pythonnative_3/app/src/main/res/values-night/themes.xml diff --git a/apps/android_pythonnative_3/app/src/main/res/values/colors.xml b/experiments/android_pythonnative_3/app/src/main/res/values/colors.xml similarity index 100% rename from apps/android_pythonnative_3/app/src/main/res/values/colors.xml rename to experiments/android_pythonnative_3/app/src/main/res/values/colors.xml diff --git a/apps/android_pythonnative/app/src/main/res/values/strings.xml b/experiments/android_pythonnative_3/app/src/main/res/values/strings.xml similarity index 100% rename from apps/android_pythonnative/app/src/main/res/values/strings.xml rename to experiments/android_pythonnative_3/app/src/main/res/values/strings.xml diff --git a/apps/android_pythonnative_3/app/src/main/res/values/themes.xml b/experiments/android_pythonnative_3/app/src/main/res/values/themes.xml similarity index 100% rename from apps/android_pythonnative_3/app/src/main/res/values/themes.xml rename to experiments/android_pythonnative_3/app/src/main/res/values/themes.xml diff --git a/apps/android_pythonnative/app/src/main/res/xml/backup_rules.xml b/experiments/android_pythonnative_3/app/src/main/res/xml/backup_rules.xml similarity index 100% rename from apps/android_pythonnative/app/src/main/res/xml/backup_rules.xml rename to experiments/android_pythonnative_3/app/src/main/res/xml/backup_rules.xml diff --git a/apps/android_pythonnative/app/src/main/res/xml/data_extraction_rules.xml b/experiments/android_pythonnative_3/app/src/main/res/xml/data_extraction_rules.xml similarity index 100% rename from apps/android_pythonnative/app/src/main/res/xml/data_extraction_rules.xml rename to experiments/android_pythonnative_3/app/src/main/res/xml/data_extraction_rules.xml diff --git a/apps/android_pythonnative/app/src/test/java/com/pythonnative/pythonnative/ExampleUnitTest.kt b/experiments/android_pythonnative_3/app/src/test/java/com/pythonnative/pythonnative/ExampleUnitTest.kt similarity index 100% rename from apps/android_pythonnative/app/src/test/java/com/pythonnative/pythonnative/ExampleUnitTest.kt rename to experiments/android_pythonnative_3/app/src/test/java/com/pythonnative/pythonnative/ExampleUnitTest.kt diff --git a/apps/android_pythonnative_2/build.gradle b/experiments/android_pythonnative_3/build.gradle similarity index 100% rename from apps/android_pythonnative_2/build.gradle rename to experiments/android_pythonnative_3/build.gradle diff --git a/apps/android_pythonnative/gradle.properties b/experiments/android_pythonnative_3/gradle.properties similarity index 100% rename from apps/android_pythonnative/gradle.properties rename to experiments/android_pythonnative_3/gradle.properties diff --git a/apps/android_pythonnative/gradle/wrapper/gradle-wrapper.jar b/experiments/android_pythonnative_3/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from apps/android_pythonnative/gradle/wrapper/gradle-wrapper.jar rename to experiments/android_pythonnative_3/gradle/wrapper/gradle-wrapper.jar diff --git a/apps/android_pythonnative_3/gradle/wrapper/gradle-wrapper.properties b/experiments/android_pythonnative_3/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from apps/android_pythonnative_3/gradle/wrapper/gradle-wrapper.properties rename to experiments/android_pythonnative_3/gradle/wrapper/gradle-wrapper.properties diff --git a/apps/android_pythonnative/gradlew b/experiments/android_pythonnative_3/gradlew similarity index 100% rename from apps/android_pythonnative/gradlew rename to experiments/android_pythonnative_3/gradlew diff --git a/apps/android_pythonnative_2/gradlew.bat b/experiments/android_pythonnative_3/gradlew.bat similarity index 100% rename from apps/android_pythonnative_2/gradlew.bat rename to experiments/android_pythonnative_3/gradlew.bat diff --git a/apps/android_pythonnative/settings.gradle b/experiments/android_pythonnative_3/settings.gradle similarity index 100% rename from apps/android_pythonnative/settings.gradle rename to experiments/android_pythonnative_3/settings.gradle diff --git a/libs/.gitkeep b/libs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/libs/metadata_generators/__init__.py b/libs/metadata_generators/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/libs/metadata_generators/android_metadata_generator.py b/libs/metadata_generators/android_metadata_generator.py deleted file mode 100644 index b50acda..0000000 --- a/libs/metadata_generators/android_metadata_generator.py +++ /dev/null @@ -1,51 +0,0 @@ -import os - -# Set the classpath to include the android.jar file -# This approach will allow Pyjnius to find the necessary Java classes when -# interacting with the Android SDK from within your Python script -os.environ[ - "CLASSPATH" -] = "/Users/owencarey/Library/Android/sdk/platforms/android-33/android.jar" - -# Use OpenJDK 8 -os.environ["JAVA_HOME"] = "/usr/local/opt/openjdk@8/libexec/openjdk.jdk/Contents/Home" - -from jnius import autoclass -import json - -# Load the necessary Java classes using pyjnius -ContextClass = autoclass("android.widget.Button") -ModifierClass = autoclass("java.lang.reflect.Modifier") - - -def extract(java_class): - # Get all the methods of the Android class - methods = java_class.getMethods() - # Extract metadata for each method - data = [] - for method in methods: - method_name = method.getName() - method_return_type = method.getReturnType().getSimpleName() - method_parameters = method.getParameterTypes() - method_parameter_types = [param.getSimpleName() for param in method_parameters] - method_modifiers = ModifierClass.toString(method.getModifiers()) - method_metadata = { - "Name": method_name, - "ReturnType": method_return_type, - "ParameterTypes": method_parameter_types, - "Modifiers": method_modifiers, - "Type": "Method", # since we're only dealing with methods here - } - data.append(method_metadata) - return data - - -def main(): - metadata = extract(ContextClass) - # Save the extracted metadata - with open("android_metadata.json", "w") as f: - json.dump(metadata, f, indent=2) - - -if __name__ == "__main__": - main() diff --git a/libs/metadata_generators/ios_metadata_generator.py b/libs/metadata_generators/ios_metadata_generator.py deleted file mode 100644 index 8de8ada..0000000 --- a/libs/metadata_generators/ios_metadata_generator.py +++ /dev/null @@ -1,67 +0,0 @@ -import clang.cindex -import json - -clang.cindex.Config.set_library_path("/usr/local/Cellar/llvm/16.0.4/lib/") -index = clang.cindex.Index.create() - - -def extract(cursor): - print(f"{cursor.kind}: {cursor.spelling}") - data = {"Name": cursor.spelling} - if cursor.kind == clang.cindex.CursorKind.OBJC_INTERFACE_DECL: - data["Type"] = "Interface" - elif cursor.kind in [ - clang.cindex.CursorKind.OBJC_INSTANCE_METHOD_DECL, - clang.cindex.CursorKind.OBJC_CLASS_METHOD_DECL, - ]: - data["Type"] = "Method" - elif cursor.kind == clang.cindex.CursorKind.OBJC_PROPERTY_DECL: - data["Type"] = "Property" - else: - data["Type"] = "Other" # Add this line - try: - children = [extract(c) for c in cursor.get_children()] - except ValueError as e: - print(f"Encountered error with cursor {cursor.spelling}: {e}") - children = [] - children = [c for c in children if c is not None] - if children: - data["Children"] = children - return data - - -def main(): - args = [ - "-x", - "objective-c", - "-arch", - "arm64", - "-fno-objc-arc", - # "-fmodules", - # "-fmodule-maps", - "-ferror-limit=0", - "-isysroot", - "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk", - "-I/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include", - "-I/usr/local/include", - "-I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/14.0.3/include", - "-I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include", - "-I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include", - "-I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks", - ] - tu = index.parse( - "/Applications/Xcode.app/Contents/Developer/Platforms/" - "iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/" - "Library/Frameworks/UIKit.framework/Headers/UIKit.h", - args, - ) - print(f"Translation unit: {tu.spelling}") - for diag in tu.diagnostics: - print(diag) - metadata = extract(tu.cursor) - with open("ios_metadata.json", "w") as f: - json.dump(metadata, f, indent=2) - - -if __name__ == "__main__": - main() diff --git a/libs/pythonnative/.gitignore b/libs/pythonnative/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/libs/pythonnative/README.md b/libs/pythonnative/README.md deleted file mode 100644 index 76bca32..0000000 --- a/libs/pythonnative/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# PythonNative - -PythonNative is a cross-platform Python tool kit for Android and iOS. It allows you to create native UI elements such as buttons and labels in a Pythonic way, regardless of whether you're running on iOS or Android. - -## Installation - -You can install PythonNative from PyPI: - -```bash -pip install pythonnative -``` - -Please note that PythonNative requires Python 3.6 or higher. - -## Usage - -Here's a simple example of how to create a button and a label: - -```python -import pythonnative as pn - -# Create a button -button = pn.Button("Click Me") -print(button.get_title()) # Outputs: Click Me - -# Create a label -label = pn.Label("Hello, World!") -print(label.get_text()) # Outputs: Hello, World! -``` - -## License - -PythonNative is licensed under the MIT License. See `LICENSE` for more information. diff --git a/libs/pythonnative/cli/__init__.py b/libs/pythonnative/cli/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/libs/pythonnative/cli/pn.py b/libs/pythonnative/cli/pn.py deleted file mode 100644 index b3b25c7..0000000 --- a/libs/pythonnative/cli/pn.py +++ /dev/null @@ -1,113 +0,0 @@ -import argparse -import os -import shutil -import subprocess - - -def init_project(args: argparse.Namespace) -> None: - """ - Initialize a new PythonNative project. - """ - # TODO: Implementation - - -def create_android_project(project_name: str, destination: str) -> bool: - """ - Create a new Android project using android command. - - :param project_name: The name of the project. - :param destination: The directory where the project will be created. - :return: True if the project was created successfully, False otherwise. - """ - # The command to create a new Android project - command = f"cd {destination} && android create project --name {project_name} --path . --target android-30 --package com.example.{project_name} --activity MainActivity" - - # Run the command - process = subprocess.run(command, shell=True, check=True, text=True) - - return process.returncode == 0 - - -def create_ios_project(project_name: str, destination: str) -> bool: - """ - Create a new Xcode project using xcodeproj gem. - - :param project_name: The name of the project. - :param destination: The directory where the project will be created. - :return: True if the project was created successfully, False otherwise. - """ - # The command to create a new Xcode project - command = f"cd {destination} && xcodeproj new {project_name}.xcodeproj" - - # Run the command - process = subprocess.run(command, shell=True, check=True, text=True) - - return process.returncode == 0 - - -def run_project(args: argparse.Namespace) -> None: - """ - Run the specified project. - """ - # Determine the platform - platform = args.platform - - # Define the build directory - build_dir = os.path.join(os.getcwd(), "build", platform) - - # Create the build directory if it doesn't exist - os.makedirs(build_dir, exist_ok=True) - - # Generate the required project files - if platform == "android": - create_android_project("MyApp", build_dir) - elif platform == "ios": - create_ios_project("MyApp", build_dir) - - # Copy the user's Python code into the project - src_dir = os.path.join(os.getcwd(), "app") - dest_dir = os.path.join( - build_dir, "app" - ) # You might need to adjust this depending on the project structure - shutil.copytree(src_dir, dest_dir, dirs_exist_ok=True) - - # Install any necessary Python packages into the project environment - requirements_file = os.path.join(os.getcwd(), "requirements.txt") - # TODO: Fill in with actual commands for installing Python packages - - # Run the project - # TODO: Fill in with actual commands for running the project - - -def clean_project(args: argparse.Namespace) -> None: - """ - Clean the specified project. - """ - # Define the build directory - build_dir = os.path.join(os.getcwd(), "build") - - # Check if the build directory exists - if os.path.exists(build_dir): - # Delete the build directory - shutil.rmtree(build_dir) - - -def main() -> None: - parser = argparse.ArgumentParser(prog="pn", description="PythonNative CLI") - subparsers = parser.add_subparsers() - - # Create a new command 'init' that calls init_project - parser_init = subparsers.add_parser("init") - parser_init.set_defaults(func=init_project) - - # Create a new command 'run' that calls run_project - parser_run = subparsers.add_parser("run") - parser_run.add_argument("platform", choices=["android", "ios"]) - parser_run.set_defaults(func=run_project) - - # Create a new command 'clean' that calls clean_project - parser_clean = subparsers.add_parser("clean") - parser_clean.set_defaults(func=clean_project) - - args = parser.parse_args() - args.func(args) diff --git a/libs/pythonnative/publish_to_pypi.sh b/libs/pythonnative/publish_to_pypi.sh deleted file mode 100644 index 899a21d..0000000 --- a/libs/pythonnative/publish_to_pypi.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Clean up old distribution, build directories, and egg-info -rm -rf dist/ -rm -rf build/ -rm -rf pythonnative.egg-info/ - -# Generate distribution archives -python setup.py sdist bdist_wheel - -# Upload the distribution archives to PyPi -twine upload dist/* diff --git a/libs/pythonnative/pythonnative/__init__.py b/libs/pythonnative/pythonnative/__init__.py deleted file mode 100644 index 3760890..0000000 --- a/libs/pythonnative/pythonnative/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .view import View -from .button import Button -from .label import Label -from .linear_layout import LinearLayout -from .screen import Screen - -__all__ = ["View", "Button", "Label", "LinearLayout", "Screen"] diff --git a/libs/pythonnative/pythonnative/button.py b/libs/pythonnative/pythonnative/button.py deleted file mode 100644 index 5002bac..0000000 --- a/libs/pythonnative/pythonnative/button.py +++ /dev/null @@ -1,36 +0,0 @@ -import platform -from .view import View - -if platform.system() == "Android": - from java import jclass - - class Button(View): - native_class = jclass("android.widget.Button") - - def __init__(self, title: str = "") -> None: - super().__init__() - self.native_instance = self.native_class() - self.set_title(title) - - def set_title(self, title: str) -> None: - self.native_instance.setText(title) - - def get_title(self) -> str: - return self.native_instance.getText().toString() - -elif platform.system() == "iOS": - from rubicon.objc import ObjCClass - - class Button(View): - native_class = ObjCClass("UIButton") - - def __init__(self, title: str = "") -> None: - super().__init__() - self.native_instance = self.native_class.alloc().init() - self.set_title(title) - - def set_title(self, title: str) -> None: - self.native_instance.setTitle_forState_(title, 0) - - def get_title(self) -> str: - return self.native_instance.titleForState_(0) diff --git a/libs/pythonnative/pythonnative/label.py b/libs/pythonnative/pythonnative/label.py deleted file mode 100644 index a7616a8..0000000 --- a/libs/pythonnative/pythonnative/label.py +++ /dev/null @@ -1,36 +0,0 @@ -import platform -from .view import View - -if platform.system() == "Android": - from java import jclass - - class Label(View): - native_class = jclass("android.widget.TextView") - - def __init__(self, text: str = "") -> None: - super().__init__() - self.native_instance = self.native_class() - self.set_text(text) - - def set_text(self, text: str) -> None: - self.native_instance.setText(text) - - def get_text(self) -> str: - return self.native_instance.getText().toString() - -elif platform.system() == "iOS": - from rubicon.objc import ObjCClass - - class Label(View): - native_class = ObjCClass("UILabel") - - def __init__(self, text: str = "") -> None: - super().__init__() - self.native_instance = self.native_class.alloc().init() - self.set_text(text) - - def set_text(self, text: str) -> None: - self.native_instance.setText_(text) - - def get_text(self) -> str: - return self.native_instance.text() diff --git a/libs/pythonnative/pythonnative/linear_layout.py b/libs/pythonnative/pythonnative/linear_layout.py deleted file mode 100644 index 318fbd6..0000000 --- a/libs/pythonnative/pythonnative/linear_layout.py +++ /dev/null @@ -1,36 +0,0 @@ -import platform -from .view import View - -if platform.system() == "Android": - from java import jclass - - class LinearLayout(View): - native_class = jclass("android.widget.LinearLayout") - - def __init__(self) -> None: - super().__init__() - self.native_instance = self.native_class() - self.native_instance.setOrientation(1) # Set orientation to vertical - self.views = [] - - def add_view(self, view): - self.views.append(view) - self.native_instance.addView(view.native_instance) - -elif platform.system() == "iOS": - from rubicon.objc import ObjCClass - - class LinearLayout(View): - native_class = ObjCClass("UIStackView") - - def __init__(self) -> None: - super().__init__() - self.native_instance = self.native_class.alloc().initWithFrame_( - ((0, 0), (0, 0)) - ) - self.native_instance.setAxis_(0) # Set axis to vertical - self.views = [] - - def add_view(self, view): - self.views.append(view) - self.native_instance.addArrangedSubview_(view.native_instance) diff --git a/libs/pythonnative/pythonnative/screen.py b/libs/pythonnative/pythonnative/screen.py deleted file mode 100644 index 321e570..0000000 --- a/libs/pythonnative/pythonnative/screen.py +++ /dev/null @@ -1,50 +0,0 @@ -import platform -from .view import View - -if platform.system() == "Android": - from java import jclass - - class Screen(View): - native_class = jclass("android.app.Activity") - - def __init__(self): - super().__init__() - self.native_instance = self.native_class() - self.layout = None - - def add_view(self, view): - if self.layout is None: - raise ValueError("You must set a layout before adding views.") - self.layout.add_view(view) - - def set_layout(self, layout): - self.layout = layout - self.native_instance.setContentView(layout.native_instance) - - def show(self): - # This method should contain code to start the Activity - pass - -elif platform.system() == "iOS": - from rubicon.objc import ObjCClass - - class Screen(View): - native_class = ObjCClass("UIViewController") - - def __init__(self): - super().__init__() - self.native_instance = self.native_class.alloc().init() - self.layout = None - - def add_view(self, view): - if self.layout is None: - raise ValueError("You must set a layout before adding views.") - self.layout.add_view(view) - - def set_layout(self, layout): - self.layout = layout - self.native_instance.view().addSubview_(layout.native_instance) - - def show(self): - # This method should contain code to present the ViewController - pass diff --git a/libs/pythonnative/pythonnative/view.py b/libs/pythonnative/pythonnative/view.py deleted file mode 100644 index 7ce8baa..0000000 --- a/libs/pythonnative/pythonnative/view.py +++ /dev/null @@ -1,13 +0,0 @@ -class View: - def __init__(self) -> None: - self.native_instance = None - self.native_class = None - - def add_view(self, view): - raise NotImplementedError("This method should be implemented in a subclass.") - - def set_layout(self, layout): - raise NotImplementedError("This method should be implemented in a subclass.") - - def show(self): - raise NotImplementedError("This method should be implemented in a subclass.") diff --git a/libs/pythonnative/setup.py b/libs/pythonnative/setup.py deleted file mode 100644 index 7976cb3..0000000 --- a/libs/pythonnative/setup.py +++ /dev/null @@ -1,28 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="pythonnative", - version="0.1.0", - author="Owen Carey", - author_email="pythonnative@gmail.com", - description="A cross-platform Python tool kit for Android and iOS", - long_description=open("README.md").read(), - long_description_content_type="text/markdown", - url="https://pythonnative.com", - packages=find_packages(), - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], - python_requires=">=3.6", - install_requires=[ - "rubicon-objc>=0.4.6,<0.5.0", - # Add more requirements here as necessary - ], - entry_points={ - "console_scripts": [ - "pn=cli.pn:main", - ], - }, -) diff --git a/libs/pythonnative/tests/__init__.py b/libs/pythonnative/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/libs/pythonnative/tests/test_pythonnative.py b/libs/pythonnative/tests/test_pythonnative.py deleted file mode 100644 index e69de29..0000000 diff --git a/libs/rubicon_java_tests.py b/libs/rubicon_java_tests.py deleted file mode 100644 index e18ea78..0000000 --- a/libs/rubicon_java_tests.py +++ /dev/null @@ -1,14 +0,0 @@ -from rubicon.java import JavaClass - - -def main(): - """ - https://github.com/beeware/rubicon-java. - """ - URL = JavaClass("java/net/URL") - url = URL("https://beeware.org") - print(url.getHost()) - - -if __name__ == "__main__": - main() diff --git a/libs/rubicon_objc_test.py b/libs/rubicon_objc_test.py deleted file mode 100644 index 26ce69c..0000000 --- a/libs/rubicon_objc_test.py +++ /dev/null @@ -1,16 +0,0 @@ -from rubicon.objc import ObjCClass - - -def main(): - """ - https://rubicon-objc.readthedocs.io/en/stable/tutorial/tutorial-1.html. - """ - NSURL = ObjCClass("NSURL") - base = NSURL.URLWithString("https://beeware.org/") - full = NSURL.URLWithString("contributing/", relativeToURL=base) - absolute = full.absoluteURL - print(absolute.description) - - -if __name__ == "__main__": - main() diff --git a/libs/ui_elements/__init__.py b/libs/ui_elements/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/libs/ui_elements/android_ui_elements.py b/libs/ui_elements/android_ui_elements.py deleted file mode 100644 index f4df562..0000000 --- a/libs/ui_elements/android_ui_elements.py +++ /dev/null @@ -1,52 +0,0 @@ -from java import jclass - -# Layouts -AbsoluteLayout = jclass("android.widget.AbsoluteLayout") -CoordinatorLayout = jclass("androidx.coordinatorlayout.widget.CoordinatorLayout") -ConstraintLayout = jclass("androidx.constraintlayout.widget.ConstraintLayout") -DrawerLayout = jclass("androidx.drawerlayout.widget.DrawerLayout") -FrameLayout = jclass("android.widget.FrameLayout") -GridLayout = jclass("android.widget.GridLayout") -HorizontalScrollView = jclass("android.widget.HorizontalScrollView") -LinearLayout = jclass("android.widget.LinearLayout") -RelativeLayout = jclass("android.widget.RelativeLayout") -RecyclerView = jclass("androidx.recyclerview.widget.RecyclerView") -ScrollView = jclass("android.widget.ScrollView") -TableLayout = jclass("android.widget.TableLayout") -TableRow = jclass("android.widget.TableRow") - -# Widgets -AutoCompleteTextView = jclass("android.widget.AutoCompleteTextView") -Button = jclass("android.widget.Button") -CheckBox = jclass("android.widget.CheckBox") -DatePicker = jclass("android.widget.DatePicker") -EditText = jclass("android.widget.EditText") -ImageView = jclass("android.widget.ImageView") -ProgressBar = jclass("android.widget.ProgressBar") -RadioButton = jclass("android.widget.RadioButton") -RatingBar = jclass("android.widget.RatingBar") -SeekBar = jclass("android.widget.SeekBar") -Spinner = jclass("android.widget.Spinner") -Switch = jclass("android.widget.Switch") -TextView = jclass("android.widget.TextView") -TimePicker = jclass("android.widget.TimePicker") -ToggleButton = jclass("android.widget.ToggleButton") -ViewFlipper = jclass("android.widget.ViewFlipper") -ViewSwitcher = jclass("android.widget.ViewSwitcher") -WebView = jclass("android.webkit.WebView") - -# Material Components -BottomNavigationView = jclass( - "com.google.android.material.bottomnavigation.BottomNavigationView" -) -BottomSheetDialogFragment = jclass( - "com.google.android.material.bottomsheet.BottomSheetDialogFragment" -) -Chip = jclass("com.google.android.material.chip.Chip") -FloatingActionButton = jclass( - "com.google.android.material.floatingactionbutton.FloatingActionButton" -) -MaterialCardView = jclass("com.google.android.material.card.MaterialCardView") -NavigationView = jclass("com.google.android.material.navigation.NavigationView") -Snackbar = jclass("com.google.android.material.snackbar.Snackbar") -TextInputLayout = jclass("com.google.android.material.textfield.TextInputLayout") diff --git a/libs/ui_elements/ios_ui_elements.py b/libs/ui_elements/ios_ui_elements.py deleted file mode 100644 index 9cae5fa..0000000 --- a/libs/ui_elements/ios_ui_elements.py +++ /dev/null @@ -1,132 +0,0 @@ -from rubicon.objc import ObjCClass - -# App and Window -UIApplication = ObjCClass("UIApplication") -UIWindow = ObjCClass("UIWindow") - -# Base classes -UIView = ObjCClass("UIView") -UIViewController = ObjCClass("UIViewController") - -# Bar classes -UIBarButtonItem = ObjCClass("UIBarButtonItem") -UINavigationBar = ObjCClass("UINavigationBar") -UITabBar = ObjCClass("UITabBar") -UITabBarItem = ObjCClass("UITabBarItem") -UIToolbar = ObjCClass("UIToolbar") - -# Controls -UIButton = ObjCClass("UIButton") -UIDatePicker = ObjCClass("UIDatePicker") -UIImageView = ObjCClass("UIImageView") -UILabel = ObjCClass("UILabel") -UIPickerView = ObjCClass("UIPickerView") -UIProgressView = ObjCClass("UIProgressView") -UISegmentedControl = ObjCClass("UISegmentedControl") -UISlider = ObjCClass("UISlider") -UISwitch = ObjCClass("UISwitch") -UITextField = ObjCClass("UITextField") -UITextView = ObjCClass("UITextView") - -# Controllers -UINavigationController = ObjCClass("UINavigationController") -UITabBarController = ObjCClass("UITabBarController") - -# Indicators -UIActivityIndicatorView = ObjCClass("UIActivityIndicatorView") -UIPageControl = ObjCClass("UIPageControl") -UIProgressView = ObjCClass("UIProgressView") -UIRefreshControl = ObjCClass("UIRefreshControl") - -# Layouts and Views -UICollectionView = ObjCClass("UICollectionView") -UIScrollView = ObjCClass("UIScrollView") -UITableView = ObjCClass("UITableView") -UIStackView = ObjCClass("UIStackView") -UIView = ObjCClass("UIView") - -# Misc -UIAlertController = ObjCClass("UIAlertController") -UIAlertAction = ObjCClass("UIAlertAction") -UIColor = ObjCClass("UIColor") - -# Web view -WKWebView = ObjCClass("WKWebView") - -# Gesture Recognizers -UIGestureRecognizer = ObjCClass("UIGestureRecognizer") -UILongPressGestureRecognizer = ObjCClass("UILongPressGestureRecognizer") -UIPanGestureRecognizer = ObjCClass("UIPanGestureRecognizer") -UIPinchGestureRecognizer = ObjCClass("UIPinchGestureRecognizer") -UIRotationGestureRecognizer = ObjCClass("UIRotationGestureRecognizer") -UIScreenEdgePanGestureRecognizer = ObjCClass("UIScreenEdgePanGestureRecognizer") -UISwipeGestureRecognizer = ObjCClass("UISwipeGestureRecognizer") -UITapGestureRecognizer = ObjCClass("UITapGestureRecognizer") - -# Audio and Video -AVAudioPlayer = ObjCClass("AVAudioPlayer") -AVPlayer = ObjCClass("AVPlayer") -AVPlayerViewController = ObjCClass("AVPlayerViewController") -MPMoviePlayerController = ObjCClass("MPMoviePlayerController") - -# Data and Documents -UIDocument = ObjCClass("UIDocument") -UIDocumentInteractionController = ObjCClass("UIDocumentInteractionController") -UIDocumentMenuViewController = ObjCClass("UIDocumentMenuViewController") -UIDocumentPickerViewController = ObjCClass("UIDocumentPickerViewController") -UIDocumentBrowserViewController = ObjCClass("UIDocumentBrowserViewController") - -# Drawing and Graphics -UIGraphicsImageRenderer = ObjCClass("UIGraphicsImageRenderer") - -# Feedback -UIFeedbackGenerator = ObjCClass("UIFeedbackGenerator") - -# Interactions and Effects -UIBlurEffect = ObjCClass("UIBlurEffect") -UIInterpolatingMotionEffect = ObjCClass("UIInterpolatingMotionEffect") -UIVibrancyEffect = ObjCClass("UIVibrancyEffect") -UIVisualEffect = ObjCClass("UIVisualEffect") -UIVisualEffectView = ObjCClass("UIVisualEffectView") - -# Popovers and Modality -UIPopoverController = ObjCClass("UIPopoverController") -UIPresentationController = ObjCClass("UIPresentationController") - -# Printing -UIPrintInteractionController = ObjCClass("UIPrintInteractionController") -UIPrintPageRenderer = ObjCClass("UIPrintPageRenderer") -UIPrintPaper = ObjCClass("UIPrintPaper") - -# Text and Fonts -UIFont = ObjCClass("UIFont") -UIFontDescriptor = ObjCClass("UIFontDescriptor") - -# Touches and Gestures -UITouch = ObjCClass("UITouch") - -# Traits and Environment -UITraitCollection = ObjCClass("UITraitCollection") - -# View Controller Presentation -UIAdaptivePresentationControllerDelegate = ObjCClass( - "UIAdaptivePresentationControllerDelegate" -) - -# View Management -UILayoutGuide = ObjCClass("UILayoutGuide") -UIResponder = ObjCClass("UIResponder") -UIScreen = ObjCClass("UIScreen") -UIViewPropertyAnimator = ObjCClass("UIViewPropertyAnimator") -UIWindowScene = ObjCClass("UIWindowScene") - -# Visualization -UIBezierPath = ObjCClass("UIBezierPath") -UIGraphicsRenderer = ObjCClass("UIGraphicsRenderer") -UIGraphicsRendererContext = ObjCClass("UIGraphicsRendererContext") -UIGraphicsRendererFormat = ObjCClass("UIGraphicsRendererFormat") -UIImage = ObjCClass("UIImage") -UIMotionEffect = ObjCClass("UIMotionEffect") - -# Miscellaneous -UILocalNotification = ObjCClass("UILocalNotification") diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c8a6339 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,44 @@ +site_name: PythonNative +site_description: Cross-platform native UI toolkit for Android and iOS +site_url: https://docs.pythonnative.com/ +repo_url: https://github.com/pythonnative/pythonnative +theme: + name: material + features: + - navigation.sections + - navigation.instant + - navigation.tracking + - content.code.copy +extra: + generator: false +nav: + - Home: index.md + - Getting Started: getting-started.md + - Concepts: + - Components: concepts/components.md + - Examples: + - Overview: examples.md + - Hello World: examples/hello-world.md + - Guides: + - Android: guides/android.md + - iOS: guides/ios.md + - API Reference: + - Package: api/pythonnative.md + - Meta: + - Roadmap: meta/roadmap.md + - Contributing: meta/contributing.md +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + show_source: false + docstring_style: google +extra_css: + - styles/brand.css +markdown_extensions: + - admonition + - codehilite + - toc: + permalink: true diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..ec0b8ad --- /dev/null +++ b/mypy.ini @@ -0,0 +1,17 @@ +[mypy] +python_version = 3.9 +ignore_missing_imports = True +warn_redundant_casts = True +warn_unused_ignores = True +warn_return_any = False +strict_optional = False +pretty = True +files = src, tests, examples +exclude = (^build/|^examples/.*/build/) + +[mypy-pythonnative.*] +implicit_reexport = True +disable_error_code = attr-defined,no-redef + +[mypy-pythonnative.button] +disable_error_code = misc diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4ca1465 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,91 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "pythonnative" +version = "0.2.0" +description = "Cross-platform native UI toolkit for Android and iOS" +authors = [ + { name = "Owen Carey" } +] +readme = "README.md" +requires-python = ">=3.9" +license = { file = "LICENSE" } +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Environment :: Handhelds/PDA", + "Topic :: Software Development :: User Interfaces", +] +dependencies = [ + "requests>=2.31.0", +] + +[project.optional-dependencies] +ios = [ + "rubicon-objc>=0.4.6,<0.5.0", +] +docs = [ + "mkdocs>=1.5", + "mkdocs-material[imaging]>=9.5", + "mkdocstrings[python]>=0.24", +] +dev = [ + "black>=24.0", + "ruff>=0.5", + "mypy>=1.10", + "pytest>=8.0", +] +ci = [ + "black>=24.0", + "ruff>=0.5", + "mypy>=1.10", + "pytest>=8.0", +] + +[project.scripts] +pn = "pythonnative.cli.pn:main" + +[project.urls] +Homepage = "https://github.com/pythonnative/pythonnative" +Repository = "https://github.com/pythonnative/pythonnative" +Issues = "https://github.com/pythonnative/pythonnative/issues" +Documentation = "https://docs.pythonnative.com/" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools] +license-files = ["LICENSE*"] + +# Include template directories inside the package so importlib.resources can find them +[tool.setuptools.package-data] +pythonnative = [ + "templates/**", +] + +[tool.ruff] +target-version = "py39" +line-length = 120 +extend-exclude = [ + "experiments", + "apps", + "templates", + "docs", +] + +[tool.ruff.lint] +select = ["E", "F", "I"] +ignore = [] + +[tool.black] +line-length = 120 +target-version = ['py39'] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..f0d9b81 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +addopts = -q +testpaths = + tests + src/pythonnative diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index e69de29..0000000 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d292d73..0000000 --- a/requirements.txt +++ /dev/null @@ -1,78 +0,0 @@ -arrow==1.2.3 -asgiref==3.7.0 -binaryornot==0.4.4 -black==23.3.0 -bleach==6.0.0 -briefcase==0.3.14 -build==0.10.0 -certifi==2023.5.7 -chardet==5.1.0 -charset-normalizer==3.1.0 -clang==16.0.1.1 -click==8.1.3 -contourpy==1.0.7 -cookiecutter==2.1.1 -cycler==0.11.0 -Cython==0.29.35 -Django==4.2.1 -dmgbuild==1.6.1 -docutils==0.20.1 -ds-store==1.3.1 -exceptiongroup==1.1.1 -fonttools==4.39.4 -gitdb==4.0.10 -GitPython==3.1.31 -idna==3.4 -importlib-metadata==6.6.0 -importlib-resources==5.12.0 -iniconfig==2.0.0 -jaraco.classes==3.2.3 -Jinja2==3.1.2 -jinja2-time==0.2.0 -keyring==23.13.1 -kiwisolver==1.4.4 -mac-alias==2.2.2 -markdown-it-py==2.2.0 -MarkupSafe==2.1.2 -matplotlib==3.7.1 -mdurl==0.1.2 -more-itertools==9.1.0 -mypy-extensions==1.0.0 -numpy==1.24.3 -packaging==23.1 -pathspec==0.11.1 -Pillow==9.5.0 -pkginfo==1.9.6 -platformdirs==3.5.1 -pluggy==1.0.0 -psutil==5.9.5 -Pygments==2.15.1 -pyjnius==1.5.0 -pyparsing==3.0.9 -pyproject_hooks==1.0.0 -pytest==7.3.1 -python-dateutil==2.8.2 -python-slugify==8.0.1 -PyYAML==6.0 -readme-renderer==37.3 -requests==2.31.0 -requests-toolbelt==1.0.0 -rfc3986==2.0.0 -rich==13.3.5 -rubicon-java==0.2.6 -rubicon-objc==0.4.6 -six==1.16.0 -smmap==5.0.0 -sqlparse==0.4.4 -std-nslog==1.0.3 -text-unidecode==1.3 -toga-cocoa==0.3.1 -toga-core==0.3.1 -tomli==2.0.1 -tomli_w==1.0.0 -travertino==0.2.0 -twine==4.0.2 -typing_extensions==4.6.3 -urllib3==1.26.7 -webencodings==0.5.1 -zipp==3.15.0 diff --git a/src/pythonnative/__init__.py b/src/pythonnative/__init__.py new file mode 100644 index 0000000..78d2ebc --- /dev/null +++ b/src/pythonnative/__init__.py @@ -0,0 +1,74 @@ +from importlib import import_module +from typing import Any, Dict + +__version__ = "0.2.0" + +__all__ = [ + "ActivityIndicatorView", + "Button", + "DatePicker", + "ImageView", + "Label", + "ListView", + "MaterialActivityIndicatorView", + "MaterialButton", + "MaterialDatePicker", + "MaterialProgressView", + "MaterialSearchBar", + "MaterialSwitch", + "MaterialTimePicker", + "MaterialBottomNavigationView", + "MaterialToolbar", + "Page", + "PickerView", + "ProgressView", + "ScrollView", + "SearchBar", + "StackView", + "Switch", + "TextField", + "TextView", + "TimePicker", + "WebView", +] + +_NAME_TO_MODULE: Dict[str, str] = { + "ActivityIndicatorView": ".activity_indicator_view", + "Button": ".button", + "DatePicker": ".date_picker", + "ImageView": ".image_view", + "Label": ".label", + "ListView": ".list_view", + "MaterialActivityIndicatorView": ".material_activity_indicator_view", + "MaterialButton": ".material_button", + "MaterialDatePicker": ".material_date_picker", + "MaterialProgressView": ".material_progress_view", + "MaterialSearchBar": ".material_search_bar", + "MaterialSwitch": ".material_switch", + "MaterialTimePicker": ".material_time_picker", + "MaterialBottomNavigationView": ".material_bottom_navigation_view", + "MaterialToolbar": ".material_toolbar", + "Page": ".page", + "PickerView": ".picker_view", + "ProgressView": ".progress_view", + "ScrollView": ".scroll_view", + "SearchBar": ".search_bar", + "StackView": ".stack_view", + "Switch": ".switch", + "TextField": ".text_field", + "TextView": ".text_view", + "TimePicker": ".time_picker", + "WebView": ".web_view", +} + + +def __getattr__(name: str) -> Any: + module_path = _NAME_TO_MODULE.get(name) + if not module_path: + raise AttributeError(f"module 'pythonnative' has no attribute {name!r}") + module = import_module(module_path, package=__name__) + return getattr(module, name) + + +def __dir__() -> Any: + return sorted(list(globals().keys()) + __all__) diff --git a/src/pythonnative/activity_indicator_view.py b/src/pythonnative/activity_indicator_view.py new file mode 100644 index 0000000..722924e --- /dev/null +++ b/src/pythonnative/activity_indicator_view.py @@ -0,0 +1,71 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID, get_android_context +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class ActivityIndicatorViewBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def start_animating(self) -> None: + pass + + @abstractmethod + def stop_animating(self) -> None: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/ProgressBar + # ======================================== + + from java import jclass + + class ActivityIndicatorView(ActivityIndicatorViewBase, ViewBase): + def __init__(self) -> None: + super().__init__() + self.native_class = jclass("android.widget.ProgressBar") + # self.native_instance = self.native_class(context, None, android.R.attr.progressBarStyleLarge) + context = get_android_context() + self.native_instance = self.native_class(context) + self.native_instance.setIndeterminate(True) + + def start_animating(self) -> None: + # self.native_instance.setVisibility(android.view.View.VISIBLE) + return + + def stop_animating(self) -> None: + # self.native_instance.setVisibility(android.view.View.GONE) + return + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uiactivityindicatorview + # ======================================== + + from rubicon.objc import ObjCClass + + class ActivityIndicatorView(ActivityIndicatorViewBase, ViewBase): + def __init__(self) -> None: + super().__init__() + self.native_class = ObjCClass("UIActivityIndicatorView") + self.native_instance = self.native_class.alloc().initWithActivityIndicatorStyle_( + 0 + ) # 0: UIActivityIndicatorViewStyleLarge + self.native_instance.hidesWhenStopped = True + + def start_animating(self) -> None: + self.native_instance.startAnimating() + + def stop_animating(self) -> None: + self.native_instance.stopAnimating() diff --git a/src/pythonnative/button.py b/src/pythonnative/button.py new file mode 100644 index 0000000..13e38b1 --- /dev/null +++ b/src/pythonnative/button.py @@ -0,0 +1,109 @@ +from abc import ABC, abstractmethod +from typing import Callable, Optional + +from .utils import IS_ANDROID, get_android_context +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class ButtonBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_title(self, title: str) -> None: + pass + + @abstractmethod + def get_title(self) -> str: + pass + + @abstractmethod + def set_on_click(self, callback: Callable[[], None]) -> None: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/Button + # ======================================== + + from java import dynamic_proxy, jclass + + class Button(ButtonBase, ViewBase): + def __init__(self, title: str = "") -> None: + super().__init__() + self.native_class = jclass("android.widget.Button") + context = get_android_context() + self.native_instance = self.native_class(context) + self.set_title(title) + + def set_title(self, title: str) -> None: + self.native_instance.setText(title) + + def get_title(self) -> str: + return self.native_instance.getText().toString() + + def set_on_click(self, callback: Callable[[], None]) -> None: + class OnClickListener(dynamic_proxy(jclass("android.view.View").OnClickListener)): + def __init__(self, callback): + super().__init__() + self.callback = callback + + def onClick(self, view): + self.callback() + + listener = OnClickListener(callback) + self.native_instance.setOnClickListener(listener) + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uibutton + # ======================================== + + from rubicon.objc import SEL, ObjCClass, objc_method + + NSObject = ObjCClass("NSObject") + + # Mypy cannot understand Rubicon's dynamic subclassing; ignore the base type here. + class _PNButtonHandler(NSObject): # type: ignore[valid-type] + # Set by the Button when wiring up the target/action callback. + _callback: Optional[Callable[[], None]] = None + + @objc_method + def onTap_(self, sender) -> None: + try: + callback = self._callback + if callback is not None: + callback() + except Exception: + # Swallow exceptions to avoid crashing the app; logging is handled at higher levels + pass + + class Button(ButtonBase, ViewBase): + def __init__(self, title: str = "") -> None: + super().__init__() + self.native_class = ObjCClass("UIButton") + self.native_instance = self.native_class.alloc().init() + self.set_title(title) + + def set_title(self, title: str) -> None: + self.native_instance.setTitle_forState_(title, 0) + + def get_title(self) -> str: + return self.native_instance.titleForState_(0) + + def set_on_click(self, callback: Callable[[], None]) -> None: + # Create a handler object with an Objective-C method `onTap:` and attach the Python callback + handler = _PNButtonHandler.new() + # Keep strong references to the handler and callback + self._click_handler = handler + handler._callback = callback + # UIControlEventTouchUpInside = 1 << 6 + self.native_instance.addTarget_action_forControlEvents_(handler, SEL("onTap:"), 1 << 6) diff --git a/apps/beeware_pythonnative/tests/__init__.py b/src/pythonnative/cli/__init__.py similarity index 100% rename from apps/beeware_pythonnative/tests/__init__.py rename to src/pythonnative/cli/__init__.py diff --git a/src/pythonnative/cli/pn.py b/src/pythonnative/cli/pn.py new file mode 100644 index 0000000..131ff38 --- /dev/null +++ b/src/pythonnative/cli/pn.py @@ -0,0 +1,616 @@ +import argparse +import hashlib +import json +import os +import shutil +import subprocess +import sys +import sysconfig +import urllib.request +from importlib import resources +from typing import Any, Dict, List, Optional + + +def init_project(args: argparse.Namespace) -> None: + """ + Initialize a new PythonNative project. + Creates `app/`, `pythonnative.json`, `requirements.txt`, `.gitignore`. + """ + project_name: str = getattr(args, "name", None) or os.path.basename(os.getcwd()) + cwd: str = os.getcwd() + + app_dir = os.path.join(cwd, "app") + config_path = os.path.join(cwd, "pythonnative.json") + requirements_path = os.path.join(cwd, "requirements.txt") + gitignore_path = os.path.join(cwd, ".gitignore") + + # Prevent accidental overwrite unless --force is provided + if not getattr(args, "force", False): + exists = [] + if os.path.exists(app_dir): + exists.append("app/") + if os.path.exists(config_path): + exists.append("pythonnative.json") + if os.path.exists(requirements_path): + exists.append("requirements.txt") + if os.path.exists(gitignore_path): + exists.append(".gitignore") + if exists: + print(f"Refusing to overwrite existing: {', '.join(exists)}. Use --force to overwrite.") + sys.exit(1) + + os.makedirs(app_dir, exist_ok=True) + + # Minimal hello world app scaffold + main_page_py = os.path.join(app_dir, "main_page.py") + if not os.path.exists(main_page_py) or args.force: + with open(main_page_py, "w", encoding="utf-8") as f: + f.write( + """import pythonnative as pn + + +class MainPage(pn.Page): + def __init__(self, native_instance): + super().__init__(native_instance) + + def on_create(self): + super().on_create() + stack = pn.StackView() + stack.add_view(pn.Label("Hello from PythonNative!")) + button = pn.Button("Tap me") + button.set_on_click(lambda: print("Button clicked")) + stack.add_view(button) + self.set_root_view(stack) + + +def bootstrap(native_instance): + '''Entry point called by the host app (Android Activity or iOS ViewController).''' + page = MainPage(native_instance) + page.on_create() + return page +""" + ) + + # Create config + config = { + "name": project_name, + "appId": "com.example." + project_name.replace(" ", "").lower(), + "entryPoint": "app/main_page.py", + "ios": {}, + "android": {}, + } + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2) + + # Requirements + if not os.path.exists(requirements_path) or args.force: + with open(requirements_path, "w", encoding="utf-8") as f: + f.write("pythonnative\n") + + # .gitignore + default_gitignore = "# PythonNative\n" "__pycache__/\n" "*.pyc\n" ".venv/\n" "build/\n" ".DS_Store\n" + if not os.path.exists(gitignore_path) or args.force: + with open(gitignore_path, "w", encoding="utf-8") as f: + f.write(default_gitignore) + + print("Initialized PythonNative project.") + + +def _copy_dir(src: str, dst: str) -> None: + os.makedirs(os.path.dirname(dst), exist_ok=True) + shutil.copytree(src, dst, dirs_exist_ok=True) + + +def _copy_bundled_template_dir(template_dir: str, destination: str) -> None: + """ + Copy a bundled template directory into the destination directory. + Tries the repository `templates/` first during development, then + package resources when installed from a wheel. + The result should be `${destination}/{template_dir}`. + """ + dest_path = os.path.join(destination, template_dir) + + # Dev-first: prefer local source templates if running from a checkout (avoid stale packaged data) + try: + # __file__ -> src/pythonnative/cli/pn.py, so go up to src/, then to repo root + src_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + # Check templates located inside the source package tree + local_pkg_templates = os.path.join(src_dir, "pythonnative", "templates", template_dir) + if os.path.isdir(local_pkg_templates): + _copy_dir(local_pkg_templates, dest_path) + return + repo_root = os.path.abspath(os.path.join(src_dir, "..")) + repo_templates = os.path.join(repo_root, "templates") + candidate_dir = os.path.join(repo_templates, template_dir) + if os.path.isdir(candidate_dir): + _copy_dir(candidate_dir, dest_path) + return + except Exception: + pass + + # Try to load from installed package resources (templates packaged inside the module) + try: + cand = resources.files("pythonnative").joinpath("templates").joinpath(template_dir) + with resources.as_file(cand) as p: + resource_path = str(p) + if os.path.isdir(resource_path): + _copy_dir(resource_path, dest_path) + return + except Exception: + pass + + # Last resort: check typical data-file locations + try: + data_paths = sysconfig.get_paths() + search_bases = [ + data_paths.get("data"), + data_paths.get("purelib"), + data_paths.get("platlib"), + ] + for base in filter(None, search_bases): + candidate_dir = os.path.join(base, "pythonnative", "templates", template_dir) + if os.path.isdir(candidate_dir): + _copy_dir(candidate_dir, dest_path) + return + except Exception: + pass + + raise FileNotFoundError(f"Could not find bundled template directory {template_dir}. Ensure templates are packaged.") + + +def _github_json(url: str) -> Any: + req = urllib.request.Request(url, headers={"User-Agent": "pythonnative-cli"}) + with urllib.request.urlopen(req) as r: + return json.loads(r.read().decode("utf-8")) + + +def _resolve_python_apple_support_asset( + py_major_minor: str = "3.11", preferred_name: str = "Python-3.11-iOS-support.b7.tar.gz" +) -> Optional[str]: + """ + Find a browser_download_url for a Python-Apple-support asset on GitHub Releases. + Prefers an exact name match (preferred_name). Falls back to the newest + asset whose name contains "Python-{py_major_minor}-iOS-support" and endswith .tar.gz. + """ + try: + releases = _github_json("https://api.github.com/repos/beeware/Python-Apple-support/releases?per_page=100") + # Search all releases for preferred_name first + for rel in releases: + for a in rel.get("assets", []) or []: + name = a.get("name") or "" + if name == preferred_name: + return a.get("browser_download_url") + # Fallback: any matching Python-{version}-iOS-support*.tar.gz (take first encountered) + needle = f"Python-{py_major_minor}-iOS-support" + for rel in releases: + for a in rel.get("assets", []) or []: + name = a.get("name") or "" + if needle in name and name.endswith(".tar.gz"): + return a.get("browser_download_url") + except Exception: + pass + return None + + +def create_android_project(project_name: str, destination: str) -> None: + """ + Create a new Android project using a template. + + :param project_name: The name of the project. + :param destination: The directory where the project will be created. + """ + # Copy the Android template project directory + _copy_bundled_template_dir("android_template", destination) + + +def create_ios_project(project_name: str, destination: str) -> None: + """ + Create a new iOS project using a template. + + :param project_name: The name of the project. + :param destination: The directory where the project will be created. + """ + # Copy the iOS template project directory + _copy_bundled_template_dir("ios_template", destination) + + +def run_project(args: argparse.Namespace) -> None: + """ + Run the specified project. + """ + # Determine the platform + platform: str = args.platform + prepare_only: bool = getattr(args, "prepare_only", False) + + # Define the build directory + build_dir: str = os.path.join(os.getcwd(), "build", platform) + + # Create the build directory if it doesn't exist + os.makedirs(build_dir, exist_ok=True) + + # Generate the required project files + if platform == "android": + create_android_project("MyApp", build_dir) + elif platform == "ios": + create_ios_project("MyApp", build_dir) + + # Copy the user's Python code into the project + src_dir: str = os.path.join(os.getcwd(), "app") + + # Adjust the destination directory for Android project + if platform == "android": + dest_dir: str = os.path.join(build_dir, "android_template", "app", "src", "main", "python", "app") + else: + # For iOS, stage the Python app in a top-level folder for later integration scripts + dest_dir = os.path.join(build_dir, "app") + + # Create the destination directory if it doesn't exist + os.makedirs(dest_dir, exist_ok=True) + shutil.copytree(src_dir, dest_dir, dirs_exist_ok=True) + + # During local development (running from repository), also bundle the + # local library sources so the app uses the in-repo version instead of + # the PyPI package. This provides faster inner-loop iteration and avoids + # version skew during development. + try: + # __file__ -> src/pythonnative/cli/pn.py, so repo root is one up from src/ + src_root = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), "..")) + local_lib = os.path.join(src_root, "pythonnative") + if os.path.isdir(local_lib): + if platform == "android": + python_root = os.path.join(build_dir, "android_template", "app", "src", "main", "python") + else: + python_root = os.path.join(build_dir) # staged at build/ios/app for iOS below + os.makedirs(python_root, exist_ok=True) + shutil.copytree(local_lib, os.path.join(python_root, "pythonnative"), dirs_exist_ok=True) + except Exception: + # Non-fatal; fallback to the packaged PyPI dependency if present + pass + + # Install any necessary Python packages into the project environment + # Skip installation during prepare-only to avoid network access and speed up scaffolding + if not prepare_only: + requirements_path = os.path.join(os.getcwd(), "requirements.txt") + if os.path.exists(requirements_path): + subprocess.run([sys.executable, "-m", "pip", "install", "-r", requirements_path], check=False) + + # Run the project + if prepare_only: + print("Prepared project in build/ without building (prepare-only).") + return + + if platform == "android": + # Change to the Android project directory + android_project_dir: str = os.path.join(build_dir, "android_template") + os.chdir(android_project_dir) + + # Add executable permissions to the gradlew script + gradlew_path: str = os.path.join(android_project_dir, "gradlew") + os.chmod(gradlew_path, 0o755) # this makes the file executable for the user + + # Build the Android project and install it on the device + env: dict[str, str] = os.environ.copy() + # Respect JAVA_HOME if set; otherwise, attempt a best-effort on macOS via Homebrew + if sys.platform == "darwin" and not env.get("JAVA_HOME"): + try: + jdk_path: str = subprocess.check_output(["brew", "--prefix", "openjdk@17"]).decode().strip() + env["JAVA_HOME"] = jdk_path + except Exception: + pass + subprocess.run(["./gradlew", "installDebug"], check=True, env=env) + + # Run the Android app + # Assumes that the package name of your app is "com.example.myapp" and the main activity is "MainActivity" + # Replace "com.example.myapp" and ".MainActivity" with your actual package name and main activity + subprocess.run( + [ + "adb", + "shell", + "am", + "start", + "-n", + "com.pythonnative.android_template/.MainActivity", + ], + check=True, + ) + elif platform == "ios": + # Attempt to build and run on iOS Simulator (best-effort) + ios_project_dir: str = os.path.join(build_dir, "ios_template") + if os.path.isdir(ios_project_dir): + # Stage embedded Python runtime inputs by downloading pinned assets + try: + assets_dir = os.path.join(build_dir, "ios_runtime") + os.makedirs(assets_dir, exist_ok=True) + # Pinned preferred asset name and checksum (b7) + preferred_name = "Python-3.11-iOS-support.b7.tar.gz" + sha256 = "2b7d8589715b9890e8dd7e1bce91c210bb5287417e17b9af120fc577675ed28e" + # Resolve a working download URL from GitHub Releases + url = _resolve_python_apple_support_asset("3.11", preferred_name=preferred_name) + if not url: + raise RuntimeError("Could not resolve Python-Apple-support asset URL from GitHub Releases.") + tar_path = os.path.join(assets_dir, os.path.basename(url)) + if not os.path.exists(tar_path): + print("Downloading Python-Apple-support (3.11 iOS)") + req = urllib.request.Request(url, headers={"User-Agent": "pythonnative-cli"}) + with urllib.request.urlopen(req) as r, open(tar_path, "wb") as f: + f.write(r.read()) + # Verify checksum + h = hashlib.sha256() + with open(tar_path, "rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + if h.hexdigest() != sha256: + raise RuntimeError("SHA256 mismatch for Python-Apple-support tarball") + # Extract only once + extract_root = os.path.join(assets_dir, "extracted") + if not os.path.isdir(extract_root): + os.makedirs(extract_root, exist_ok=True) + subprocess.run(["tar", "-xzf", tar_path, "-C", extract_root], check=True) + # Provide Python.xcframework to the Xcode project and stdlib for bundling + # Try both common layouts + cand_frameworks = [ + os.path.join(extract_root, "Python.xcframework"), + os.path.join(extract_root, "support", "Python.xcframework"), + ] + xc_src = next((p for p in cand_frameworks if os.path.isdir(p)), None) + if xc_src: + shutil.copytree(xc_src, os.path.join(ios_project_dir, "Python.xcframework"), dirs_exist_ok=True) + # Stdlib path + cand_stdlib = [ + os.path.join(extract_root, "Python.xcframework", "ios-arm64_x86_64-simulator", "lib", "python3.11"), + os.path.join( + extract_root, "support", "Python.xcframework", "ios-arm64_x86_64-simulator", "lib", "python3.11" + ), + ] + stdlib_src = next((p for p in cand_stdlib if os.path.isdir(p)), None) + except Exception as e: + print(f"Warning: failed to prepare Python runtime: {e}") + + os.chdir(ios_project_dir) + derived_data = os.path.join(ios_project_dir, "build") + try: + # Detect a simulator UDID to target: prefer Booted; else any iPhone + sim_udid: Optional[str] = None + try: + import json as _json + + devices_out = subprocess.run( + ["xcrun", "simctl", "list", "devices", "available", "--json"], + check=False, + capture_output=True, + text=True, + ) + devs = _json.loads(devices_out.stdout or "{}").get("devices") or {} + all_devs = [d for lst in devs.values() for d in (lst or [])] + for d in all_devs: + if d.get("state") == "Booted": + sim_udid = d.get("udid") + break + if not sim_udid: + for d in all_devs: + if (d.get("isAvailable") or d.get("availability")) and ( + d.get("name") or "" + ).lower().startswith("iphone"): + sim_udid = d.get("udid") + break + except Exception: + pass + + xcode_dest = ( + ["-destination", f"id={sim_udid}"] if sim_udid else ["-destination", "platform=iOS Simulator"] + ) + + # Provide header and lib paths for CPython (Simulator slice) ONLY if the + # XCFramework is not already added to the Xcode project. When the project + # contains `Python.xcframework`, Xcode manages headers and linking to avoid + # duplicate module.modulemap definitions. + extra_xcode_settings: list[str] = [] + try: + xc_present = os.path.isdir(os.path.join(ios_project_dir, "Python.xcframework")) + if not xc_present and "extract_root" in locals(): + sim_headers = os.path.join( + extract_root, "Python.xcframework", "ios-arm64_x86_64-simulator", "Headers" + ) + sim_lib = os.path.join( + extract_root, "Python.xcframework", "ios-arm64_x86_64-simulator", "libPython3.11.a" + ) + if os.path.isdir(sim_headers): + extra_xcode_settings.extend( + [ + f"HEADER_SEARCH_PATHS={sim_headers}", + f"SWIFT_INCLUDE_PATHS={sim_headers}", + ] + ) + if os.path.exists(sim_lib): + extra_xcode_settings.append(f"OTHER_LDFLAGS=-force_load {sim_lib}") + except Exception: + pass + + subprocess.run( + [ + "xcodebuild", + "-project", + "ios_template.xcodeproj", + "-scheme", + "ios_template", + "-configuration", + "Debug", + *xcode_dest, + "-derivedDataPath", + derived_data, + "build", + *extra_xcode_settings, + ], + check=False, + ) + except FileNotFoundError: + print("xcodebuild not found. Skipping iOS build step.") + return + + # Locate built app + app_path = os.path.join(derived_data, "Build", "Products", "Debug-iphonesimulator", "ios_template.app") + if not os.path.isdir(app_path): + print("Could not locate built .app; open the project in Xcode to run.") + return + + # Copy staged Python app and optional embedded runtime into the .app bundle + try: + staged_app_src = os.path.join(build_dir, "app") + if os.path.isdir(staged_app_src): + shutil.copytree(staged_app_src, os.path.join(app_path, "app"), dirs_exist_ok=True) + # Also copy local library sources if present for dev flow + src_root = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), "..")) + local_lib = os.path.join(src_root, "pythonnative") + if os.path.isdir(local_lib): + shutil.copytree(local_lib, os.path.join(app_path, "pythonnative"), dirs_exist_ok=True) + # Copy stdlib from downloaded support if available + if "stdlib_src" in locals() and stdlib_src and os.path.isdir(stdlib_src): + shutil.copytree(stdlib_src, os.path.join(app_path, "python-stdlib"), dirs_exist_ok=True) + # Embed Python.framework for Simulator so PythonKit can dlopen it (from downloaded XCFramework) + sim_fw = None + if "extract_root" in locals(): + cand_fw = [ + os.path.join( + extract_root, "Python.xcframework", "ios-arm64_x86_64-simulator", "Python.framework" + ), + os.path.join( + extract_root, + "support", + "Python.xcframework", + "ios-arm64_x86_64-simulator", + "Python.framework", + ), + ] + sim_fw = next((p for p in cand_fw if os.path.isdir(p)), None) + fw_dest_dir = os.path.join(app_path, "Frameworks") + os.makedirs(fw_dest_dir, exist_ok=True) + if sim_fw and os.path.isdir(sim_fw): + shutil.copytree(sim_fw, os.path.join(fw_dest_dir, "Python.framework"), dirs_exist_ok=True) + # Install rubicon-objc into platform-site + + # Ensure importlib.metadata finds package metadata for rubicon-objc by + # installing it into a site-like dir that is on sys.path (platform-site). + try: + tmp_site = os.path.join(build_dir, "ios_site") + if os.path.isdir(tmp_site): + shutil.rmtree(tmp_site) + os.makedirs(tmp_site, exist_ok=True) + # Install pure-Python rubicon-objc distribution metadata and package + subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "--no-deps", + "--upgrade", + "rubicon-objc", + "-t", + tmp_site, + ], + check=False, + ) + platform_site_dir = os.path.join(app_path, "platform-site") + os.makedirs(platform_site_dir, exist_ok=True) + for entry in os.listdir(tmp_site): + src_entry = os.path.join(tmp_site, entry) + dst_entry = os.path.join(platform_site_dir, entry) + if os.path.isdir(src_entry): + shutil.copytree(src_entry, dst_entry, dirs_exist_ok=True) + else: + shutil.copy2(src_entry, dst_entry) + except Exception: + # Non-fatal; if metadata isn't present, rubicon import may fail and fallback UI will appear + pass + # Note: Python.xcframework provides a static library for Simulator; it must be linked at build time. + # We copy the XCFramework into the project directory above so Xcode can link it. + except Exception: + # Non-fatal; fallback UI will appear if import fails + pass + + # Find an available simulator and boot it + try: + import json as _json + + result = subprocess.run( + ["xcrun", "simctl", "list", "devices", "available", "--json"], + check=False, + capture_output=True, + text=True, + ) + devices_json = _json.loads(result.stdout or "{}") + all_devices: List[Dict[str, Any]] = [] + for _runtime, devices in (devices_json.get("devices") or {}).items(): + all_devices.extend(devices or []) + # Prefer iPhone 15/15 Pro names; else first available iPhone + preferred = None + for d in all_devices: + name = (d.get("name") or "").lower() + if "iphone 15" in name and d.get("isAvailable"): + preferred = d + break + if not preferred: + for d in all_devices: + if d.get("isAvailable") and (d.get("name") or "").lower().startswith("iphone"): + preferred = d + break + if not preferred: + print("No available iOS Simulators found; open the project in Xcode to run.") + return + + udid = preferred.get("udid") + # Boot (no-op if already booted) + subprocess.run(["xcrun", "simctl", "boot", udid], check=False) + # Install and launch + subprocess.run(["xcrun", "simctl", "install", udid, app_path], check=False) + subprocess.run(["xcrun", "simctl", "launch", udid, "com.pythonnative.ios-template"], check=False) + print("Launched iOS app on Simulator (best-effort).") + except Exception: + print("Failed to auto-run on Simulator; open the project in Xcode to run.") + + +def clean_project(args: argparse.Namespace) -> None: + """ + Clean the specified project. + """ + # Define the build directory + build_dir: str = os.path.join(os.getcwd(), "build") + + # Check if the build directory exists + if os.path.exists(build_dir): + shutil.rmtree(build_dir) + print("Removed build/ directory.") + else: + print("No build/ directory to remove.") + + +def main() -> None: + parser = argparse.ArgumentParser(prog="pn", description="PythonNative CLI") + subparsers = parser.add_subparsers() + + # Create a new command 'init' that calls init_project + parser_init = subparsers.add_parser("init") + parser_init.add_argument("name", nargs="?", help="Project name (defaults to current directory name)") + parser_init.add_argument("--force", action="store_true", help="Overwrite existing files if present") + parser_init.set_defaults(func=init_project) + + # Create a new command 'run' that calls run_project + parser_run = subparsers.add_parser("run") + parser_run.add_argument("platform", choices=["android", "ios"]) + parser_run.add_argument( + "--prepare-only", + action="store_true", + help="Extract templates and stage app without building", + ) + parser_run.set_defaults(func=run_project) + + # Create a new command 'clean' that calls clean_project + parser_clean = subparsers.add_parser("clean") + parser_clean.set_defaults(func=clean_project) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/apps/pythonnative_demo/app/__init__.py b/src/pythonnative/collection_view.py similarity index 100% rename from apps/pythonnative_demo/app/__init__.py rename to src/pythonnative/collection_view.py diff --git a/src/pythonnative/date_picker.py b/src/pythonnative/date_picker.py new file mode 100644 index 0000000..cb86006 --- /dev/null +++ b/src/pythonnative/date_picker.py @@ -0,0 +1,72 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class DatePickerBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_date(self, year: int, month: int, day: int) -> None: + pass + + @abstractmethod + def get_date(self) -> tuple: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/DatePicker + # ======================================== + + from java import jclass + + class DatePicker(DatePickerBase, ViewBase): + def __init__(self, context, year: int = 0, month: int = 0, day: int = 0) -> None: + super().__init__() + self.native_class = jclass("android.widget.DatePicker") + self.native_instance = self.native_class(context) + self.set_date(year, month, day) + + def set_date(self, year: int, month: int, day: int) -> None: + self.native_instance.updateDate(year, month, day) + + def get_date(self) -> tuple: + year = self.native_instance.getYear() + month = self.native_instance.getMonth() + day = self.native_instance.getDayOfMonth() + return year, month, day + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uidatepicker + # ======================================== + + from datetime import datetime + + from rubicon.objc import ObjCClass + + class DatePicker(DatePickerBase, ViewBase): + def __init__(self, year: int = 0, month: int = 0, day: int = 0) -> None: + super().__init__() + self.native_class = ObjCClass("UIDatePicker") + self.native_instance = self.native_class.alloc().init() + self.set_date(year, month, day) + + def set_date(self, year: int, month: int, day: int) -> None: + date = datetime(year, month, day) + self.native_instance.setDate_(date) + + def get_date(self) -> tuple: + date = self.native_instance.date() + return date.year, date.month, date.day diff --git a/src/pythonnative/image_view.py b/src/pythonnative/image_view.py new file mode 100644 index 0000000..78cb1ff --- /dev/null +++ b/src/pythonnative/image_view.py @@ -0,0 +1,76 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID, get_android_context +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class ImageViewBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_image(self, image: str) -> None: + pass + + @abstractmethod + def get_image(self) -> str: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/ImageView + # ======================================== + + from android.graphics import BitmapFactory + from java import jclass + + class ImageView(ImageViewBase, ViewBase): + def __init__(self, image: str = "") -> None: + super().__init__() + self.native_class = jclass("android.widget.ImageView") + context = get_android_context() + self.native_instance = self.native_class(context) + if image: + self.set_image(image) + + def set_image(self, image: str) -> None: + bitmap = BitmapFactory.decodeFile(image) + self.native_instance.setImageBitmap(bitmap) + + def get_image(self) -> str: + # Please note that this is a simplistic representation, getting image from ImageView + # in Android would require converting Drawable to Bitmap and then to File + return "Image file path in Android" + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uiimageview + # ======================================== + + from rubicon.objc import ObjCClass + from rubicon.objc.api import NSString, UIImage + + class ImageView(ImageViewBase, ViewBase): + def __init__(self, image: str = "") -> None: + super().__init__() + self.native_class = ObjCClass("UIImageView") + self.native_instance = self.native_class.alloc().init() + if image: + self.set_image(image) + + def set_image(self, image: str) -> None: + ns_str = NSString.alloc().initWithUTF8String_(image) + ui_image = UIImage.imageNamed_(ns_str) + self.native_instance.setImage_(ui_image) + + def get_image(self) -> str: + # Similar to Android, getting the image from UIImageView isn't straightforward. + return "Image file name in iOS" diff --git a/src/pythonnative/label.py b/src/pythonnative/label.py new file mode 100644 index 0000000..c34eec2 --- /dev/null +++ b/src/pythonnative/label.py @@ -0,0 +1,66 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID, get_android_context +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class LabelBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_text(self, text: str) -> None: + pass + + @abstractmethod + def get_text(self) -> str: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/TextView + # ======================================== + + from java import jclass + + class Label(LabelBase, ViewBase): + def __init__(self, text: str = "") -> None: + super().__init__() + self.native_class = jclass("android.widget.TextView") + context = get_android_context() + self.native_instance = self.native_class(context) + self.set_text(text) + + def set_text(self, text: str) -> None: + self.native_instance.setText(text) + + def get_text(self) -> str: + return self.native_instance.getText().toString() + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uilabel + # ======================================== + + from rubicon.objc import ObjCClass + + class Label(LabelBase, ViewBase): + def __init__(self, text: str = "") -> None: + super().__init__() + self.native_class = ObjCClass("UILabel") + self.native_instance = self.native_class.alloc().init() + self.set_text(text) + + def set_text(self, text: str) -> None: + self.native_instance.setText_(text) + + def get_text(self) -> str: + return self.native_instance.text() diff --git a/src/pythonnative/list_view.py b/src/pythonnative/list_view.py new file mode 100644 index 0000000..d2378d2 --- /dev/null +++ b/src/pythonnative/list_view.py @@ -0,0 +1,73 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class ListViewBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_data(self, data: list) -> None: + pass + + @abstractmethod + def get_data(self) -> list: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/ListView + # ======================================== + + from java import jclass + + class ListView(ListViewBase, ViewBase): + def __init__(self, context, data: list = []) -> None: + super().__init__() + self.context = context + self.native_class = jclass("android.widget.ListView") + self.native_instance = self.native_class(context) + self.set_data(data) + + def set_data(self, data: list) -> None: + adapter = jclass("android.widget.ArrayAdapter")( + self.context, jclass("android.R$layout").simple_list_item_1, data + ) + self.native_instance.setAdapter(adapter) + + def get_data(self) -> list: + adapter = self.native_instance.getAdapter() + return [adapter.getItem(i) for i in range(adapter.getCount())] + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uitableview + # ======================================== + + from rubicon.objc import ObjCClass + + class ListView(ListViewBase, ViewBase): + def __init__(self, data: list = []) -> None: + super().__init__() + self.native_class = ObjCClass("UITableView") + self.native_instance = self.native_class.alloc().init() + self.set_data(data) + + def set_data(self, data: list) -> None: + # Note: This is a simplified representation. Normally, you would need to create a UITableViewDataSource. + self.native_instance.reloadData() + + def get_data(self) -> list: + # Note: This is a simplified representation. + # Normally, you would need to get data from the UITableViewDataSource. + return [] diff --git a/src/pythonnative/material_activity_indicator_view.py b/src/pythonnative/material_activity_indicator_view.py new file mode 100644 index 0000000..a568ced --- /dev/null +++ b/src/pythonnative/material_activity_indicator_view.py @@ -0,0 +1,69 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class MaterialActivityIndicatorViewBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def start_animating(self) -> None: + pass + + @abstractmethod + def stop_animating(self) -> None: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/com/google/android/material/progressindicator/CircularProgressIndicator + # ======================================== + + from java import jclass + + class MaterialActivityIndicatorView(MaterialActivityIndicatorViewBase, ViewBase): + def __init__(self, context) -> None: + super().__init__() + self.native_class = jclass("com.google.android.material.progressindicator.CircularProgressIndicator") + self.native_instance = self.native_class(context) + self.native_instance.setIndeterminate(True) + + def start_animating(self) -> None: + # self.native_instance.setVisibility(android.view.View.VISIBLE) + return + + def stop_animating(self) -> None: + # self.native_instance.setVisibility(android.view.View.GONE) + return + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uiactivityindicatorview + # ======================================== + + from rubicon.objc import ObjCClass + + class MaterialActivityIndicatorView(MaterialActivityIndicatorViewBase, ViewBase): + def __init__(self) -> None: + super().__init__() + self.native_class = ObjCClass("UIActivityIndicatorView") + self.native_instance = self.native_class.alloc().initWithActivityIndicatorStyle_( + 0 + ) # 0: UIActivityIndicatorViewStyleLarge + self.native_instance.hidesWhenStopped = True + + def start_animating(self) -> None: + self.native_instance.startAnimating() + + def stop_animating(self) -> None: + self.native_instance.stopAnimating() diff --git a/apps/pythonnative_demo/pythonnative.json b/src/pythonnative/material_bottom_navigation_view.py similarity index 100% rename from apps/pythonnative_demo/pythonnative.json rename to src/pythonnative/material_bottom_navigation_view.py diff --git a/src/pythonnative/material_button.py b/src/pythonnative/material_button.py new file mode 100644 index 0000000..1db600a --- /dev/null +++ b/src/pythonnative/material_button.py @@ -0,0 +1,65 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class MaterialButtonBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_title(self, title: str) -> None: + pass + + @abstractmethod + def get_title(self) -> str: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/com/google/android/material/button/MaterialButton + # ======================================== + + from java import jclass + + class MaterialButton(MaterialButtonBase, ViewBase): + def __init__(self, context, title: str = "") -> None: + super().__init__() + self.native_class = jclass("com.google.android.material.button.MaterialButton") + self.native_instance = self.native_class(context) + self.set_title(title) + + def set_title(self, title: str) -> None: + self.native_instance.setText(title) + + def get_title(self) -> str: + return self.native_instance.getText().toString() + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uibutton + # ======================================== + + from rubicon.objc import ObjCClass + + class MaterialButton(MaterialButtonBase, ViewBase): + def __init__(self, title: str = "") -> None: + super().__init__() + self.native_class = ObjCClass("UIButton") # Apple does not have a direct equivalent for MaterialButton + self.native_instance = self.native_class.alloc().init() + self.set_title(title) + + def set_title(self, title: str) -> None: + self.native_instance.setTitle_forState_(title, 0) + + def get_title(self) -> str: + return self.native_instance.titleForState_(0) diff --git a/src/pythonnative/material_date_picker.py b/src/pythonnative/material_date_picker.py new file mode 100644 index 0000000..0eadeec --- /dev/null +++ b/src/pythonnative/material_date_picker.py @@ -0,0 +1,85 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class MaterialDatePickerBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_date(self, year: int, month: int, day: int) -> None: + pass + + @abstractmethod + def get_date(self) -> tuple: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/com/google/android/material/datepicker/MaterialDatePicker + # ======================================== + + from java import jclass + + class MaterialDatePicker(MaterialDatePickerBase, ViewBase): + def __init__(self, year: int = 0, month: int = 0, day: int = 0) -> None: + super().__init__() + self.native_class = jclass("com.google.android.material.datepicker.MaterialDatePicker") + self.builder = self.native_class.Builder.datePicker() + self.set_date(year, month, day) + self.native_instance = self.builder.build() + + def set_date(self, year: int, month: int, day: int) -> None: + # MaterialDatePicker uses milliseconds since epoch to set date + from java.util import Calendar + + cal = Calendar.getInstance() + cal.set(year, month, day) + milliseconds = cal.getTimeInMillis() + self.builder.setSelection(milliseconds) + + def get_date(self) -> tuple: + # Convert selection (milliseconds since epoch) back to a date + from java.util import Calendar + + cal = Calendar.getInstance() + cal.setTimeInMillis(self.native_instance.getSelection()) + return ( + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH), + cal.get(Calendar.DAY_OF_MONTH), + ) + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uidatepicker + # ======================================== + + from datetime import datetime + + from rubicon.objc import ObjCClass + + class MaterialDatePicker(MaterialDatePickerBase, ViewBase): + def __init__(self, year: int = 0, month: int = 0, day: int = 0) -> None: + super().__init__() + self.native_class = ObjCClass("UIDatePicker") + self.native_instance = self.native_class.alloc().init() + self.set_date(year, month, day) + + def set_date(self, year: int, month: int, day: int) -> None: + date = datetime(year, month, day) + self.native_instance.setDate_(date) + + def get_date(self) -> tuple: + date = self.native_instance.date() + return date.year, date.month, date.day diff --git a/src/pythonnative/material_progress_view.py b/src/pythonnative/material_progress_view.py new file mode 100644 index 0000000..23ca565 --- /dev/null +++ b/src/pythonnative/material_progress_view.py @@ -0,0 +1,66 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class MaterialProgressViewBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_progress(self, progress: float) -> None: + pass + + @abstractmethod + def get_progress(self) -> float: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/com/google/android/material/progressindicator/LinearProgressIndicator + # ======================================== + + from java import jclass + + class MaterialProgressView(MaterialProgressViewBase, ViewBase): + def __init__(self, context) -> None: + super().__init__() + self.native_class = jclass("com.google.android.material.progressindicator.LinearProgressIndicator") + self.native_instance = self.native_class(context) + self.native_instance.setIndeterminate(False) + + def set_progress(self, progress: float) -> None: + self.native_instance.setProgress(int(progress * 100)) + + def get_progress(self) -> float: + return self.native_instance.getProgress() / 100.0 + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uiprogressview + # ======================================== + + from rubicon.objc import ObjCClass + + class MaterialProgressView(MaterialProgressViewBase, ViewBase): + def __init__(self) -> None: + super().__init__() + self.native_class = ObjCClass("UIProgressView") + self.native_instance = self.native_class.alloc().initWithProgressViewStyle_( + 0 + ) # 0: UIProgressViewStyleDefault + + def set_progress(self, progress: float) -> None: + self.native_instance.setProgress_animated_(progress, False) + + def get_progress(self) -> float: + return self.native_instance.progress() diff --git a/src/pythonnative/material_search_bar.py b/src/pythonnative/material_search_bar.py new file mode 100644 index 0000000..0693323 --- /dev/null +++ b/src/pythonnative/material_search_bar.py @@ -0,0 +1,65 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class MaterialSearchBarBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_query(self, query: str) -> None: + pass + + @abstractmethod + def get_query(self) -> str: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/com/google/android/material/search/SearchBar + # ======================================== + + from java import jclass + + class MaterialSearchBar(MaterialSearchBarBase, ViewBase): + def __init__(self, context, query: str = "") -> None: + super().__init__() + self.native_class = jclass("com.google.android.material.search.SearchBar") + self.native_instance = self.native_class(context) + self.set_query(query) + + def set_query(self, query: str) -> None: + self.native_instance.setQuery(query, False) + + def get_query(self) -> str: + return self.native_instance.getQuery().toString() + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uisearchbar + # ======================================== + + from rubicon.objc import ObjCClass + + class MaterialSearchBar(MaterialSearchBarBase, ViewBase): + def __init__(self, query: str = "") -> None: + super().__init__() + self.native_class = ObjCClass("UISearchBar") + self.native_instance = self.native_class.alloc().init() + self.set_query(query) + + def set_query(self, query: str) -> None: + self.native_instance.set_searchText_(query) + + def get_query(self) -> str: + return self.native_instance.searchText() diff --git a/src/pythonnative/material_switch.py b/src/pythonnative/material_switch.py new file mode 100644 index 0000000..21003b5 --- /dev/null +++ b/src/pythonnative/material_switch.py @@ -0,0 +1,65 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class MaterialSwitchBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_on(self, value: bool) -> None: + pass + + @abstractmethod + def is_on(self) -> bool: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/com/google/android/material/materialswitch/MaterialSwitch + # ======================================== + + from java import jclass + + class MaterialSwitch(MaterialSwitchBase, ViewBase): + def __init__(self, context, value: bool = False) -> None: + super().__init__() + self.native_class = jclass("com.google.android.material.switch.MaterialSwitch") + self.native_instance = self.native_class(context) + self.set_on(value) + + def set_on(self, value: bool) -> None: + self.native_instance.setChecked(value) + + def is_on(self) -> bool: + return self.native_instance.isChecked() + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uiswitch + # ======================================== + + from rubicon.objc import ObjCClass + + class MaterialSwitch(MaterialSwitchBase, ViewBase): + def __init__(self, value: bool = False) -> None: + super().__init__() + self.native_class = ObjCClass("UISwitch") + self.native_instance = self.native_class.alloc().init() + self.set_on(value) + + def set_on(self, value: bool) -> None: + self.native_instance.setOn_animated_(value, False) + + def is_on(self) -> bool: + return self.native_instance.isOn() diff --git a/src/pythonnative/material_time_picker.py b/src/pythonnative/material_time_picker.py new file mode 100644 index 0000000..03d7303 --- /dev/null +++ b/src/pythonnative/material_time_picker.py @@ -0,0 +1,72 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class MaterialTimePickerBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_time(self, hour: int, minute: int) -> None: + pass + + @abstractmethod + def get_time(self) -> tuple: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/com/google/android/material/timepicker/MaterialTimePicker + # ======================================== + + from java import jclass + + class MaterialTimePicker(MaterialTimePickerBase, ViewBase): + def __init__(self, context, hour: int = 0, minute: int = 0) -> None: + super().__init__() + self.native_class = jclass("com.google.android.material.timepicker.MaterialTimePicker") + self.native_instance = self.native_class(context) + self.set_time(hour, minute) + + def set_time(self, hour: int, minute: int) -> None: + self.native_instance.setTime(hour, minute) + + def get_time(self) -> tuple: + hour = self.native_instance.getHour() + minute = self.native_instance.getMinute() + return hour, minute + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uidatepicker + # ======================================== + + from datetime import time + + from rubicon.objc import ObjCClass + + class MaterialTimePicker(MaterialTimePickerBase, ViewBase): + def __init__(self, hour: int = 0, minute: int = 0) -> None: + super().__init__() + self.native_class = ObjCClass("UIDatePicker") + self.native_instance = self.native_class.alloc().init() + self.native_instance.setDatePickerMode_(1) # Setting mode to Time + self.set_time(hour, minute) + + def set_time(self, hour: int, minute: int) -> None: + t = time(hour, minute) + self.native_instance.setTime_(t) + + def get_time(self) -> tuple: + t = self.native_instance.time() + return t.hour, t.minute diff --git a/apps/pythonnative_demo/requirements.txt b/src/pythonnative/material_toolbar.py similarity index 100% rename from apps/pythonnative_demo/requirements.txt rename to src/pythonnative/material_toolbar.py diff --git a/src/pythonnative/page.py b/src/pythonnative/page.py new file mode 100644 index 0000000..70301f6 --- /dev/null +++ b/src/pythonnative/page.py @@ -0,0 +1,209 @@ +""" +Your current approach, which involves creating an Android Activity in Kotlin +and then passing it to Python, is necessary due to the restrictions inherent +in Android's lifecycle. You are correctly following the Android way of managing +Activities. In Android, the system is in control of when and how Activities are +created and destroyed. It is not possible to directly create an instance of an +Activity from Python because that would bypass Android's lifecycle management, +leading to unpredictable results. + +Your Button example works because Button is a View, not an Activity. View +instances in Android can be created and managed directly by your code. This is +why you are able to create an instance of Button from Python. + +Remember that Activities in Android are not just containers for your UI like a +ViewGroup, they are also the main entry points into your app and are closely +tied to the app's lifecycle. Therefore, Android needs to maintain tight control +over them. Activities aren't something you instantiate whenever you need them; +they are created in response to a specific intent and their lifecycle is +managed by Android. + +So, to answer your question: Yes, you need to follow this approach for +Activities in Android. You cannot instantiate an Activity from Python like you +do for Views. + +On the other hand, for iOS, you can instantiate a UIViewController directly +from Python. The example code you provided for this is correct. + +Just ensure that your PythonNative UI framework is aware of these platform +differences and handles them appropriately. +""" + +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID, set_android_context +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class PageBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_root_view(self, view) -> None: + pass + + @abstractmethod + def on_create(self) -> None: + pass + + @abstractmethod + def on_start(self) -> None: + pass + + @abstractmethod + def on_resume(self) -> None: + pass + + @abstractmethod + def on_pause(self) -> None: + pass + + @abstractmethod + def on_stop(self) -> None: + pass + + @abstractmethod + def on_destroy(self) -> None: + pass + + @abstractmethod + def on_restart(self) -> None: + pass + + @abstractmethod + def on_save_instance_state(self) -> None: + pass + + @abstractmethod + def on_restore_instance_state(self) -> None: + pass + + @abstractmethod + def navigate_to(self, page) -> None: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/app/Activity + # ======================================== + + from java import jclass + + class Page(PageBase, ViewBase): + def __init__(self, native_instance) -> None: + super().__init__() + self.native_class = jclass("android.app.Activity") + self.native_instance = native_instance + # self.native_instance = self.native_class() + # Stash the Activity so child views can implicitly acquire a Context + set_android_context(native_instance) + + def set_root_view(self, view) -> None: + self.native_instance.setContentView(view.native_instance) + + def on_create(self) -> None: + print("Android on_create() called") + + def on_start(self) -> None: + print("Android on_start() called") + + def on_resume(self) -> None: + print("Android on_resume() called") + + def on_pause(self) -> None: + print("Android on_pause() called") + + def on_stop(self) -> None: + print("Android on_stop() called") + + def on_destroy(self) -> None: + print("Android on_destroy() called") + + def on_restart(self) -> None: + print("Android on_restart() called") + + def on_save_instance_state(self) -> None: + print("Android on_save_instance_state() called") + + def on_restore_instance_state(self) -> None: + print("Android on_restore_instance_state() called") + + def navigate_to(self, page) -> None: + # intent = jclass("android.content.Intent")(self.native_instance, page.native_class) + intent = jclass("android.content.Intent")( + self.native_instance, + jclass("com.pythonnative.pythonnative.SecondActivity"), + ) + self.native_instance.startActivity(intent) + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uiviewcontroller + # ======================================== + + from rubicon.objc import ObjCClass, ObjCInstance + + class Page(PageBase, ViewBase): + def __init__(self, native_instance) -> None: + super().__init__() + self.native_class = ObjCClass("UIViewController") + # If Swift passed us an integer pointer, wrap it as an ObjCInstance. + if isinstance(native_instance, int): + try: + native_instance = ObjCInstance(native_instance) + except Exception: + native_instance = None + self.native_instance = native_instance + # self.native_instance = self.native_class.alloc().init() + + def set_root_view(self, view) -> None: + # UIViewController.view is a property; access without calling. + root_view = self.native_instance.view + # Size the root child to fill the controller's view and enable autoresizing + try: + bounds = root_view.bounds + view.native_instance.setFrame_(bounds) + # UIViewAutoresizingFlexibleWidth (2) | UIViewAutoresizingFlexibleHeight (16) + view.native_instance.setAutoresizingMask_(2 | 16) + except Exception: + pass + root_view.addSubview_(view.native_instance) + + def on_create(self) -> None: + print("iOS on_create() called") + + def on_start(self) -> None: + print("iOS on_start() called") + + def on_resume(self) -> None: + print("iOS on_resume() called") + + def on_pause(self) -> None: + print("iOS on_pause() called") + + def on_stop(self) -> None: + print("iOS on_stop() called") + + def on_destroy(self) -> None: + print("iOS on_destroy() called") + + def on_restart(self) -> None: + print("iOS on_restart() called") + + def on_save_instance_state(self) -> None: + print("iOS on_save_instance_state() called") + + def on_restore_instance_state(self) -> None: + print("iOS on_restore_instance_state() called") + + def navigate_to(self, page) -> None: + self.native_instance.navigationController().pushViewControllerAnimated_(page.native_instance, True) diff --git a/src/pythonnative/picker_view.py b/src/pythonnative/picker_view.py new file mode 100644 index 0000000..fc7ca98 --- /dev/null +++ b/src/pythonnative/picker_view.py @@ -0,0 +1,65 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class PickerViewBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_selected(self, index: int) -> None: + pass + + @abstractmethod + def get_selected(self) -> int: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/Spinner + # ======================================== + + from java import jclass + + class PickerView(PickerViewBase, ViewBase): + def __init__(self, context, index: int = 0) -> None: + super().__init__() + self.native_class = jclass("android.widget.Spinner") + self.native_instance = self.native_class(context) + self.set_selected(index) + + def set_selected(self, index: int) -> None: + self.native_instance.setSelection(index) + + def get_selected(self) -> int: + return self.native_instance.getSelectedItemPosition() + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uipickerview + # ======================================== + + from rubicon.objc import ObjCClass + + class PickerView(PickerViewBase, ViewBase): + def __init__(self, index: int = 0) -> None: + super().__init__() + self.native_class = ObjCClass("UIPickerView") + self.native_instance = self.native_class.alloc().init() + self.set_selected(index) + + def set_selected(self, index: int) -> None: + self.native_instance.selectRow_inComponent_animated_(index, 0, False) + + def get_selected(self) -> int: + return self.native_instance.selectedRowInComponent_(0) diff --git a/src/pythonnative/progress_view.py b/src/pythonnative/progress_view.py new file mode 100644 index 0000000..c1a08b3 --- /dev/null +++ b/src/pythonnative/progress_view.py @@ -0,0 +1,68 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID, get_android_context +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class ProgressViewBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_progress(self, progress: float) -> None: + pass + + @abstractmethod + def get_progress(self) -> float: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/ProgressBar + # ======================================== + + from java import jclass + + class ProgressView(ProgressViewBase, ViewBase): + def __init__(self) -> None: + super().__init__() + self.native_class = jclass("android.widget.ProgressBar") + # self.native_instance = self.native_class(context, None, android.R.attr.progressBarStyleHorizontal) + context = get_android_context() + self.native_instance = self.native_class(context, None, jclass("android.R$attr").progressBarStyleHorizontal) + self.native_instance.setIndeterminate(False) + + def set_progress(self, progress: float) -> None: + self.native_instance.setProgress(int(progress * 100)) + + def get_progress(self) -> float: + return self.native_instance.getProgress() / 100.0 + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uiprogressview + # ======================================== + + from rubicon.objc import ObjCClass + + class ProgressView(ProgressViewBase, ViewBase): + def __init__(self) -> None: + super().__init__() + self.native_class = ObjCClass("UIProgressView") + self.native_instance = self.native_class.alloc().initWithProgressViewStyle_( + 0 + ) # 0: UIProgressViewStyleDefault + + def set_progress(self, progress: float) -> None: + self.native_instance.setProgress_animated_(progress, False) + + def get_progress(self) -> float: + return self.native_instance.progress() diff --git a/src/pythonnative/scroll_view.py b/src/pythonnative/scroll_view.py new file mode 100644 index 0000000..1c19d62 --- /dev/null +++ b/src/pythonnative/scroll_view.py @@ -0,0 +1,63 @@ +from abc import ABC, abstractmethod +from typing import Any, List + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class ScrollViewBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + self.views: List[Any] = [] + + @abstractmethod + def add_view(self, view) -> None: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/ScrollView + # ======================================== + + from java import jclass + + class ScrollView(ScrollViewBase, ViewBase): + def __init__(self, context) -> None: + super().__init__() + self.native_class = jclass("android.widget.ScrollView") + self.native_instance = self.native_class(context) + + def add_view(self, view): + self.views.append(view) + # In Android, ScrollView can host only one direct child + if len(self.views) == 1: + self.native_instance.addView(view.native_instance) + else: + raise Exception("ScrollView can host only one direct child") + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uiscrollview + # ======================================== + + from rubicon.objc import ObjCClass + + class ScrollView(ScrollViewBase, ViewBase): + def __init__(self) -> None: + super().__init__() + self.native_class = ObjCClass("UIScrollView") + self.native_instance = self.native_class.alloc().initWithFrame_(((0, 0), (0, 0))) + + def add_view(self, view): + self.views.append(view) + # Ensure view is a subview of scrollview + if view.native_instance not in self.native_instance.subviews: + self.native_instance.addSubview_(view.native_instance) diff --git a/src/pythonnative/search_bar.py b/src/pythonnative/search_bar.py new file mode 100644 index 0000000..72609ff --- /dev/null +++ b/src/pythonnative/search_bar.py @@ -0,0 +1,65 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class SearchBarBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_query(self, query: str) -> None: + pass + + @abstractmethod + def get_query(self) -> str: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/SearchView + # ======================================== + + from java import jclass + + class SearchBar(SearchBarBase, ViewBase): + def __init__(self, context, query: str = "") -> None: + super().__init__() + self.native_class = jclass("android.widget.SearchView") + self.native_instance = self.native_class(context) + self.set_query(query) + + def set_query(self, query: str) -> None: + self.native_instance.setQuery(query, False) + + def get_query(self) -> str: + return self.native_instance.getQuery().toString() + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uisearchbar + # ======================================== + + from rubicon.objc import ObjCClass + + class SearchBar(SearchBarBase, ViewBase): + def __init__(self, query: str = "") -> None: + super().__init__() + self.native_class = ObjCClass("UISearchBar") + self.native_instance = self.native_class.alloc().init() + self.set_query(query) + + def set_query(self, query: str) -> None: + self.native_instance.set_text_(query) + + def get_query(self) -> str: + return self.native_instance.text() diff --git a/src/pythonnative/stack_view.py b/src/pythonnative/stack_view.py new file mode 100644 index 0000000..3fc57b1 --- /dev/null +++ b/src/pythonnative/stack_view.py @@ -0,0 +1,60 @@ +from abc import ABC, abstractmethod +from typing import Any, List + +from .utils import IS_ANDROID, get_android_context +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class StackViewBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + self.views: List[Any] = [] + + @abstractmethod + def add_view(self, view) -> None: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/LinearLayout + # ======================================== + + from java import jclass + + class StackView(StackViewBase, ViewBase): + def __init__(self) -> None: + super().__init__() + self.native_class = jclass("android.widget.LinearLayout") + context = get_android_context() + self.native_instance = self.native_class(context) + self.native_instance.setOrientation(self.native_class.VERTICAL) + + def add_view(self, view): + self.views.append(view) + self.native_instance.addView(view.native_instance) + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uistackview + # ======================================== + + from rubicon.objc import ObjCClass + + class StackView(StackViewBase, ViewBase): + def __init__(self) -> None: + super().__init__() + self.native_class = ObjCClass("UIStackView") + self.native_instance = self.native_class.alloc().initWithFrame_(((0, 0), (0, 0))) + self.native_instance.setAxis_(0) # Set axis to vertical + + def add_view(self, view): + self.views.append(view) + self.native_instance.addArrangedSubview_(view.native_instance) diff --git a/src/pythonnative/switch.py b/src/pythonnative/switch.py new file mode 100644 index 0000000..bdd38bc --- /dev/null +++ b/src/pythonnative/switch.py @@ -0,0 +1,66 @@ +from abc import ABC, abstractmethod + +from .utils import IS_ANDROID, get_android_context +from .view import ViewBase + +# ======================================== +# Base class +# ======================================== + + +class SwitchBase(ABC): + @abstractmethod + def __init__(self) -> None: + super().__init__() + + @abstractmethod + def set_on(self, value: bool) -> None: + pass + + @abstractmethod + def is_on(self) -> bool: + pass + + +if IS_ANDROID: + # ======================================== + # Android class + # https://developer.android.com/reference/android/widget/Switch + # ======================================== + + from java import jclass + + class Switch(SwitchBase, ViewBase): + def __init__(self, value: bool = False) -> None: + super().__init__() + self.native_class = jclass("android.widget.Switch") + context = get_android_context() + self.native_instance = self.native_class(context) + self.set_on(value) + + def set_on(self, value: bool) -> None: + self.native_instance.setChecked(value) + + def is_on(self) -> bool: + return self.native_instance.isChecked() + +else: + # ======================================== + # iOS class + # https://developer.apple.com/documentation/uikit/uiswitch + # ======================================== + + from rubicon.objc import ObjCClass + + class Switch(SwitchBase, ViewBase): + def __init__(self, value: bool = False) -> None: + super().__init__() + self.native_class = ObjCClass("UISwitch") + self.native_instance = self.native_class.alloc().init() + self.set_on(value) + + def set_on(self, value: bool) -> None: + self.native_instance.setOn_animated_(value, False) + + def is_on(self) -> bool: + return self.native_instance.isOn() diff --git a/apps/android_pythonnative_2/.gitignore b/src/pythonnative/templates/android_template/.gitignore similarity index 100% rename from apps/android_pythonnative_2/.gitignore rename to src/pythonnative/templates/android_template/.gitignore diff --git a/apps/android_pythonnative_2/app/.gitignore b/src/pythonnative/templates/android_template/app/.gitignore similarity index 100% rename from apps/android_pythonnative_2/app/.gitignore rename to src/pythonnative/templates/android_template/app/.gitignore diff --git a/apps/android_pythonnative_2/app/build.gradle b/src/pythonnative/templates/android_template/app/build.gradle similarity index 70% rename from apps/android_pythonnative_2/app/build.gradle rename to src/pythonnative/templates/android_template/app/build.gradle index eb08dc1..4098326 100644 --- a/apps/android_pythonnative_2/app/build.gradle +++ b/src/pythonnative/templates/android_template/app/build.gradle @@ -5,11 +5,11 @@ plugins { } android { - namespace 'com.pythonnative.pythonnative' + namespace 'com.pythonnative.android_template' compileSdk 33 defaultConfig { - applicationId "com.pythonnative.pythonnative" + applicationId "com.pythonnative.android_template" minSdk 24 targetSdk 33 versionCode 1 @@ -20,9 +20,14 @@ android { abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" } python { + version "3.8" pip { install "matplotlib" - install "rubicon-java" + install "pythonnative" + + // "-r"` followed by a requirements filename, relative to the + // project directory: +// install "-r", "requirements.txt" } } } @@ -40,9 +45,6 @@ android { kotlinOptions { jvmTarget = '1.8' } - buildFeatures { - viewBinding true - } } dependencies { @@ -51,11 +53,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.3' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' - implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2' - implementation 'androidx.navigation:navigation-ui-ktx:2.5.2' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } \ No newline at end of file diff --git a/apps/android_pythonnative_2/app/proguard-rules.pro b/src/pythonnative/templates/android_template/app/proguard-rules.pro similarity index 100% rename from apps/android_pythonnative_2/app/proguard-rules.pro rename to src/pythonnative/templates/android_template/app/proguard-rules.pro diff --git a/apps/android_pythonnative_3/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt b/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt similarity index 82% rename from apps/android_pythonnative_3/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt rename to src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt index e2269ba..33b2b12 100644 --- a/apps/android_pythonnative_3/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt +++ b/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt @@ -1,4 +1,4 @@ -package com.pythonnative.pythonnative +package com.pythonnative.android_template import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -19,6 +19,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.pythonnative.pythonnative", appContext.packageName) + assertEquals("com.pythonnative.android_template", appContext.packageName) } } \ No newline at end of file diff --git a/apps/android_pythonnative_3/app/src/main/AndroidManifest.xml b/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml similarity index 94% rename from apps/android_pythonnative_3/app/src/main/AndroidManifest.xml rename to src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml index 57be5da..52e4ce3 100644 --- a/apps/android_pythonnative_3/app/src/main/AndroidManifest.xml +++ b/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.Pythonnative" + android:theme="@style/Theme.Android_template" tools:targetApi="31"> + tools:context=".MainActivity"> + \ No newline at end of file diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-hdpi/ic_launcher.webp rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-mdpi/ic_launcher.webp rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/apps/android_pythonnative_2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from apps/android_pythonnative_2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml b/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..27c7264 --- /dev/null +++ b/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml b/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c8524cd --- /dev/null +++ b/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml b/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..eac65ba --- /dev/null +++ b/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + android_template + \ No newline at end of file diff --git a/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml b/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..d44785c --- /dev/null +++ b/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + +