diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000000..c9f1c1e8dbc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,25 @@
+---
+name: Bug report
+about: Epsilon is not working like it should? Let us know!
+labels: 'bug'
+
+---
+#### Describe the bug
+A clear and concise description of what the bug is. Please describe a **single** bug per issue. Feel free to create multiple issues though!
+
+#### Screenshots
+Please provide at least one screenshot of the issue happening. This is by far the best way to quickly show any issue! To attach a screenshot, just go to our [online simulator](https://www.numworks.com/simulator), navigate to reproduce your issue, and click the "screenshot" button. Then drag'n'drop the file here!
+
+#### To Reproduce
+Steps to reproduce the behavior:
+1. Go to the '...' app
+2. Type '....'
+3. Scroll down to '....'
+4. See error
+
+#### Expected behavior
+A clear and concise description of what you expected to happen.
+
+#### Environment
+ - Epsilon version (Settings > About > Software version).
+ - The platform(s) on which the problem happens: online simulator, actual device, etc...
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000000..6bc39c490f2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,22 @@
+---
+name: Feature request
+about: Suggest an idea for an improvement of Epsilon
+labels: 'enhancement'
+
+---
+#### Problem you'd like to fix
+Is your feature request related to a problem? Please provide a clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+Please describe a **single** improvement per issue. Feel free to open multiple issues though!
+
+#### Screenshots
+If possible, please attach a screenshot. You can go on our [online simulator](https://www.numworks.com/simulator), use the screenshot button, and drag'n'drop the file here.
+
+#### Describe the solution you'd like
+A clear and concise description of what you want to happen.
+
+#### Describe alternatives you've considered
+A clear and concise description of any alternative solutions or features you've considered.
+
+#### Additional context
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml
new file mode 100644
index 00000000000..612614a85a5
--- /dev/null
+++ b/.github/workflows/ci-workflow.yml
@@ -0,0 +1,141 @@
+name: Continuous integration
+#on: [pull_request, push]
+on:
+ pull_request:
+ workflow_dispatch:
+ inputs:
+ triggerIos:
+ description: 'Run iOS tests'
+ required: true
+ default: 'no'
+ triggerMacos:
+ description: 'Run macOS tests'
+ required: true
+ default: 'no'
+
+jobs:
+ android:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - run: make -j2 PLATFORM=simulator TARGET=android
+ - run: make -j2 PLATFORM=simulator TARGET=android epsilon.official.apk
+ - run: make -j2 PLATFORM=simulator TARGET=android test.apk
+ - uses: actions/upload-artifact@master
+ with:
+ name: epsilon-android.apk
+ path: output/release/simulator/android/epsilon.apk
+ n0100:
+ runs-on: ubuntu-latest
+ steps:
+ - run: sudo apt-get install build-essential imagemagick libfreetype6-dev libjpeg-dev libpng-dev pkg-config
+ - uses: numworks/setup-arm-toolchain@2020-q4
+ - uses: actions/checkout@v2
+ - run: make -j2 MODEL=n0100 epsilon.dfu
+ - run: make -j2 MODEL=n0100 epsilon.onboarding.dfu
+ - run: make -j2 MODEL=n0100 epsilon.official.onboarding.dfu
+ - run: make -j2 MODEL=n0100 epsilon.onboarding.update.dfu
+ - run: make -j2 MODEL=n0100 epsilon.onboarding.beta.dfu
+ - run: make -j2 MODEL=n0100 flasher.light.dfu
+ - run: make -j2 MODEL=n0100 flasher.verbose.dfu
+ - run: make -j2 MODEL=n0100 test.elf
+ - uses: actions/upload-artifact@master
+ with:
+ name: epsilon-n0100.dfu
+ path: output/release/device/n0100/epsilon.dfu
+ n0110:
+ runs-on: ubuntu-latest
+ steps:
+ - run: sudo apt-get install build-essential imagemagick libfreetype6-dev libjpeg-dev libpng-dev pkg-config
+ - uses: numworks/setup-arm-toolchain@2020-q4
+ - uses: actions/checkout@v2
+ - run: make -j2 epsilon.dfu
+ - run: make -j2 epsilon.onboarding.dfu
+ - run: make -j2 epsilon.official.onboarding.dfu
+ - run: make -j2 epsilon.onboarding.update.dfu
+ - run: make -j2 epsilon.onboarding.beta.dfu
+ - run: make -j2 flasher.light.dfu
+ - run: make -j2 flasher.verbose.dfu
+ - run: make -j2 bench.ram.dfu
+ - run: make -j2 bench.flash.dfu
+ - run: make -j2 test.elf
+ - uses: actions/upload-artifact@master
+ with:
+ name: epsilon-n0110.dfu
+ path: output/release/device/n0110/epsilon.dfu
+ windows:
+ runs-on: windows-latest
+ defaults:
+ run:
+ shell: msys2 {0}
+ steps:
+ - uses: msys2/setup-msys2@v2
+ - uses: actions/checkout@v2
+ - run: pacman -S --noconfirm mingw-w64-x86_64-gcc mingw-w64-x86_64-freetype mingw-w64-x86_64-pkg-config make mingw-w64-x86_64-python3 mingw-w64-x86_64-libjpeg-turbo mingw-w64-x86_64-libpng
+ - run: make -j2 PLATFORM=simulator
+ - run: make -j2 PLATFORM=simulator epsilon.official.exe
+ - run: make -j2 PLATFORM=simulator test.headless.exe
+ - run: output/release/simulator/windows/test.headless.exe
+ - uses: actions/upload-artifact@master
+ with:
+ name: epsilon-windows.exe
+ path: output/release/simulator/windows/epsilon.exe
+ web:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: numworks/setup-emscripten@v1
+ with:
+ sdk: 1.39.16-fastcomp
+ - uses: actions/checkout@v2
+ - run: make -j2 PLATFORM=simulator TARGET=web
+ - run: make -j2 PLATFORM=simulator TARGET=web epsilon.official.zip
+ - run: make -j2 PLATFORM=simulator TARGET=web test.headless.js
+ - run: node output/release/simulator/web/test.headless.js
+ - uses: actions/upload-artifact@master
+ with:
+ name: epsilon-web.zip
+ path: output/release/simulator/web/epsilon.zip
+ linux:
+ runs-on: ubuntu-latest
+ steps:
+ - run: sudo apt-get install build-essential imagemagick libfreetype6-dev libjpeg-dev libpng-dev pkg-config
+ - uses: actions/checkout@v2
+ - run: make -j2 PLATFORM=simulator
+ - run: make -j2 PLATFORM=simulator epsilon.official.bin
+ - run: make -j2 PLATFORM=simulator test.headless.bin
+ - run: output/release/simulator/linux/test.headless.bin
+ - uses: actions/upload-artifact@master
+ with:
+ name: epsilon-linux.bin
+ path: output/release/simulator/linux/epsilon.bin
+ macos:
+ if: github.event.inputs.triggerMacos == 'yes'
+ runs-on: macOS-latest
+ steps:
+ - run: brew install numworks/tap/epsilon-sdk
+ - uses: actions/checkout@v2
+ - run: make -j2 PLATFORM=simulator
+ - run: make -j2 PLATFORM=simulator epsilon.official.app
+ - run: make -j2 PLATFORM=simulator ARCH=x86_64 test.headless.bin
+ - run: output/release/simulator/macos/x86_64/test.headless.bin
+ - uses: actions/upload-artifact@master
+ with:
+ name: epsilon-macos.zip
+ path: output/release/simulator/macos/epsilon.app
+ ios:
+ if: github.event.inputs.triggerIos == 'yes'
+ runs-on: macOS-latest
+ steps:
+ - run: brew install numworks/tap/epsilon-sdk
+ - uses: actions/checkout@v2
+ - run: make -j2 PLATFORM=simulator TARGET=ios EPSILON_TELEMETRY=0
+ - run: make -j2 PLATFORM=simulator TARGET=ios EPSILON_TELEMETRY=0 epsilon.official.ipa
+ - run: make -j2 PLATFORM=simulator TARGET=ios EPSILON_TELEMETRY=0 test.ipa
+ - run: make -j2 PLATFORM=simulator TARGET=ios EPSILON_TELEMETRY=0 APPLE_PLATFORM=ios-simulator
+ - uses: actions/upload-artifact@master
+ with:
+ name: epsilon-ios.ipa
+ path: output/release/simulator/ios/epsilon.ipa
+
+env:
+ ACCEPT_OFFICIAL_TOS: 1
diff --git a/.github/workflows/metrics-workflow.yml b/.github/workflows/metrics-workflow.yml
new file mode 100644
index 00000000000..fa9b22f253f
--- /dev/null
+++ b/.github/workflows/metrics-workflow.yml
@@ -0,0 +1,39 @@
+name: Metrics
+on: [pull_request_target]
+
+jobs:
+ binary-size:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install dependencies
+ run: sudo apt-get install build-essential imagemagick libfreetype6-dev libjpeg-dev libpng-dev pkg-config
+ - name: Install ARM toolchain
+ uses: numworks/setup-arm-toolchain@2020-q4
+ - name: Checkout PR base
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.event.pull_request.base.sha }}
+ path: base
+ - name: Build base
+ run: make -j2 -C base epsilon.elf
+ - name: Checkout PR head
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ path: head
+ - name: Build head
+ run: make -j2 -C head epsilon.elf
+ - name: Retrieve binary size analysis
+ id: binary_size
+ run: echo "::set-output name=table::$(python3 head/build/metrics/binary_size.py base/output/release/device/n0110/epsilon.elf head/output/release/device/n0110/epsilon.elf --labels Base Head --sections .text .rodata .bss .data --escape)"
+ - name: Add comment
+ uses: actions/github-script@v3.0.0
+ with:
+ github-token: ${{secrets.GITHUB_TOKEN}}
+ script: |
+ await github.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.payload.pull_request.number,
+ body: `${{ steps.binary_size.outputs.table }}`,
+ });
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000000..b69802123ed
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/output/
+/build/artifacts/
+build/device/**/*.pyc
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000000..1852d7fef18
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,382 @@
+Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
+Public License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution-NonCommercial-ShareAlike 4.0 International Public License
+("Public License"). To the extent this Public License may be
+interpreted as a contract, You are granted the Licensed Rights in
+consideration of Your acceptance of these terms and conditions, and the
+Licensor grants You such rights in consideration of benefits the
+Licensor receives from making the Licensed Material available under
+these terms and conditions.
+
+
+Section 1 -- Definitions.
+
+ a. Adapted Material means material subject to Copyright and Similar
+ Rights that is derived from or based upon the Licensed Material
+ and in which the Licensed Material is translated, altered,
+ arranged, transformed, or otherwise modified in a manner requiring
+ permission under the Copyright and Similar Rights held by the
+ Licensor. For purposes of this Public License, where the Licensed
+ Material is a musical work, performance, or sound recording,
+ Adapted Material is always produced where the Licensed Material is
+ synched in timed relation with a moving image.
+
+ b. Adapter's License means the license You apply to Your Copyright
+ and Similar Rights in Your contributions to Adapted Material in
+ accordance with the terms and conditions of this Public License.
+
+ c. BY-NC-SA Compatible License means a license listed at
+ creativecommons.org/compatiblelicenses, approved by Creative
+ Commons as essentially the equivalent of this Public License.
+
+ d. Copyright and Similar Rights means copyright and/or similar rights
+ closely related to copyright including, without limitation,
+ performance, broadcast, sound recording, and Sui Generis Database
+ Rights, without regard to how the rights are labeled or
+ categorized. For purposes of this Public License, the rights
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
+ Rights.
+
+ e. Effective Technological Measures means those measures that, in the
+ absence of proper authority, may not be circumvented under laws
+ fulfilling obligations under Article 11 of the WIPO Copyright
+ Treaty adopted on December 20, 1996, and/or similar international
+ agreements.
+
+ f. Exceptions and Limitations means fair use, fair dealing, and/or
+ any other exception or limitation to Copyright and Similar Rights
+ that applies to Your use of the Licensed Material.
+
+ g. License Elements means the license attributes listed in the name
+ of a Creative Commons Public License. The License Elements of this
+ Public License are Attribution, NonCommercial, and ShareAlike.
+
+ h. Licensed Material means the artistic or literary work, database,
+ or other material to which the Licensor applied this Public
+ License.
+
+ i. Licensed Rights means the rights granted to You subject to the
+ terms and conditions of this Public License, which are limited to
+ all Copyright and Similar Rights that apply to Your use of the
+ Licensed Material and that the Licensor has authority to license.
+
+ j. Licensor means the individual(s) or entity(ies) granting rights
+ under this Public License.
+
+ k. NonCommercial means not primarily intended for or directed towards
+ commercial advantage or monetary compensation. For purposes of
+ this Public License, the exchange of the Licensed Material for
+ other material subject to Copyright and Similar Rights by digital
+ file-sharing or similar means is NonCommercial provided there is
+ no payment of monetary compensation in connection with the
+ exchange.
+
+ l. Share means to provide material to the public by any means or
+ process that requires permission under the Licensed Rights, such
+ as reproduction, public display, public performance, distribution,
+ dissemination, communication, or importation, and to make material
+ available to the public including in ways that members of the
+ public may access the material from a place and at a time
+ individually chosen by them.
+
+ m. Sui Generis Database Rights means rights other than copyright
+ resulting from Directive 96/9/EC of the European Parliament and of
+ the Council of 11 March 1996 on the legal protection of databases,
+ as amended and/or succeeded, as well as other essentially
+ equivalent rights anywhere in the world.
+
+ n. You means the individual or entity exercising the Licensed Rights
+ under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+ a. License grant.
+
+ 1. Subject to the terms and conditions of this Public License,
+ the Licensor hereby grants You a worldwide, royalty-free,
+ non-sublicensable, non-exclusive, irrevocable license to
+ exercise the Licensed Rights in the Licensed Material to:
+
+ a. reproduce and Share the Licensed Material, in whole or
+ in part, for NonCommercial purposes only; and
+
+ b. produce, reproduce, and Share Adapted Material for
+ NonCommercial purposes only.
+
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
+ Exceptions and Limitations apply to Your use, this Public
+ License does not apply, and You do not need to comply with
+ its terms and conditions.
+
+ 3. Term. The term of this Public License is specified in Section
+ 6(a).
+
+ 4. Media and formats; technical modifications allowed. The
+ Licensor authorizes You to exercise the Licensed Rights in
+ all media and formats whether now known or hereafter created,
+ and to make technical modifications necessary to do so. The
+ Licensor waives and/or agrees not to assert any right or
+ authority to forbid You from making technical modifications
+ necessary to exercise the Licensed Rights, including
+ technical modifications necessary to circumvent Effective
+ Technological Measures. For purposes of this Public License,
+ simply making modifications authorized by this Section 2(a)
+ (4) never produces Adapted Material.
+
+ 5. Downstream recipients.
+
+ a. Offer from the Licensor -- Licensed Material. Every
+ recipient of the Licensed Material automatically
+ receives an offer from the Licensor to exercise the
+ Licensed Rights under the terms and conditions of this
+ Public License.
+
+ b. Additional offer from the Licensor -- Adapted Material.
+ Every recipient of Adapted Material from You
+ automatically receives an offer from the Licensor to
+ exercise the Licensed Rights in the Adapted Material
+ under the conditions of the Adapter's License You apply.
+
+ c. No downstream restrictions. You may not offer or impose
+ any additional or different terms or conditions on, or
+ apply any Effective Technological Measures to, the
+ Licensed Material if doing so restricts exercise of the
+ Licensed Rights by any recipient of the Licensed
+ Material.
+
+ 6. No endorsement. Nothing in this Public License constitutes or
+ may be construed as permission to assert or imply that You
+ are, or that Your use of the Licensed Material is, connected
+ with, or sponsored, endorsed, or granted official status by,
+ the Licensor or others designated to receive attribution as
+ provided in Section 3(a)(1)(A)(i).
+
+ b. Other rights.
+
+ 1. Moral rights, such as the right of integrity, are not
+ licensed under this Public License, nor are publicity,
+ privacy, and/or other similar personality rights; however, to
+ the extent possible, the Licensor waives and/or agrees not to
+ assert any such rights held by the Licensor to the limited
+ extent necessary to allow You to exercise the Licensed
+ Rights, but not otherwise.
+
+ 2. Patent and trademark rights are not licensed under this
+ Public License.
+
+ 3. To the extent possible, the Licensor waives any right to
+ collect royalties from You for the exercise of the Licensed
+ Rights, whether directly or through a collecting society
+ under any voluntary or waivable statutory or compulsory
+ licensing scheme. In all other cases the Licensor expressly
+ reserves any right to collect such royalties, including when
+ the Licensed Material is used other than for NonCommercial
+ purposes.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+ a. Attribution.
+
+ 1. If You Share the Licensed Material (including in modified
+ form), You must:
+
+ a. retain the following if it is supplied by the Licensor
+ with the Licensed Material:
+
+ i. identification of the creator(s) of the Licensed
+ Material and any others designated to receive
+ attribution, in any reasonable manner requested by
+ the Licensor (including by pseudonym if
+ designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of
+ warranties;
+
+ v. a URI or hyperlink to the Licensed Material to the
+ extent reasonably practicable;
+
+ b. indicate if You modified the Licensed Material and
+ retain an indication of any previous modifications; and
+
+ c. indicate the Licensed Material is licensed under this
+ Public License, and include the text of, or the URI or
+ hyperlink to, this Public License.
+
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
+ reasonable manner based on the medium, means, and context in
+ which You Share the Licensed Material. For example, it may be
+ reasonable to satisfy the conditions by providing a URI or
+ hyperlink to a resource that includes the required
+ information.
+ 3. If requested by the Licensor, You must remove any of the
+ information required by Section 3(a)(1)(A) to the extent
+ reasonably practicable.
+
+ b. ShareAlike.
+
+ In addition to the conditions in Section 3(a), if You Share
+ Adapted Material You produce, the following conditions also apply.
+
+ 1. The Adapter's License You apply must be a Creative Commons
+ license with the same License Elements, this version or
+ later, or a BY-NC-SA Compatible License.
+
+ 2. You must include the text of, or the URI or hyperlink to, the
+ Adapter's License You apply. You may satisfy this condition
+ in any reasonable manner based on the medium, means, and
+ context in which You Share Adapted Material.
+
+ 3. You may not offer or impose any additional or different terms
+ or conditions on, or apply any Effective Technological
+ Measures to, Adapted Material that restrict exercise of the
+ rights granted under the Adapter's License You apply.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+ to extract, reuse, reproduce, and Share all or a substantial
+ portion of the contents of the database for NonCommercial purposes
+ only;
+
+ b. if You include all or a substantial portion of the database
+ contents in a database in which You have Sui Generis Database
+ Rights, then the database in which You have Sui Generis Database
+ Rights (but not its individual contents) is Adapted Material,
+ including for purposes of Section 3(b); and
+
+ c. You must comply with the conditions in Section 3(a) if You Share
+ all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+ c. The disclaimer of warranties and limitation of liability provided
+ above shall be interpreted in a manner that, to the extent
+ possible, most closely approximates an absolute disclaimer and
+ waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+ a. This Public License applies for the term of the Copyright and
+ Similar Rights licensed here. However, if You fail to comply with
+ this Public License, then Your rights under this Public License
+ terminate automatically.
+
+ b. Where Your right to use the Licensed Material has terminated under
+ Section 6(a), it reinstates:
+
+ 1. automatically as of the date the violation is cured, provided
+ it is cured within 30 days of Your discovery of the
+ violation; or
+
+ 2. upon express reinstatement by the Licensor.
+
+ For the avoidance of doubt, this Section 6(b) does not affect any
+ right the Licensor may have to seek remedies for Your violations
+ of this Public License.
+
+ c. For the avoidance of doubt, the Licensor may also offer the
+ Licensed Material under separate terms or conditions or stop
+ distributing the Licensed Material at any time; however, doing so
+ will not terminate this Public License.
+
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+ License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+ a. The Licensor shall not be bound by any additional or different
+ terms or conditions communicated by You unless expressly agreed.
+
+ b. Any arrangements, understandings, or agreements regarding the
+ Licensed Material not stated herein are separate from and
+ independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+ a. For the avoidance of doubt, this Public License does not, and
+ shall not be interpreted to, reduce, limit, restrict, or impose
+ conditions on any use of the Licensed Material that could lawfully
+ be made without permission under this Public License.
+
+ b. To the extent possible, if any provision of this Public License is
+ deemed unenforceable, it shall be automatically reformed to the
+ minimum extent necessary to make it enforceable. If the provision
+ cannot be reformed, it shall be severed from this Public License
+ without affecting the enforceability of the remaining terms and
+ conditions.
+
+ c. No term or condition of this Public License will be waived and no
+ failure to comply consented to unless expressly agreed to by the
+ Licensor.
+
+ d. Nothing in this Public License constitutes or may be interpreted
+ as a limitation upon, or waiver of, any privileges and immunities
+ that apply to the Licensor or You, including from the legal
+ processes of any jurisdiction or authority.
+
+=======================================================================
+
+Creative Commons is not a party to its public
+licenses. Notwithstanding, Creative Commons may elect to apply one of
+its public licenses to material it publishes and in those instances
+will be considered the “Licensor.” The text of the Creative Commons
+public licenses is dedicated to the public domain under the CC0 Public
+Domain Dedication. Except for the limited purpose of indicating that
+material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the
+public licenses.
+
+Creative Commons may be contacted at creativecommons.org.
+
diff --git a/Makefile b/Makefile
index 55257d260e8..c5ed4b53de2 100644
--- a/Makefile
+++ b/Makefile
@@ -1,12 +1,125 @@
-OBJS = tree_pool.o tree_node.o tree_reference.o test.o
-CXXFLAGS = -std=c++11 -g -O0
+# Disable default Make rules
+.SUFFIXES:
-test: $(OBJS)
- clang++ $(CXXFLAGS) $^ -o $@
+# Define the default recipe
+default:
+include build/config.mak
+include build/defaults.mak
+include build/platform.$(PLATFORM).mak
+include build/toolchain.$(TOOLCHAIN).mak
+include build/variants.mak
+include build/helpers.mk
+
+.PHONY: info
+info:
+ @echo "EPSILON_VERSION = $(EPSILON_VERSION)"
+ @echo "EPSILON_APPS = $(EPSILON_APPS)"
+ @echo "EPSILON_I18N = $(EPSILON_I18N)"
+ @echo "PLATFORM" = $(PLATFORM)
+ @echo "DEBUG" = $(DEBUG)
+ @echo "EPSILON_GETOPT" = $(EPSILON_GETOPT)
+ @echo "ESCHER_LOG_EVENTS_BINARY" = $(ESCHER_LOG_EVENTS_BINARY)
+ @echo "QUIZ_USE_CONSOLE" = $(QUIZ_USE_CONSOLE)
+ @echo "ION_STORAGE_LOG" = $(ION_STORAGE_LOG)
+ @echo "POINCARE_TREE_LOG" = $(POINCARE_TREE_LOG)
+ @echo "POINCARE_TESTS_PRINT_EXPRESSIONS" = $(POINCARE_TESTS_PRINT_EXPRESSIONS)
+
+.PHONY: help
+help:
+ @echo "Device targets"
+ @echo " make epsilon_flash"
+ @echo " make epsilon.dfu"
+ @echo " make epsilon.onboarding.dfu"
+ @echo " make epsilon.onboarding.update.dfu"
+ @echo " make epsilon.onboarding.beta.dfu"
+ @echo " make flasher.light.bin"
+ @echo " make flasher.verbose.dfu"
+ @echo " make bench.ram.bin"
+ @echo " make bench.flash.bin"
+ @echo " make binpack"
+ @echo ""
+ @echo "Simulator targets"
+ @echo " make PLATFORM=simulator"
+ @echo " make PLATFORM=simulator TARGET=android"
+ @echo " make PLATFORM=simulator TARGET=ios"
+ @echo " make PLATFORM=simulator TARGET=macos"
+ @echo " make PLATFORM=simulator TARGET=web"
+ @echo " make PLATFORM=simulator TARGET=windows"
+
+# Since we're building out-of-tree, we need to make sure the output directories
+# are created, otherwise the receipes will fail (e.g. gcc will fail to create
+# "output/foo/bar.o" because the directory "output/foo" doesn't exist).
+# We need to mark those directories as precious, otherwise Make will try to get
+# rid of them upon completion (and fail, since those folders won't be empty).
+.PRECIOUS: $(BUILD_DIR)/. $(BUILD_DIR)%/.
+$(BUILD_DIR)/.:
+ $(Q) mkdir -p $(dir $@)
+$(BUILD_DIR)%/.:
+ $(Q) mkdir -p $(dir $@)
+
+# To make objects dependent on their directory, we need a second expansion
+.SECONDEXPANSION:
+
+# Each sub-Makefile can either add sources to $(%_src) variables or define a
+# new executable target. The $(%_src) variables list the sources that can be
+# built and linked to executables being generated.
+ifndef USE_LIBA
+ $(error platform.mak should define USE_LIBA)
+endif
+ifeq ($(USE_LIBA),0)
+include liba/Makefile.bridge
+else
+SFLAGS += -ffreestanding -nostdinc -nostdlib
+include liba/Makefile
+include libaxx/Makefile
+endif
+include ion/Makefile
+include kandinsky/Makefile
+include poincare/Makefile
+include python/Makefile
+include escher/Makefile
+# Executable Makefiles
+include apps/Makefile
+include build/struct_layout/Makefile
+include build/scenario/Makefile
+include quiz/Makefile # Quiz needs to be included at the end
+
+all_src = $(apps_src) $(escher_src) $(ion_src) $(kandinsky_src) $(liba_src) $(libaxx_src) $(poincare_src) $(python_src) $(runner_src) $(ion_device_flasher_src) $(ion_device_bench_src) $(tests_src)
+all_objs = $(call object_for,$(all_src))
+.SECONDARY: $(all_objs)
+
+# Load source-based dependencies
+# Compilers can generate Makefiles that states the dependencies of a given
+# objet to other source and headers. This serve no purpose for a clean build,
+# but allows correct yet optimal incremental builds.
+-include $(all_objs:.o=.d)
+
+# Define main and shortcut targets
+include build/targets.mak
+
+# Fill in the default recipe
+default: $(firstword $(HANDY_TARGETS)).$(firstword $(HANDY_TARGETS_EXTENSIONS))
+
+# Load standard build rules
+include build/rules.mk
+
+.PHONY: clean
clean:
- rm -f $(OBJS) test
+ @echo "CLEAN"
+ $(Q) rm -rf $(BUILD_DIR)
+
+.PHONY: cowsay_%
+cowsay_%:
+ @echo " -------"
+ @echo "| $(*F) |"
+ @echo " -------"
+ @echo " \\ ^__^"
+ @echo " \\ (oo)\\_______"
+ @echo " (__)\\ )\\/\\"
+ @echo " ||----w |"
+ @echo " || ||"
-%.o: %.cpp
- clang++ $(CXXFLAGS) -c $< -o $@
+.PHONY: clena
+clena: cowsay_CLENA clean
diff --git a/README.md b/README.md
new file mode 100644
index 00000000000..cdcbbcb0b4a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,21 @@
+
+
+[![Build Status](https://github.com/numworks/epsilon/workflows/Continuous%20integration/badge.svg)](https://github.com/numworks/epsilon/actions?workflow=Continuous+integration)
+
+Epsilon is a high-performance graphing calculator operating system. It includes eight apps that cover the high school mathematics curriculum.
+
+You can try Epsilon straight from your browser in the [online simulator](https://www.numworks.com/simulator/).
+
+## Diving in
+
+We highly recommend you start by reading the [online documentation](https://www.numworks.com/resources/engineering/software/) for this project. You'll learn how to install the [SDK](https://www.numworks.com/resources/engineering/software/build/) and about the overall architecture of the Epsilon.
+
+## Contributing
+
+If you run into an issue, we would be very happy if you would file a bug on the [issue tracker](https://github.com/numworks/epsilon/issues).
+
+We welcome contributions. For smaller changes just open a pull request straight away. For larger changes we recommend you open an issue first for discussion.
+
+## License
+
+NumWorks Epsilon is released under a [CC BY-NC-SA License](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode). NumWorks is a registered trademark.
diff --git a/addition_node.cpp b/addition_node.cpp
deleted file mode 100644
index 78bf1c70f77..00000000000
--- a/addition_node.cpp
+++ /dev/null
@@ -1,48 +0,0 @@
-#include "addition_node.h"
-#include "float_node.h"
-
-bool AdditionNode::shallowReduce() {
- if (ExpressionNode::shallowReduce()) {
- return true;
- }
- /* Step 1: Addition is associative, so let's start by merging children which
- * also are additions themselves. */
- int i = 0;
- int initialNumberOfChildren = numberOfChildren();
- while (i < initialNumberOfChildren) {
- ExpressionNode * currentChild = child(i);
- if (currentChild->type() == Type::Addition) {
- TreeRef(this).mergeChildren(TreeRef(currentChild));
- // Is it ok to modify memory while executing ?
- continue;
- }
- i++;
- }
-
- // Step 2: Sort the operands
- sortChildren();
-
- /* Step 3: Factorize like terms. Thanks to the simplification order, those are
- * next to each other at this point. */
- i = 0;
- while (i < numberOfChildren()-1) {
- ExpressionNode * e1 = child(i);
- ExpressionNode * e2 = child(i+1);
- if (e1->type() == Type::Float && e2->type() == Type::Float) {
- float sum = e1->approximate() + e2->approximate();
- // Remove first e2 then e1, else the pointers change
- removeChild(e2);
- removeChild(e1);
- FloatRef f(sum);
- addChildAtIndex(f.node(), i);
- continue;
- }
- /*if (TermsHaveIdenticalNonRationalFactors(e1, e2)) { //TODO
- factorizeOperands(e1, e2); //TODO
- continue;
- }*/
- i++;
- }
-
- return false;
-}
diff --git a/addition_node.h b/addition_node.h
deleted file mode 100644
index 00edffee372..00000000000
--- a/addition_node.h
+++ /dev/null
@@ -1,50 +0,0 @@
-#ifndef ADDITION_NODE_H
-#define ADDITION_NODE_H
-
-#include "expression_reference.h"
-#include "expression_node.h"
-
-class AdditionNode : public ExpressionNode {
-public:
- const char * description() const override { return "Addition"; }
- size_t size() const override { return sizeof(AdditionNode); }
- Type type() const override { return Type::Addition; }
-
- float approximate() override {
- float result = 0.0f;
- for (int i=0; iapproximate();
- if (approximateI == -1) {
- return -1;
- }
- result += approximateI;
- }
- return result;
- }
-
- bool shallowReduce() override;
- int numberOfChildren() const override { return m_numberOfChildren; }
- void incrementNumberOfChildren(int increment = 1) override { m_numberOfChildren+= increment; }
- void decrementNumberOfChildren(int decrement = 1) override {
- assert(m_numberOfChildren > 0);
- m_numberOfChildren-= decrement;
- }
- void eraseNumberOfChildren() override {
- m_numberOfChildren = 0;
- }
-
-private:
- int m_numberOfChildren;
-};
-
-class AdditionRef : public ExpressionReference {
-public:
- AdditionRef(ExpressionRef e1, ExpressionRef e2) :
- ExpressionReference()
- {
- addChild(e2);
- addChild(e1);
- }
-};
-
-#endif
diff --git a/allocation_failed_expression_node.h b/allocation_failed_expression_node.h
deleted file mode 100644
index 03283957e68..00000000000
--- a/allocation_failed_expression_node.h
+++ /dev/null
@@ -1,26 +0,0 @@
-#ifndef ALLOCATION_FAILED_EXPRESSION_NODE_H
-#define ALLOCATION_FAILED_EXPRESSION_NODE_H
-
-#include "expression_node.h"
-#include "expression_reference.h"
-#include
-
-class AllocationFailedExpressionNode : public ExpressionNode {
-public:
- // ExpressionNode
- float approximate() override { return -1; } // Should return nan
-
- // TreeNode
- size_t size() const override { return sizeof(AllocationFailedExpressionNode); }
- const char * description() const override { return "Allocation Failed"; }
- Type type() const override { return Type::AllocationFailure; }
- int numberOfChildren() const override { return 0; }
- bool isAllocationFailure() const override { return true; }
-};
-
-class AllocationFailedExpressionRef : public ExpressionReference {
-public:
- using ExpressionReference::ExpressionReference;
-};
-
-#endif
diff --git a/allocation_failed_layout_node.h b/allocation_failed_layout_node.h
deleted file mode 100644
index d41c329ed11..00000000000
--- a/allocation_failed_layout_node.h
+++ /dev/null
@@ -1,21 +0,0 @@
-#ifndef ALLOCATION_FAILED_LAYOUT_NODE_H
-#define ALLOCATION_FAILED_LAYOUT_NODE_H
-
-#include "layout_node.h"
-#include "layout_reference.h"
-
-class AllocationFailedLayoutNode : public LayoutNode {
-public:
- // TreeNode
- size_t size() const override { return sizeof(AllocationFailedLayoutNode); }
- const char * description() const override { return "Allocation Failed"; }
- int numberOfChildren() const override { return 0; }
- bool isAllocationFailure() const override { return true; }
-};
-
-class AllocationFailedLayoutRef : public LayoutReference {
-public:
- AllocationFailedLayoutRef() : LayoutReference() {}
-};
-
-#endif
diff --git a/apps/Makefile b/apps/Makefile
new file mode 100644
index 00000000000..da4dc6bbad3
--- /dev/null
+++ b/apps/Makefile
@@ -0,0 +1,98 @@
+include apps/helpers.mk
+include apps/shared/Makefile
+include apps/home/Makefile
+include apps/on_boarding/Makefile
+include apps/hardware_test/Makefile
+include apps/usb/Makefile
+apps =
+
+# All selected apps are included. Each Makefile below is responsible for setting
+# the $apps variable (name of the app class) and the $app_headers
+# (path to the apps header).
+$(foreach i,${EPSILON_APPS},$(eval include apps/$(i)/Makefile))
+
+apps_src += $(addprefix apps/,\
+ alternate_empty_nested_menu_controller.cpp \
+ apps_container.cpp \
+ apps_container_launch_default.cpp:-onboarding \
+ apps_container_launch_on_boarding.cpp:+onboarding \
+ apps_container_prompt_beta.cpp:+beta \
+ apps_container_prompt_none.cpp:-beta \
+ apps_container_prompt_none.cpp:-update \
+ apps_container_prompt_update.cpp:+update \
+ apps_container_storage.cpp \
+ apps_window.cpp \
+ backlight_dimming_timer.cpp \
+ battery_timer.cpp \
+ battery_view.cpp \
+ empty_battery_window.cpp \
+ exam_pop_up_controller.cpp \
+ exam_mode_configuration_official.cpp:+official \
+ exam_mode_configuration_non_official.cpp:-official \
+ global_preferences.cpp \
+ i18n.py \
+ lock_view.cpp \
+ main.cpp \
+ math_toolbox.cpp \
+ math_variable_box_controller.cpp \
+ math_variable_box_empty_controller.cpp \
+ shift_alpha_lock_view.cpp \
+ suspend_timer.cpp \
+ title_bar_view.cpp \
+)
+
+tests_src += $(addprefix apps/,\
+ exam_mode_configuration_official.cpp \
+)
+
+
+snapshots_declaration = $(foreach i,$(apps),$(i)::Snapshot m_snapshot$(subst :,,$(i))Snapshot;)
+apps_declaration = $(foreach i,$(apps),$(i) m_$(subst :,,$(i));)
+snapshots_construction = $(foreach i,$(apps),,m_snapshot$(subst :,,$(i))Snapshot())
+snapshots_list = $(foreach i,$(apps),,&m_snapshot$(subst :,,$(i))Snapshot)
+snapshots_count = $(words $(apps))
+snapshot_includes = $(foreach i,$(app_headers),-include $(i) )
+epsilon_app_names = '$(foreach i,${EPSILON_APPS},"$(i)", )'
+
+$(call object_for,apps/apps_container_storage.cpp apps/apps_container.cpp apps/main.cpp): CXXFLAGS += $(snapshot_includes) -DAPPS_CONTAINER_APPS_DECLARATION="$(apps_declaration)" -DAPPS_CONTAINER_SNAPSHOT_DECLARATIONS="$(snapshots_declaration)" -DAPPS_CONTAINER_SNAPSHOT_CONSTRUCTORS="$(snapshots_construction)" -DAPPS_CONTAINER_SNAPSHOT_LIST="$(snapshots_list)" -DAPPS_CONTAINER_SNAPSHOT_COUNT=$(snapshots_count) -DEPSILON_APPS_NAMES=$(epsilon_app_names)
+
+# I18n file generation
+
+country_preferences = apps/country_preferences.csv
+language_preferences = apps/language_preferences.csv
+
+# The header is refered to as so make sure it's findable this way
+SFLAGS += -I$(BUILD_DIR)
+
+i18n_files += $(addprefix apps/language_,$(addsuffix .universal.i18n, $(EPSILON_I18N)))
+ifeq ($(EPSILON_GETOPT),1)
+i18n_files += $(addprefix apps/language_,$(addsuffix _iso6391.universal.i18n, $(EPSILON_I18N)))
+endif
+
+i18n_files += $(call i18n_with_universal_for,shared)
+i18n_files += $(call i18n_with_universal_for,toolbox)
+i18n_files += $(call i18n_without_universal_for,variables)
+
+$(eval $(call rule_for, \
+ I18N, \
+ apps/i18n.cpp, \
+ $(i18n_files), \
+ $$(PYTHON) apps/i18n.py --codepoints $(code_points) --countrypreferences $(country_preferences) --languagepreferences $(language_preferences) --header $$(subst .cpp,.h,$$@) --implementation $$@ --locales $$(EPSILON_I18N) --countries $$(EPSILON_COUNTRIES) --files $$^ --generateISO6391locales $$(EPSILON_GETOPT), \
+ global \
+))
+
+$(BUILD_DIR)/apps/i18n.h: $(BUILD_DIR)/apps/i18n.cpp
+
+# Handle PNG files
+
+$(eval $(call depends_on_image,apps/title_bar_view.cpp,apps/exam_icon.png))
+
+$(call object_for,$(apps_src) $(tests_src)): $(BUILD_DIR)/apps/i18n.h
+$(call object_for,$(apps_src) $(tests_src)): $(BUILD_DIR)/python/port/genhdr/qstrdefs.generated.h
+
+apps_tests_src = $(app_calculation_test_src) $(app_code_test_src) $(app_graph_test_src) $(app_probability_test_src) $(app_regression_test_src) $(app_sequence_test_src) $(app_shared_test_src) $(app_statistics_test_src) $(app_settings_test_src) $(app_solver_test_src)
+
+apps_tests_src += $(addprefix apps/,\
+ alternate_empty_nested_menu_controller.cpp \
+ global_preferences.cpp \
+)
diff --git a/apps/alternate_empty_nested_menu_controller.cpp b/apps/alternate_empty_nested_menu_controller.cpp
new file mode 100644
index 00000000000..d3a89f0f9b4
--- /dev/null
+++ b/apps/alternate_empty_nested_menu_controller.cpp
@@ -0,0 +1,18 @@
+#include "alternate_empty_nested_menu_controller.h"
+
+void AlternateEmptyNestedMenuController::viewDidDisappear() {
+ if (isDisplayingEmptyController()) {
+ pop();
+ }
+ NestedMenuController::viewDidDisappear();
+}
+
+bool AlternateEmptyNestedMenuController::displayEmptyControllerIfNeeded() {
+ assert(!isDisplayingEmptyController());
+ // If the content is empty, we push an empty controller.
+ if (numberOfRows() == 0) {
+ push(emptyViewController());
+ return true;
+ }
+ return false;
+}
diff --git a/apps/alternate_empty_nested_menu_controller.h b/apps/alternate_empty_nested_menu_controller.h
new file mode 100644
index 00000000000..d310b15b675
--- /dev/null
+++ b/apps/alternate_empty_nested_menu_controller.h
@@ -0,0 +1,19 @@
+#ifndef APPS_ALTERNATE_EMPTY_NESTED_MENU_CONTROLLER_H
+#define APPS_ALTERNATE_EMPTY_NESTED_MENU_CONTROLLER_H
+
+#include
+
+class AlternateEmptyNestedMenuController : public NestedMenuController {
+public:
+ AlternateEmptyNestedMenuController(I18n::Message title) :
+ NestedMenuController(nullptr, title)
+ {}
+ // View Controller
+ void viewDidDisappear() override;
+protected:
+ virtual ViewController * emptyViewController() = 0;
+ bool isDisplayingEmptyController() { return StackViewController::depth() == 2; }
+ bool displayEmptyControllerIfNeeded();
+};
+
+#endif
diff --git a/apps/apps_container.cpp b/apps/apps_container.cpp
new file mode 100644
index 00000000000..d8631f3ac53
--- /dev/null
+++ b/apps/apps_container.cpp
@@ -0,0 +1,384 @@
+#include "apps_container.h"
+#include "apps_container_storage.h"
+#include "global_preferences.h"
+#include "exam_mode_configuration.h"
+#include
+#include
+#include
+
+extern "C" {
+#include
+}
+
+using namespace Shared;
+
+AppsContainer * AppsContainer::sharedAppsContainer() {
+ static AppsContainerStorage appsContainerStorage;
+ return &appsContainerStorage;
+}
+
+AppsContainer::AppsContainer() :
+ Container(),
+ m_window(),
+ m_emptyBatteryWindow(),
+ m_globalContext(),
+ m_variableBoxController(),
+ m_examPopUpController(this),
+ m_promptController(k_promptMessages, k_promptColors, k_promptNumberOfMessages),
+ m_batteryTimer(),
+ m_suspendTimer(),
+ m_backlightDimmingTimer(),
+ m_homeSnapshot(),
+ m_onBoardingSnapshot(),
+ m_hardwareTestSnapshot(),
+ m_usbConnectedSnapshot()
+{
+ m_emptyBatteryWindow.setFrame(KDRect(0, 0, Ion::Display::Width, Ion::Display::Height), false);
+#if __EMSCRIPTEN__
+ /* AppsContainer::poincareCircuitBreaker uses Ion::Keyboard::scan(), which
+ * calls emscripten_sleep. If we set the poincare circuit breaker, we would
+ * need to whitelist all the methods that might be in the call stack when
+ * poincareCircuitBreaker is run. This means either whitelisting all Epsilon
+ * (which makes bigger files to download and slower execution), or
+ * whitelisting all the symbols (that's a big amount of symbols to find and
+ * quite painy to maintain).
+ * We just remove the circuit breaker for now.
+ * TODO: Put the Poincare circuit breaker back on epsilon's web emulator */
+#else
+ Poincare::Expression::SetCircuitBreaker(AppsContainer::poincareCircuitBreaker);
+#endif
+ Ion::Storage::sharedStorage()->setDelegate(this);
+}
+
+bool AppsContainer::poincareCircuitBreaker() {
+ constexpr uint64_t minimalPressDuration = 20;
+ static uint64_t beginningOfInterruption = 0;
+ Ion::Keyboard::State state = Ion::Keyboard::scan();
+ bool interrupt = state.keyDown(Ion::Keyboard::Key::Back) || state.keyDown(Ion::Keyboard::Key::Home) || state.keyDown(Ion::Keyboard::Key::OnOff);
+ if (!interrupt) {
+ beginningOfInterruption = 0;
+ return false;
+ }
+ if (beginningOfInterruption == 0) {
+ beginningOfInterruption = Ion::Timing::millis();
+ return false;
+ }
+ if (Ion::Timing::millis() - beginningOfInterruption > minimalPressDuration) {
+ beginningOfInterruption = 0;
+ return true;
+ }
+ return false;
+}
+
+App::Snapshot * AppsContainer::hardwareTestAppSnapshot() {
+ return &m_hardwareTestSnapshot;
+}
+
+App::Snapshot * AppsContainer::onBoardingAppSnapshot() {
+ return &m_onBoardingSnapshot;
+}
+
+App::Snapshot * AppsContainer::usbConnectedAppSnapshot() {
+ return &m_usbConnectedSnapshot;
+}
+
+void AppsContainer::reset() {
+ // Empty storage (delete functions, variables, python scripts)
+ Ion::Storage::sharedStorage()->destroyAllRecords();
+ // Empty clipboard
+ Clipboard::sharedClipboard()->reset();
+ for (int i = 0; i < numberOfApps(); i++) {
+ appSnapshotAtIndex(i)->reset();
+ }
+}
+
+Poincare::Context * AppsContainer::globalContext() {
+ return &m_globalContext;
+}
+
+MathToolbox * AppsContainer::mathToolbox() {
+ return &m_mathToolbox;
+}
+
+MathVariableBoxController * AppsContainer::variableBoxController() {
+ return &m_variableBoxController;
+}
+
+void AppsContainer::suspend(bool checkIfOnOffKeyReleased) {
+ resetShiftAlphaStatus();
+ GlobalPreferences * globalPreferences = GlobalPreferences::sharedGlobalPreferences();
+ // Display the prompt if it has a message to display
+ if (promptController() != nullptr && s_activeApp->snapshot()!= onBoardingAppSnapshot() && s_activeApp->snapshot() != hardwareTestAppSnapshot() && globalPreferences->showPopUp()) {
+ s_activeApp->displayModalViewController(promptController(), 0.f, 0.f);
+ }
+ Ion::Power::suspend(checkIfOnOffKeyReleased);
+ /* Ion::Power::suspend() completely shuts down the LCD controller. Therefore
+ * the frame memory is lost. That's why we need to force a window redraw
+ * upon wakeup, otherwise the screen is filled with noise. */
+ Ion::Backlight::setBrightness(globalPreferences->brightnessLevel());
+ m_backlightDimmingTimer.reset();
+ window()->redraw(true);
+}
+
+bool AppsContainer::dispatchEvent(Ion::Events::Event event) {
+ bool alphaLockWantsRedraw = updateAlphaLock();
+ bool didProcessEvent = false;
+
+ if (event == Ion::Events::USBEnumeration || event == Ion::Events::USBPlug || event == Ion::Events::BatteryCharging) {
+ Ion::LED::updateColorWithPlugAndCharge();
+ }
+ if (event == Ion::Events::USBEnumeration) {
+ if (Ion::USB::isPlugged()) {
+ App::Snapshot * activeSnapshot = (s_activeApp == nullptr ? appSnapshotAtIndex(0) : s_activeApp->snapshot());
+ /* Just after a software update, the battery timer does not have time to
+ * fire before the calculator enters DFU mode. As the DFU mode blocks the
+ * event loop, we update the battery state "manually" here.
+ * We do it before switching to USB application to redraw the battery
+ * pictogram. */
+ updateBatteryState();
+ if (switchTo(usbConnectedAppSnapshot())) {
+ Ion::USB::DFU();
+ // Update LED when exiting DFU mode
+ Ion::LED::updateColorWithPlugAndCharge();
+ bool switched = switchTo(activeSnapshot);
+ assert(switched);
+ (void) switched; // Silence compilation warning about unused variable.
+ didProcessEvent = true;
+ } else {
+ /* We could not switch apps, which means that the current app needs
+ * another event loop to prepare for being switched off.
+ * Discard the current enumeration interruption.
+ * The USB host tries a few times in a row to enumerate the device, so
+ * hopefully the device will get another enumeration event soon and this
+ * time the device will be ready to go in DFU mode. Otherwise, the user
+ * needs to re-plug the device to go into DFU mode. */
+ Ion::USB::clearEnumerationInterrupt();
+ }
+ } else {
+ /* Sometimes, the device gets an ENUMDNE interrupts when being unplugged
+ * from a non-USB communicating host (e.g. a USB charger). The interrupt
+ * must me cleared: if not the next enumeration attempts will not be
+ * detected. */
+ Ion::USB::clearEnumerationInterrupt();
+ }
+ } else {
+ didProcessEvent = Container::dispatchEvent(event);
+ }
+
+ if (!didProcessEvent) {
+ didProcessEvent = processEvent(event);
+ }
+ if (event.isKeyboardEvent()) {
+ m_backlightDimmingTimer.reset();
+ m_suspendTimer.reset();
+ Ion::Backlight::setBrightness(GlobalPreferences::sharedGlobalPreferences()->brightnessLevel());
+ }
+ if (!didProcessEvent && alphaLockWantsRedraw) {
+ window()->redraw();
+ return true;
+ }
+ return didProcessEvent || alphaLockWantsRedraw;
+}
+
+bool AppsContainer::processEvent(Ion::Events::Event event) {
+ // Warning: if the window is dirtied, you need to call window()->redraw()
+ if (event == Ion::Events::USBPlug) {
+ if (Ion::USB::isPlugged()) {
+ if (GlobalPreferences::sharedGlobalPreferences()->isInExamMode()) {
+ displayExamModePopUp(GlobalPreferences::ExamMode::Off);
+ window()->redraw();
+ } else {
+ Ion::USB::enable();
+ }
+ Ion::Backlight::setBrightness(GlobalPreferences::sharedGlobalPreferences()->brightnessLevel());
+ } else {
+ Ion::USB::disable();
+ }
+ return true;
+ }
+ if (event == Ion::Events::Home || event == Ion::Events::Back) {
+ switchTo(appSnapshotAtIndex(0));
+ return true;
+ }
+ if (event == Ion::Events::OnOff) {
+ suspend(true);
+ return true;
+ }
+ return false;
+}
+
+bool AppsContainer::switchTo(App::Snapshot * snapshot) {
+ if (s_activeApp && snapshot != s_activeApp->snapshot()) {
+ resetShiftAlphaStatus();
+ }
+ if (snapshot == hardwareTestAppSnapshot() || snapshot == onBoardingAppSnapshot()) {
+ m_window.hideTitleBarView(true);
+ } else {
+ m_window.hideTitleBarView(false);
+ }
+ if (snapshot) {
+ m_window.setTitle(snapshot->descriptor()->upperName());
+ }
+ return Container::switchTo(snapshot);
+}
+
+void AppsContainer::run() {
+ KDRect screenRect = KDRect(0, 0, Ion::Display::Width, Ion::Display::Height);
+ window()->setFrame(screenRect, false);
+ /* We push a white screen here, because fetching the exam mode takes some time
+ * and it is visible when reflashing a N0100 (there is some noise on the
+ * screen before the logo appears). */
+ Ion::Display::pushRectUniform(screenRect, KDColorWhite);
+ if (GlobalPreferences::sharedGlobalPreferences()->isInExamMode()) {
+ activateExamMode(GlobalPreferences::sharedGlobalPreferences()->examMode());
+ }
+ refreshPreferences();
+
+ /* ExceptionCheckpoint stores the value of the stack pointer when setjump is
+ * called. During a longjump, the stack pointer is set to this stored stack
+ * pointer value, so the method where we call setjump must remain in the call
+ * tree for the jump to work. */
+ Poincare::ExceptionCheckpoint ecp;
+
+ if (ExceptionRun(ecp)) {
+ /* Normal execution. The exception checkpoint must be created before
+ * switching to the first app, because the first app might create nodes on
+ * the pool. */
+ bool switched = switchTo(initialAppSnapshot());
+ assert(switched);
+ (void) switched; // Silence compilation warning about unused variable.
+ } else {
+ // Exception
+ if (s_activeApp != nullptr) {
+ /* The app models can reference layouts or expressions that have been
+ * destroyed from the pool. To avoid using them before packing the app
+ * (in App::willBecomeInactive for instance), we tidy them early on. */
+ s_activeApp->snapshot()->tidy();
+ /* When an app encoutered an exception due to a full pool, the next time
+ * the user enters the app, the same exception could happen again which
+ * would prevent from reopening the app. To avoid being stuck outside the
+ * app causing the issue, we reset its snapshot when leaving it due to
+ * exception. For instance, the calculation app can encounter an
+ * exception when displaying too many huge layouts, if we don't clean the
+ * history here, we will be stuck outside the calculation app. */
+ s_activeApp->snapshot()->reset();
+ }
+ bool switched = switchTo(appSnapshotAtIndex(0));
+ assert(switched);
+ (void) switched; // Silence compilation warning about unused variable.
+ Poincare::Tidy();
+ s_activeApp->displayWarning(I18n::Message::PoolMemoryFull1, I18n::Message::PoolMemoryFull2, true);
+ }
+ Container::run();
+ switchTo(nullptr);
+}
+
+bool AppsContainer::updateBatteryState() {
+ bool batteryLevelUpdated = m_window.updateBatteryLevel();
+ bool pluggedStateUpdated = m_window.updatePluggedState();
+ bool chargingStateUpdated = m_window.updateIsChargingState();
+ if (batteryLevelUpdated || pluggedStateUpdated || chargingStateUpdated) {
+ return true;
+ }
+ return false;
+}
+
+void AppsContainer::refreshPreferences() {
+ m_window.refreshPreferences();
+}
+
+void AppsContainer::reloadTitleBarView() {
+ m_window.reloadTitleBarView();
+}
+
+void AppsContainer::displayExamModePopUp(GlobalPreferences::ExamMode mode) {
+ m_examPopUpController.setTargetExamMode(mode);
+ s_activeApp->displayModalViewController(&m_examPopUpController, 0.f, 0.f, Metric::ExamPopUpTopMargin, Metric::PopUpRightMargin, Metric::ExamPopUpBottomMargin, Metric::PopUpLeftMargin);
+}
+
+void AppsContainer::shutdownDueToLowBattery() {
+ if (Ion::Battery::level() != Ion::Battery::Charge::EMPTY) {
+ /* We early escape here. When the battery switches from LOW to EMPTY, it
+ * oscillates a few times before stabilizing to EMPTY. So we might call
+ * 'shutdownDueToLowBattery' but the battery level still answers LOW instead
+ * of EMPTY. We want to avoid uselessly redrawing the whole window in that
+ * case. */
+ return;
+ }
+ while (Ion::Battery::level() == Ion::Battery::Charge::EMPTY && !Ion::USB::isPlugged()) {
+ Ion::Backlight::setBrightness(0);
+ if (!GlobalPreferences::sharedGlobalPreferences()->isInExamMode()) {
+ /* Unless the LED is lit up for the exam mode, switch off the LED. IF the
+ * low battery event happened during the Power-On Self-Test, a LED might
+ * have stayed lit up. */
+ Ion::LED::setColor(KDColorBlack);
+ }
+ m_emptyBatteryWindow.redraw(true);
+ Ion::Timing::msleep(3000);
+ Ion::Power::suspend();
+ }
+ window()->redraw(true);
+}
+
+void AppsContainer::setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus newStatus) {
+ Ion::Events::setShiftAlphaStatus(newStatus);
+ updateAlphaLock();
+}
+
+bool AppsContainer::updateAlphaLock() {
+ return m_window.updateAlphaLock();
+}
+
+OnBoarding::PromptController * AppsContainer::promptController() {
+ if (k_promptNumberOfMessages == 0) {
+ return nullptr;
+ }
+ return &m_promptController;
+}
+
+void AppsContainer::redrawWindow() {
+ m_window.redraw();
+}
+
+void AppsContainer::activateExamMode(GlobalPreferences::ExamMode examMode) {
+ assert(examMode != GlobalPreferences::ExamMode::Off && examMode != GlobalPreferences::ExamMode::Unknown);
+ reset();
+ Ion::LED::setColor(ExamModeConfiguration::examModeColor(examMode));
+ Ion::LED::setBlinking(1000, 0.1f);
+}
+
+void AppsContainer::examDeactivatingPopUpIsDismissed() {
+ if (Ion::USB::isPlugged()) {
+ Ion::USB::enable();
+ }
+}
+
+void AppsContainer::storageDidChangeForRecord(const Ion::Storage::Record record) {
+ if (s_activeApp) {
+ s_activeApp->snapshot()->storageDidChangeForRecord(record);
+ }
+}
+
+void AppsContainer::storageIsFull() {
+ if (s_activeApp) {
+ s_activeApp->displayWarning(I18n::Message::StorageMemoryFull1, I18n::Message::StorageMemoryFull2, true);
+ }
+}
+
+Window * AppsContainer::window() {
+ return &m_window;
+}
+
+int AppsContainer::numberOfContainerTimers() {
+ return 3;
+}
+
+Timer * AppsContainer::containerTimerAtIndex(int i) {
+ Timer * timers[3] = {&m_batteryTimer, &m_suspendTimer, &m_backlightDimmingTimer};
+ return timers[i];
+}
+
+void AppsContainer::resetShiftAlphaStatus() {
+ Ion::Events::setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus::Default);
+ updateAlphaLock();
+}
diff --git a/apps/apps_container.h b/apps/apps_container.h
new file mode 100644
index 00000000000..e217f2495c4
--- /dev/null
+++ b/apps/apps_container.h
@@ -0,0 +1,85 @@
+#ifndef APPS_CONTAINER_H
+#define APPS_CONTAINER_H
+
+#include "home/app.h"
+#include "on_boarding/app.h"
+#include "hardware_test/app.h"
+#include "usb/app.h"
+#include "apps_window.h"
+#include "empty_battery_window.h"
+#include "math_toolbox.h"
+#include "math_variable_box_controller.h"
+#include "exam_pop_up_controller.h"
+#include "exam_pop_up_controller_delegate.h"
+#include "battery_timer.h"
+#include "suspend_timer.h"
+#include "global_preferences.h"
+#include "backlight_dimming_timer.h"
+#include "shared/global_context.h"
+#include "on_boarding/prompt_controller.h"
+
+#include
+
+class AppsContainer : public Container, ExamPopUpControllerDelegate, Ion::StorageDelegate {
+public:
+ static AppsContainer * sharedAppsContainer();
+ AppsContainer();
+ static bool poincareCircuitBreaker();
+ virtual int numberOfApps() = 0;
+ virtual App::Snapshot * appSnapshotAtIndex(int index) = 0;
+ App::Snapshot * initialAppSnapshot();
+ App::Snapshot * hardwareTestAppSnapshot();
+ App::Snapshot * onBoardingAppSnapshot();
+ App::Snapshot * usbConnectedAppSnapshot();
+ void reset();
+ Poincare::Context * globalContext();
+ MathToolbox * mathToolbox();
+ MathVariableBoxController * variableBoxController();
+ void suspend(bool checkIfOnOffKeyReleased = false);
+ bool dispatchEvent(Ion::Events::Event event) override;
+ bool switchTo(App::Snapshot * snapshot) override;
+ void run() override;
+ bool updateBatteryState();
+ void refreshPreferences();
+ void reloadTitleBarView();
+ void displayExamModePopUp(GlobalPreferences::ExamMode mode);
+ void shutdownDueToLowBattery();
+ void setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus newStatus);
+ OnBoarding::PromptController * promptController();
+ void redrawWindow();
+ void activateExamMode(GlobalPreferences::ExamMode examMode);
+ // Exam pop-up controller delegate
+ void examDeactivatingPopUpIsDismissed() override;
+ // Ion::StorageDelegate
+ void storageDidChangeForRecord(const Ion::Storage::Record record) override;
+ void storageIsFull() override;
+protected:
+ Home::App::Snapshot * homeAppSnapshot() { return &m_homeSnapshot; }
+private:
+ Window * window() override;
+ int numberOfContainerTimers() override;
+ Timer * containerTimerAtIndex(int i) override;
+ bool processEvent(Ion::Events::Event event);
+ void resetShiftAlphaStatus();
+ bool updateAlphaLock();
+
+ static I18n::Message k_promptMessages[];
+ static KDColor k_promptColors[];
+ static int k_promptNumberOfMessages;
+ AppsWindow m_window;
+ EmptyBatteryWindow m_emptyBatteryWindow;
+ Shared::GlobalContext m_globalContext;
+ MathToolbox m_mathToolbox;
+ MathVariableBoxController m_variableBoxController;
+ ExamPopUpController m_examPopUpController;
+ OnBoarding::PromptController m_promptController;
+ BatteryTimer m_batteryTimer;
+ SuspendTimer m_suspendTimer;
+ BacklightDimmingTimer m_backlightDimmingTimer;
+ Home::App::Snapshot m_homeSnapshot;
+ OnBoarding::App::Snapshot m_onBoardingSnapshot;
+ HardwareTest::App::Snapshot m_hardwareTestSnapshot;
+ USB::App::Snapshot m_usbConnectedSnapshot;
+};
+
+#endif
diff --git a/apps/apps_container_launch_default.cpp b/apps/apps_container_launch_default.cpp
new file mode 100644
index 00000000000..ce36dc509ef
--- /dev/null
+++ b/apps/apps_container_launch_default.cpp
@@ -0,0 +1,7 @@
+#include "apps_container.h"
+
+App::Snapshot * AppsContainer::initialAppSnapshot() {
+ // The backlight has not been initialized
+ Ion::Backlight::init();
+ return appSnapshotAtIndex(numberOfApps() == 2 ? 1 : 0);
+}
diff --git a/apps/apps_container_launch_on_boarding.cpp b/apps/apps_container_launch_on_boarding.cpp
new file mode 100644
index 00000000000..18adecc707c
--- /dev/null
+++ b/apps/apps_container_launch_on_boarding.cpp
@@ -0,0 +1,5 @@
+#include "apps_container.h"
+
+App::Snapshot * AppsContainer::initialAppSnapshot() {
+ return onBoardingAppSnapshot();
+}
diff --git a/apps/apps_container_prompt_beta.cpp b/apps/apps_container_prompt_beta.cpp
new file mode 100644
index 00000000000..85be074d143
--- /dev/null
+++ b/apps/apps_container_prompt_beta.cpp
@@ -0,0 +1,23 @@
+#include "apps_container.h"
+
+I18n::Message AppsContainer::k_promptMessages[] = {
+ I18n::Message::BetaVersion,
+ I18n::Message::BetaVersionMessage1,
+ I18n::Message::BetaVersionMessage2,
+ I18n::Message::BetaVersionMessage3,
+ I18n::Message::BlankMessage,
+ I18n::Message::BetaVersionMessage4,
+ I18n::Message::BetaVersionMessage5,
+ I18n::Message::BetaVersionMessage6};
+
+KDColor AppsContainer::k_promptColors[] = {
+ KDColorBlack,
+ KDColorBlack,
+ KDColorBlack,
+ KDColorBlack,
+ KDColorWhite,
+ KDColorBlack,
+ KDColorBlack,
+ Palette::YellowDark};
+
+int AppsContainer::k_promptNumberOfMessages = 8;
diff --git a/apps/apps_container_prompt_none.cpp b/apps/apps_container_prompt_none.cpp
new file mode 100644
index 00000000000..65556d670f8
--- /dev/null
+++ b/apps/apps_container_prompt_none.cpp
@@ -0,0 +1,8 @@
+#include "apps_container.h"
+
+I18n::Message AppsContainer::k_promptMessages[] = {};
+
+KDColor AppsContainer::k_promptColors[] = {};
+
+int AppsContainer::k_promptNumberOfMessages = 0;
+
diff --git a/apps/apps_container_prompt_update.cpp b/apps/apps_container_prompt_update.cpp
new file mode 100644
index 00000000000..25fe11b646b
--- /dev/null
+++ b/apps/apps_container_prompt_update.cpp
@@ -0,0 +1,19 @@
+#include "apps_container.h"
+
+I18n::Message AppsContainer::k_promptMessages[] = {
+ I18n::Message::UpdateAvailable,
+ I18n::Message::UpdateMessage1,
+ I18n::Message::UpdateMessage2,
+ I18n::Message::BlankMessage,
+ I18n::Message::UpdateMessage3,
+ I18n::Message::UpdateMessage4};
+
+KDColor AppsContainer::k_promptColors[] = {
+ KDColorBlack,
+ KDColorBlack,
+ KDColorBlack,
+ KDColorWhite,
+ KDColorBlack,
+ Palette::YellowDark};
+
+int AppsContainer::k_promptNumberOfMessages = 6;
diff --git a/apps/apps_container_storage.cpp b/apps/apps_container_storage.cpp
new file mode 100644
index 00000000000..2ef60aafa5c
--- /dev/null
+++ b/apps/apps_container_storage.cpp
@@ -0,0 +1,38 @@
+#include "apps_container_storage.h"
+
+#ifndef APPS_CONTAINER_SNAPSHOT_CONSTRUCTORS
+#error Missing snapshot constructors
+#endif
+
+#ifndef APPS_CONTAINER_SNAPSHOT_LIST
+#error Missing snapshot list
+#endif
+
+#ifndef APPS_CONTAINER_SNAPSHOT_COUNT
+#error Missing snapshot count
+#endif
+
+constexpr int k_numberOfCommonApps = 1+APPS_CONTAINER_SNAPSHOT_COUNT; // Take the Home app into account
+
+AppsContainerStorage::AppsContainerStorage() :
+ AppsContainer()
+ APPS_CONTAINER_SNAPSHOT_CONSTRUCTORS
+{
+}
+
+int AppsContainerStorage::numberOfApps() {
+ return k_numberOfCommonApps;
+}
+
+App::Snapshot * AppsContainerStorage::appSnapshotAtIndex(int index) {
+ if (index < 0) {
+ return nullptr;
+ }
+ App::Snapshot * snapshots[] = {
+ homeAppSnapshot()
+ APPS_CONTAINER_SNAPSHOT_LIST
+ };
+ assert(sizeof(snapshots)/sizeof(snapshots[0]) == k_numberOfCommonApps);
+ assert(index >= 0 && index < k_numberOfCommonApps);
+ return snapshots[index];
+}
diff --git a/apps/apps_container_storage.h b/apps/apps_container_storage.h
new file mode 100644
index 00000000000..9abd3c27ad4
--- /dev/null
+++ b/apps/apps_container_storage.h
@@ -0,0 +1,34 @@
+#ifndef APPS_CONTAINER_STORAGE_H
+#define APPS_CONTAINER_STORAGE_H
+
+#include "apps_container.h"
+
+#ifndef APPS_CONTAINER_SNAPSHOT_DECLARATIONS
+#error Missing snapshot declarations
+#endif
+
+class AppsContainerStorage : public AppsContainer {
+public:
+ AppsContainerStorage();
+ int numberOfApps() override;
+ App::Snapshot * appSnapshotAtIndex(int index) override;
+ void * currentAppBuffer() override { return &m_apps; };
+private:
+ union Apps {
+ public:
+ /* Enforce a trivial constructor and destructor that just leave the memory
+ * unmodified. This way, m_apps can be trivially destructed. */
+ Apps() {};
+ ~Apps() {};
+ private:
+ APPS_CONTAINER_APPS_DECLARATION
+ Home::App m_homeApp;
+ OnBoarding::App m_onBoardingApp;
+ HardwareTest::App m_hardwareTestApp;
+ USB::App m_usbApp;
+ };
+ Apps m_apps;
+ APPS_CONTAINER_SNAPSHOT_DECLARATIONS
+};
+
+#endif
diff --git a/apps/apps_window.cpp b/apps/apps_window.cpp
new file mode 100644
index 00000000000..68a1501333b
--- /dev/null
+++ b/apps/apps_window.cpp
@@ -0,0 +1,73 @@
+#include "apps_window.h"
+#include
+extern "C" {
+#include
+}
+
+AppsWindow::AppsWindow() :
+ Window(),
+ m_titleBarView(),
+ m_hideTitleBarView(false)
+{
+}
+
+void AppsWindow::setTitle(I18n::Message title) {
+ m_titleBarView.setTitle(title);
+}
+
+bool AppsWindow::updateBatteryLevel() {
+ return m_titleBarView.setChargeState(Ion::Battery::level());
+}
+
+bool AppsWindow::updateIsChargingState() {
+ return m_titleBarView.setIsCharging(Ion::Battery::isCharging());
+}
+
+bool AppsWindow::updatePluggedState() {
+ return m_titleBarView.setIsPlugged(Ion::USB::isPlugged());
+}
+
+void AppsWindow::refreshPreferences() {
+ m_titleBarView.refreshPreferences();
+}
+
+void AppsWindow::reloadTitleBarView() {
+ m_titleBarView.reload();
+}
+
+bool AppsWindow::updateAlphaLock() {
+ return m_titleBarView.setShiftAlphaLockStatus(Ion::Events::shiftAlphaStatus());
+}
+
+void AppsWindow::hideTitleBarView(bool hide) {
+ if (m_hideTitleBarView != hide) {
+ m_hideTitleBarView = hide;
+ layoutSubviews();
+ }
+}
+
+int AppsWindow::numberOfSubviews() const {
+ return (m_contentView == nullptr ? 1 : 2);
+}
+
+View * AppsWindow::subviewAtIndex(int index) {
+ if (index == 0) {
+ return &m_titleBarView;
+ }
+ assert(m_contentView != nullptr && index == 1);
+ return m_contentView;
+}
+
+void AppsWindow::layoutSubviews(bool force) {
+ KDCoordinate titleHeight = m_hideTitleBarView ? 0 : Metric::TitleBarHeight;
+ m_titleBarView.setFrame(KDRect(0, 0, bounds().width(), titleHeight), force);
+ if (m_contentView != nullptr) {
+ m_contentView->setFrame(KDRect(0, titleHeight, bounds().width(), bounds().height()-titleHeight), force);
+ }
+}
+
+#if ESCHER_VIEW_LOGGING
+const char * AppsWindow::className() const {
+ return "Window";
+}
+#endif
diff --git a/apps/apps_window.h b/apps/apps_window.h
new file mode 100644
index 00000000000..08f1bb651b0
--- /dev/null
+++ b/apps/apps_window.h
@@ -0,0 +1,26 @@
+#ifndef APPS_WINDOW_H
+#define APPS_WINDOW_H
+
+#include
+#include "title_bar_view.h"
+
+class AppsWindow : public Window {
+public:
+ AppsWindow();
+ void setTitle(I18n::Message title);
+ bool updateBatteryLevel();
+ bool updateIsChargingState();
+ bool updatePluggedState();
+ void refreshPreferences();
+ void reloadTitleBarView();
+ bool updateAlphaLock();
+ void hideTitleBarView(bool hide);
+private:
+ int numberOfSubviews() const override;
+ void layoutSubviews(bool force = false) override;
+ View * subviewAtIndex(int index) override;
+ TitleBarView m_titleBarView;
+ bool m_hideTitleBarView;
+};
+
+#endif
diff --git a/apps/backlight_dimming_timer.cpp b/apps/backlight_dimming_timer.cpp
new file mode 100644
index 00000000000..2f60e809be5
--- /dev/null
+++ b/apps/backlight_dimming_timer.cpp
@@ -0,0 +1,11 @@
+#include "backlight_dimming_timer.h"
+
+BacklightDimmingTimer::BacklightDimmingTimer() :
+ Timer(k_idleBeforeDimmingDuration/Timer::TickDuration)
+{
+}
+
+bool BacklightDimmingTimer::fire() {
+ Ion::Backlight::setBrightness(k_dimBacklightBrightness);
+ return false;
+}
diff --git a/apps/backlight_dimming_timer.h b/apps/backlight_dimming_timer.h
new file mode 100644
index 00000000000..17059e408de
--- /dev/null
+++ b/apps/backlight_dimming_timer.h
@@ -0,0 +1,16 @@
+#ifndef APPS_BACKLIGHT_DIMMING_TIMER_H
+#define APPS_BACKLIGHT_DIMMING_TIMER_H
+
+#include
+
+class BacklightDimmingTimer : public Timer {
+public:
+ BacklightDimmingTimer();
+private:
+ constexpr static int k_idleBeforeDimmingDuration = 30*1000; // In miliseconds
+ constexpr static int k_dimBacklightBrightness = 0;
+ bool fire() override;
+};
+
+#endif
+
diff --git a/apps/battery_timer.cpp b/apps/battery_timer.cpp
new file mode 100644
index 00000000000..bde13c21cf2
--- /dev/null
+++ b/apps/battery_timer.cpp
@@ -0,0 +1,16 @@
+#include "battery_timer.h"
+#include "apps_container.h"
+
+BatteryTimer::BatteryTimer() :
+ Timer(1)
+{
+}
+
+bool BatteryTimer::fire() {
+ AppsContainer * container = AppsContainer::sharedAppsContainer();
+ bool needRedrawing = container->updateBatteryState();
+ if (Ion::Battery::level() == Ion::Battery::Charge::EMPTY && !Ion::USB::isPlugged()) {
+ container->shutdownDueToLowBattery();
+ }
+ return needRedrawing;
+}
diff --git a/apps/battery_timer.h b/apps/battery_timer.h
new file mode 100644
index 00000000000..90ad16c1199
--- /dev/null
+++ b/apps/battery_timer.h
@@ -0,0 +1,14 @@
+#ifndef APPS_BATTERY_TIMER_H
+#define APPS_BATTERY_TIMER_H
+
+#include
+
+class BatteryTimer : public Timer {
+public:
+ BatteryTimer();
+private:
+ bool fire() override;
+};
+
+#endif
+
diff --git a/apps/battery_view.cpp b/apps/battery_view.cpp
new file mode 100644
index 00000000000..abd03c73121
--- /dev/null
+++ b/apps/battery_view.cpp
@@ -0,0 +1,104 @@
+#include "battery_view.h"
+
+const uint8_t flashMask[BatteryView::k_flashHeight][BatteryView::k_flashWidth] = {
+ {0xDB, 0x00, 0x00, 0xFF},
+ {0xB7, 0x00, 0x6D, 0xFF},
+ {0x6D, 0x00, 0xDB, 0xFF},
+ {0x24, 0x00, 0x00, 0x00},
+ {0x00, 0x00, 0x00, 0x24},
+ {0xFF, 0xDB, 0x00, 0x6D},
+ {0xFF, 0x6D, 0x00, 0xB7},
+ {0xFF, 0x00, 0x00, 0xDB},
+};
+
+const uint8_t tickMask[BatteryView::k_tickHeight][BatteryView::k_tickWidth] = {
+ {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDB, 0x00, 0x24},
+ {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x6D, 0x00, 0xDB},
+ {0x6D, 0x00, 0xB7, 0xFF, 0xB7, 0x00, 0x24, 0xFF},
+ {0xDB, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xFF},
+ {0xFF, 0xB7, 0x00, 0x24, 0x00, 0xB7, 0xFF, 0xFF},
+ {0xFF, 0xFF, 0x24, 0x00, 0x24, 0xFF, 0xFF, 0xFF},
+
+};
+
+bool BatteryView::setChargeState(Ion::Battery::Charge chargeState) {
+ /* There is no specific battery picto for 'empty' battery as the whole device
+ * shut down. Still, there might be a redrawing of the window before shutting
+ * down so we handle this case as the 'low' battery one. Plus, we avoid
+ * trigerring a redrawing by not marking anything as dirty when switching
+ * from 'low' to 'empty' battery. */
+ chargeState = chargeState == Ion::Battery::Charge::EMPTY ? Ion::Battery::Charge::LOW : chargeState;
+ if (chargeState != m_chargeState) {
+ m_chargeState = chargeState;
+ markRectAsDirty(bounds());
+ return true;
+ }
+ return false;
+}
+
+bool BatteryView::setIsCharging(bool isCharging) {
+ if (m_isCharging != isCharging) {
+ m_isCharging = isCharging;
+ markRectAsDirty(bounds());
+ return true;
+ }
+ return false;
+}
+
+bool BatteryView::setIsPlugged(bool isPlugged) {
+ if (m_isPlugged != isPlugged) {
+ m_isPlugged = isPlugged;
+ markRectAsDirty(bounds());
+ return true;
+ }
+ return false;
+}
+
+void BatteryView::drawRect(KDContext * ctx, KDRect rect) const {
+ assert(m_chargeState != Ion::Battery::Charge::EMPTY);
+ /* We draw from left to right. The middle part representing the battery
+ *'content' depends on the charge */
+
+ // Draw the left part
+ ctx->fillRect(KDRect(0, 0, k_elementWidth, k_batteryHeight), KDColorWhite);
+
+ // Draw the middle part
+ constexpr KDCoordinate batteryInsideX = k_elementWidth+k_separatorThickness;
+ constexpr KDCoordinate batteryInsideWidth = k_batteryWidth-3*k_elementWidth-2*k_separatorThickness;
+ if (m_isCharging) {
+ // Charging: Yellow background with flash
+ ctx->fillRect(KDRect(batteryInsideX, 0, batteryInsideWidth, k_batteryHeight), Palette::YellowLight);
+ KDRect frame((k_batteryWidth-k_flashWidth)/2, 0, k_flashWidth, k_flashHeight);
+ KDColor flashWorkingBuffer[BatteryView::k_flashHeight*BatteryView::k_flashWidth];
+ ctx->blendRectWithMask(frame, KDColorWhite, (const uint8_t *)flashMask, flashWorkingBuffer);
+ } else if (m_chargeState == Ion::Battery::Charge::LOW) {
+ assert(!m_isPlugged);
+ // Low: Quite empty battery
+ ctx->fillRect(KDRect(batteryInsideX, 0, 2*k_elementWidth, k_batteryHeight), Palette::LowBattery);
+ ctx->fillRect(KDRect(3*k_elementWidth+k_separatorThickness, 0, k_batteryWidth-5*k_elementWidth-2*k_separatorThickness, k_batteryHeight), Palette::YellowLight);
+ } else if (m_chargeState == Ion::Battery::Charge::SOMEWHERE_INBETWEEN) {
+ assert(!m_isPlugged);
+ // Middle: Half full battery
+ constexpr KDCoordinate middleChargeWidth = batteryInsideWidth/2;
+ ctx->fillRect(KDRect(batteryInsideX, 0, middleChargeWidth, k_batteryHeight), KDColorWhite);
+ ctx->fillRect(KDRect(batteryInsideX+middleChargeWidth, 0, middleChargeWidth, k_batteryHeight), Palette::YellowLight);
+ } else {
+ assert(m_chargeState == Ion::Battery::Charge::FULL);
+ // Full but not plugged: Full battery
+ ctx->fillRect(KDRect(batteryInsideX, 0, batteryInsideWidth, k_batteryHeight), KDColorWhite);
+ if (m_isPlugged) {
+ // Plugged and full: Full battery with tick
+ KDRect frame((k_batteryWidth-k_tickWidth)/2, (k_batteryHeight-k_tickHeight)/2, k_tickWidth, k_tickHeight);
+ KDColor tickWorkingBuffer[BatteryView::k_tickHeight*BatteryView::k_tickWidth];
+ ctx->blendRectWithMask(frame, Palette::YellowDark, (const uint8_t *)tickMask, tickWorkingBuffer);
+ }
+ }
+
+ // Draw the right part
+ ctx->fillRect(KDRect(k_batteryWidth-2*k_elementWidth, 0, k_elementWidth, k_batteryHeight), KDColorWhite);
+ ctx->fillRect(KDRect(k_batteryWidth-k_elementWidth, (k_batteryHeight-k_capHeight)/2, k_elementWidth, k_capHeight), KDColorWhite);
+}
+
+KDSize BatteryView::minimalSizeForOptimalDisplay() const {
+ return KDSize(k_batteryWidth, k_batteryHeight);
+}
diff --git a/apps/battery_view.h b/apps/battery_view.h
new file mode 100644
index 00000000000..5c1d373aa74
--- /dev/null
+++ b/apps/battery_view.h
@@ -0,0 +1,33 @@
+#ifndef APPS_BATTERY_VIEW_H
+#define APPS_BATTERY_VIEW_H
+
+#include
+
+class BatteryView : public TransparentView {
+public:
+ BatteryView() :
+ m_chargeState(Ion::Battery::Charge::SOMEWHERE_INBETWEEN),
+ m_isCharging(false),
+ m_isPlugged(false)
+ {}
+ bool setChargeState(Ion::Battery::Charge chargeState);
+ bool setIsCharging(bool isCharging);
+ bool setIsPlugged(bool isPlugged);
+ void drawRect(KDContext * ctx, KDRect rect) const override;
+ KDSize minimalSizeForOptimalDisplay() const override;
+ constexpr static int k_flashHeight = 8;
+ constexpr static int k_flashWidth = 4;
+ constexpr static int k_tickHeight = 6;
+ constexpr static int k_tickWidth = 8;
+private:
+ constexpr static KDCoordinate k_batteryHeight = 8;
+ constexpr static KDCoordinate k_batteryWidth = 15;
+ constexpr static KDCoordinate k_elementWidth = 1;
+ constexpr static KDCoordinate k_capHeight = 4;
+ constexpr static KDCoordinate k_separatorThickness = Metric::CellSeparatorThickness;
+ Ion::Battery::Charge m_chargeState;
+ bool m_isCharging;
+ bool m_isPlugged;
+};
+
+#endif
diff --git a/apps/calculation/Makefile b/apps/calculation/Makefile
new file mode 100644
index 00000000000..43f8f92c3fd
--- /dev/null
+++ b/apps/calculation/Makefile
@@ -0,0 +1,43 @@
+apps += Calculation::App
+app_headers += apps/calculation/app.h
+
+app_calculation_test_src += $(addprefix apps/calculation/,\
+ calculation.cpp \
+ calculation_store.cpp \
+)
+
+app_calculation_src = $(addprefix apps/calculation/,\
+ additional_outputs/complex_graph_cell.cpp \
+ additional_outputs/complex_model.cpp \
+ additional_outputs/complex_list_controller.cpp \
+ additional_outputs/expression_with_equal_sign_view.cpp \
+ additional_outputs/expressions_list_controller.cpp \
+ additional_outputs/illustrated_list_controller.cpp \
+ additional_outputs/illustration_cell.cpp \
+ additional_outputs/integer_list_controller.cpp \
+ additional_outputs/scrollable_three_expressions_cell.cpp \
+ additional_outputs/list_controller.cpp \
+ additional_outputs/matrix_list_controller.cpp \
+ additional_outputs/rational_list_controller.cpp \
+ additional_outputs/trigonometry_graph_cell.cpp \
+ additional_outputs/trigonometry_list_controller.cpp \
+ additional_outputs/trigonometry_model.cpp \
+ additional_outputs/unit_list_controller.cpp \
+ app.cpp \
+ edit_expression_controller.cpp \
+ expression_field.cpp \
+ history_view_cell.cpp \
+ history_controller.cpp \
+ selectable_table_view.cpp \
+)
+
+app_calculation_src += $(app_calculation_test_src)
+apps_src += $(app_calculation_src)
+
+i18n_files += $(call i18n_without_universal_for,calculation/base)
+
+tests_src += $(addprefix apps/calculation/test/,\
+ calculation_store.cpp\
+)
+
+$(eval $(call depends_on_image,apps/calculation/app.cpp,apps/calculation/calculation_icon.png))
diff --git a/apps/calculation/additional_outputs/complex_graph_cell.cpp b/apps/calculation/additional_outputs/complex_graph_cell.cpp
new file mode 100644
index 00000000000..3bd48645dde
--- /dev/null
+++ b/apps/calculation/additional_outputs/complex_graph_cell.cpp
@@ -0,0 +1,93 @@
+#include "complex_graph_cell.h"
+
+using namespace Shared;
+using namespace Poincare;
+
+namespace Calculation {
+
+ComplexGraphView::ComplexGraphView(ComplexModel * complexModel) :
+ LabeledCurveView(complexModel),
+ m_complex(complexModel)
+{
+}
+
+void ComplexGraphView::drawRect(KDContext * ctx, KDRect rect) const {
+ ctx->fillRect(rect, KDColorWhite);
+
+ // Draw grid, axes and graduations
+ drawGrid(ctx, rect);
+ drawAxes(ctx, rect);
+ drawLabelsAndGraduations(ctx, rect, Axis::Vertical, true);
+ drawLabelsAndGraduations(ctx, rect, Axis::Horizontal, true);
+
+ float real = m_complex->real();
+ float imag = m_complex->imag();
+
+ assert(!std::isnan(real) && !std::isnan(imag) && !std::isinf(real) && !std::isinf(imag));
+ // Draw the segment from the origin to the dot (real, imag)
+ drawSegment(ctx, rect, 0.0f, 0.0f, m_complex->real(), m_complex->imag(), Palette::GrayDark, false);
+
+ /* Draw the partial ellipse indicating the angle θ
+ * - the ellipse parameters are a = |real|/5 and b = |imag|/5,
+ * - the parametric ellipse equation is x(t) = a*cos(th*t) and y(t) = b*sin(th*t)
+ * with th computed in order to be the intersection of the line forming an
+ * angle θ with the abscissa and the ellipsis
+ * - we draw the ellipse for t in [0,1] to represent it from the abscissa axis
+ * to the phase of the complex
+ */
+ /* Compute th: th is the intersection of ellipsis of equation (a*cos(t), b*sin(t))
+ * and the line of equation (real*t,imag*t).
+ * (a*cos(t), b*sin(t)) = (real*t,imag*t) --> tan(t) = sign(a)*sign(b) (± π)
+ * --> t = π/4 [π/2] according to sign(a) and sign(b). */
+ float th = real < 0.0f ? (float)(3.0*M_PI_4) : (float)M_PI_4;
+ th = imag < 0.0f ? -th : th;
+ // Compute ellipsis parameters a and b
+ float factor = 5.0f;
+ float a = std::fabs(real)/factor;
+ float b = std::fabs(imag)/factor;
+ // Avoid flat ellipsis for edge cases (for real = 0, the case imag = 0 is excluded)
+ if (real == 0.0f) {
+ a = 1.0f/factor;
+ th = imag < 0.0f ? (float)-M_PI_2 : (float)M_PI_2;
+ }
+ std::complex parameters(a,b);
+ drawCurve(ctx, rect, 0.0f, 1.0f, 0.01f,
+ [](float t, void * model, void * context) {
+ std::complex parameters = *(std::complex *)model;
+ float th = *(float *)context;
+ float a = parameters.real();
+ float b = parameters.imag();
+ return Poincare::Coordinate2D(a*std::cos(t*th), b*std::sin(t*th));
+ }, ¶meters, &th, false, Palette::GrayDark, false);
+
+ // Draw dashed segment to indicate real and imaginary
+ drawHorizontalOrVerticalSegment(ctx, rect, Axis::Vertical, real, 0.0f, imag, Palette::Red, 1, 3);
+ drawHorizontalOrVerticalSegment(ctx, rect, Axis::Horizontal, imag, 0.0f, real, Palette::Red, 1, 3);
+
+ // Draw complex position on the plan
+ drawDot(ctx, rect, real, imag, Palette::Red, Size::Large);
+
+ // Draw labels
+ // 're(z)' label
+ drawLabel(ctx, rect, real, 0.0f, "re(z)", Palette::Red, CurveView::RelativePosition::None, imag >= 0.0f ? CurveView::RelativePosition::Before : CurveView::RelativePosition::After);
+ // 'im(z)' label
+ drawLabel(ctx, rect, 0.0f, imag, "im(z)", Palette::Red, real >= 0.0f ? CurveView::RelativePosition::Before : CurveView::RelativePosition::After, CurveView::RelativePosition::None);
+ // '|z|' label, the relative horizontal position of this label depends on the quadrant
+ CurveView::RelativePosition verticalPosition = real*imag < 0.0f ? CurveView::RelativePosition::Before : CurveView::RelativePosition::After;
+ if (real == 0.0f) {
+ // Edge case: pure imaginary
+ verticalPosition = CurveView::RelativePosition::None;
+ }
+ drawLabel(ctx, rect, real/2.0f, imag/2.0f, "|z|", Palette::Red, CurveView::RelativePosition::None, verticalPosition);
+ // 'arg(z)' label, the absolute and relative horizontal/vertical positions of this label depends on the quadrant
+ CurveView::RelativePosition horizontalPosition = real >= 0.0f ? CurveView::RelativePosition::After : CurveView::RelativePosition::None;
+ verticalPosition = imag >= 0.0f ? CurveView::RelativePosition::After : CurveView::RelativePosition::Before;
+ /* anglePositionRatio is the ratio of the angle where we position the label
+ * For the right half plan, we position the label close to the abscissa axis
+ * and for the left half plan, we position the label at the half angle. The
+ * relative position is chosen accordingly. */
+ float anglePositionRatio = real >= 0.0f ? 0.0f : 0.5f;
+ drawLabel(ctx, rect, a*std::cos(anglePositionRatio*th), b*std::sin(anglePositionRatio*th), "arg(z)", Palette::Red, horizontalPosition, verticalPosition);
+}
+
+}
diff --git a/apps/calculation/additional_outputs/complex_graph_cell.h b/apps/calculation/additional_outputs/complex_graph_cell.h
new file mode 100644
index 00000000000..7777c999177
--- /dev/null
+++ b/apps/calculation/additional_outputs/complex_graph_cell.h
@@ -0,0 +1,32 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_COMPLEX_GRAPH_CELL_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_COMPLEX_GRAPH_CELL_H
+
+#include "../../shared/labeled_curve_view.h"
+#include "complex_model.h"
+#include "illustration_cell.h"
+
+namespace Calculation {
+
+class ComplexGraphView : public Shared::LabeledCurveView {
+public:
+ ComplexGraphView(ComplexModel * complexModel);
+ void drawRect(KDContext * ctx, KDRect rect) const override;
+private:
+ // '-' + significant digits + ".E-" + 2 digits (the represented dot is a float, so it is bounded by 1E38 and 1E-38
+ size_t labelMaxGlyphLengthSize() const override { return 1 + Poincare::Preferences::VeryShortNumberOfSignificantDigits + 3 + 2; }
+ ComplexModel * m_complex;
+};
+
+class ComplexGraphCell : public IllustrationCell {
+public:
+ ComplexGraphCell(ComplexModel * complexModel) : m_view(complexModel) {}
+ void reload() { m_view.reload(); }
+private:
+ View * view() override { return &m_view; }
+ ComplexGraphView m_view;
+};
+
+}
+
+#endif
+
diff --git a/apps/calculation/additional_outputs/complex_list_controller.cpp b/apps/calculation/additional_outputs/complex_list_controller.cpp
new file mode 100644
index 00000000000..2bbf450c2a7
--- /dev/null
+++ b/apps/calculation/additional_outputs/complex_list_controller.cpp
@@ -0,0 +1,44 @@
+#include "complex_list_controller.h"
+#include "../app.h"
+#include "../../shared/poincare_helpers.h"
+#include
+#include
+#include "complex_list_controller.h"
+
+using namespace Poincare;
+using namespace Shared;
+
+namespace Calculation {
+
+void ComplexListController::viewWillAppear() {
+ IllustratedListController::viewWillAppear();
+ m_complexGraphCell.reload(); // compute labels
+}
+
+void ComplexListController::setExpression(Poincare::Expression e) {
+ IllustratedListController::setExpression(e);
+
+ Poincare::Preferences * preferences = Poincare::Preferences::sharedPreferences();
+ Poincare::Preferences::ComplexFormat currentComplexFormat = preferences->complexFormat();
+ if (currentComplexFormat == Poincare::Preferences::ComplexFormat::Real) {
+ // Temporary change complex format to avoid all additional expressions to be "unreal"
+ preferences->setComplexFormat(Poincare::Preferences::ComplexFormat::Cartesian);
+ }
+ Poincare::Context * context = App::app()->localContext();
+ // Fill Calculation Store
+ m_calculationStore.push("im(z)", context, CalculationHeight);
+ m_calculationStore.push("re(z)", context, CalculationHeight);
+ m_calculationStore.push("arg(z)", context, CalculationHeight);
+ m_calculationStore.push("abs(z)", context, CalculationHeight);
+
+ // Set Complex illustration
+ // Compute a and b as in Expression::hasDefinedComplexApproximation to ensure the same defined result
+ float a = Shared::PoincareHelpers::ApproximateToScalar(RealPart::Builder(e.clone()), context);
+ float b = Shared::PoincareHelpers::ApproximateToScalar(ImaginaryPart::Builder(e.clone()), context);
+ m_model.setComplex(std::complex(a,b));
+
+ // Reset complex format as before
+ preferences->setComplexFormat(currentComplexFormat);
+}
+
+}
diff --git a/apps/calculation/additional_outputs/complex_list_controller.h b/apps/calculation/additional_outputs/complex_list_controller.h
new file mode 100644
index 00000000000..addb7c3424a
--- /dev/null
+++ b/apps/calculation/additional_outputs/complex_list_controller.h
@@ -0,0 +1,30 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_COMPLEX_LIST_CONTROLLER_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_COMPLEX_LIST_CONTROLLER_H
+
+#include "complex_graph_cell.h"
+#include "complex_model.h"
+#include "illustrated_list_controller.h"
+
+namespace Calculation {
+
+class ComplexListController : public IllustratedListController {
+public:
+ ComplexListController(EditExpressionController * editExpressionController) :
+ IllustratedListController(editExpressionController),
+ m_complexGraphCell(&m_model) {}
+
+ // ViewController
+ void viewWillAppear() override;
+
+ void setExpression(Poincare::Expression e) override;
+private:
+ CodePoint expressionSymbol() const override { return 'z'; }
+ HighlightCell * illustrationCell() override { return &m_complexGraphCell; }
+ ComplexGraphCell m_complexGraphCell;
+ ComplexModel m_model;
+};
+
+}
+
+#endif
+
diff --git a/apps/calculation/additional_outputs/complex_model.cpp b/apps/calculation/additional_outputs/complex_model.cpp
new file mode 100644
index 00000000000..5a232241564
--- /dev/null
+++ b/apps/calculation/additional_outputs/complex_model.cpp
@@ -0,0 +1,43 @@
+#include "complex_model.h"
+
+namespace Calculation {
+
+ComplexModel::ComplexModel(std::complex c) :
+ Shared::CurveViewRange(),
+ std::complex(c)
+{
+}
+
+float ComplexModel::rangeBound(float direction, bool horizontal) const {
+ float minFactor = k_minVerticalMarginFactor;
+ float maxFactor = k_maxVerticalMarginFactor;
+ float value = imag();
+ if (horizontal) {
+ minFactor = k_minHorizontalMarginFactor;
+ maxFactor = k_maxHorizontalMarginFactor;
+ value = real();
+ }
+ float factor = direction*value >= 0.0f ? maxFactor : minFactor;
+ if (std::isnan(value) || std::isinf(value) || value == 0.0f) {
+ return direction*factor;
+ }
+ return factor*value;
+}
+
+float ComplexModel::xMin() const {
+ return rangeBound(-1.0f, true);
+}
+
+float ComplexModel::xMax() const {
+ return rangeBound(1.0f, true);
+}
+
+float ComplexModel::yMin() const {
+ return rangeBound(-1.0f, false);
+}
+
+float ComplexModel::yMax() const {
+ return rangeBound(1.0f, false);
+}
+
+}
diff --git a/apps/calculation/additional_outputs/complex_model.h b/apps/calculation/additional_outputs/complex_model.h
new file mode 100644
index 00000000000..685d6bdafdb
--- /dev/null
+++ b/apps/calculation/additional_outputs/complex_model.h
@@ -0,0 +1,75 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_COMPLEX_MODEL_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_COMPLEX_MODEL_H
+
+#include "../../shared/curve_view_range.h"
+#include "illustrated_list_controller.h"
+#include
+
+namespace Calculation {
+
+class ComplexModel : public Shared::CurveViewRange, public std::complex {
+public:
+ ComplexModel(std::complex c = std::complex(NAN, NAN));
+ // CurveViewRange
+ float xMin() const override;
+ float xMax() const override;
+ float yMin() const override;
+ float yMax() const override;
+
+ void setComplex(std::complex c) { *this = ComplexModel(c); }
+
+ /* The range is computed from these criteria:
+ * - The real part is centered horizontally
+ * - Both left and right margins are equal to the real length
+ * - The imaginary part is the same length as the real part
+ * - The remaining vertical margin are splitted as one third at the top, 2
+ * thirds at the bottom
+ *
+ * | | 1/3 * vertical_margin
+ * +----------+
+ * | / | |
+ * | / | | Imaginary
+ * | / | |
+ * | / | |
+ * ----------+----------+----------
+ * |
+ * | 2/3 * vertical_margin
+ * -----------
+ * Real
+ *
+ */
+ // Horizontal range
+ static constexpr float k_minHorizontalMarginFactor = -1.0f;
+ static constexpr float k_maxHorizontalMarginFactor = 2.0f;
+ // Vertical range
+ static constexpr KDCoordinate k_width = Ion::Display::Width - Metric::PopUpRightMargin - Metric::PopUpLeftMargin;
+ static constexpr KDCoordinate k_height = IllustratedListController::k_illustrationHeight;
+ static constexpr KDCoordinate k_unit = k_width/3;
+ /*
+ * VerticalMaring = k_height - k_unit
+ *
+ * Values | Coordinates
+ * --------+----------------------------------
+ * imag | k_unit
+ * Ymax | k_unit + (1/3)*VerticalMargin
+ * Ymin | -(2/3)*VerticalMargin
+ *
+ * Thus:
+ * Ymin = -(2/3)*k_verticalMargin*imag/k_unit
+ * = -(2/3)*(k_height/k_unit - 1)*imag
+ * = 2/3*(1 - k_height/k_unit)*imag
+ * Ymax = (k_unit + (1/3)*VerticalMargin)*imag/k_unit
+ * = (1 + (1/3)*(k_height/k_unit - 1))*imag
+ * = 1/3*(2 + k_height/k_unit)*imag
+ *
+ * */
+ static constexpr float k_minVerticalMarginFactor = 2.0f/3.0f*(1.0f - (float)k_height/(float)k_unit);
+ static constexpr float k_maxVerticalMarginFactor = 1.0f/3.0f*(2.0f + (float)k_height/(float)k_unit);
+
+private:
+ float rangeBound(float direction, bool horizontal) const;
+};
+
+}
+
+#endif
diff --git a/apps/calculation/additional_outputs/expression_with_equal_sign_view.cpp b/apps/calculation/additional_outputs/expression_with_equal_sign_view.cpp
new file mode 100644
index 00000000000..2e480bb3974
--- /dev/null
+++ b/apps/calculation/additional_outputs/expression_with_equal_sign_view.cpp
@@ -0,0 +1,33 @@
+#include "expression_with_equal_sign_view.h"
+
+namespace Calculation {
+
+KDSize ExpressionWithEqualSignView::minimalSizeForOptimalDisplay() const {
+ KDSize expressionSize = ExpressionView::minimalSizeForOptimalDisplay();
+ KDSize equalSize = m_equalSign.minimalSizeForOptimalDisplay();
+ return KDSize(expressionSize.width() + equalSize.width() + Metric::CommonLargeMargin, expressionSize.height());
+}
+
+void ExpressionWithEqualSignView::drawRect(KDContext * ctx, KDRect rect) const {
+ if (m_layout.isUninitialized()) {
+ return;
+ }
+ // Do not color the whole background to avoid coloring behind the equal symbol
+ KDSize expressionSize = ExpressionView::minimalSizeForOptimalDisplay();
+ ctx->fillRect(KDRect(0, 0, expressionSize), m_backgroundColor);
+ m_layout.draw(ctx, drawingOrigin(), m_textColor, m_backgroundColor, m_selectionStart, m_selectionEnd, Palette::Select);
+}
+
+View * ExpressionWithEqualSignView::subviewAtIndex(int index) {
+ assert(index == 0);
+ return &m_equalSign;
+}
+
+void ExpressionWithEqualSignView::layoutSubviews(bool force) {
+ KDSize expressionSize = ExpressionView::minimalSizeForOptimalDisplay();
+ KDSize equalSize = m_equalSign.minimalSizeForOptimalDisplay();
+ KDCoordinate expressionBaseline = layout().baseline();
+ m_equalSign.setFrame(KDRect(expressionSize.width() + Metric::CommonLargeMargin, expressionBaseline - equalSize.height()/2, equalSize), force);
+}
+
+}
diff --git a/apps/calculation/additional_outputs/expression_with_equal_sign_view.h b/apps/calculation/additional_outputs/expression_with_equal_sign_view.h
new file mode 100644
index 00000000000..865a7393fe7
--- /dev/null
+++ b/apps/calculation/additional_outputs/expression_with_equal_sign_view.h
@@ -0,0 +1,26 @@
+#ifndef CALCULATION_EXPRESSION_WITH_EQUAL_SIGN_VIEW_H
+#define CALCULATION_EXPRESSION_WITH_EQUAL_SIGN_VIEW_H
+
+#include
+#include
+#include
+
+namespace Calculation {
+
+class ExpressionWithEqualSignView : public ExpressionView {
+public:
+ ExpressionWithEqualSignView() :
+ m_equalSign(KDFont::LargeFont, I18n::Message::Equal, 0.5f, 0.5f, KDColorBlack)
+ {}
+ KDSize minimalSizeForOptimalDisplay() const override;
+ void drawRect(KDContext * ctx, KDRect rect) const override;
+private:
+ View * subviewAtIndex(int index) override;
+ void layoutSubviews(bool force = false) override;
+ int numberOfSubviews() const override { return 1; }
+ MessageTextView m_equalSign;
+};
+
+}
+
+#endif
diff --git a/apps/calculation/additional_outputs/expressions_list_controller.cpp b/apps/calculation/additional_outputs/expressions_list_controller.cpp
new file mode 100644
index 00000000000..be02125612f
--- /dev/null
+++ b/apps/calculation/additional_outputs/expressions_list_controller.cpp
@@ -0,0 +1,87 @@
+#include "expressions_list_controller.h"
+#include "../app.h"
+
+using namespace Poincare;
+
+namespace Calculation {
+
+/* Expressions list controller */
+
+ExpressionsListController::ExpressionsListController(EditExpressionController * editExpressionController) :
+ ListController(editExpressionController),
+ m_cells{}
+{
+ for (int i = 0; i < k_maxNumberOfRows; i++) {
+ m_cells[i].setParentResponder(m_listController.selectableTableView());
+ }
+}
+
+void ExpressionsListController::didEnterResponderChain(Responder * previousFirstResponder) {
+ selectCellAtLocation(0, 0);
+}
+
+int ExpressionsListController::reusableCellCount(int type) {
+ return k_maxNumberOfRows;
+}
+
+void ExpressionsListController::viewDidDisappear() {
+ ListController::viewDidDisappear();
+ // Reset layout and cell memoization to avoid taking extra space in the pool
+ for (int i = 0; i < k_maxNumberOfRows; i++) {
+ m_cells[i].setLayout(Layout());
+ /* By reseting m_layouts, numberOfRow will go down to 0, and the highlighted
+ * cells won't be unselected. Therefore we unselect them here. */
+ m_cells[i].setHighlighted(false);
+ m_layouts[i] = Layout();
+ }
+ m_expression = Expression();
+}
+
+HighlightCell * ExpressionsListController::reusableCell(int index, int type) {
+ return &m_cells[index];
+}
+
+KDCoordinate ExpressionsListController::rowHeight(int j) {
+ Layout l = layoutAtIndex(j);
+ assert(!l.isUninitialized());
+ return l.layoutSize().height() + 2 * Metric::CommonSmallMargin + Metric::CellSeparatorThickness;
+}
+
+void ExpressionsListController::willDisplayCellForIndex(HighlightCell * cell, int index) {
+ /* Note : To further optimize memoization space in the pool, layout
+ * serialization could be memoized instead, and layout would be recomputed
+ * here, when setting cell's layout. */
+ ExpressionTableCellWithPointer * myCell = static_cast(cell);
+ myCell->setLayout(layoutAtIndex(index));
+ myCell->setAccessoryMessage(messageAtIndex(index));
+ myCell->reloadScroll();
+}
+
+int ExpressionsListController::numberOfRows() const {
+ int nbOfRows = 0;
+ for (size_t i = 0; i < k_maxNumberOfRows; i++) {
+ if (!m_layouts[i].isUninitialized()) {
+ nbOfRows++;
+ }
+ }
+ return nbOfRows;
+}
+
+void ExpressionsListController::setExpression(Poincare::Expression e) {
+ // Reinitialize memoization
+ for (int i = 0; i < k_maxNumberOfRows; i++) {
+ m_layouts[i] = Layout();
+ }
+ m_expression = e;
+}
+
+Poincare::Layout ExpressionsListController::layoutAtIndex(int index) {
+ assert(!m_layouts[index].isUninitialized());
+ return m_layouts[index];
+}
+
+int ExpressionsListController::textAtIndex(char * buffer, size_t bufferSize, int index) {
+ return m_layouts[index].serializeParsedExpression(buffer, bufferSize, App::app()->localContext());
+}
+
+}
diff --git a/apps/calculation/additional_outputs/expressions_list_controller.h b/apps/calculation/additional_outputs/expressions_list_controller.h
new file mode 100644
index 00000000000..f03947a260b
--- /dev/null
+++ b/apps/calculation/additional_outputs/expressions_list_controller.h
@@ -0,0 +1,46 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_EXPRESSIONS_LIST_CONTROLLER_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_EXPRESSIONS_LIST_CONTROLLER_H
+
+#include
+#include
+#include
+#include "list_controller.h"
+
+namespace Calculation {
+
+class ExpressionsListController : public ListController {
+public:
+ ExpressionsListController(EditExpressionController * editExpressionController);
+
+ // Responder
+ void viewDidDisappear() override;
+ void didEnterResponderChain(Responder * previousFirstResponder) override;
+
+ //ListViewDataSource
+ int reusableCellCount(int type) override;
+ HighlightCell * reusableCell(int index, int type) override;
+ KDCoordinate rowHeight(int j) override;
+ int typeAtLocation(int i, int j) override { return 0; }
+ void willDisplayCellForIndex(HighlightCell * cell, int index) override;
+ int numberOfRows() const override;
+
+ // IllustratedListController
+ void setExpression(Poincare::Expression e) override;
+
+protected:
+ constexpr static int k_maxNumberOfRows = 5;
+ int textAtIndex(char * buffer, size_t bufferSize, int index) override;
+ Poincare::Expression m_expression;
+ // Memoization of layouts
+ mutable Poincare::Layout m_layouts[k_maxNumberOfRows];
+private:
+ Poincare::Layout layoutAtIndex(int index);
+ virtual I18n::Message messageAtIndex(int index) = 0;
+ // Cells
+ ExpressionTableCellWithPointer m_cells[k_maxNumberOfRows];
+};
+
+}
+
+#endif
+
diff --git a/apps/calculation/additional_outputs/illustrated_list_controller.cpp b/apps/calculation/additional_outputs/illustrated_list_controller.cpp
new file mode 100644
index 00000000000..b850ad48a5b
--- /dev/null
+++ b/apps/calculation/additional_outputs/illustrated_list_controller.cpp
@@ -0,0 +1,133 @@
+#include "illustrated_list_controller.h"
+#include
+#include
+#include "../app.h"
+
+using namespace Poincare;
+
+namespace Calculation {
+
+/* Illustrated list controller */
+
+IllustratedListController::IllustratedListController(EditExpressionController * editExpressionController) :
+ ListController(editExpressionController, this),
+ m_calculationStore(m_calculationStoreBuffer, k_calculationStoreBufferSize),
+ m_additionalCalculationCells{}
+{
+ for (int i = 0; i < k_maxNumberOfAdditionalCalculations; i++) {
+ m_additionalCalculationCells[i].setParentResponder(m_listController.selectableTableView());
+ }
+}
+
+void IllustratedListController::didEnterResponderChain(Responder * previousFirstResponder) {
+ // Select the left subview on all cells and reinitialize scroll
+ for (int i = 0; i < k_maxNumberOfAdditionalCalculations; i++) {
+ m_additionalCalculationCells[i].reinitSelection();
+ }
+ selectCellAtLocation(0, 1);
+}
+
+void IllustratedListController::viewDidDisappear() {
+ ListController::viewDidDisappear();
+ // Reset the context as it was before displaying the IllustratedListController
+ Poincare::Context * context = App::app()->localContext();
+ if (m_savedExpression.isUninitialized()) {
+ /* If no expression was stored in the symbol used by the
+ * IllustratedListController, we delete the record we stored */
+ char symbolName[3];
+ size_t length = UTF8Decoder::CodePointToChars(expressionSymbol(), symbolName, 3);
+ assert(length < 3);
+ symbolName[length] = 0;
+ const char * const extensions[2] = {"exp", "func"};
+ Ion::Storage::sharedStorage()->recordBaseNamedWithExtensions(symbolName, extensions, 2).destroy();
+ } else {
+ Poincare::Symbol s = Poincare::Symbol::Builder(expressionSymbol());
+ context->setExpressionForSymbolAbstract(m_savedExpression, s);
+ }
+ // Reset cell memoization to avoid taking extra space in the pool
+ for (int i = 0; i < k_maxNumberOfAdditionalCalculations; i++) {
+ m_additionalCalculationCells[i].resetMemoization();
+ }
+}
+
+int IllustratedListController::numberOfRows() const {
+ return m_calculationStore.numberOfCalculations() + 1;
+}
+
+int IllustratedListController::reusableCellCount(int type) {
+ assert(type < 2);
+ if (type == 0) {
+ return 1;
+ }
+ return k_maxNumberOfAdditionalCalculations;
+}
+
+HighlightCell * IllustratedListController::reusableCell(int index, int type) {
+ assert(type < 2);
+ assert(index >= 0);
+ if (type == 0) {
+ return illustrationCell();
+ }
+ return &m_additionalCalculationCells[index];
+}
+
+KDCoordinate IllustratedListController::rowHeight(int j) {
+ if (j == 0) {
+ return k_illustrationHeight;
+ }
+ int calculationIndex = j-1;
+ if (calculationIndex >= m_calculationStore.numberOfCalculations()) {
+ return 0;
+ }
+ Shared::ExpiringPointer calculation = m_calculationStore.calculationAtIndex(calculationIndex);
+ constexpr bool expanded = true;
+ return calculation->height(expanded) + Metric::CellSeparatorThickness;
+}
+
+int IllustratedListController::typeAtLocation(int i, int j) {
+ return j == 0 ? 0 : 1;
+}
+
+void IllustratedListController::willDisplayCellForIndex(HighlightCell * cell, int index) {
+ if (index == 0) {
+ return;
+ }
+ Poincare::Context * context = App::app()->localContext();
+ ScrollableThreeExpressionsCell * myCell = (ScrollableThreeExpressionsCell *)cell;
+ Calculation * c = m_calculationStore.calculationAtIndex(index-1).pointer();
+ myCell->setCalculation(c);
+ myCell->setDisplayCenter(c->displayOutput(context) != Calculation::DisplayOutput::ApproximateOnly);
+}
+
+void IllustratedListController::tableViewDidChangeSelection(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY, bool withinTemporarySelection) {
+ if (withinTemporarySelection) {
+ return;
+ }
+ // Forbid selecting Illustration cell
+ if (t->selectedRow() == 0) {
+ t->selectCellAtLocation(0, 1);
+ }
+ /* But scroll to the top when we select the first
+ * ScrollableThreeExpressionsCell in order display the
+ * illustration cell. */
+ if (t->selectedRow() == 1) {
+ t->scrollToCell(0, 0);
+ }
+}
+
+void IllustratedListController::setExpression(Poincare::Expression e) {
+ m_calculationStore.deleteAll();
+ Poincare::Context * context = App::app()->localContext();
+ Poincare::Symbol s = Poincare::Symbol::Builder(expressionSymbol());
+ m_savedExpression = context->expressionForSymbolAbstract(s, false);
+ context->setExpressionForSymbolAbstract(e, s);
+}
+
+int IllustratedListController::textAtIndex(char * buffer, size_t bufferSize, int index) {
+ ScrollableThreeExpressionsCell * myCell = static_cast(m_listController.selectableTableView()->selectedCell());
+ Shared::ExpiringPointer c = m_calculationStore.calculationAtIndex(index-1);
+ const char * text = myCell->selectedSubviewPosition() == ScrollableThreeExpressionsView::SubviewPosition::Right ? c->approximateOutputText(Calculation::NumberOfSignificantDigits::Maximal) : c->exactOutputText();
+ return strlcpy(buffer, text, bufferSize);
+}
+
+}
diff --git a/apps/calculation/additional_outputs/illustrated_list_controller.h b/apps/calculation/additional_outputs/illustrated_list_controller.h
new file mode 100644
index 00000000000..c0137969ebb
--- /dev/null
+++ b/apps/calculation/additional_outputs/illustrated_list_controller.h
@@ -0,0 +1,54 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_ILLUSTRATED_LIST_CONTROLLER_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_ILLUSTRATED_LIST_CONTROLLER_H
+
+#include
+#include "scrollable_three_expressions_cell.h"
+#include "list_controller.h"
+#include "../calculation_store.h"
+#include
+
+namespace Calculation {
+
+class IllustratedListController : public ListController, public SelectableTableViewDelegate {
+public:
+ IllustratedListController(EditExpressionController * editExpressionController);
+
+ // Responder
+ void viewDidDisappear() override;
+ void didEnterResponderChain(Responder * previousFirstResponder) override;
+
+ //ListViewDataSource
+ int numberOfRows() const override;
+ int reusableCellCount(int type) override;
+ HighlightCell * reusableCell(int index, int type) override;
+ KDCoordinate rowHeight(int j) override;
+ int typeAtLocation(int i, int j) override;
+ void willDisplayCellForIndex(HighlightCell * cell, int index) override;
+
+ // SelectableTableViewDelegate
+ void tableViewDidChangeSelection(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY, bool withinTemporarySelection) override;
+
+ // IllustratedListController
+ void setExpression(Poincare::Expression e) override;
+
+ constexpr static KDCoordinate k_illustrationHeight = 120;
+protected:
+ static KDCoordinate CalculationHeight(Calculation * c, bool expanded) { return ScrollableThreeExpressionsCell::Height(c); }
+ Poincare::Expression m_savedExpression;
+ CalculationStore m_calculationStore;
+private:
+ int textAtIndex(char * buffer, size_t bufferSize, int index) override;
+ virtual CodePoint expressionSymbol() const = 0;
+ // Set the size of the buffer needed to store the additional calculation
+ constexpr static int k_maxNumberOfAdditionalCalculations = 4;
+ constexpr static int k_calculationStoreBufferSize = k_maxNumberOfAdditionalCalculations * (sizeof(Calculation) + Calculation::k_numberOfExpressions * Constant::MaxSerializedExpressionSize + sizeof(Calculation *));
+ char m_calculationStoreBuffer[k_calculationStoreBufferSize];
+ // Cells
+ virtual HighlightCell * illustrationCell() = 0;
+ ScrollableThreeExpressionsCell m_additionalCalculationCells[k_maxNumberOfAdditionalCalculations];
+};
+
+}
+
+#endif
+
diff --git a/apps/calculation/additional_outputs/illustration_cell.cpp b/apps/calculation/additional_outputs/illustration_cell.cpp
new file mode 100644
index 00000000000..eb61b037b77
--- /dev/null
+++ b/apps/calculation/additional_outputs/illustration_cell.cpp
@@ -0,0 +1,16 @@
+#include "illustration_cell.h"
+
+using namespace Shared;
+using namespace Poincare;
+
+namespace Calculation {
+
+void IllustrationCell::layoutSubviews(bool force) {
+ view()->setFrame(KDRect(Metric::CellSeparatorThickness, Metric::CellSeparatorThickness, bounds().width() - 2*Metric::CellSeparatorThickness, bounds().height() - 2*Metric::CellSeparatorThickness), force);
+}
+
+void IllustrationCell::drawRect(KDContext * ctx, KDRect rect) const {
+ drawBorderOfRect(ctx, bounds(), Palette::GrayBright);
+}
+
+}
diff --git a/apps/calculation/additional_outputs/illustration_cell.h b/apps/calculation/additional_outputs/illustration_cell.h
new file mode 100644
index 00000000000..5878053e313
--- /dev/null
+++ b/apps/calculation/additional_outputs/illustration_cell.h
@@ -0,0 +1,23 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_ILLUSTRATION_CELL_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_ILLUSTRATION_CELL_H
+
+#include
+#include
+
+namespace Calculation {
+
+class IllustrationCell : public Bordered, public HighlightCell {
+public:
+ void setHighlighted(bool highlight) override { return; }
+ void drawRect(KDContext * ctx, KDRect rect) const override;
+private:
+ int numberOfSubviews() const override { return 1; }
+ View * subviewAtIndex(int index) override { return view(); }
+ void layoutSubviews(bool force = false) override;
+ virtual View * view() = 0;
+};
+
+}
+
+#endif
+
diff --git a/apps/calculation/additional_outputs/integer_list_controller.cpp b/apps/calculation/additional_outputs/integer_list_controller.cpp
new file mode 100644
index 00000000000..634c24f3a96
--- /dev/null
+++ b/apps/calculation/additional_outputs/integer_list_controller.cpp
@@ -0,0 +1,55 @@
+#include "integer_list_controller.h"
+#include
+#include
+#include
+#include
+#include "../app.h"
+#include "../../shared/poincare_helpers.h"
+
+using namespace Poincare;
+using namespace Shared;
+
+namespace Calculation {
+
+Integer::Base baseAtIndex(int index) {
+ switch (index) {
+ case 0:
+ return Integer::Base::Decimal;
+ case 1:
+ return Integer::Base::Hexadecimal;
+ default:
+ assert(index == 2);
+ return Integer::Base::Binary;
+ }
+}
+
+void IntegerListController::setExpression(Poincare::Expression e) {
+ ExpressionsListController::setExpression(e);
+ static_assert(k_maxNumberOfRows >= k_indexOfFactorExpression + 1, "k_maxNumberOfRows must be greater than k_indexOfFactorExpression");
+ assert(!m_expression.isUninitialized() && m_expression.type() == ExpressionNode::Type::BasedInteger);
+ Integer integer = static_cast(m_expression).integer();
+ for (int index = 0; index < k_indexOfFactorExpression; ++index) {
+ m_layouts[index] = integer.createLayout(baseAtIndex(index));
+ }
+ // Computing factorExpression
+ Expression factor = Factor::Builder(m_expression.clone());
+ PoincareHelpers::Simplify(&factor, App::app()->localContext(), ExpressionNode::ReductionTarget::User);
+ if (!factor.isUndefined()) {
+ m_layouts[k_indexOfFactorExpression] = PoincareHelpers::CreateLayout(factor);
+ }
+}
+
+I18n::Message IntegerListController::messageAtIndex(int index) {
+ switch (index) {
+ case 0:
+ return I18n::Message::DecimalBase;
+ case 1:
+ return I18n::Message::HexadecimalBase;
+ case 2:
+ return I18n::Message::BinaryBase;
+ default:
+ return I18n::Message::PrimeFactors;
+ }
+}
+
+}
diff --git a/apps/calculation/additional_outputs/integer_list_controller.h b/apps/calculation/additional_outputs/integer_list_controller.h
new file mode 100644
index 00000000000..e052632f906
--- /dev/null
+++ b/apps/calculation/additional_outputs/integer_list_controller.h
@@ -0,0 +1,24 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_INTEGER_LIST_CONTROLLER_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_INTEGER_LIST_CONTROLLER_H
+
+#include "expressions_list_controller.h"
+
+namespace Calculation {
+
+class IntegerListController : public ExpressionsListController {
+public:
+ IntegerListController(EditExpressionController * editExpressionController) :
+ ExpressionsListController(editExpressionController) {}
+
+ void setExpression(Poincare::Expression e) override;
+
+private:
+ static constexpr int k_indexOfFactorExpression = 3;
+ I18n::Message messageAtIndex(int index) override;
+};
+
+}
+
+#endif
+
+
diff --git a/apps/calculation/additional_outputs/list_controller.cpp b/apps/calculation/additional_outputs/list_controller.cpp
new file mode 100644
index 00000000000..16f9c9ef6b8
--- /dev/null
+++ b/apps/calculation/additional_outputs/list_controller.cpp
@@ -0,0 +1,50 @@
+#include "list_controller.h"
+#include "../edit_expression_controller.h"
+
+using namespace Poincare;
+
+namespace Calculation {
+
+/* Inner List Controller */
+
+ListController::InnerListController::InnerListController(ListController * dataSource, SelectableTableViewDelegate * delegate) :
+ ViewController(dataSource),
+ m_selectableTableView(this, dataSource, dataSource, delegate)
+{
+ m_selectableTableView.setMargins(0);
+ m_selectableTableView.setDecoratorType(ScrollView::Decorator::Type::None);
+}
+
+void ListController::InnerListController::didBecomeFirstResponder() {
+ m_selectableTableView.reloadData();
+}
+
+/* List Controller */
+
+ListController::ListController(EditExpressionController * editExpressionController, SelectableTableViewDelegate * delegate) :
+ StackViewController(nullptr, &m_listController, KDColorWhite, Palette::PurpleBright, Palette::PurpleDark),
+ m_listController(this, delegate),
+ m_editExpressionController(editExpressionController)
+{
+}
+
+bool ListController::handleEvent(Ion::Events::Event event) {
+ if (event == Ion::Events::OK || event == Ion::Events::EXE) {
+ char buffer[Constant::MaxSerializedExpressionSize];
+ textAtIndex(buffer, Constant::MaxSerializedExpressionSize, selectedRow());
+ /* The order is important here: we dismiss the pop-up first because it
+ * clears the Poincare pool from the layouts used to display the pop-up.
+ * Thereby it frees memory to do Poincare computations required by
+ * insertTextBody. */
+ Container::activeApp()->dismissModalViewController();
+ m_editExpressionController->insertTextBody(buffer);
+ return true;
+ }
+ return false;
+}
+
+void ListController::didBecomeFirstResponder() {
+ Container::activeApp()->setFirstResponder(&m_listController);
+}
+
+}
diff --git a/apps/calculation/additional_outputs/list_controller.h b/apps/calculation/additional_outputs/list_controller.h
new file mode 100644
index 00000000000..0378c90b6ac
--- /dev/null
+++ b/apps/calculation/additional_outputs/list_controller.h
@@ -0,0 +1,41 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_LIST_CONTROLLER_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_LIST_CONTROLLER_H
+
+#include
+#include
+
+namespace Calculation {
+
+class EditExpressionController;
+
+class ListController : public StackViewController, public ListViewDataSource, public SelectableTableViewDataSource {
+public:
+ ListController(EditExpressionController * editExpressionController, SelectableTableViewDelegate * delegate = nullptr);
+
+ // Responder
+ bool handleEvent(Ion::Events::Event event) override;
+ void didBecomeFirstResponder() override;
+
+ // ListController
+ virtual void setExpression(Poincare::Expression e) = 0;
+
+protected:
+ class InnerListController : public ViewController {
+ public:
+ InnerListController(ListController * dataSource, SelectableTableViewDelegate * delegate = nullptr);
+ const char * title() override { return I18n::translate(I18n::Message::AdditionalResults); }
+ View * view() override { return &m_selectableTableView; }
+ void didBecomeFirstResponder() override;
+ SelectableTableView * selectableTableView() { return &m_selectableTableView; }
+ private:
+ SelectableTableView m_selectableTableView;
+ };
+ virtual int textAtIndex(char * buffer, size_t bufferSize, int index) = 0;
+ InnerListController m_listController;
+ EditExpressionController * m_editExpressionController;
+};
+
+}
+
+#endif
+
diff --git a/apps/calculation/additional_outputs/matrix_list_controller.cpp b/apps/calculation/additional_outputs/matrix_list_controller.cpp
new file mode 100644
index 00000000000..a8bf46559a8
--- /dev/null
+++ b/apps/calculation/additional_outputs/matrix_list_controller.cpp
@@ -0,0 +1,106 @@
+#include "matrix_list_controller.h"
+#include "../app.h"
+#include "../../shared/poincare_helpers.h"
+#include
+#include
+#include
+#include
+
+using namespace Poincare;
+using namespace Shared;
+
+namespace Calculation {
+
+void MatrixListController::setExpression(Poincare::Expression e) {
+ ExpressionsListController::setExpression(e);
+ assert(!m_expression.isUninitialized());
+ static_assert(k_maxNumberOfRows >= k_maxNumberOfOutputRows, "k_maxNumberOfRows must be greater than k_maxNumberOfOutputRows");
+
+ Poincare::Preferences * preferences = Poincare::Preferences::sharedPreferences();
+ Poincare::Preferences::ComplexFormat currentComplexFormat = preferences->complexFormat();
+ if (currentComplexFormat == Poincare::Preferences::ComplexFormat::Real) {
+ /* Temporary change complex format to avoid all additional expressions to be
+ * "unreal" (with [i] for instance). As additional results are computed from
+ * the output, which is built taking ComplexFormat into account, there are
+ * no risks of displaying additional results on an unreal output. */
+ preferences->setComplexFormat(Poincare::Preferences::ComplexFormat::Cartesian);
+ }
+
+ Context * context = App::app()->localContext();
+ ExpressionNode::ReductionContext reductionContext(
+ context,
+ preferences->complexFormat(),
+ preferences->angleUnit(),
+ GlobalPreferences::sharedGlobalPreferences()->unitFormat(),
+ ExpressionNode::ReductionTarget::SystemForApproximation,
+ ExpressionNode::SymbolicComputation::ReplaceAllSymbolsWithDefinitionsOrUndefined);
+
+ // The expression must be reduced to call methods such as determinant or trace
+ assert(m_expression.type() == ExpressionNode::Type::Matrix);
+
+ bool mIsSquared = (static_cast(m_expression).numberOfRows() == static_cast(m_expression).numberOfColumns());
+ size_t index = 0;
+ size_t messageIndex = 0;
+ // 1. Matrix determinant if square matrix
+ if (mIsSquared) {
+ /* Determinant is reduced so that a null determinant can be detected.
+ * However, some exceptions remain such as cos(x)^2+sin(x)^2-1 which will
+ * not be reduced to a rational, but will be null in theory. */
+ Expression determinant = Determinant::Builder(m_expression.clone()).reduce(reductionContext);
+ m_indexMessageMap[index] = messageIndex++;
+ m_layouts[index++] = getLayoutFromExpression(determinant, context, preferences);
+ // 2. Matrix inverse if invertible matrix
+ // A squared matrix is invertible if and only if determinant is non null
+ if (!determinant.isUndefined() && determinant.nullStatus(context) != ExpressionNode::NullStatus::Null) {
+ // TODO: Handle ExpressionNode::NullStatus::Unknown
+ m_indexMessageMap[index] = messageIndex++;
+ m_layouts[index++] = getLayoutFromExpression(MatrixInverse::Builder(m_expression.clone()), context, preferences);
+ }
+ }
+ // 3. Matrix row echelon form
+ messageIndex = 2;
+ Expression rowEchelonForm = MatrixRowEchelonForm::Builder(m_expression.clone());
+ m_indexMessageMap[index] = messageIndex++;
+ m_layouts[index++] = getLayoutFromExpression(rowEchelonForm, context, preferences);
+ /* 4. Matrix reduced row echelon form
+ * it can be computed from row echelon form to save computation time.*/
+ m_indexMessageMap[index] = messageIndex++;
+ m_layouts[index++] = getLayoutFromExpression(MatrixReducedRowEchelonForm::Builder(rowEchelonForm), context, preferences);
+ // 5. Matrix trace if square matrix
+ if (mIsSquared) {
+ m_indexMessageMap[index] = messageIndex++;
+ m_layouts[index++] = getLayoutFromExpression(MatrixTrace::Builder(m_expression.clone()), context, preferences);
+ }
+ // Reset complex format as before
+ preferences->setComplexFormat(currentComplexFormat);
+}
+
+Poincare::Layout MatrixListController::getLayoutFromExpression(Expression e, Context * context, Poincare::Preferences * preferences) {
+ assert(!e.isUninitialized());
+ // Simplify or approximate expression
+ Expression approximateExpression;
+ Expression simplifiedExpression;
+ e.simplifyAndApproximate(&simplifiedExpression, &approximateExpression, context,
+ preferences->complexFormat(), preferences->angleUnit(), GlobalPreferences::sharedGlobalPreferences()->unitFormat(),
+ ExpressionNode::SymbolicComputation::ReplaceAllSymbolsWithDefinitionsOrUndefined);
+ // simplify might have been interrupted, in which case we use approximate
+ if (simplifiedExpression.isUninitialized()) {
+ assert(!approximateExpression.isUninitialized());
+ return Shared::PoincareHelpers::CreateLayout(approximateExpression);
+ }
+ return Shared::PoincareHelpers::CreateLayout(simplifiedExpression);
+}
+
+I18n::Message MatrixListController::messageAtIndex(int index) {
+ // Message index is mapped in setExpression because it depends on the Matrix.
+ assert(index < k_maxNumberOfOutputRows && index >=0);
+ I18n::Message messages[k_maxNumberOfOutputRows] = {
+ I18n::Message::AdditionalDeterminant,
+ I18n::Message::AdditionalInverse,
+ I18n::Message::AdditionalRowEchelonForm,
+ I18n::Message::AdditionalReducedRowEchelonForm,
+ I18n::Message::AdditionalTrace};
+ return messages[m_indexMessageMap[index]];
+}
+
+}
diff --git a/apps/calculation/additional_outputs/matrix_list_controller.h b/apps/calculation/additional_outputs/matrix_list_controller.h
new file mode 100644
index 00000000000..b0774e42c6c
--- /dev/null
+++ b/apps/calculation/additional_outputs/matrix_list_controller.h
@@ -0,0 +1,27 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_MATRIX_LIST_CONTROLLER_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_MATRIX_LIST_CONTROLLER_H
+
+#include "expressions_list_controller.h"
+
+namespace Calculation {
+
+class MatrixListController : public ExpressionsListController {
+public:
+ MatrixListController(EditExpressionController * editExpressionController) :
+ ExpressionsListController(editExpressionController) {}
+
+ void setExpression(Poincare::Expression e) override;
+
+private:
+ I18n::Message messageAtIndex(int index) override;
+ Poincare::Layout getLayoutFromExpression(Poincare::Expression e, Poincare::Context * context, Poincare::Preferences * preferences);
+ // Map from cell index to message index
+ constexpr static int k_maxNumberOfOutputRows = 5;
+ int m_indexMessageMap[k_maxNumberOfOutputRows];
+};
+
+}
+
+#endif
+
+
diff --git a/apps/calculation/additional_outputs/rational_list_controller.cpp b/apps/calculation/additional_outputs/rational_list_controller.cpp
new file mode 100644
index 00000000000..c55837927e0
--- /dev/null
+++ b/apps/calculation/additional_outputs/rational_list_controller.cpp
@@ -0,0 +1,60 @@
+#include "rational_list_controller.h"
+#include "../app.h"
+#include "../../shared/poincare_helpers.h"
+#include
+#include
+
+using namespace Poincare;
+using namespace Shared;
+
+namespace Calculation {
+
+Integer extractInteger(const Expression e) {
+ assert(e.type() == ExpressionNode::Type::BasedInteger);
+ return static_cast(e).integer();
+}
+
+void RationalListController::setExpression(Poincare::Expression e) {
+ ExpressionsListController::setExpression(e);
+ assert(!m_expression.isUninitialized());
+ static_assert(k_maxNumberOfRows >= 2, "k_maxNumberOfRows must be greater than 2");
+
+ bool negative = false;
+ Expression div = m_expression;
+ if (m_expression.type() == ExpressionNode::Type::Opposite) {
+ negative = true;
+ div = m_expression.childAtIndex(0);
+ }
+
+ assert(div.type() == ExpressionNode::Type::Division);
+ Integer numerator = extractInteger(div.childAtIndex(0));
+ numerator.setNegative(negative);
+ Integer denominator = extractInteger(div.childAtIndex(1));
+
+ int index = 0;
+ m_layouts[index++] = PoincareHelpers::CreateLayout(Integer::CreateMixedFraction(numerator, denominator));
+ m_layouts[index++] = PoincareHelpers::CreateLayout(Integer::CreateEuclideanDivision(numerator, denominator));
+}
+
+I18n::Message RationalListController::messageAtIndex(int index) {
+ switch (index) {
+ case 0:
+ return I18n::Message::MixedFraction;
+ default:
+ return I18n::Message::EuclideanDivision;
+ }
+}
+
+int RationalListController::textAtIndex(char * buffer, size_t bufferSize, int index) {
+ int length = ExpressionsListController::textAtIndex(buffer, bufferSize, index);
+ if (index == 1) {
+ // Get rid of the left part of the equality
+ char * equalPosition = strchr(buffer, '=');
+ assert(equalPosition != nullptr);
+ strlcpy(buffer, equalPosition + 1, bufferSize);
+ return buffer + length - 1 - equalPosition;
+ }
+ return length;
+}
+
+}
diff --git a/apps/calculation/additional_outputs/rational_list_controller.h b/apps/calculation/additional_outputs/rational_list_controller.h
new file mode 100644
index 00000000000..5ceae4e9b32
--- /dev/null
+++ b/apps/calculation/additional_outputs/rational_list_controller.h
@@ -0,0 +1,24 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_RATIONAL_LIST_CONTROLLER_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_RATIONAL_LIST_CONTROLLER_H
+
+#include "expressions_list_controller.h"
+
+namespace Calculation {
+
+class RationalListController : public ExpressionsListController {
+public:
+ RationalListController(EditExpressionController * editExpressionController) :
+ ExpressionsListController(editExpressionController) {}
+
+ void setExpression(Poincare::Expression e) override;
+
+private:
+ I18n::Message messageAtIndex(int index) override;
+ int textAtIndex(char * buffer, size_t bufferSize, int index) override;
+};
+
+}
+
+#endif
+
+
diff --git a/apps/calculation/additional_outputs/scrollable_three_expressions_cell.cpp b/apps/calculation/additional_outputs/scrollable_three_expressions_cell.cpp
new file mode 100644
index 00000000000..563d49e1a26
--- /dev/null
+++ b/apps/calculation/additional_outputs/scrollable_three_expressions_cell.cpp
@@ -0,0 +1,101 @@
+#include "scrollable_three_expressions_cell.h"
+#include
+#include "../app.h"
+
+namespace Calculation {
+
+void ScrollableThreeExpressionsView::resetMemoization() {
+ setLayouts(Poincare::Layout(), Poincare::Layout(), Poincare::Layout());
+}
+
+// TODO: factorize with HistoryViewCell!
+void ScrollableThreeExpressionsView::setCalculation(Calculation * calculation, bool canChangeDisplayOutput) {
+ Poincare::Context * context = App::app()->localContext();
+
+ // Clean the layouts to make room in the pool
+ resetMemoization();
+
+ // Create the input layout
+ Poincare::Layout inputLayout = calculation->createInputLayout();
+
+ // Create the exact output layout
+ Poincare::Layout exactOutputLayout = Poincare::Layout();
+ if (Calculation::DisplaysExact(calculation->displayOutput(context))) {
+ bool couldNotCreateExactLayout = false;
+ exactOutputLayout = calculation->createExactOutputLayout(&couldNotCreateExactLayout);
+ if (couldNotCreateExactLayout) {
+ if (canChangeDisplayOutput && calculation->displayOutput(context) != ::Calculation::Calculation::DisplayOutput::ExactOnly) {
+ calculation->forceDisplayOutput(::Calculation::Calculation::DisplayOutput::ApproximateOnly);
+ } else {
+ Poincare::ExceptionCheckpoint::Raise();
+ }
+ }
+ }
+ Calculation::DisplayOutput displayOutput = calculation->displayOutput(context);
+
+ // Create the approximate output layout
+ Poincare::Layout approximateOutputLayout = Poincare::Layout();
+ if (displayOutput == Calculation::DisplayOutput::ExactOnly) {
+ approximateOutputLayout = exactOutputLayout;
+ } else {
+ bool couldNotCreateApproximateLayout = false;
+ approximateOutputLayout = calculation->createApproximateOutputLayout(context, &couldNotCreateApproximateLayout);
+ if (couldNotCreateApproximateLayout) {
+ if (canChangeDisplayOutput && calculation->displayOutput(context) != ::Calculation::Calculation::DisplayOutput::ApproximateOnly) {
+ /* Set the display output to ApproximateOnly, make room in the pool by
+ * erasing the exact layout, and retry to create the approximate layout */
+ calculation->forceDisplayOutput(::Calculation::Calculation::DisplayOutput::ApproximateOnly);
+ exactOutputLayout = Poincare::Layout();
+ couldNotCreateApproximateLayout = false;
+ approximateOutputLayout = calculation->createApproximateOutputLayout(context, &couldNotCreateApproximateLayout);
+ if (couldNotCreateApproximateLayout) {
+ Poincare::ExceptionCheckpoint::Raise();
+ }
+ } else {
+ Poincare::ExceptionCheckpoint::Raise();
+ }
+ }
+
+ }
+ setLayouts(inputLayout, exactOutputLayout, approximateOutputLayout);
+ I18n::Message equalMessage = calculation->exactAndApproximateDisplayedOutputsAreEqual(context) == Calculation::EqualSign::Equal ? I18n::Message::Equal : I18n::Message::AlmostEqual;
+ setEqualMessage(equalMessage);
+
+ /* The displayed input and outputs have changed. We need to re-layout the cell
+ * and re-initialize the scroll. */
+ layoutSubviews();
+}
+
+KDCoordinate ScrollableThreeExpressionsCell::Height(Calculation * calculation) {
+ ScrollableThreeExpressionsCell cell;
+ cell.setCalculation(calculation, true);
+ KDRect leftFrame = KDRectZero;
+ KDRect centerFrame = KDRectZero;
+ KDRect approximateSignFrame = KDRectZero;
+ KDRect rightFrame = KDRectZero;
+ cell.subviewFrames(&leftFrame, ¢erFrame, &approximateSignFrame, &rightFrame);
+ KDRect unionedFrame = leftFrame.unionedWith(centerFrame).unionedWith(rightFrame);
+ return unionedFrame.height() + 2 * ScrollableThreeExpressionsView::k_margin;
+}
+
+void ScrollableThreeExpressionsCell::didBecomeFirstResponder() {
+ reinitSelection();
+ Container::activeApp()->setFirstResponder(&m_view);
+}
+
+void ScrollableThreeExpressionsCell::reinitSelection() {
+ m_view.setSelectedSubviewPosition(ScrollableThreeExpressionsView::SubviewPosition::Left);
+ m_view.reloadScroll();
+}
+
+void ScrollableThreeExpressionsCell::setCalculation(Calculation * calculation, bool canChangeDisplayOutput) {
+ m_view.setCalculation(calculation, canChangeDisplayOutput);
+ layoutSubviews();
+}
+
+void ScrollableThreeExpressionsCell::setDisplayCenter(bool display) {
+ m_view.setDisplayCenter(display);
+ layoutSubviews();
+}
+
+}
diff --git a/apps/calculation/additional_outputs/scrollable_three_expressions_cell.h b/apps/calculation/additional_outputs/scrollable_three_expressions_cell.h
new file mode 100644
index 00000000000..792c65e1dc4
--- /dev/null
+++ b/apps/calculation/additional_outputs/scrollable_three_expressions_cell.h
@@ -0,0 +1,80 @@
+#ifndef CALCULATION_SCROLLABLE_THREE_EXPRESSIONS_CELL_H
+#define CALCULATION_SCROLLABLE_THREE_EXPRESSIONS_CELL_H
+
+#include
+#include "../../shared/scrollable_multiple_expressions_view.h"
+#include "../calculation.h"
+#include "expression_with_equal_sign_view.h"
+
+namespace Calculation {
+
+/* TODO There is factorizable code between this and Calculation::HistoryViewCell
+ * (at least setCalculation). */
+
+class ScrollableThreeExpressionsView : public Shared::AbstractScrollableMultipleExpressionsView {
+public:
+ static constexpr KDCoordinate k_margin = Metric::CommonSmallMargin;
+ ScrollableThreeExpressionsView(Responder * parentResponder) : Shared::AbstractScrollableMultipleExpressionsView(parentResponder, &m_contentCell), m_contentCell() {
+ setMargins(k_margin, k_margin, k_margin, k_margin); // Left Right margins are already added by TableCell
+ setBackgroundColor(KDColorWhite);
+ }
+ void resetMemoization();
+ void setCalculation(Calculation * calculation, bool canChangeDisplayOutput);
+ void subviewFrames(KDRect * leftFrame, KDRect * centerFrame, KDRect * approximateSignFrame, KDRect * rightFrame) {
+ return m_contentCell.subviewFrames(leftFrame, centerFrame, approximateSignFrame, rightFrame);
+ }
+private:
+ class ContentCell : public Shared::AbstractScrollableMultipleExpressionsView::ContentCell {
+ public:
+ ContentCell() : m_leftExpressionView() {}
+ KDColor backgroundColor() const override { return KDColorWhite; }
+ void setEven(bool even) override { return; }
+ ExpressionView * leftExpressionView() const override { return const_cast(&m_leftExpressionView); }
+ private:
+ ExpressionWithEqualSignView m_leftExpressionView;
+ };
+
+ ContentCell * contentCell() override { return &m_contentCell; };
+ const ContentCell * constContentCell() const override { return &m_contentCell; };
+ ContentCell m_contentCell;
+};
+
+class ScrollableThreeExpressionsCell : public TableCell, public Responder {
+public:
+ static KDCoordinate Height(Calculation * calculation);
+ ScrollableThreeExpressionsCell() :
+ Responder(nullptr),
+ m_view(this) {}
+
+ // Cell
+ Poincare::Layout layout() const override { return m_view.layout(); }
+
+ // Responder cell
+ Responder * responder() override {
+ return this;
+ }
+ void didBecomeFirstResponder() override;
+
+ // Table cell
+ View * labelView() const override { return (View *)&m_view; }
+
+ void setHighlighted(bool highlight) override { m_view.evenOddCell()->setHighlighted(highlight); }
+ void resetMemoization() { m_view.resetMemoization(); }
+ void setCalculation(Calculation * calculation, bool canChangeDisplayOutput = false);
+ void setDisplayCenter(bool display);
+ ScrollableThreeExpressionsView::SubviewPosition selectedSubviewPosition() { return m_view.selectedSubviewPosition(); }
+ void setSelectedSubviewPosition(ScrollableThreeExpressionsView::SubviewPosition subviewPosition) { m_view.setSelectedSubviewPosition(subviewPosition); }
+
+ void reinitSelection();
+ void subviewFrames(KDRect * leftFrame, KDRect * centerFrame, KDRect * approximateSignFrame, KDRect * rightFrame) {
+ return m_view.subviewFrames(leftFrame, centerFrame, approximateSignFrame, rightFrame);
+ }
+private:
+ // Remove label margin added by TableCell because they're already handled by ScrollableThreeExpressionsView
+ KDCoordinate labelMargin() const override { return 0; }
+ ScrollableThreeExpressionsView m_view;
+};
+
+}
+
+#endif
diff --git a/apps/calculation/additional_outputs/trigonometry_graph_cell.cpp b/apps/calculation/additional_outputs/trigonometry_graph_cell.cpp
new file mode 100644
index 00000000000..903674ce5ed
--- /dev/null
+++ b/apps/calculation/additional_outputs/trigonometry_graph_cell.cpp
@@ -0,0 +1,37 @@
+#include "trigonometry_graph_cell.h"
+
+using namespace Shared;
+using namespace Poincare;
+
+namespace Calculation {
+
+TrigonometryGraphView::TrigonometryGraphView(TrigonometryModel * model) :
+ CurveView(model),
+ m_model(model)
+{
+}
+
+void TrigonometryGraphView::drawRect(KDContext * ctx, KDRect rect) const {
+ float s = std::sin(m_model->angle());
+ float c = std::cos(m_model->angle());
+ ctx->fillRect(rect, KDColorWhite);
+ drawGrid(ctx, rect);
+ drawAxes(ctx, rect);
+ // Draw the circle
+ drawCurve(ctx, rect, 0.0f, 2.0f*M_PI, M_PI/180.0f, [](float t, void * model, void * context) {
+ return Poincare::Coordinate2D(std::cos(t), std::sin(t));
+ }, nullptr, nullptr, true, Palette::GrayDark, false);
+ // Draw dashed segment to indicate sine and cosine
+ drawHorizontalOrVerticalSegment(ctx, rect, Axis::Vertical, c, 0.0f, s, Palette::Red, 1, 3);
+ drawHorizontalOrVerticalSegment(ctx, rect, Axis::Horizontal, s, 0.0f, c, Palette::Red, 1, 3);
+ // Draw angle position on the circle
+ drawDot(ctx, rect, c, s, Palette::Red, Size::Large);
+ // Draw graduations
+ drawLabelsAndGraduations(ctx, rect, Axis::Vertical, false, true);
+ drawLabelsAndGraduations(ctx, rect, Axis::Horizontal, false, true);
+ // Draw labels
+ drawLabel(ctx, rect, 0.0f, s, "sin(θ)", Palette::Red, c >= 0.0f ? CurveView::RelativePosition::Before : CurveView::RelativePosition::After, CurveView::RelativePosition::None);
+ drawLabel(ctx, rect, c, 0.0f, "cos(θ)", Palette::Red, CurveView::RelativePosition::None, s >= 0.0f ? CurveView::RelativePosition::Before : CurveView::RelativePosition::After);
+}
+
+}
diff --git a/apps/calculation/additional_outputs/trigonometry_graph_cell.h b/apps/calculation/additional_outputs/trigonometry_graph_cell.h
new file mode 100644
index 00000000000..aea81926af3
--- /dev/null
+++ b/apps/calculation/additional_outputs/trigonometry_graph_cell.h
@@ -0,0 +1,29 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_TRIGONOMETRY_GRAPH_CELL_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_TRIGONOMETRY_GRAPH_CELL_H
+
+#include "../../shared/curve_view.h"
+#include "trigonometry_model.h"
+#include "illustration_cell.h"
+
+namespace Calculation {
+
+class TrigonometryGraphView : public Shared::CurveView {
+public:
+ TrigonometryGraphView(TrigonometryModel * model);
+ void drawRect(KDContext * ctx, KDRect rect) const override;
+private:
+ TrigonometryModel * m_model;
+};
+
+class TrigonometryGraphCell : public IllustrationCell {
+public:
+ TrigonometryGraphCell(TrigonometryModel * model) : m_view(model) {}
+private:
+ View * view() override { return &m_view; }
+ TrigonometryGraphView m_view;
+};
+
+}
+
+#endif
+
diff --git a/apps/calculation/additional_outputs/trigonometry_list_controller.cpp b/apps/calculation/additional_outputs/trigonometry_list_controller.cpp
new file mode 100644
index 00000000000..b8bdcb0764e
--- /dev/null
+++ b/apps/calculation/additional_outputs/trigonometry_list_controller.cpp
@@ -0,0 +1,23 @@
+#include "trigonometry_list_controller.h"
+#include "../app.h"
+
+using namespace Poincare;
+
+namespace Calculation {
+
+void TrigonometryListController::setExpression(Poincare::Expression e) {
+ assert(e.type() == ExpressionNode::Type::Cosine || e.type() == ExpressionNode::Type::Sine);
+ IllustratedListController::setExpression(e.childAtIndex(0));
+
+ // Fill calculation store
+ Poincare::Context * context = App::app()->localContext();
+ m_calculationStore.push("sin(θ)", context, CalculationHeight);
+ m_calculationStore.push("cos(θ)", context, CalculationHeight);
+ m_calculationStore.push("θ", context, CalculationHeight);
+
+ // Set trigonometry illustration
+ float angle = Shared::PoincareHelpers::ApproximateToScalar(m_calculationStore.calculationAtIndex(0)->approximateOutput(context, Calculation::NumberOfSignificantDigits::Maximal), context);
+ m_model.setAngle(angle);
+}
+
+}
diff --git a/apps/calculation/additional_outputs/trigonometry_list_controller.h b/apps/calculation/additional_outputs/trigonometry_list_controller.h
new file mode 100644
index 00000000000..12880e1a0e2
--- /dev/null
+++ b/apps/calculation/additional_outputs/trigonometry_list_controller.h
@@ -0,0 +1,25 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_TRIGONOMETRY_LIST_CONTROLLER_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_TRIGONOMETRY_LIST_CONTROLLER_H
+
+#include "trigonometry_graph_cell.h"
+#include "trigonometry_model.h"
+#include "illustrated_list_controller.h"
+
+namespace Calculation {
+
+class TrigonometryListController : public IllustratedListController {
+public:
+ TrigonometryListController(EditExpressionController * editExpressionController) :
+ IllustratedListController(editExpressionController),
+ m_graphCell(&m_model) {}
+ void setExpression(Poincare::Expression e) override;
+private:
+ CodePoint expressionSymbol() const override { return UCodePointGreekSmallLetterTheta; }
+ HighlightCell * illustrationCell() override { return &m_graphCell; }
+ TrigonometryGraphCell m_graphCell;
+ TrigonometryModel m_model;
+};
+
+}
+
+#endif
diff --git a/apps/calculation/additional_outputs/trigonometry_model.cpp b/apps/calculation/additional_outputs/trigonometry_model.cpp
new file mode 100644
index 00000000000..ef7c708c371
--- /dev/null
+++ b/apps/calculation/additional_outputs/trigonometry_model.cpp
@@ -0,0 +1,11 @@
+#include "trigonometry_model.h"
+
+namespace Calculation {
+
+TrigonometryModel::TrigonometryModel() :
+ Shared::CurveViewRange(),
+ m_angle(NAN)
+{
+}
+
+}
diff --git a/apps/calculation/additional_outputs/trigonometry_model.h b/apps/calculation/additional_outputs/trigonometry_model.h
new file mode 100644
index 00000000000..c299565a861
--- /dev/null
+++ b/apps/calculation/additional_outputs/trigonometry_model.h
@@ -0,0 +1,40 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_TRIGONOMETRY_MODEL_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_TRIGONOMETRY_MODEL_H
+
+#include "../../shared/curve_view_range.h"
+#include "illustrated_list_controller.h"
+#include
+#include
+
+namespace Calculation {
+
+class TrigonometryModel : public Shared::CurveViewRange {
+public:
+ TrigonometryModel();
+ // CurveViewRange
+ float xMin() const override { return -k_xHalfRange; }
+ float xMax() const override { return k_xHalfRange; }
+ float yMin() const override { return yCenter() - yHalfRange(); }
+ float yMax() const override { return yCenter() + yHalfRange(); }
+
+ void setAngle(float f) { m_angle = f; }
+ float angle() const { return m_angle*(float)M_PI/(float)Poincare::Trigonometry::PiInAngleUnit(Poincare::Preferences::sharedPreferences()->angleUnit()); }
+private:
+ constexpr static float k_xHalfRange = 2.1f;
+ // We center the yRange around the semi-circle where the angle is
+ float yCenter() const { return std::sin(angle()) >= 0.0f ? 0.5f : -0.5f; }
+
+ /* We want to normalize the displayed trigonometry circle:
+ * - On the X axis, we display 4.4 units on an available pixel width of
+ * (Ion::Display::Width - Metric::PopUpRightMargin - Metric::PopUpLeftMargin)
+ * - On the Y axis, the available pixel height is
+ * IllustratedListController::k_illustrationHeight
+ */
+ float yHalfRange() const { return IllustratedListController::k_illustrationHeight*k_xHalfRange/(Ion::Display::Width - Metric::PopUpRightMargin - Metric::PopUpLeftMargin); }
+
+ float m_angle;
+};
+
+}
+
+#endif
diff --git a/apps/calculation/additional_outputs/unit_list_controller.cpp b/apps/calculation/additional_outputs/unit_list_controller.cpp
new file mode 100644
index 00000000000..869826d5e4f
--- /dev/null
+++ b/apps/calculation/additional_outputs/unit_list_controller.cpp
@@ -0,0 +1,96 @@
+#include "unit_list_controller.h"
+#include "../app.h"
+#include "../../shared/poincare_helpers.h"
+#include
+#include
+#include
+#include
+#include
+
+using namespace Poincare;
+using namespace Shared;
+
+namespace Calculation {
+
+void UnitListController::setExpression(Poincare::Expression e) {
+ ExpressionsListController::setExpression(e);
+ assert(!m_expression.isUninitialized());
+ static_assert(k_maxNumberOfRows >= 3, "k_maxNumberOfRows must be greater than 3");
+
+ Poincare::Expression expressions[k_maxNumberOfRows];
+ // Initialize expressions
+ for (size_t i = 0; i < k_maxNumberOfRows; i++) {
+ expressions[i] = Expression();
+ }
+
+ /* 1. First rows: miscellaneous classic units for some dimensions, in both
+ * metric and imperial units. */
+ Expression copy = m_expression.clone();
+ Expression units;
+ // Reduce to be able to recognize units
+ PoincareHelpers::ReduceAndRemoveUnit(©, App::app()->localContext(), ExpressionNode::ReductionTarget::User, &units);
+ double value = Shared::PoincareHelpers::ApproximateToScalar(copy, App::app()->localContext());
+ ExpressionNode::ReductionContext reductionContext(
+ App::app()->localContext(),
+ Preferences::sharedPreferences()->complexFormat(),
+ Preferences::sharedPreferences()->angleUnit(),
+ GlobalPreferences::sharedGlobalPreferences()->unitFormat(),
+ ExpressionNode::ReductionTarget::User,
+ ExpressionNode::SymbolicComputation::ReplaceAllSymbolsWithDefinitionsOrUndefined);
+ int numberOfExpressions = Unit::SetAdditionalExpressions(units, value, expressions, k_maxNumberOfRows, reductionContext);
+
+ // 2. SI units only
+ assert(numberOfExpressions < k_maxNumberOfRows - 1);
+ expressions[numberOfExpressions] = m_expression.clone();
+ Shared::PoincareHelpers::Simplify(&expressions[numberOfExpressions], App::app()->localContext(), ExpressionNode::ReductionTarget::User, Poincare::ExpressionNode::SymbolicComputation::ReplaceAllDefinedSymbolsWithDefinition, Poincare::ExpressionNode::UnitConversion::InternationalSystem);
+ numberOfExpressions++;
+
+ /* 3. Get rid of duplicates
+ * We find duplicates by comparing the serializations, to eliminate
+ * expressions that only differ by the types of their number nodes. */
+ Expression reduceExpression = m_expression.clone();
+ // Make m_expression comparable to expressions (turn BasedInteger into Rational for instance)
+ Shared::PoincareHelpers::Simplify(&reduceExpression, App::app()->localContext(), ExpressionNode::ReductionTarget::User, Poincare::ExpressionNode::SymbolicComputation::ReplaceAllDefinedSymbolsWithDefinition, Poincare::ExpressionNode::UnitConversion::None);
+ int currentExpressionIndex = 0;
+ while (currentExpressionIndex < numberOfExpressions) {
+ bool duplicateFound = false;
+ constexpr int buffersSize = Constant::MaxSerializedExpressionSize;
+ char buffer1[buffersSize];
+ int size1 = PoincareHelpers::Serialize(expressions[currentExpressionIndex], buffer1, buffersSize);
+ for (int i = 0; i < currentExpressionIndex + 1; i++) {
+ // Compare the currentExpression to all previous expressions and to m_expression
+ Expression comparedExpression = i == currentExpressionIndex ? reduceExpression : expressions[i];
+ assert(!comparedExpression.isUninitialized());
+ char buffer2[buffersSize];
+ int size2 = PoincareHelpers::Serialize(comparedExpression, buffer2, buffersSize);
+ if (size1 == size2 && strcmp(buffer1, buffer2) == 0) {
+ numberOfExpressions--;
+ // Shift next expressions
+ for (int j = currentExpressionIndex; j < numberOfExpressions; j++) {
+ expressions[j] = expressions[j+1];
+ }
+ // Remove last expression
+ expressions[numberOfExpressions] = Expression();
+ // The current expression has been discarded, no need to increment the current index
+ duplicateFound = true;
+ break;
+ }
+ }
+ if (!duplicateFound) {
+ // The current expression is not a duplicate, check next expression
+ currentExpressionIndex++;
+ }
+ }
+ // Memoize layouts
+ for (size_t i = 0; i < k_maxNumberOfRows; i++) {
+ if (!expressions[i].isUninitialized()) {
+ m_layouts[i] = Shared::PoincareHelpers::CreateLayout(expressions[i]);
+ }
+ }
+}
+
+I18n::Message UnitListController::messageAtIndex(int index) {
+ return (I18n::Message)0;
+}
+
+}
diff --git a/apps/calculation/additional_outputs/unit_list_controller.h b/apps/calculation/additional_outputs/unit_list_controller.h
new file mode 100644
index 00000000000..58f6d1e0d90
--- /dev/null
+++ b/apps/calculation/additional_outputs/unit_list_controller.h
@@ -0,0 +1,21 @@
+#ifndef CALCULATION_ADDITIONAL_OUTPUTS_UNIT_LIST_CONTROLLER_H
+#define CALCULATION_ADDITIONAL_OUTPUTS_UNIT_LIST_CONTROLLER_H
+
+#include "expressions_list_controller.h"
+
+namespace Calculation {
+
+class UnitListController : public ExpressionsListController {
+public:
+ UnitListController(EditExpressionController * editExpressionController) :
+ ExpressionsListController(editExpressionController) {}
+
+ void setExpression(Poincare::Expression e) override;
+
+private:
+ I18n::Message messageAtIndex(int index) override;
+};
+
+}
+
+#endif
diff --git a/apps/calculation/app.cpp b/apps/calculation/app.cpp
new file mode 100644
index 00000000000..8cc0e3c9275
--- /dev/null
+++ b/apps/calculation/app.cpp
@@ -0,0 +1,84 @@
+#include "app.h"
+#include "calculation_icon.h"
+#include
+#include
+
+using namespace Poincare;
+
+using namespace Shared;
+
+namespace Calculation {
+
+I18n::Message App::Descriptor::name() {
+ return I18n::Message::CalculApp;
+}
+
+I18n::Message App::Descriptor::upperName() {
+ return I18n::Message::CalculAppCapital;
+}
+
+const Image * App::Descriptor::icon() {
+ return ImageStore::CalculationIcon;
+}
+
+App * App::Snapshot::unpack(Container * container) {
+ return new (container->currentAppBuffer()) App(this);
+}
+
+void App::Snapshot::reset() {
+ m_calculationStore.deleteAll();
+ m_cacheBuffer[0] = 0;
+ m_cacheBufferInformation = 0;
+}
+
+App::Descriptor * App::Snapshot::descriptor() {
+ static Descriptor descriptor;
+ return &descriptor;
+}
+
+App::Snapshot::Snapshot() : m_calculationStore(m_calculationBuffer, k_calculationBufferSize)
+{
+}
+
+App::App(Snapshot * snapshot) :
+ ExpressionFieldDelegateApp(snapshot, &m_editExpressionController),
+ m_historyController(&m_editExpressionController, snapshot->calculationStore()),
+ m_editExpressionController(&m_modalViewController, this, snapshot->cacheBuffer(), snapshot->cacheBufferInformationAddress(), &m_historyController, snapshot->calculationStore())
+{
+}
+
+bool App::textFieldDidReceiveEvent(::TextField * textField, Ion::Events::Event event) {
+ if (textField->isEditing() && textField->shouldFinishEditing(event) && textField->text()[0] == 0) {
+ return true;
+ }
+ return Shared::ExpressionFieldDelegateApp::textFieldDidReceiveEvent(textField, event);
+}
+
+bool App::layoutFieldDidReceiveEvent(::LayoutField * layoutField, Ion::Events::Event event) {
+ if (layoutField->isEditing() && layoutField->shouldFinishEditing(event) && !layoutField->hasText()) {
+ return true;
+ }
+ return Shared::ExpressionFieldDelegateApp::layoutFieldDidReceiveEvent(layoutField, event);
+}
+
+bool App::isAcceptableExpression(const Poincare::Expression expression) {
+ {
+ Expression ansExpression = static_cast(snapshot())->calculationStore()->ansExpression(localContext());
+ if (!TextFieldDelegateApp::ExpressionCanBeSerialized(expression, true, ansExpression, localContext())) {
+ return false;
+ }
+ }
+ return !(expression.isUninitialized() || expression.type() == ExpressionNode::Type::Equal);
+}
+
+void App::didBecomeActive(Window * window) {
+ m_editExpressionController.restoreInput();
+ Shared::ExpressionFieldDelegateApp::didBecomeActive(window);
+}
+
+void App::willBecomeInactive() {
+ m_editExpressionController.memoizeInput();
+ Shared::ExpressionFieldDelegateApp::willBecomeInactive();
+}
+
+}
diff --git a/apps/calculation/app.h b/apps/calculation/app.h
new file mode 100644
index 00000000000..56cb218541e
--- /dev/null
+++ b/apps/calculation/app.h
@@ -0,0 +1,57 @@
+#ifndef CALCULATION_APP_H
+#define CALCULATION_APP_H
+
+#include "calculation_store.h"
+#include "edit_expression_controller.h"
+#include "history_controller.h"
+#include "../shared/text_field_delegate_app.h"
+#include
+#include "../shared/shared_app.h"
+
+namespace Calculation {
+
+class App : public Shared::ExpressionFieldDelegateApp {
+public:
+ class Descriptor : public ::App::Descriptor {
+ public:
+ I18n::Message name() override;
+ I18n::Message upperName() override;
+ const Image * icon() override;
+ };
+ class Snapshot : public ::SharedApp::Snapshot {
+ public:
+ Snapshot();
+ App * unpack(Container * container) override;
+ void reset() override;
+ Descriptor * descriptor() override;
+ CalculationStore * calculationStore() { return &m_calculationStore; }
+ char * cacheBuffer() { return m_cacheBuffer; }
+ size_t * cacheBufferInformationAddress() { return &m_cacheBufferInformation; }
+ private:
+ CalculationStore m_calculationStore;
+ // Set the size of the buffer needed to store the calculations
+ static constexpr int k_calculationBufferSize = 10 * (sizeof(Calculation) + Calculation::k_numberOfExpressions * Constant::MaxSerializedExpressionSize + sizeof(Calculation *));
+ char m_calculationBuffer[k_calculationBufferSize];
+ char m_cacheBuffer[EditExpressionController::k_cacheBufferSize];
+ size_t m_cacheBufferInformation;
+ };
+ static App * app() {
+ return static_cast(Container::activeApp());
+ }
+ TELEMETRY_ID("Calculation");
+ bool textFieldDidReceiveEvent(::TextField * textField, Ion::Events::Event event) override;
+ bool layoutFieldDidReceiveEvent(::LayoutField * layoutField, Ion::Events::Event event) override;
+ // TextFieldDelegateApp
+ bool isAcceptableExpression(const Poincare::Expression expression) override;
+
+private:
+ App(Snapshot * snapshot);
+ HistoryController m_historyController;
+ void didBecomeActive(Window * window) override;
+ void willBecomeInactive() override;
+ EditExpressionController m_editExpressionController;
+};
+
+}
+
+#endif
diff --git a/apps/calculation/base.de.i18n b/apps/calculation/base.de.i18n
new file mode 100644
index 00000000000..406f27e8499
--- /dev/null
+++ b/apps/calculation/base.de.i18n
@@ -0,0 +1,14 @@
+CalculApp = "Berechnung"
+CalculAppCapital = "BERECHNUNG"
+AdditionalResults = "Weitere Ergebnisse"
+DecimalBase = "Dezimal"
+HexadecimalBase = "Hexadezimal"
+BinaryBase = "Binär"
+PrimeFactors = "Primfaktoren"
+MixedFraction = "Gemischte Zahl"
+EuclideanDivision = "Division mit Rest"
+AdditionalDeterminant = "Determinante"
+AdditionalInverse = "Inverse"
+AdditionalRowEchelonForm = "Stufenform"
+AdditionalReducedRowEchelonForm = "Reduzierte Stufenform"
+AdditionalTrace = "Spur"
\ No newline at end of file
diff --git a/apps/calculation/base.en.i18n b/apps/calculation/base.en.i18n
new file mode 100644
index 00000000000..0e54f24cfd4
--- /dev/null
+++ b/apps/calculation/base.en.i18n
@@ -0,0 +1,14 @@
+CalculApp = "Calculation"
+CalculAppCapital = "CALCULATION"
+AdditionalResults = "Additional results"
+DecimalBase = "Decimal"
+HexadecimalBase = "Hexadecimal"
+BinaryBase = "Binary"
+PrimeFactors = "Prime factors"
+MixedFraction = "Mixed fraction"
+EuclideanDivision = "Euclidean division"
+AdditionalDeterminant = "Determinant"
+AdditionalInverse = "Inverse"
+AdditionalRowEchelonForm = "Row echelon form"
+AdditionalReducedRowEchelonForm = "Reduced row echelon form"
+AdditionalTrace = "Trace"
\ No newline at end of file
diff --git a/apps/calculation/base.es.i18n b/apps/calculation/base.es.i18n
new file mode 100644
index 00000000000..057481a0dbd
--- /dev/null
+++ b/apps/calculation/base.es.i18n
@@ -0,0 +1,14 @@
+CalculApp = "Cálculo"
+CalculAppCapital = "CÁLCULO"
+AdditionalResults = "Resultados adicionales"
+DecimalBase = "Decimal"
+HexadecimalBase = "Hexadecimal"
+BinaryBase = "Binario"
+PrimeFactors = "Factores primos"
+MixedFraction = "Fracción mixta"
+EuclideanDivision = "División euclidiana"
+AdditionalDeterminant = "Determinante"
+AdditionalInverse = "Inversa"
+AdditionalRowEchelonForm = "Matriz escalonada"
+AdditionalReducedRowEchelonForm = "Matriz escalonada reducida"
+AdditionalTrace = "Traza"
\ No newline at end of file
diff --git a/apps/calculation/base.fr.i18n b/apps/calculation/base.fr.i18n
new file mode 100644
index 00000000000..0e155e29474
--- /dev/null
+++ b/apps/calculation/base.fr.i18n
@@ -0,0 +1,14 @@
+CalculApp = "Calculs"
+CalculAppCapital = "CALCULS"
+AdditionalResults = "Résultats complémentaires"
+DecimalBase = "Décimal"
+HexadecimalBase = "Hexadécimal"
+BinaryBase = "Binaire"
+PrimeFactors = "Facteurs premiers"
+MixedFraction = "Fraction mixte"
+EuclideanDivision = "Division euclidienne"
+AdditionalDeterminant = "Déterminant"
+AdditionalInverse = "Inverse"
+AdditionalRowEchelonForm = "Forme échelonnée"
+AdditionalReducedRowEchelonForm = "Forme échelonnée réduite"
+AdditionalTrace = "Trace"
\ No newline at end of file
diff --git a/apps/calculation/base.it.i18n b/apps/calculation/base.it.i18n
new file mode 100644
index 00000000000..ca41028937e
--- /dev/null
+++ b/apps/calculation/base.it.i18n
@@ -0,0 +1,14 @@
+CalculApp = "Calcolo"
+CalculAppCapital = "CALCOLO"
+AdditionalResults = "Risultati complementari"
+DecimalBase = "Decimale"
+HexadecimalBase = "Esadecimale"
+BinaryBase = "Binario"
+PrimeFactors = "Fattorizzazione"
+MixedFraction = "Frazione mista"
+EuclideanDivision = "Divisione euclidea"
+AdditionalDeterminant = "Determinante"
+AdditionalInverse = "Inversa"
+AdditionalRowEchelonForm = "Matrice a scalini"
+AdditionalReducedRowEchelonForm = "Matrice ridotta a scalini"
+AdditionalTrace = "Traccia"
\ No newline at end of file
diff --git a/apps/calculation/base.nl.i18n b/apps/calculation/base.nl.i18n
new file mode 100644
index 00000000000..51e412cb40c
--- /dev/null
+++ b/apps/calculation/base.nl.i18n
@@ -0,0 +1,14 @@
+CalculApp = "Rekenen"
+CalculAppCapital = "REKENEN"
+AdditionalResults = "Aanvullende resultaten"
+DecimalBase = "Decimaal"
+HexadecimalBase = "Hexadecimaal"
+BinaryBase = "Binair"
+PrimeFactors = "Ontbinding"
+MixedFraction = "Gemengde breuk"
+EuclideanDivision = "Geheeltallige deling"
+AdditionalDeterminant = "Determinant"
+AdditionalInverse = "Inverse"
+AdditionalRowEchelonForm = "Echelonvorm"
+AdditionalReducedRowEchelonForm = "Gereduceerde echelonvorm"
+AdditionalTrace = "Spoor"
\ No newline at end of file
diff --git a/apps/calculation/base.pt.i18n b/apps/calculation/base.pt.i18n
new file mode 100644
index 00000000000..941363a04b7
--- /dev/null
+++ b/apps/calculation/base.pt.i18n
@@ -0,0 +1,14 @@
+CalculApp = "Cálculo"
+CalculAppCapital = "CÁLCULO"
+AdditionalResults = "Resultados adicionais"
+DecimalBase = "Decimal"
+HexadecimalBase = "Hexadecimal"
+BinaryBase = "Binário"
+PrimeFactors = "Fatorização"
+MixedFraction = "Fração mista"
+EuclideanDivision = "Divisão euclidiana"
+AdditionalDeterminant = "Determinante"
+AdditionalInverse = "Matriz inversa"
+AdditionalRowEchelonForm = "Matriz escalonada"
+AdditionalReducedRowEchelonForm = "Matriz escalonada reduzida"
+AdditionalTrace = "Traço"
\ No newline at end of file
diff --git a/apps/calculation/calculation.cpp b/apps/calculation/calculation.cpp
new file mode 100644
index 00000000000..c1ee3692ed1
--- /dev/null
+++ b/apps/calculation/calculation.cpp
@@ -0,0 +1,278 @@
+#include "calculation.h"
+#include "../shared/poincare_helpers.h"
+#include "../shared/scrollable_multiple_expressions_view.h"
+#include "../global_preferences.h"
+#include "../exam_mode_configuration.h"
+#include "app.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace Poincare;
+using namespace Shared;
+
+namespace Calculation {
+
+bool Calculation::operator==(const Calculation& c) {
+ return strcmp(inputText(), c.inputText()) == 0
+ && strcmp(approximateOutputText(NumberOfSignificantDigits::Maximal), c.approximateOutputText(NumberOfSignificantDigits::Maximal)) == 0
+ && strcmp(approximateOutputText(NumberOfSignificantDigits::UserDefined), c.approximateOutputText(NumberOfSignificantDigits::UserDefined)) == 0
+ /* Some calculations can make appear trigonometric functions in their
+ * exact output. Their argument will be different with the angle unit
+ * preferences but both input and approximate output will be the same.
+ * For example, i^(sqrt(3)) = cos(sqrt(3)*pi/2)+i*sin(sqrt(3)*pi/2) if
+ * angle unit is radian and i^(sqrt(3)) = cos(sqrt(3)*90+i*sin(sqrt(3)*90)
+ * in degree. */
+ && strcmp(exactOutputText(), c.exactOutputText()) == 0;
+}
+
+Calculation * Calculation::next() const {
+ const char * result = reinterpret_cast(this) + sizeof(Calculation);
+ for (int i = 0; i < k_numberOfExpressions; i++) {
+ result = result + strlen(result) + 1; // Pass inputText, exactOutputText, ApproximateOutputText x2
+ }
+ return reinterpret_cast(const_cast(result));
+}
+
+const char * Calculation::approximateOutputText(NumberOfSignificantDigits numberOfSignificantDigits) const {
+ const char * exactOutput = exactOutputText();
+ const char * approximateOutputTextWithMaxNumberOfDigits = exactOutput + strlen(exactOutput) + 1;
+ if (numberOfSignificantDigits == NumberOfSignificantDigits::Maximal) {
+ return approximateOutputTextWithMaxNumberOfDigits;
+ }
+ return approximateOutputTextWithMaxNumberOfDigits + strlen(approximateOutputTextWithMaxNumberOfDigits) + 1;
+}
+
+Expression Calculation::input() {
+ return Expression::Parse(m_inputText, nullptr);
+}
+
+Expression Calculation::exactOutput() {
+ /* Because the angle unit might have changed, we do not simplify again. We
+ * thereby avoid turning cos(Pi/4) into sqrt(2)/2 and displaying
+ * 'sqrt(2)/2 = 0.999906' (which is totally wrong) instead of
+ * 'cos(pi/4) = 0.999906' (which is true in degree). */
+ Expression exactOutput = Expression::Parse(exactOutputText(), nullptr);
+ assert(!exactOutput.isUninitialized());
+ return exactOutput;
+}
+
+Expression Calculation::approximateOutput(Context * context, NumberOfSignificantDigits numberOfSignificantDigits) {
+ Expression exp = Expression::Parse(approximateOutputText(numberOfSignificantDigits), nullptr);
+ assert(!exp.isUninitialized());
+ /* Warning:
+ * Since quite old versions of Epsilon, the Expression 'exp' was used to be
+ * approximated again to ensure its content was in the expected form - a
+ * linear combination of Decimal.
+ * However, since the approximate output may contain units and that a
+ * Poincare::Unit approximates to undef, thus it must not be approximated
+ * anymore.
+ * We have to keep two serializations of the approximation outputs:
+ * - one with the maximal significant digits, to be used by 'ans' or when
+ * handling 'OK' event on the approximation output.
+ * - one with the displayed number of significant digits that we parse to
+ * create the displayed layout. If we used the other serialization to
+ * create the layout, the result of the parsing could be an Integer which
+ * does not take the number of significant digits into account when creating
+ * its layout. This would lead to wrong number of significant digits in the
+ * layout.
+ * For instance:
+ * Number of asked significant digits: 7
+ * Input: "123456780", Approximate output: "1.234567E8"
+ *
+ * |--------------------------------------------------------------------------------------|
+ * | Number of significant digits | Approximate text | Parse expression | Layout |
+ * |------------------------------+------------------+---------------------+--------------|
+ * | Maximal | "123456780" | Integer(123456780) | "123456780" |
+ * |------------------------------+------------------+---------------------+--------------|
+ * | User defined | "1.234567E8" | Decimal(1.234567E8) | "1.234567E8" |
+ * |--------------------------------------------------------------------------------------|
+ *
+ */
+ return exp;
+}
+
+Layout Calculation::createInputLayout() {
+ return input().createLayout(Preferences::PrintFloatMode::Decimal, PrintFloat::k_numberOfStoredSignificantDigits);
+}
+
+Layout Calculation::createExactOutputLayout(bool * couldNotCreateExactLayout) {
+ Poincare::ExceptionCheckpoint ecp;
+ if (ExceptionRun(ecp)) {
+ return PoincareHelpers::CreateLayout(exactOutput());
+ } else {
+ *couldNotCreateExactLayout = true;
+ return Layout();
+ }
+}
+
+Layout Calculation::createApproximateOutputLayout(Context * context, bool * couldNotCreateApproximateLayout) {
+ Poincare::ExceptionCheckpoint ecp;
+ if (ExceptionRun(ecp)) {
+ return PoincareHelpers::CreateLayout(approximateOutput(context, NumberOfSignificantDigits::UserDefined));
+ } else {
+ *couldNotCreateApproximateLayout = true;
+ return Layout();
+ }
+}
+
+KDCoordinate Calculation::height(bool expanded) {
+ KDCoordinate h = expanded ? m_expandedHeight : m_height;
+ assert(h >= 0);
+ return h;
+}
+
+void Calculation::setHeights(KDCoordinate height, KDCoordinate expandedHeight) {
+ m_height = height;
+ m_expandedHeight = expandedHeight;
+}
+
+Calculation::DisplayOutput Calculation::displayOutput(Context * context) {
+ if (m_displayOutput != DisplayOutput::Unknown) {
+ return m_displayOutput;
+ }
+ if (shouldOnlyDisplayExactOutput()) {
+ m_displayOutput = DisplayOutput::ExactOnly;
+ } else if (
+ /* If the exact and approximate outputs are equal (with the
+ * UserDefined number of significant digits), do not display the exact
+ * output. Indeed, in this case, the layouts are identical. */
+ strcmp(exactOutputText(), approximateOutputText(NumberOfSignificantDigits::UserDefined)) == 0
+ ||
+ // If the approximate output is 'unreal' or the exact result is 'undef'
+ strcmp(exactOutputText(), Undefined::Name()) == 0 ||
+ strcmp(approximateOutputText(NumberOfSignificantDigits::Maximal), Unreal::Name()) == 0
+ ||
+ /* If the approximate output is 'undef' and the input and exactOutput are
+ * equal */
+ (strcmp(approximateOutputText(NumberOfSignificantDigits::Maximal), Undefined::Name()) == 0 &&
+ strcmp(inputText(), exactOutputText()) == 0)
+ ||
+ // Force all outputs to be ApproximateOnly if required by the exam mode configuration
+ ExamModeConfiguration::exactExpressionsAreForbidden(GlobalPreferences::sharedGlobalPreferences()->examMode())
+ ||
+ /* If the input contains the following types, we only display the
+ * approximate output. */
+ input().recursivelyMatches(
+ [](const Expression e, Context * c) {
+ ExpressionNode::Type approximateOnlyTypes[] = {
+ ExpressionNode::Type::Random,
+ ExpressionNode::Type::Unit,
+ ExpressionNode::Type::Round,
+ ExpressionNode::Type::FracPart,
+ ExpressionNode::Type::Integral,
+ ExpressionNode::Type::Product,
+ ExpressionNode::Type::Sum,
+ ExpressionNode::Type::Derivative,
+ ExpressionNode::Type::ConfidenceInterval,
+ ExpressionNode::Type::PredictionInterval,
+ ExpressionNode::Type::Sequence
+ };
+ return e.isOfType(approximateOnlyTypes, sizeof(approximateOnlyTypes)/sizeof(ExpressionNode::Type));
+ }, context)
+ )
+ {
+ m_displayOutput = DisplayOutput::ApproximateOnly;
+ } else if (input().recursivelyMatches(Expression::IsApproximate, context)
+ || exactOutput().recursivelyMatches(Expression::IsApproximate, context))
+ {
+ m_displayOutput = DisplayOutput::ExactAndApproximateToggle;
+ } else {
+ m_displayOutput = DisplayOutput::ExactAndApproximate;
+ }
+ return m_displayOutput;
+}
+
+void Calculation::forceDisplayOutput(DisplayOutput d) {
+ // Heights haven't been computed yet
+ assert(m_height == -1 && m_expandedHeight == -1);
+ m_displayOutput = d;
+}
+
+bool Calculation::shouldOnlyDisplayExactOutput() {
+ /* If the input is a "store in a function", do not display the approximate
+ * result. This prevents x->f(x) from displaying x = undef. */
+ Expression i = input();
+ return i.type() == ExpressionNode::Type::Store
+ && i.childAtIndex(1).type() == ExpressionNode::Type::Function;
+}
+
+Calculation::EqualSign Calculation::exactAndApproximateDisplayedOutputsAreEqual(Poincare::Context * context) {
+ if (m_equalSign != EqualSign::Unknown) {
+ return m_equalSign;
+ }
+ /* Displaying the right equal symbol is less important than displaying a
+ * result, so we do not want exactAndApproximateDisplayedOutputsAreEqual to
+ * create a pool failure that would prevent from displaying a result that we
+ * managed to compute. We thus encapsulate the method in an exception
+ * checkpoint: if there was not enough memory on the pool to compute the equal
+ * sign, just return EqualSign::Approximation.
+ * We can safely use an exception checkpoint here because we are sure of not
+ * modifying any pre-existing node in the pool. We are sure there cannot be a
+ * Store in the exactOutput. */
+ Poincare::ExceptionCheckpoint ecp;
+ if (ExceptionRun(ecp)) {
+ Preferences * preferences = Preferences::sharedPreferences();
+ // TODO: complex format should not be needed here (as it is not used to create layouts)
+ Preferences::ComplexFormat complexFormat = Expression::UpdatedComplexFormatWithTextInput(preferences->complexFormat(), m_inputText);
+ m_equalSign = Expression::ParsedExpressionsAreEqual(exactOutputText(), approximateOutputText(NumberOfSignificantDigits::UserDefined), context, complexFormat, preferences->angleUnit(), GlobalPreferences::sharedGlobalPreferences()->unitFormat()) ? EqualSign::Equal : EqualSign::Approximation;
+ return m_equalSign;
+ } else {
+ /* Do not override m_equalSign in case there is enough room in the pool
+ * later to compute it. */
+ return EqualSign::Approximation;
+ }
+}
+
+Calculation::AdditionalInformationType Calculation::additionalInformationType(Context * context) {
+ if (ExamModeConfiguration::exactExpressionsAreForbidden(GlobalPreferences::sharedGlobalPreferences()->examMode())) {
+ return AdditionalInformationType::None;
+ }
+ Preferences * preferences = Preferences::sharedPreferences();
+ Preferences::ComplexFormat complexFormat = Expression::UpdatedComplexFormatWithTextInput(preferences->complexFormat(), m_inputText);
+ Expression i = input();
+ Expression o = exactOutput();
+ /* Special case for Equal and Store:
+ * Equal/Store nodes have to be at the root of the expression, which prevents
+ * from creating new expressions with equal/store node as a child. We don't
+ * return any additional outputs for them to avoid bothering with special
+ * cases. */
+ if (i.type() == ExpressionNode::Type::Equal || i.type() == ExpressionNode::Type::Store) {
+ return AdditionalInformationType::None;
+ }
+ /* Trigonometry additional results are displayed if either input or output is a sin or a cos. Indeed, we want to capture both cases:
+ * - > input: cos(60)
+ * > output: 1/2
+ * - > input: 2cos(2) - cos(2)
+ * > output: cos(2)
+ */
+ if (input().isDefinedCosineOrSine(context, complexFormat, preferences->angleUnit()) || o.isDefinedCosineOrSine(context, complexFormat, preferences->angleUnit())) {
+ return AdditionalInformationType::Trigonometry;
+ }
+ if (o.hasUnit()) {
+ Expression unit;
+ PoincareHelpers::ReduceAndRemoveUnit(&o, App::app()->localContext(), ExpressionNode::ReductionTarget::User, &unit, ExpressionNode::SymbolicComputation::ReplaceAllSymbolsWithDefinitionsOrUndefined, ExpressionNode::UnitConversion::None);
+ double value = PoincareHelpers::ApproximateToScalar(o, App::app()->localContext());
+ return (Unit::ShouldDisplayAdditionalOutputs(value, unit, GlobalPreferences::sharedGlobalPreferences()->unitFormat())) ? AdditionalInformationType::Unit : AdditionalInformationType::None;
+ }
+ if (o.isBasedIntegerCappedBy(k_maximalIntegerWithAdditionalInformation)) {
+ return AdditionalInformationType::Integer;
+ }
+ // Find forms like [12]/[23] or -[12]/[23]
+ if (o.isDivisionOfIntegers() || (o.type() == ExpressionNode::Type::Opposite && o.childAtIndex(0).isDivisionOfIntegers())) {
+ return AdditionalInformationType::Rational;
+ }
+ if (o.hasDefinedComplexApproximation(context, complexFormat, preferences->angleUnit())) {
+ return AdditionalInformationType::Complex;
+ }
+ if (o.type() == ExpressionNode::Type::Matrix) {
+ return AdditionalInformationType::Matrix;
+ }
+ return AdditionalInformationType::None;
+}
+
+}
diff --git a/apps/calculation/calculation.h b/apps/calculation/calculation.h
new file mode 100644
index 00000000000..be7f87c9bcc
--- /dev/null
+++ b/apps/calculation/calculation.h
@@ -0,0 +1,115 @@
+#ifndef CALCULATION_CALCULATION_H
+#define CALCULATION_CALCULATION_H
+
+#include
+#include
+#include
+#include
+#include "../shared/poincare_helpers.h"
+
+namespace Calculation {
+
+class CalculationStore;
+
+
+/* A calculation is:
+ * | uint8_t |KDCoordinate| KDCoordinate | uint8_t | ... | ... | ... |
+ * |m_displayOutput| m_height |m_expandedHeight|m_equalSign|m_inputText|m_exactOuputText|m_approximateOuputText|
+ *
+ * */
+
+class Calculation {
+friend CalculationStore;
+public:
+ static constexpr int k_numberOfExpressions = 4;
+ enum class EqualSign : uint8_t {
+ Unknown,
+ Approximation,
+ Equal
+ };
+
+ enum class DisplayOutput : uint8_t {
+ Unknown,
+ ExactOnly,
+ ApproximateOnly,
+ ExactAndApproximate,
+ ExactAndApproximateToggle
+ };
+ enum class AdditionalInformationType {
+ None = 0,
+ Integer,
+ Rational,
+ Trigonometry,
+ Unit,
+ Matrix,
+ Complex
+ };
+ static bool DisplaysExact(DisplayOutput d) { return d != DisplayOutput::ApproximateOnly; }
+
+ /* It is not really the minimal size, but it clears enough space for most
+ * calculations instead of clearing less space, then fail to serialize, clear
+ * more space, fail to serialize, clear more space, etc., until reaching
+ * sufficient free space. */
+ static int MinimalSize() { return sizeof(uint8_t) + 2*sizeof(KDCoordinate) + sizeof(uint8_t) + 3*Constant::MaxSerializedExpressionSize + sizeof(Calculation *); }
+
+ Calculation() :
+ m_displayOutput(DisplayOutput::Unknown),
+ m_height(-1),
+ m_expandedHeight(-1),
+ m_equalSign(EqualSign::Unknown)
+ {
+ assert(sizeof(m_inputText) == 0);
+ }
+ bool operator==(const Calculation& c);
+ Calculation * next() const;
+
+ // Texts
+ enum class NumberOfSignificantDigits {
+ Maximal,
+ UserDefined
+ };
+ const char * inputText() const { return m_inputText; }
+ const char * exactOutputText() const { return m_inputText + strlen(m_inputText) + 1; }
+ // See comment in approximateOutput implementation explaining the need of two approximateOutputTexts
+ const char * approximateOutputText(NumberOfSignificantDigits numberOfSignificantDigits) const;
+
+ // Expressions
+ Poincare::Expression input();
+ Poincare::Expression exactOutput();
+ Poincare::Expression approximateOutput(Poincare::Context * context, NumberOfSignificantDigits numberOfSignificantDigits);
+
+ // Layouts
+ Poincare::Layout createInputLayout();
+ Poincare::Layout createExactOutputLayout(bool * couldNotCreateExactLayout);
+ Poincare::Layout createApproximateOutputLayout(Poincare::Context * context, bool * couldNotCreateApproximateLayout);
+
+ // Heights
+ KDCoordinate height(bool expanded);
+
+ // Displayed output
+ DisplayOutput displayOutput(Poincare::Context * context);
+ void forceDisplayOutput(DisplayOutput d);
+ bool shouldOnlyDisplayExactOutput();
+ EqualSign exactAndApproximateDisplayedOutputsAreEqual(Poincare::Context * context);
+
+ // Additional Information
+ AdditionalInformationType additionalInformationType(Poincare::Context * context);
+private:
+ static constexpr KDCoordinate k_heightComputationFailureHeight = 50;
+ static constexpr const char * k_maximalIntegerWithAdditionalInformation = "10000000000000000";
+
+ void setHeights(KDCoordinate height, KDCoordinate expandedHeight);
+
+ /* Buffers holding text expressions have to be longer than the text written
+ * by user (of maximum length TextField::maxBufferSize()) because when we
+ * print an expression we add omitted signs (multiplications, parenthesis...) */
+ DisplayOutput m_displayOutput;
+ KDCoordinate m_height __attribute__((packed));
+ KDCoordinate m_expandedHeight __attribute__((packed));
+ EqualSign m_equalSign;
+ char m_inputText[0]; // MUST be the last member variable
+};
+
+}
+
+#endif
diff --git a/apps/calculation/calculation_icon.png b/apps/calculation/calculation_icon.png
new file mode 100644
index 00000000000..06fcc45baae
Binary files /dev/null and b/apps/calculation/calculation_icon.png differ
diff --git a/apps/calculation/calculation_store.cpp b/apps/calculation/calculation_store.cpp
new file mode 100644
index 00000000000..be2055cedbf
--- /dev/null
+++ b/apps/calculation/calculation_store.cpp
@@ -0,0 +1,225 @@
+#include "calculation_store.h"
+#include "../shared/poincare_helpers.h"
+#include
+#include
+#include
+#include "../exam_mode_configuration.h"
+#include
+
+using namespace Poincare;
+using namespace Shared;
+
+namespace Calculation {
+
+CalculationStore::CalculationStore(char * buffer, int size) :
+ m_buffer(buffer),
+ m_bufferSize(size),
+ m_calculationAreaEnd(m_buffer),
+ m_numberOfCalculations(0)
+{
+ assert(m_buffer != nullptr);
+ assert(m_bufferSize > 0);
+}
+
+// Returns an expiring pointer to the calculation of index i
+ExpiringPointer CalculationStore::calculationAtIndex(int i) {
+ assert(i >= 0 && i < m_numberOfCalculations);
+ // m_buffer is the adress of the oldest calculation in calculation store
+ Calculation * c = (Calculation *) m_buffer;
+ if (i != m_numberOfCalculations-1) {
+ // The calculation we want is not the oldest one so we get its pointer
+ c = *reinterpret_cast(addressOfPointerToCalculationOfIndex(i+1));
+ }
+ return ExpiringPointer(c);
+}
+
+// Pushes an expression in the store
+ExpiringPointer CalculationStore::push(const char * text, Context * context, HeightComputer heightComputer) {
+ /* Compute ans now, before the buffer is updated and before the calculation
+ * might be deleted */
+ Expression ans = ansExpression(context);
+
+ /* Prepare the buffer for the new calculation
+ *The minimal size to store the new calculation is the minimal size of a calculation plus the pointer to its end */
+ int minSize = Calculation::MinimalSize() + sizeof(Calculation *);
+ assert(m_bufferSize > minSize);
+ while (remainingBufferSize() < minSize) {
+ // If there is no more space to store a calculation, we delete the oldest one
+ deleteOldestCalculation();
+ }
+
+ // Getting the adresses of the limits of the free space
+ char * beginingOfFreeSpace = (char *)m_calculationAreaEnd;
+ char * endOfFreeSpace = beginingOfMemoizationArea();
+ char * previousCalc = beginingOfFreeSpace;
+
+ // Add the beginning of the calculation
+ {
+ /* Copy the begining of the calculation. The calculation minimal size is
+ * available, so this memmove will not overide anything. */
+ Calculation newCalc = Calculation();
+ size_t calcSize = sizeof(newCalc);
+ memcpy(beginingOfFreeSpace, &newCalc, calcSize);
+ beginingOfFreeSpace += calcSize;
+ }
+
+ /* Add the input expression.
+ * We do not store directly the text entered by the user because we do not
+ * want to keep Ans symbol in the calculation store. */
+ const char * inputSerialization = beginingOfFreeSpace;
+ {
+ Expression input = Expression::Parse(text, context).replaceSymbolWithExpression(Symbol::Ans(), ans);
+ if (!pushSerializeExpression(input, beginingOfFreeSpace, &endOfFreeSpace)) {
+ /* If the input does not fit in the store (event if the current
+ * calculation is the only calculation), just replace the calculation with
+ * undef. */
+ return emptyStoreAndPushUndef(context, heightComputer);
+ }
+ beginingOfFreeSpace += strlen(beginingOfFreeSpace) + 1;
+ }
+
+ // Compute and serialize the outputs
+ /* The serialized outputs are:
+ * - the exact ouput
+ * - the approximate output with the maximal number of significant digits
+ * - the approximate output with the displayed number of significant digits */
+ {
+ // Outputs hold exact output, approximate output and its duplicate
+ constexpr static int numberOfOutputs = Calculation::k_numberOfExpressions - 1;
+ Expression outputs[numberOfOutputs] = {Expression(), Expression(), Expression()};
+ PoincareHelpers::ParseAndSimplifyAndApproximate(inputSerialization, &(outputs[0]), &(outputs[1]), context, Poincare::ExpressionNode::SymbolicComputation::ReplaceAllSymbolsWithDefinitionsOrUndefined);
+ if (ExamModeConfiguration::exactExpressionsAreForbidden(GlobalPreferences::sharedGlobalPreferences()->examMode()) && outputs[1].hasUnit()) {
+ // Hide results with units on units if required by the exam mode configuration
+ outputs[1] = Undefined::Builder();
+ }
+ outputs[2] = outputs[1];
+ int numberOfSignificantDigits = Poincare::PrintFloat::k_numberOfStoredSignificantDigits;
+ for (int i = 0; i < numberOfOutputs; i++) {
+ if (i == numberOfOutputs - 1) {
+ numberOfSignificantDigits = Poincare::Preferences::sharedPreferences()->numberOfSignificantDigits();
+ }
+ if (!pushSerializeExpression(outputs[i], beginingOfFreeSpace, &endOfFreeSpace, numberOfSignificantDigits)) {
+ /* If the exat/approximate output does not fit in the store (event if the
+ * current calculation is the only calculation), replace the output with
+ * undef if it fits, else replace the whole calcualtion with undef. */
+ Expression undef = Undefined::Builder();
+ if (!pushSerializeExpression(undef, beginingOfFreeSpace, &endOfFreeSpace)) {
+ return emptyStoreAndPushUndef(context, heightComputer);
+ }
+ }
+ beginingOfFreeSpace += strlen(beginingOfFreeSpace) + 1;
+ }
+ }
+ // Storing the pointer of the end of the new calculation
+ memcpy(endOfFreeSpace-sizeof(Calculation*),&beginingOfFreeSpace,sizeof(beginingOfFreeSpace));
+
+ // The new calculation is now stored
+ m_numberOfCalculations++;
+
+ // The end of the calculation storage area is updated
+ m_calculationAreaEnd += beginingOfFreeSpace - previousCalc;
+ ExpiringPointer calculation = ExpiringPointer(reinterpret_cast(previousCalc));
+ /* Heights are computed now to make sure that the display output is decided
+ * accordingly to the remaining size in the Poincare pool. Once it is, it
+ * can't change anymore: the calculation heights are fixed which ensures that
+ * scrolling computation is right. */
+ calculation->setHeights(
+ heightComputer(calculation.pointer(), false),
+ heightComputer(calculation.pointer(), true));
+ return calculation;
+}
+
+// Delete the calculation of index i
+void CalculationStore::deleteCalculationAtIndex(int i) {
+ assert(i >= 0 && i < m_numberOfCalculations);
+ if (i == 0) {
+ ExpiringPointer lastCalculationPointer = calculationAtIndex(0);
+ m_calculationAreaEnd = (char *)(lastCalculationPointer.pointer());
+ m_numberOfCalculations--;
+ return;
+ }
+ char * calcI = (char *)calculationAtIndex(i).pointer();
+ char * nextCalc = (char *) calculationAtIndex(i-1).pointer();
+ assert(m_calculationAreaEnd >= nextCalc);
+ size_t slidingSize = m_calculationAreaEnd - nextCalc;
+ // Slide the i-1 most recent calculations right after the i+1'th
+ memmove(calcI, nextCalc, slidingSize);
+ m_calculationAreaEnd -= nextCalc - calcI;
+ // Recompute pointer to calculations after the i'th
+ recomputeMemoizedPointersAfterCalculationIndex(i);
+ m_numberOfCalculations--;
+}
+
+// Delete the oldest calculation in the store and returns the amount of space freed by the operation
+size_t CalculationStore::deleteOldestCalculation() {
+ char * oldBufferEnd = (char *) m_calculationAreaEnd;
+ deleteCalculationAtIndex(numberOfCalculations()-1);
+ char * newBufferEnd = (char *) m_calculationAreaEnd;
+ return oldBufferEnd - newBufferEnd;
+}
+
+// Delete all calculations
+void CalculationStore::deleteAll() {
+ m_calculationAreaEnd = m_buffer;
+ m_numberOfCalculations = 0;
+}
+
+// Replace "Ans" by its expression
+Expression CalculationStore::ansExpression(Context * context) {
+ if (numberOfCalculations() == 0) {
+ return Rational::Builder(0);
+ }
+ ExpiringPointer mostRecentCalculation = calculationAtIndex(0);
+ /* Special case: the exact output is a Store/Equal expression.
+ * Store/Equal expression can only be at the root of an expression.
+ * To avoid turning 'ans->A' in '2->A->A' or '2=A->A' (which cannot be
+ * parsed), ans is replaced by the approximation output when any Store or
+ * Equal expression appears. */
+ Expression e = mostRecentCalculation->exactOutput();
+ bool exactOuptutInvolvesStoreEqual = e.type() == ExpressionNode::Type::Store || e.type() == ExpressionNode::Type::Equal;
+ if (mostRecentCalculation->input().recursivelyMatches(Expression::IsApproximate, context) || exactOuptutInvolvesStoreEqual) {
+ return mostRecentCalculation->approximateOutput(context, Calculation::NumberOfSignificantDigits::Maximal);
+ }
+ return mostRecentCalculation->exactOutput();
+}
+
+// Push converted expression in the buffer
+bool CalculationStore::pushSerializeExpression(Expression e, char * location, char * * newCalculationsLocation, int numberOfSignificantDigits) {
+ assert(*newCalculationsLocation <= m_buffer + m_bufferSize);
+ bool expressionIsPushed = false;
+ while (true) {
+ size_t locationSize = *newCalculationsLocation - location;
+ expressionIsPushed = (PoincareHelpers::Serialize(e, location, locationSize, numberOfSignificantDigits) < (int)locationSize-1);
+ if (expressionIsPushed || *newCalculationsLocation >= m_buffer + m_bufferSize) {
+ break;
+ }
+ *newCalculationsLocation = *newCalculationsLocation + deleteOldestCalculation();
+ assert(*newCalculationsLocation <= m_buffer + m_bufferSize);
+ }
+ return expressionIsPushed;
+}
+
+
+
+Shared::ExpiringPointer CalculationStore::emptyStoreAndPushUndef(Context * context, HeightComputer heightComputer) {
+ /* We end up here as a result of a failed calculation push. The store
+ * attributes are not necessarily clean, so we need to reset them. */
+ deleteAll();
+ return push(Undefined::Name(), context, heightComputer);
+}
+
+// Recompute memoized pointers to the calculations after index i
+void CalculationStore::recomputeMemoizedPointersAfterCalculationIndex(int index) {
+ assert(index < m_numberOfCalculations);
+ // Clear pointer and recompute new ones
+ Calculation * c = calculationAtIndex(index).pointer();
+ Calculation * nextCalc;
+ while (index != 0) {
+ nextCalc = c->next();
+ memcpy(addressOfPointerToCalculationOfIndex(index), &nextCalc, sizeof(Calculation *));
+ c = nextCalc;
+ index--;
+ }
+}
+
+}
diff --git a/apps/calculation/calculation_store.h b/apps/calculation/calculation_store.h
new file mode 100644
index 00000000000..53bc3fdf4f6
--- /dev/null
+++ b/apps/calculation/calculation_store.h
@@ -0,0 +1,84 @@
+#ifndef CALCULATION_CALCULATION_STORE_H
+#define CALCULATION_CALCULATION_STORE_H
+
+#include "calculation.h"
+#include
+#include
+
+namespace Calculation {
+
+/*
+ To optimize the storage space, we use one big buffer for all calculations.
+ The calculations are stored one after another while pointers to the end of each
+ calculation are stored at the end of the buffer, in the opposite direction.
+ By doing so, we can memoize every calculation entered while not limiting
+ the number of calculation stored in the buffer.
+
+ If the remaining space is too small for storing a new calculation, we
+ delete the oldest one.
+
+ Memory layout :
+ <- Available space for new calculations ->
++--------------------------------------------------------------------------------------------------------------------+
+| | | | | | | | | |
+| Calculation 3 | Calculation 2 | Calculation 1 | Calculation O | |p0|p1|p2|p3|
+| Oldest | | | | | | | | |
++--------------------------------------------------------------------------------------------------------------------+
+^ ^ ^ ^ ^ ^
+m_buffer p3 p2 p1 p0 a
+
+m_calculationAreaEnd = p0
+a = addressOfPointerToCalculation(0)
+*/
+
+class CalculationStore {
+public:
+ CalculationStore();
+ CalculationStore(char * buffer, int size);
+ Shared::ExpiringPointer calculationAtIndex(int i);
+ typedef KDCoordinate (*HeightComputer)(Calculation * c, bool expanded);
+ Shared::ExpiringPointer push(const char * text, Poincare::Context * context, HeightComputer heightComputer);
+ void deleteCalculationAtIndex(int i);
+ void deleteAll();
+ int remainingBufferSize() const { assert(m_calculationAreaEnd >= m_buffer); return m_bufferSize - (m_calculationAreaEnd - m_buffer) - m_numberOfCalculations*sizeof(Calculation*); }
+ int numberOfCalculations() const { return m_numberOfCalculations; }
+ Poincare::Expression ansExpression(Poincare::Context * context);
+ int bufferSize() { return m_bufferSize; }
+
+private:
+
+ class CalculationIterator {
+ public:
+ CalculationIterator(const char * c) : m_calculation(reinterpret_cast(const_cast(c))) {}
+ Calculation * operator*() { return m_calculation; }
+ bool operator!=(const CalculationIterator& it) const { return (m_calculation != it.m_calculation); }
+ CalculationIterator & operator++() {
+ m_calculation = m_calculation->next();
+ return *this;
+ }
+ protected:
+ Calculation * m_calculation;
+ };
+
+ CalculationIterator begin() const { return CalculationIterator(m_buffer); }
+ CalculationIterator end() const { return CalculationIterator(m_calculationAreaEnd); }
+
+ bool pushSerializeExpression(Poincare::Expression e, char * location, char * * newCalculationsLocation, int numberOfSignificantDigits = Poincare::PrintFloat::k_numberOfStoredSignificantDigits);
+ Shared::ExpiringPointer emptyStoreAndPushUndef(Poincare::Context * context, HeightComputer heightComputer);
+
+ char * m_buffer;
+ int m_bufferSize;
+ const char * m_calculationAreaEnd;
+ int m_numberOfCalculations;
+
+ size_t deleteOldestCalculation();
+ char * addressOfPointerToCalculationOfIndex(int i) {return m_buffer + m_bufferSize - (m_numberOfCalculations - i)*sizeof(Calculation *);}
+
+ // Memoization
+ char * beginingOfMemoizationArea() {return addressOfPointerToCalculationOfIndex(0);};
+ void recomputeMemoizedPointersAfterCalculationIndex(int index);
+};
+
+}
+
+#endif
diff --git a/apps/calculation/edit_expression_controller.cpp b/apps/calculation/edit_expression_controller.cpp
new file mode 100644
index 00000000000..7f747d3598f
--- /dev/null
+++ b/apps/calculation/edit_expression_controller.cpp
@@ -0,0 +1,173 @@
+#include "edit_expression_controller.h"
+#include "app.h"
+#include
+#include
+#include
+
+using namespace Shared;
+using namespace Poincare;
+
+namespace Calculation {
+
+EditExpressionController::ContentView::ContentView(Responder * parentResponder, CalculationSelectableTableView * subview, InputEventHandlerDelegate * inputEventHandlerDelegate, TextFieldDelegate * textFieldDelegate, LayoutFieldDelegate * layoutFieldDelegate) :
+ View(),
+ m_mainView(subview),
+ m_expressionField(parentResponder, inputEventHandlerDelegate, textFieldDelegate, layoutFieldDelegate)
+{
+}
+
+View * EditExpressionController::ContentView::subviewAtIndex(int index) {
+ assert(index >= 0 && index < numberOfSubviews());
+ if (index == 0) {
+ return m_mainView;
+ }
+ assert(index == 1);
+ return &m_expressionField;
+}
+
+void EditExpressionController::ContentView::layoutSubviews(bool force) {
+ KDCoordinate inputViewFrameHeight = m_expressionField.minimalSizeForOptimalDisplay().height();
+ KDRect mainViewFrame(0, 0, bounds().width(), bounds().height() - inputViewFrameHeight);
+ m_mainView->setFrame(mainViewFrame, force);
+ KDRect inputViewFrame(0, bounds().height() - inputViewFrameHeight, bounds().width(), inputViewFrameHeight);
+ m_expressionField.setFrame(inputViewFrame, force);
+}
+
+void EditExpressionController::ContentView::reload() {
+ layoutSubviews();
+ markRectAsDirty(bounds());
+}
+
+EditExpressionController::EditExpressionController(Responder * parentResponder, InputEventHandlerDelegate * inputEventHandlerDelegate, char * cacheBuffer, size_t * cacheBufferInformation, HistoryController * historyController, CalculationStore * calculationStore) :
+ ViewController(parentResponder),
+ m_cacheBuffer(cacheBuffer),
+ m_cacheBufferInformation(cacheBufferInformation),
+ m_historyController(historyController),
+ m_calculationStore(calculationStore),
+ m_contentView(this, static_cast(m_historyController->view()), inputEventHandlerDelegate, this, this)
+{
+}
+
+void EditExpressionController::insertTextBody(const char * text) {
+ Container::activeApp()->setFirstResponder(this);
+ m_contentView.expressionField()->handleEventWithText(text, false, true);
+}
+
+void EditExpressionController::didBecomeFirstResponder() {
+ m_contentView.mainView()->scrollToBottom();
+ m_contentView.expressionField()->setEditing(true, false);
+ Container::activeApp()->setFirstResponder(m_contentView.expressionField());
+}
+
+void EditExpressionController::restoreInput() {
+ m_contentView.expressionField()->restoreContent(m_cacheBuffer, *m_cacheBufferInformation);
+ clearCacheBuffer();
+}
+
+void EditExpressionController::memoizeInput() {
+ *m_cacheBufferInformation = m_contentView.expressionField()->moveCursorAndDumpContent(m_cacheBuffer, k_cacheBufferSize);
+}
+
+void EditExpressionController::viewWillAppear() {
+ m_historyController->viewWillAppear();
+}
+
+bool EditExpressionController::textFieldDidReceiveEvent(::TextField * textField, Ion::Events::Event event) {
+ bool shouldDuplicateLastCalculation = textField->isEditing() && textField->shouldFinishEditing(event) && textField->draftTextLength() == 0;
+ if (inputViewDidReceiveEvent(event, shouldDuplicateLastCalculation)) {
+ return true;
+ }
+ return textFieldDelegateApp()->textFieldDidReceiveEvent(textField, event);
+}
+
+bool EditExpressionController::textFieldDidFinishEditing(::TextField * textField, const char * text, Ion::Events::Event event) {
+ return inputViewDidFinishEditing(text, nullptr);
+}
+
+bool EditExpressionController::textFieldDidAbortEditing(::TextField * textField) {
+ return inputViewDidAbortEditing(textField->text());
+}
+
+bool EditExpressionController::layoutFieldDidReceiveEvent(::LayoutField * layoutField, Ion::Events::Event event) {
+ bool shouldDuplicateLastCalculation = layoutField->isEditing() && layoutField->shouldFinishEditing(event) && !layoutField->hasText();
+ if (inputViewDidReceiveEvent(event, shouldDuplicateLastCalculation)) {
+ return true;
+ }
+ return expressionFieldDelegateApp()->layoutFieldDidReceiveEvent(layoutField, event);
+}
+
+bool EditExpressionController::layoutFieldDidFinishEditing(::LayoutField * layoutField, Layout layoutR, Ion::Events::Event event) {
+ return inputViewDidFinishEditing(nullptr, layoutR);
+}
+
+bool EditExpressionController::layoutFieldDidAbortEditing(::LayoutField * layoutField) {
+ return inputViewDidAbortEditing(nullptr);
+}
+
+void EditExpressionController::layoutFieldDidChangeSize(::LayoutField * layoutField) {
+ if (m_contentView.expressionField()->inputViewHeightDidChange()) {
+ /* Reload the whole view only if the ExpressionField's height did actually
+ * change. */
+ reloadView();
+ } else {
+ /* The input view is already at maximal size so we do not need to relayout
+ * the view underneath, but the view inside the input view might still need
+ * to be relayouted.
+ * We force the relayout because the frame stays the same but we need to
+ * propagate a relayout to the content of the field scroll view. */
+ m_contentView.expressionField()->layoutSubviews(true);
+ }
+}
+
+void EditExpressionController::reloadView() {
+ m_contentView.reload();
+ m_historyController->reload();
+}
+
+bool EditExpressionController::inputViewDidReceiveEvent(Ion::Events::Event event, bool shouldDuplicateLastCalculation) {
+ if (shouldDuplicateLastCalculation && m_cacheBuffer[0] != 0) {
+ /* The input text store in m_cacheBuffer might have been correct the first
+ * time but then be too long when replacing ans in another context */
+ Shared::TextFieldDelegateApp * myApp = textFieldDelegateApp();
+ if (!myApp->isAcceptableText(m_cacheBuffer)) {
+ return true;
+ }
+ m_calculationStore->push(m_cacheBuffer, myApp->localContext(), HistoryViewCell::Height);
+ m_historyController->reload();
+ return true;
+ }
+ if (event == Ion::Events::Up) {
+ if (m_calculationStore->numberOfCalculations() > 0) {
+ clearCacheBuffer();
+ m_contentView.expressionField()->setEditing(false, false);
+ Container::activeApp()->setFirstResponder(m_historyController);
+ }
+ return true;
+ }
+ return false;
+}
+
+bool EditExpressionController::inputViewDidFinishEditing(const char * text, Layout layoutR) {
+ Context * context = textFieldDelegateApp()->localContext();
+ if (layoutR.isUninitialized()) {
+ assert(text);
+ strlcpy(m_cacheBuffer, text, k_cacheBufferSize);
+ } else {
+ layoutR.serializeParsedExpression(m_cacheBuffer, k_cacheBufferSize, context);
+ }
+ m_calculationStore->push(m_cacheBuffer, context, HistoryViewCell::Height);
+ m_historyController->reload();
+ m_contentView.expressionField()->setEditing(true, true);
+ telemetryReportEvent("Input", m_cacheBuffer);
+ return true;
+}
+
+bool EditExpressionController::inputViewDidAbortEditing(const char * text) {
+ if (text != nullptr) {
+ m_contentView.expressionField()->setEditing(true, true);
+ m_contentView.expressionField()->setText(text);
+ }
+ return false;
+}
+
+}
diff --git a/apps/calculation/edit_expression_controller.h b/apps/calculation/edit_expression_controller.h
new file mode 100644
index 00000000000..32a6ec84cb2
--- /dev/null
+++ b/apps/calculation/edit_expression_controller.h
@@ -0,0 +1,74 @@
+#ifndef CALCULATION_EDIT_EXPRESSION_CONTROLLER_H
+#define CALCULATION_EDIT_EXPRESSION_CONTROLLER_H
+
+#include
+#include
+#include "expression_field.h"
+#include "../shared/text_field_delegate.h"
+#include "../shared/layout_field_delegate.h"
+#include "history_controller.h"
+#include "selectable_table_view.h"
+
+namespace Calculation {
+
+/* TODO: implement a split view */
+class EditExpressionController : public ViewController, public Shared::TextFieldDelegate, public Shared::LayoutFieldDelegate {
+public:
+ EditExpressionController(Responder * parentResponder, InputEventHandlerDelegate * inputEventHandlerDelegate, char * cacheBuffer, size_t * cacheBufferInformation, HistoryController * historyController, CalculationStore * calculationStore);
+
+ /* k_layoutBufferMaxSize dictates the size under which the expression being
+ * edited can be remembered when the user leaves Calculation. */
+ static constexpr int k_layoutBufferMaxSize = 1024;
+ /* k_cacheBufferSize is the size of the array to which m_cacheBuffer points.
+ * It is used both as a way to buffer expression when pushing them the
+ * CalculationStore, and as a storage for the current input when leaving the
+ * application. */
+ static constexpr int k_cacheBufferSize = (k_layoutBufferMaxSize < Constant::MaxSerializedExpressionSize) ? Constant::MaxSerializedExpressionSize : k_layoutBufferMaxSize;
+
+ View * view() override { return &m_contentView; }
+ void didBecomeFirstResponder() override;
+ void viewWillAppear() override;
+ void insertTextBody(const char * text);
+ void restoreInput();
+ void memoizeInput();
+
+ /* TextFieldDelegate */
+ bool textFieldDidReceiveEvent(::TextField * textField, Ion::Events::Event event) override;
+ bool textFieldDidFinishEditing(::TextField * textField, const char * text, Ion::Events::Event event) override;
+ bool textFieldDidAbortEditing(::TextField * textField) override;
+
+ /* LayoutFieldDelegate */
+ bool layoutFieldDidReceiveEvent(::LayoutField * layoutField, Ion::Events::Event event) override;
+ bool layoutFieldDidFinishEditing(::LayoutField * layoutField, Poincare::Layout layoutR, Ion::Events::Event event) override;
+ bool layoutFieldDidAbortEditing(::LayoutField * layoutField) override;
+ void layoutFieldDidChangeSize(::LayoutField * layoutField) override;
+
+private:
+ class ContentView : public View {
+ public:
+ ContentView(Responder * parentResponder, CalculationSelectableTableView * subview, InputEventHandlerDelegate * inputEventHandlerDelegate, TextFieldDelegate * textFieldDelegate, LayoutFieldDelegate * layoutFieldDelegate);
+ void reload();
+ CalculationSelectableTableView * mainView() { return m_mainView; }
+ ExpressionField * expressionField() { return &m_expressionField; }
+ private:
+ int numberOfSubviews() const override { return 2; }
+ View * subviewAtIndex(int index) override;
+ void layoutSubviews(bool force = false) override;
+ CalculationSelectableTableView * m_mainView;
+ ExpressionField m_expressionField;
+ };
+ void reloadView();
+ void clearCacheBuffer() { m_cacheBuffer[0] = 0; *m_cacheBufferInformation = 0; }
+ bool inputViewDidReceiveEvent(Ion::Events::Event event, bool shouldDuplicateLastCalculation);
+ bool inputViewDidFinishEditing(const char * text, Poincare::Layout layoutR);
+ bool inputViewDidAbortEditing(const char * text);
+ char * m_cacheBuffer;
+ size_t * m_cacheBufferInformation;
+ HistoryController * m_historyController;
+ CalculationStore * m_calculationStore;
+ ContentView m_contentView;
+};
+
+}
+
+#endif
diff --git a/apps/calculation/expression_field.cpp b/apps/calculation/expression_field.cpp
new file mode 100644
index 00000000000..a190a9558d1
--- /dev/null
+++ b/apps/calculation/expression_field.cpp
@@ -0,0 +1,26 @@
+#include "expression_field.h"
+#include
+
+namespace Calculation {
+
+bool ExpressionField::handleEvent(Ion::Events::Event event) {
+ if (event == Ion::Events::Back) {
+ return false;
+ }
+ if (event == Ion::Events::Ans) {
+ handleEventWithText(Poincare::Symbol::k_ans);
+ return true;
+ }
+ if (isEditing() && isEmpty() &&
+ (event == Ion::Events::Multiplication ||
+ event == Ion::Events::Plus ||
+ event == Ion::Events::Power ||
+ event == Ion::Events::Square ||
+ event == Ion::Events::Division ||
+ event == Ion::Events::Sto)) {
+ handleEventWithText(Poincare::Symbol::k_ans);
+ }
+ return(::ExpressionField::handleEvent(event));
+}
+
+}
diff --git a/apps/calculation/expression_field.h b/apps/calculation/expression_field.h
new file mode 100644
index 00000000000..2844046286b
--- /dev/null
+++ b/apps/calculation/expression_field.h
@@ -0,0 +1,20 @@
+#ifndef CALCULATION_EXPRESSION_FIELD_H
+#define CALCULATION_EXPRESSION_FIELD_H
+
+#include
+
+namespace Calculation {
+
+class ExpressionField : public ::ExpressionField {
+public:
+ ExpressionField(Responder * parentResponder, InputEventHandlerDelegate * inputEventHandler, TextFieldDelegate * textFieldDelegate, LayoutFieldDelegate * layoutFieldDelegate) :
+ ::ExpressionField(parentResponder, inputEventHandler, textFieldDelegate, layoutFieldDelegate) {
+ setLayoutInsertionCursorEvent(Ion::Events::Up);
+ }
+protected:
+ bool handleEvent(Ion::Events::Event event) override;
+};
+
+}
+
+#endif
diff --git a/apps/calculation/history_controller.cpp b/apps/calculation/history_controller.cpp
new file mode 100644
index 00000000000..6d51e7862f9
--- /dev/null
+++ b/apps/calculation/history_controller.cpp
@@ -0,0 +1,256 @@
+#include "history_controller.h"
+#include "app.h"
+#include
+#include
+
+using namespace Shared;
+using namespace Poincare;
+
+namespace Calculation {
+
+HistoryController::HistoryController(EditExpressionController * editExpressionController, CalculationStore * calculationStore) :
+ ViewController(editExpressionController),
+ m_selectableTableView(this, this, this, this),
+ m_calculationHistory{},
+ m_calculationStore(calculationStore),
+ m_complexController(editExpressionController),
+ m_integerController(editExpressionController),
+ m_rationalController(editExpressionController),
+ m_trigonometryController(editExpressionController),
+ m_unitController(editExpressionController),
+ m_matrixController(editExpressionController)
+{
+ for (int i = 0; i < k_maxNumberOfDisplayedRows; i++) {
+ m_calculationHistory[i].setParentResponder(&m_selectableTableView);
+ m_calculationHistory[i].setDataSource(this);
+ }
+}
+
+void HistoryController::reload() {
+ /* When reloading, we might not used anymore cell that hold previous layouts.
+ * We clean them all before reloading their content to avoid taking extra
+ * useless space in the Poincare pool. */
+ for (int i = 0; i < k_maxNumberOfDisplayedRows; i++) {
+ m_calculationHistory[i].resetMemoization();
+ }
+
+ m_selectableTableView.reloadData();
+ /* TODO
+ * Replace the following by selectCellAtLocation in order to avoid laying out
+ * the table view twice.
+ */
+ if (numberOfRows() > 0) {
+ m_selectableTableView.scrollToBottom();
+ // Force to reload last added cell (hide the burger and exact output if necessary)
+ tableViewDidChangeSelectionAndDidScroll(&m_selectableTableView, 0, numberOfRows()-1);
+ }
+}
+
+void HistoryController::viewWillAppear() {
+ ViewController::viewWillAppear();
+ reload();
+}
+
+void HistoryController::didBecomeFirstResponder() {
+ selectCellAtLocation(0, numberOfRows()-1);
+ Container::activeApp()->setFirstResponder(&m_selectableTableView);
+}
+
+void HistoryController::willExitResponderChain(Responder * nextFirstResponder) {
+ if (nextFirstResponder == nullptr) {
+ return;
+ }
+ if (nextFirstResponder == parentResponder()) {
+ m_selectableTableView.deselectTable();
+ }
+}
+
+bool HistoryController::handleEvent(Ion::Events::Event event) {
+ if (event == Ion::Events::Down) {
+ m_selectableTableView.deselectTable();
+ Container::activeApp()->setFirstResponder(parentResponder());
+ return true;
+ }
+ if (event == Ion::Events::Up) {
+ return true;
+ }
+ if (event == Ion::Events::OK || event == Ion::Events::EXE) {
+ int focusRow = selectedRow();
+ HistoryViewCell * selectedCell = (HistoryViewCell *)m_selectableTableView.selectedCell();
+ SubviewType subviewType = selectedSubviewType();
+ EditExpressionController * editController = (EditExpressionController *)parentResponder();
+ if (subviewType == SubviewType::Input) {
+ m_selectableTableView.deselectTable();
+ editController->insertTextBody(calculationAtIndex(focusRow)->inputText());
+ } else if (subviewType == SubviewType::Output) {
+ m_selectableTableView.deselectTable();
+ Shared::ExpiringPointer calculation = calculationAtIndex(focusRow);
+ ScrollableTwoExpressionsView::SubviewPosition outputSubviewPosition = selectedCell->outputView()->selectedSubviewPosition();
+ if (outputSubviewPosition == ScrollableTwoExpressionsView::SubviewPosition::Right
+ && !calculation->shouldOnlyDisplayExactOutput())
+ {
+ editController->insertTextBody(calculation->approximateOutputText(Calculation::NumberOfSignificantDigits::Maximal));
+ } else {
+ editController->insertTextBody(calculation->exactOutputText());
+ }
+ } else {
+ assert(subviewType == SubviewType::Ellipsis);
+ Calculation::AdditionalInformationType additionalInfoType = selectedCell->additionalInformationType();
+ ListController * vc = nullptr;
+ Expression e = calculationAtIndex(focusRow)->exactOutput();
+ if (additionalInfoType == Calculation::AdditionalInformationType::Complex) {
+ vc = &m_complexController;
+ } else if (additionalInfoType == Calculation::AdditionalInformationType::Trigonometry) {
+ vc = &m_trigonometryController;
+ // Find which of the input or output is the cosine/sine
+ ExpressionNode::Type t = e.type();
+ e = t == ExpressionNode::Type::Cosine || t == ExpressionNode::Type::Sine ? e : calculationAtIndex(focusRow)->input();
+ } else if (additionalInfoType == Calculation::AdditionalInformationType::Integer) {
+ vc = &m_integerController;
+ } else if (additionalInfoType == Calculation::AdditionalInformationType::Rational) {
+ vc = &m_rationalController;
+ } else if (additionalInfoType == Calculation::AdditionalInformationType::Unit) {
+ vc = &m_unitController;
+ } else if (additionalInfoType == Calculation::AdditionalInformationType::Matrix) {
+ vc = &m_matrixController;
+ }
+ if (vc) {
+ vc->setExpression(e);
+ Container::activeApp()->displayModalViewController(vc, 0.f, 0.f, Metric::CommonTopMargin, Metric::PopUpLeftMargin, 0, Metric::PopUpRightMargin);
+ }
+ }
+ return true;
+ }
+ if (event == Ion::Events::Backspace) {
+ int focusRow = selectedRow();
+ SubviewType subviewType = selectedSubviewType();
+ m_selectableTableView.deselectTable();
+ m_calculationStore->deleteCalculationAtIndex(storeIndex(focusRow));
+ reload();
+ if (numberOfRows()== 0) {
+ Container::activeApp()->setFirstResponder(parentResponder());
+ return true;
+ }
+ m_selectableTableView.selectCellAtLocation(0, focusRow > 0 ? focusRow - 1 : 0);
+ /* The parameters 'sameCell' and 'previousSelectedY' are chosen to enforce
+ * toggling of the output when necessary. */
+ setSelectedSubviewType(subviewType, false, 0, (subviewType == SubviewType::Input) ? selectedRow() : -1);
+ return true;
+ }
+ if (event == Ion::Events::Clear) {
+ m_selectableTableView.deselectTable();
+ m_calculationStore->deleteAll();
+ reload();
+ Container::activeApp()->setFirstResponder(parentResponder());
+ return true;
+ }
+ if (event == Ion::Events::Back) {
+ m_selectableTableView.deselectTable();
+ Container::activeApp()->setFirstResponder(parentResponder());
+ return true;
+ }
+ return false;
+}
+
+Shared::ExpiringPointer HistoryController::calculationAtIndex(int i) {
+ return m_calculationStore->calculationAtIndex(storeIndex(i));
+}
+
+void HistoryController::tableViewDidChangeSelectionAndDidScroll(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY, bool withinTemporarySelection) {
+ if (withinTemporarySelection || previousSelectedCellY == selectedRow()) {
+ return;
+ }
+ if (previousSelectedCellY == -1) {
+ setSelectedSubviewType(SubviewType::Output, false, previousSelectedCellX, previousSelectedCellY);
+ } else if (selectedRow() == -1) {
+ setSelectedSubviewType(SubviewType::Input, false, previousSelectedCellX, previousSelectedCellY);
+ } else {
+ HistoryViewCell * selectedCell = (HistoryViewCell *)(t->selectedCell());
+ SubviewType nextSelectedSubviewType = selectedSubviewType();
+ if (selectedCell && !selectedCell->displaysSingleLine()) {
+ nextSelectedSubviewType = previousSelectedCellY < selectedRow() ? SubviewType::Input : SubviewType::Output;
+ }
+ setSelectedSubviewType(nextSelectedSubviewType, false, previousSelectedCellX, previousSelectedCellY);
+ }
+ // The selectedCell may change during setSelectedSubviewType
+ HistoryViewCell * selectedCell = (HistoryViewCell *)(t->selectedCell());
+ if (selectedCell == nullptr) {
+ return;
+ }
+ Container::activeApp()->setFirstResponder(selectedCell);
+}
+
+int HistoryController::numberOfRows() const {
+ return m_calculationStore->numberOfCalculations();
+};
+
+HighlightCell * HistoryController::reusableCell(int index, int type) {
+ assert(type == 0);
+ assert(index >= 0);
+ assert(index < k_maxNumberOfDisplayedRows);
+ return &m_calculationHistory[index];
+}
+
+int HistoryController::reusableCellCount(int type) {
+ assert(type == 0);
+ return k_maxNumberOfDisplayedRows;
+}
+
+void HistoryController::willDisplayCellForIndex(HighlightCell * cell, int index) {
+ HistoryViewCell * myCell = (HistoryViewCell *)cell;
+ myCell->setCalculation(calculationAtIndex(index).pointer(), index == selectedRow() && selectedSubviewType() == SubviewType::Output);
+ myCell->setEven(index%2 == 0);
+ myCell->reloadSubviewHighlight();
+}
+
+KDCoordinate HistoryController::rowHeight(int j) {
+ if (j >= m_calculationStore->numberOfCalculations()) {
+ return 0;
+ }
+ Shared::ExpiringPointer calculation = calculationAtIndex(j);
+ bool expanded = j == selectedRow() && selectedSubviewType() == SubviewType::Output;
+ return calculation->height(expanded);
+}
+
+int HistoryController::typeAtLocation(int i, int j) {
+ return 0;
+}
+
+bool HistoryController::calculationAtIndexToggles(int index) {
+ Context * context = App::app()->localContext();
+ return index >= 0 && index < m_calculationStore->numberOfCalculations() && calculationAtIndex(index)->displayOutput(context) == Calculation::DisplayOutput::ExactAndApproximateToggle;
+}
+
+
+void HistoryController::setSelectedSubviewType(SubviewType subviewType, bool sameCell, int previousSelectedX, int previousSelectedY) {
+ // Avoid selecting non-displayed ellipsis
+ HistoryViewCell * selectedCell = static_cast(m_selectableTableView.selectedCell());
+ if (subviewType == SubviewType::Ellipsis && selectedCell && selectedCell->additionalInformationType() == Calculation::AdditionalInformationType::None) {
+ subviewType = SubviewType::Output;
+ }
+ HistoryViewCellDataSource::setSelectedSubviewType(subviewType, sameCell, previousSelectedX, previousSelectedY);
+}
+
+void HistoryController::historyViewCellDidChangeSelection(HistoryViewCell ** cell, HistoryViewCell ** previousCell, int previousSelectedCellX, int previousSelectedCellY, SubviewType type, SubviewType previousType) {
+ /* If the selection change triggers the toggling of the outputs, we update
+ * the whole table as the height of the selected cell row might have changed. */
+ if ((type == SubviewType::Output || previousType == SubviewType::Output) && (calculationAtIndexToggles(selectedRow()) || calculationAtIndexToggles(previousSelectedCellY))) {
+ m_selectableTableView.reloadData();
+ }
+
+ // It might be necessary to scroll to the sub type if the cell overflows the screen
+ if (selectedRow() >= 0) {
+ m_selectableTableView.scrollToSubviewOfTypeOfCellAtLocation(type, m_selectableTableView.selectedColumn(), m_selectableTableView.selectedRow());
+ }
+ // Fill the selected cell and the previous selected cell because cells repartition might have changed
+ *cell = static_cast(m_selectableTableView.selectedCell());
+ *previousCell = static_cast(m_selectableTableView.cellAtLocation(previousSelectedCellX, previousSelectedCellY));
+ /* 'reloadData' calls 'willDisplayCellForIndex' for each cell while the table
+ * has been deselected. To reload the expanded cell, we call one more time
+ * 'willDisplayCellForIndex' but once the right cell has been selected. */
+ if (*cell) {
+ willDisplayCellForIndex(*cell, selectedRow());
+ }
+}
+
+}
diff --git a/apps/calculation/history_controller.h b/apps/calculation/history_controller.h
new file mode 100644
index 00000000000..e289eb5fc43
--- /dev/null
+++ b/apps/calculation/history_controller.h
@@ -0,0 +1,57 @@
+#ifndef CALCULATION_HISTORY_CONTROLLER_H
+#define CALCULATION_HISTORY_CONTROLLER_H
+
+#include
+#include "history_view_cell.h"
+#include "calculation_store.h"
+#include "selectable_table_view.h"
+#include "additional_outputs/complex_list_controller.h"
+#include "additional_outputs/integer_list_controller.h"
+#include "additional_outputs/rational_list_controller.h"
+#include "additional_outputs/trigonometry_list_controller.h"
+#include "additional_outputs/unit_list_controller.h"
+#include "additional_outputs/matrix_list_controller.h"
+
+namespace Calculation {
+
+class App;
+
+class HistoryController : public ViewController, public ListViewDataSource, public SelectableTableViewDataSource, public SelectableTableViewDelegate, public HistoryViewCellDataSource {
+public:
+ HistoryController(EditExpressionController * editExpressionController, CalculationStore * calculationStore);
+ View * view() override { return &m_selectableTableView; }
+ bool handleEvent(Ion::Events::Event event) override;
+ void viewWillAppear() override;
+ TELEMETRY_ID("");
+ void didBecomeFirstResponder() override;
+ void willExitResponderChain(Responder * nextFirstResponder) override;
+ void reload();
+ int numberOfRows() const override;
+ HighlightCell * reusableCell(int index, int type) override;
+ int reusableCellCount(int type) override;
+ void willDisplayCellForIndex(HighlightCell * cell, int index) override;
+ KDCoordinate rowHeight(int j) override;
+ int typeAtLocation(int i, int j) override;
+ void setSelectedSubviewType(SubviewType subviewType, bool sameCell, int previousSelectedX = -1, int previousSelectedY = -1) override;
+ void tableViewDidChangeSelectionAndDidScroll(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY, bool withinTemporarySelection = false) override;
+private:
+ int storeIndex(int i) { return numberOfRows() - i - 1; }
+ Shared::ExpiringPointer calculationAtIndex(int i);
+ CalculationSelectableTableView * selectableTableView();
+ bool calculationAtIndexToggles(int index);
+ void historyViewCellDidChangeSelection(HistoryViewCell ** cell, HistoryViewCell ** previousCell, int previousSelectedCellX, int previousSelectedCellY, SubviewType type, SubviewType previousType) override;
+ constexpr static int k_maxNumberOfDisplayedRows = 8;
+ CalculationSelectableTableView m_selectableTableView;
+ HistoryViewCell m_calculationHistory[k_maxNumberOfDisplayedRows];
+ CalculationStore * m_calculationStore;
+ ComplexListController m_complexController;
+ IntegerListController m_integerController;
+ RationalListController m_rationalController;
+ TrigonometryListController m_trigonometryController;
+ UnitListController m_unitController;
+ MatrixListController m_matrixController;
+};
+
+}
+
+#endif
diff --git a/apps/calculation/history_view_cell.cpp b/apps/calculation/history_view_cell.cpp
new file mode 100644
index 00000000000..e30995750fb
--- /dev/null
+++ b/apps/calculation/history_view_cell.cpp
@@ -0,0 +1,361 @@
+#include "history_view_cell.h"
+#include "app.h"
+#include "../constant.h"
+#include "selectable_table_view.h"
+#include
+#include
+#include
+#include
+
+namespace Calculation {
+
+/* HistoryViewCellDataSource */
+
+void HistoryViewCellDataSource::setSelectedSubviewType(SubviewType subviewType, bool sameCell, int previousSelectedCellX, int previousSelectedCellY) {
+ HistoryViewCell * selectedCell = nullptr;
+ HistoryViewCell * previouslySelectedCell = nullptr;
+ SubviewType previousSubviewType = m_selectedSubviewType;
+ m_selectedSubviewType = subviewType;
+ /* We need to notify the whole table that the selection changed if it
+ * involves the selection/deselection of an output. Indeed, only them can
+ * trigger change in the displayed expressions. */
+ historyViewCellDidChangeSelection(&selectedCell, &previouslySelectedCell, previousSelectedCellX, previousSelectedCellY, subviewType, previousSubviewType);
+
+ previousSubviewType = sameCell ? previousSubviewType : SubviewType::None;
+ if (selectedCell) {
+ selectedCell->reloadSubviewHighlight();
+ selectedCell->cellDidSelectSubview(subviewType, previousSubviewType);
+ Container::activeApp()->setFirstResponder(selectedCell);
+ }
+ if (previouslySelectedCell) {
+ previouslySelectedCell->cellDidSelectSubview(SubviewType::Input);
+ }
+}
+
+/* HistoryViewCell */
+
+KDCoordinate HistoryViewCell::Height(Calculation * calculation, bool expanded) {
+ HistoryViewCell cell(nullptr);
+ cell.setCalculation(calculation, expanded, true);
+ KDRect ellipsisFrame = KDRectZero;
+ KDRect inputFrame = KDRectZero;
+ KDRect outputFrame = KDRectZero;
+ cell.computeSubviewFrames(Ion::Display::Width, KDCOORDINATE_MAX, &ellipsisFrame, &inputFrame, &outputFrame);
+ return k_margin + inputFrame.unionedWith(outputFrame).height() + k_margin;
+}
+
+HistoryViewCell::HistoryViewCell(Responder * parentResponder) :
+ Responder(parentResponder),
+ m_calculationCRC32(0),
+ m_calculationDisplayOutput(Calculation::DisplayOutput::Unknown),
+ m_calculationAdditionInformation(Calculation::AdditionalInformationType::None),
+ m_inputView(this, k_inputViewHorizontalMargin, k_inputOutputViewsVerticalMargin),
+ m_scrollableOutputView(this),
+ m_calculationExpanded(false),
+ m_calculationSingleLine(false)
+{
+}
+
+void HistoryViewCell::setEven(bool even) {
+ EvenOddCell::setEven(even);
+ m_inputView.setBackgroundColor(backgroundColor());
+ m_scrollableOutputView.setBackgroundColor(backgroundColor());
+ m_scrollableOutputView.evenOddCell()->setEven(even);
+ m_ellipsis.setEven(even);
+}
+
+void HistoryViewCell::setHighlighted(bool highlight) {
+ if (m_highlighted == highlight) {
+ return;
+ }
+ m_highlighted = highlight;
+ reloadSubviewHighlight();
+ // Re-layout as the ellispsis subview might have appear/disappear
+ layoutSubviews();
+}
+
+void HistoryViewCell::reloadSubviewHighlight() {
+ assert(m_dataSource);
+ m_inputView.setExpressionBackgroundColor(backgroundColor());
+ m_scrollableOutputView.evenOddCell()->setHighlighted(false);
+ m_ellipsis.setHighlighted(false);
+ if (isHighlighted()) {
+ if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Input) {
+ m_inputView.setExpressionBackgroundColor(Palette::Select);
+ } else if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Output) {
+ m_scrollableOutputView.evenOddCell()->setHighlighted(true);
+ } else {
+ assert(m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Ellipsis);
+ m_ellipsis.setHighlighted(true);
+ }
+ }
+}
+
+Poincare::Layout HistoryViewCell::layout() const {
+ assert(m_dataSource);
+ if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Input) {
+ return m_inputView.layout();
+ } else {
+ return m_scrollableOutputView.layout();
+ }
+}
+
+void HistoryViewCell::reloadScroll() {
+ m_inputView.reloadScroll();
+ m_scrollableOutputView.reloadScroll();
+}
+
+void HistoryViewCell::reloadOutputSelection(HistoryViewCellDataSource::SubviewType previousType) {
+ /* Select the right output according to the calculation display output. This
+ * will reload the scroll to display the selected output. */
+ if (m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate) {
+ m_scrollableOutputView.setSelectedSubviewPosition(
+ previousType == HistoryViewCellDataSource::SubviewType::Ellipsis ?
+ Shared::ScrollableTwoExpressionsView::SubviewPosition::Right :
+ Shared::ScrollableTwoExpressionsView::SubviewPosition::Center
+ );
+ } else {
+ assert((m_calculationDisplayOutput == Calculation::DisplayOutput::ApproximateOnly)
+ || (m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximateToggle)
+ || (m_calculationDisplayOutput == Calculation::DisplayOutput::ExactOnly));
+ m_scrollableOutputView.setSelectedSubviewPosition(Shared::ScrollableTwoExpressionsView::SubviewPosition::Right);
+ }
+}
+
+void HistoryViewCell::cellDidSelectSubview(HistoryViewCellDataSource::SubviewType type, HistoryViewCellDataSource::SubviewType previousType) {
+ // Init output selection
+ if (type == HistoryViewCellDataSource::SubviewType::Output) {
+ reloadOutputSelection(previousType);
+ }
+
+ // Update m_calculationExpanded
+ m_calculationExpanded = (type == HistoryViewCellDataSource::SubviewType::Output && m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximateToggle);
+ /* The selected subview has changed. The displayed outputs might have changed.
+ * For example, for the calculation 1.2+2 --> 3.2, selecting the output would
+ * display 1.2+2 --> 16/5 = 3.2. */
+ m_scrollableOutputView.setDisplayCenter(m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate || m_calculationExpanded);
+
+ /* The displayed outputs have changed. We need to re-layout the cell
+ * and re-initialize the scroll. */
+ layoutSubviews();
+ reloadScroll();
+}
+
+View * HistoryViewCell::subviewAtIndex(int index) {
+ /* The order of the subviews should not matter here as they don't overlap.
+ * However, the order determines the order of redrawing as well. For several
+ * reasons listed after, changing subview selection often redraws the entire
+ * m_scrollableOutputView even if it seems unecessary:
+ * - Before feeding new Layouts to ExpressionViews, we reset the hold layouts
+ * in order to empty the Poincare pool and have more space to compute new
+ * layouts.
+ * - Even if we did not do that, ExpressionView::setLayout doesn't avoid
+ * redrawing when the previous expression is identical (for reasons
+ * explained in expression_view.cpp)
+ * - Because of the toggling burger view, ExpressionViews often have the same
+ * absolute frame but a different relative frame which leads to redrawing
+ * them anyway.
+ * All these reasons cause a blinking which can be avoided if we redraw the
+ * output view before the input view (starting with redrawing the more
+ * complex view enables to redraw it before the vblank thereby preventing
+ * blinking).
+ * TODO: this is a dirty hack which should be fixed! */
+ View * views[3] = {&m_scrollableOutputView, &m_inputView, &m_ellipsis};
+ return views[index];
+}
+
+bool HistoryViewCell::ViewsCanBeSingleLine(KDCoordinate inputViewWidth, KDCoordinate outputViewWidth) {
+ // k_margin is the separation between the input and output.
+ return (inputViewWidth + k_margin + outputViewWidth) < Ion::Display::Width - Metric::EllipsisCellWidth;
+}
+
+void HistoryViewCell::layoutSubviews(bool force) {
+ KDRect frameBounds = bounds();
+ if (bounds().width() <= 0 || bounds().height() <= 0) {
+ // TODO Make this behaviour in a non-virtual layoutSublviews, and all layout subviews should become privateLayoutSubviews
+ return;
+ }
+ KDRect ellipsisFrame = KDRectZero;
+ KDRect inputFrame = KDRectZero;
+ KDRect outputFrame = KDRectZero;
+ computeSubviewFrames(frameBounds.width(), frameBounds.height(), &ellipsisFrame, &inputFrame, &outputFrame);
+
+ m_ellipsis.setFrame(ellipsisFrame, force); // Required even if ellipsisFrame is KDRectZero, to mark previous rect as dirty
+ m_inputView.setFrame(inputFrame,force);
+ m_scrollableOutputView.setFrame(outputFrame, force);
+}
+
+void HistoryViewCell::computeSubviewFrames(KDCoordinate frameWidth, KDCoordinate frameHeight, KDRect * ellipsisFrame, KDRect * inputFrame, KDRect * outputFrame) {
+ assert(ellipsisFrame != nullptr && inputFrame != nullptr && outputFrame != nullptr);
+
+ if (displayedEllipsis()) {
+ *ellipsisFrame = KDRect(frameWidth - Metric::EllipsisCellWidth, 0, Metric::EllipsisCellWidth, frameHeight);
+ frameWidth -= Metric::EllipsisCellWidth;
+ } else {
+ *ellipsisFrame = KDRectZero;
+ }
+
+ KDSize inputSize = m_inputView.minimalSizeForOptimalDisplay();
+ KDSize outputSize = m_scrollableOutputView.minimalSizeForOptimalDisplay();
+
+ /* To compute if the calculation is on a single line, use the expanded width
+ * if there is both an exact and an approximate layout. */
+ m_calculationSingleLine = ViewsCanBeSingleLine(inputSize.width(), m_scrollableOutputView.minimalSizeForOptimalDisplayFullSize().width());
+
+ KDCoordinate inputY = k_margin;
+ KDCoordinate outputY = k_margin;
+ if (m_calculationSingleLine && !m_inputView.layout().isUninitialized()) {
+ KDCoordinate inputBaseline = m_inputView.layout().baseline();
+ KDCoordinate outputBaseline = m_scrollableOutputView.baseline();
+ KDCoordinate baselineDifference = outputBaseline - inputBaseline;
+ if (baselineDifference > 0) {
+ inputY += baselineDifference;
+ } else {
+ outputY += -baselineDifference;
+ }
+ } else {
+ outputY += inputSize.height();
+ }
+
+ *inputFrame = KDRect(
+ 0,
+ inputY,
+ std::min(frameWidth, inputSize.width()),
+ inputSize.height());
+ *outputFrame = KDRect(
+ std::max(0, frameWidth - outputSize.width()),
+ outputY,
+ std::min(frameWidth, outputSize.width()),
+ outputSize.height());
+}
+
+void HistoryViewCell::resetMemoization() {
+ // Clean the layouts to make room in the pool
+ // TODO: maybe do this only when the layout won't change to avoid blinking
+ m_inputView.setLayout(Poincare::Layout());
+ m_scrollableOutputView.setLayouts(Poincare::Layout(), Poincare::Layout(), Poincare::Layout());
+ m_calculationCRC32 = 0;
+}
+
+void HistoryViewCell::setCalculation(Calculation * calculation, bool expanded, bool canChangeDisplayOutput) {
+ uint32_t newCalculationCRC = Ion::crc32Byte((const uint8_t *)calculation, ((char *)calculation->next()) - ((char *) calculation));
+ if (newCalculationCRC == m_calculationCRC32 && m_calculationExpanded == expanded) {
+ return;
+ }
+ Poincare::Context * context = App::app()->localContext();
+
+ // TODO: maybe do this only when the layout won't change to avoid blinking
+ resetMemoization();
+
+ // Memoization
+ m_calculationCRC32 = newCalculationCRC;
+ m_calculationExpanded = expanded && calculation->displayOutput(context) == ::Calculation::Calculation::DisplayOutput::ExactAndApproximateToggle;
+ m_calculationAdditionInformation = calculation->additionalInformationType(context);
+ m_inputView.setLayout(calculation->createInputLayout());
+
+ /* All expressions have to be updated at the same time. Otherwise,
+ * when updating one layout, if the second one still points to a deleted
+ * layout, calling to layoutSubviews() would fail. */
+
+ // Create the exact output layout
+ Poincare::Layout exactOutputLayout = Poincare::Layout();
+ if (Calculation::DisplaysExact(calculation->displayOutput(context))) {
+ bool couldNotCreateExactLayout = false;
+ exactOutputLayout = calculation->createExactOutputLayout(&couldNotCreateExactLayout);
+ if (couldNotCreateExactLayout) {
+ if (canChangeDisplayOutput && calculation->displayOutput(context) != ::Calculation::Calculation::DisplayOutput::ExactOnly) {
+ calculation->forceDisplayOutput(::Calculation::Calculation::DisplayOutput::ApproximateOnly);
+ } else {
+ /* We should only display the exact result, but we cannot create it
+ * -> raise an exception. */
+ Poincare::ExceptionCheckpoint::Raise();
+ }
+ }
+ }
+
+ // Create the approximate output layout
+ Poincare::Layout approximateOutputLayout;
+ if (calculation->displayOutput(context) == ::Calculation::Calculation::DisplayOutput::ExactOnly) {
+ approximateOutputLayout = exactOutputLayout;
+ } else {
+ bool couldNotCreateApproximateLayout = false;
+ approximateOutputLayout = calculation->createApproximateOutputLayout(context, &couldNotCreateApproximateLayout);
+ if (couldNotCreateApproximateLayout) {
+ if (canChangeDisplayOutput && calculation->displayOutput(context) != ::Calculation::Calculation::DisplayOutput::ApproximateOnly) {
+ /* Set the display output to ApproximateOnly, make room in the pool by
+ * erasing the exact layout, and retry to create the approximate layout */
+ calculation->forceDisplayOutput(::Calculation::Calculation::DisplayOutput::ApproximateOnly);
+ exactOutputLayout = Poincare::Layout();
+ couldNotCreateApproximateLayout = false;
+ approximateOutputLayout = calculation->createApproximateOutputLayout(context, &couldNotCreateApproximateLayout);
+ if (couldNotCreateApproximateLayout) {
+ Poincare::ExceptionCheckpoint::Raise();
+ }
+ } else {
+ Poincare::ExceptionCheckpoint::Raise();
+ }
+ }
+ }
+ m_calculationDisplayOutput = calculation->displayOutput(context);
+
+ // We must set which subviews are displayed before setLayouts to mark the right rectangle as dirty
+ m_scrollableOutputView.setDisplayableCenter(m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate || m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximateToggle);
+ m_scrollableOutputView.setDisplayCenter(m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate || m_calculationExpanded);
+ m_scrollableOutputView.setLayouts(Poincare::Layout(), exactOutputLayout, approximateOutputLayout);
+ I18n::Message equalMessage = calculation->exactAndApproximateDisplayedOutputsAreEqual(context) == Calculation::EqualSign::Equal ? I18n::Message::Equal : I18n::Message::AlmostEqual;
+ m_scrollableOutputView.setEqualMessage(equalMessage);
+
+ /* The displayed input and outputs have changed. We need to re-layout the cell
+ * and re-initialize the scroll. */
+ layoutSubviews();
+ reloadScroll();
+}
+
+void HistoryViewCell::didBecomeFirstResponder() {
+ assert(m_dataSource);
+ if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Input) {
+ Container::activeApp()->setFirstResponder(&m_inputView);
+ } else if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Output) {
+ Container::activeApp()->setFirstResponder(&m_scrollableOutputView);
+ }
+}
+
+bool HistoryViewCell::handleEvent(Ion::Events::Event event) {
+ assert(m_dataSource != nullptr);
+ HistoryViewCellDataSource::SubviewType type = m_dataSource->selectedSubviewType();
+ assert(type != HistoryViewCellDataSource::SubviewType::None);
+ HistoryViewCellDataSource::SubviewType otherSubviewType = HistoryViewCellDataSource::SubviewType::None;
+ if (m_calculationSingleLine) {
+ static_assert(
+ static_cast(HistoryViewCellDataSource::SubviewType::None) == 0
+ && static_cast(HistoryViewCellDataSource::SubviewType::Input) == 1
+ && static_cast(HistoryViewCellDataSource::SubviewType::Output) == 2
+ && static_cast(HistoryViewCellDataSource::SubviewType::Ellipsis) == 3,
+ "The array types is not well-formed anymore");
+ HistoryViewCellDataSource::SubviewType types[] = {
+ HistoryViewCellDataSource::SubviewType::None,
+ HistoryViewCellDataSource::SubviewType::Input,
+ HistoryViewCellDataSource::SubviewType::Output,
+ displayedEllipsis() ? HistoryViewCellDataSource::SubviewType::Ellipsis : HistoryViewCellDataSource::SubviewType::None,
+ HistoryViewCellDataSource::SubviewType::None,
+ };
+ if (event == Ion::Events::Right || event == Ion::Events::Left) {
+ otherSubviewType = types[static_cast(type) + (event == Ion::Events::Right ? 1 : -1)];
+ }
+ } else if ((event == Ion::Events::Down && type == HistoryViewCellDataSource::SubviewType::Input)
+ || (event == Ion::Events::Left && type == HistoryViewCellDataSource::SubviewType::Ellipsis))
+ {
+ otherSubviewType = HistoryViewCellDataSource::SubviewType::Output;
+ } else if (event == Ion::Events::Up && type == HistoryViewCellDataSource::SubviewType::Output) {
+ otherSubviewType = HistoryViewCellDataSource::SubviewType::Input;
+ } else if (event == Ion::Events::Right && type != HistoryViewCellDataSource::SubviewType::Ellipsis && displayedEllipsis()) {
+ otherSubviewType = HistoryViewCellDataSource::SubviewType::Ellipsis;
+ }
+ if (otherSubviewType == HistoryViewCellDataSource::SubviewType::None) {
+ return false;
+ }
+ m_dataSource->setSelectedSubviewType(otherSubviewType, true);
+ return true;
+}
+
+}
diff --git a/apps/calculation/history_view_cell.h b/apps/calculation/history_view_cell.h
new file mode 100644
index 00000000000..5e296f529f6
--- /dev/null
+++ b/apps/calculation/history_view_cell.h
@@ -0,0 +1,84 @@
+#ifndef CALCULATION_HISTORY_VIEW_CELL_H
+#define CALCULATION_HISTORY_VIEW_CELL_H
+
+#include
+#include "calculation.h"
+#include "../shared/scrollable_multiple_expressions_view.h"
+
+namespace Calculation {
+
+class HistoryViewCell;
+
+class HistoryViewCellDataSource {
+public:
+ enum class SubviewType {
+ None = 0,
+ Input = 1,
+ Output = 2,
+ Ellipsis = 3
+ };
+ HistoryViewCellDataSource() : m_selectedSubviewType(SubviewType::Output) {}
+ virtual void setSelectedSubviewType(SubviewType subviewType, bool sameCell, int previousSelectedX = -1, int previousSelectedY = -1);
+ SubviewType selectedSubviewType() const { return m_selectedSubviewType; }
+private:
+ /* This method should belong to a delegate instead of a data source but as
+ * both the data source and the delegate will be the same controller, we
+ * avoid keeping 2 pointers in HistoryViewCell. */
+ // It returns the selected cell at the end of the method
+ virtual void historyViewCellDidChangeSelection(HistoryViewCell ** cell, HistoryViewCell ** previousCell, int previousSelectedCellX, int previousSelectedCellY, SubviewType type, SubviewType previousType) = 0;
+ SubviewType m_selectedSubviewType;
+};
+
+class HistoryViewCell : public ::EvenOddCell, public Responder {
+public:
+ constexpr static KDCoordinate k_margin = Metric::CommonSmallMargin;
+ constexpr static KDCoordinate k_inputOutputViewsVerticalMargin = k_margin;
+ constexpr static KDCoordinate k_inputViewHorizontalMargin = Shared::AbstractScrollableMultipleExpressionsView::k_horizontalMargin;
+ static KDCoordinate Height(Calculation * calculation, bool expanded);
+ HistoryViewCell(Responder * parentResponder = nullptr);
+ static bool ViewsCanBeSingleLine(KDCoordinate inputViewWidth, KDCoordinate outputViewWidth);
+ void cellDidSelectSubview(HistoryViewCellDataSource::SubviewType type, HistoryViewCellDataSource::SubviewType previousType = HistoryViewCellDataSource::SubviewType::None);
+ void setEven(bool even) override;
+ void setHighlighted(bool highlight) override;
+ void reloadSubviewHighlight();
+ void setDataSource(HistoryViewCellDataSource * dataSource) { m_dataSource = dataSource; }
+ bool displaysSingleLine() const {
+ return m_calculationSingleLine;
+ }
+ Responder * responder() override {
+ return this;
+ }
+ Poincare::Layout layout() const override;
+ KDColor backgroundColor() const override { return m_even ? KDColorWhite : Palette::WallScreen; }
+ void resetMemoization();
+ void setCalculation(Calculation * calculation, bool expanded, bool canChangeDisplayOutput = false);
+ int numberOfSubviews() const override { return 2 + displayedEllipsis(); }
+ View * subviewAtIndex(int index) override;
+ void layoutSubviews(bool force = false) override;
+ void didBecomeFirstResponder() override;
+ bool handleEvent(Ion::Events::Event event) override;
+ Shared::ScrollableTwoExpressionsView * outputView() { return &m_scrollableOutputView; }
+ ScrollableExpressionView * inputView() { return &m_inputView; }
+ Calculation::AdditionalInformationType additionalInformationType() const { return m_calculationAdditionInformation; }
+private:
+ constexpr static KDCoordinate k_resultWidth = 80;
+ void computeSubviewFrames(KDCoordinate frameWidth, KDCoordinate frameHeight, KDRect * ellipsisFrame, KDRect * inputFrame, KDRect * outputFrame);
+ void reloadScroll();
+ void reloadOutputSelection(HistoryViewCellDataSource::SubviewType previousType);
+ bool displayedEllipsis() const {
+ return m_highlighted && m_calculationAdditionInformation != Calculation::AdditionalInformationType::None;
+ }
+ uint32_t m_calculationCRC32;
+ Calculation::DisplayOutput m_calculationDisplayOutput;
+ Calculation::AdditionalInformationType m_calculationAdditionInformation;
+ ScrollableExpressionView m_inputView;
+ Shared::ScrollableTwoExpressionsView m_scrollableOutputView;
+ EvenOddCellWithEllipsis m_ellipsis;
+ HistoryViewCellDataSource * m_dataSource;
+ bool m_calculationExpanded;
+ bool m_calculationSingleLine;
+};
+
+}
+
+#endif
diff --git a/apps/calculation/selectable_table_view.cpp b/apps/calculation/selectable_table_view.cpp
new file mode 100644
index 00000000000..8a2884ce1d0
--- /dev/null
+++ b/apps/calculation/selectable_table_view.cpp
@@ -0,0 +1,103 @@
+#include "selectable_table_view.h"
+#include
+
+namespace Calculation {
+
+CalculationSelectableTableView::CalculationSelectableTableView(Responder * parentResponder, TableViewDataSource * dataSource,
+ SelectableTableViewDataSource * selectionDataSource, SelectableTableViewDelegate * delegate) :
+ ::SelectableTableView(parentResponder, dataSource, selectionDataSource, delegate)
+{
+ setVerticalCellOverlap(0);
+ setMargins(0);
+ setDecoratorType(ScrollView::Decorator::Type::None);
+}
+
+void CalculationSelectableTableView::scrollToBottom() {
+ KDCoordinate contentOffsetX = contentOffset().x();
+ KDCoordinate contentOffsetY = dataSource()->cumulatedHeightFromIndex(dataSource()->numberOfRows()) - maxContentHeightDisplayableWithoutScrolling();
+ setContentOffset(KDPoint(contentOffsetX, contentOffsetY));
+}
+
+void CalculationSelectableTableView::scrollToCell(int i, int j) {
+ if (m_contentView.bounds().height() < bounds().height()) {
+ setTopMargin(bounds().height() - m_contentView.bounds().height());
+ } else {
+ setTopMargin(0);
+ }
+ ::SelectableTableView::scrollToCell(i, j);
+ ScrollView::layoutSubviews();
+ if (m_contentView.bounds().height() - contentOffset().y() < bounds().height()) {
+ // Avoid empty space at the end of the table
+ scrollToBottom();
+ }
+}
+
+void CalculationSelectableTableView::scrollToSubviewOfTypeOfCellAtLocation(HistoryViewCellDataSource::SubviewType subviewType, int i, int j) {
+ if (dataSource()->rowHeight(j) <= bounds().height()) {
+ return;
+ }
+ /* As we scroll, the selected calculation does not use the same history view
+ * cell, thus, we want to deselect the previous used history view cell. (*) */
+ unhighlightSelectedCell();
+
+ /* Main part of the scroll */
+ HistoryViewCell * cell = static_cast(selectedCell());
+ assert(cell);
+ KDCoordinate contentOffsetX = contentOffset().x();
+
+ KDCoordinate contentOffsetY = dataSource()->cumulatedHeightFromIndex(j);
+ if (cell->displaysSingleLine() && dataSource()->rowHeight(j) > maxContentHeightDisplayableWithoutScrolling()) {
+ /* If we cannot display the full calculation, we display the selected
+ * layout as close as possible to the top of the screen without drawing
+ * empty space between the history and the input field.
+ *
+ * Below are some values we can assign to contentOffsetY, and the kinds of
+ * display they entail :
+ * (the selected cell is at index j)
+ *
+ * 1 - cumulatedHeightFromIndex(j)
+ * Aligns the top of the cell with the top of the zone in which the
+ * history can be drawn.
+ *
+ * 2 - (cumulatedHeightFromIndex(j+1)
+ * - maxContentHeightDisplayableWithoutScrolling())
+ * Aligns the bottom of the cell with the top of the input field.
+ *
+ * 3 - cumulatedHeightFromIndex(j) + baseline1 - baseline2
+ * Aligns the top of the selected layout with the top of the screen (only
+ * used when the selected layout is the smallest).
+ *
+ * The following drawing shows where the calculation would be aligned with
+ * each value of contentOffsetY, for the calculation (1/3)/(4/2) = 1/6.
+ *
+ * (1) (2) (3)
+ * +--------------+ +--------------+ +--------------+
+ * | 1 | | --- - | | 3 1 |
+ * | - | | 4 6 | | --- - |
+ * | 3 1 | | - | | 4 6 |
+ * | --- - | | 2 | | - |
+ * +--------------+ +--------------+ +--------------+
+ * | (1/3)/(4/2) | | (1/3)/(4/2) | | (1/3)/(4/2) |
+ * +--------------+ +--------------+ +--------------+
+ *
+ * */
+ contentOffsetY += std::min(
+ dataSource()->rowHeight(j) - maxContentHeightDisplayableWithoutScrolling(),
+ std::max(0, (cell->inputView()->layout().baseline() - cell->outputView()->baseline()) * (subviewType == HistoryViewCellDataSource::SubviewType::Input ? -1 : 1)));
+ } else if (subviewType != HistoryViewCellDataSource::SubviewType::Input) {
+ contentOffsetY += dataSource()->rowHeight(j) - maxContentHeightDisplayableWithoutScrolling();
+ }
+
+ setContentOffset(KDPoint(contentOffsetX, contentOffsetY));
+ /* For the same reason as (*), we have to rehighlight the new history view
+ * cell and reselect the first responder.
+ * We have to recall "selectedCell" because when the table might have been
+ * relayouted in "setContentOffset".*/
+ cell = static_cast(selectedCell());
+ assert(cell);
+ cell->setHighlighted(true);
+ Container::activeApp()->setFirstResponder(cell);
+}
+
+
+}
diff --git a/apps/calculation/selectable_table_view.h b/apps/calculation/selectable_table_view.h
new file mode 100644
index 00000000000..d1740ab7856
--- /dev/null
+++ b/apps/calculation/selectable_table_view.h
@@ -0,0 +1,19 @@
+#ifndef CALCULATION_SELECTABLE_TABLE_VIEW_H
+#define CALCULATION_SELECTABLE_TABLE_VIEW_H
+
+#include
+#include "history_view_cell.h"
+namespace Calculation {
+
+class CalculationSelectableTableView : public ::SelectableTableView {
+public:
+ CalculationSelectableTableView(Responder * parentResponder, TableViewDataSource * dataSource,
+ SelectableTableViewDataSource * selectionDataSource, SelectableTableViewDelegate * delegate = nullptr);
+ void scrollToBottom();
+ void scrollToCell(int i, int j) override;
+ void scrollToSubviewOfTypeOfCellAtLocation(HistoryViewCellDataSource::SubviewType subviewType, int i, int j);
+};
+
+}
+
+#endif
diff --git a/apps/calculation/test/calculation_store.cpp b/apps/calculation/test/calculation_store.cpp
new file mode 100644
index 00000000000..1ce0c4fe1b4
--- /dev/null
+++ b/apps/calculation/test/calculation_store.cpp
@@ -0,0 +1,207 @@
+#include
+#include
+#include
+#include
+#include
+#include "../calculation_store.h"
+
+typedef ::Calculation::Calculation::DisplayOutput DisplayOutput;
+typedef ::Calculation::Calculation::EqualSign EqualSign ;
+typedef ::Calculation::Calculation::NumberOfSignificantDigits NumberOfSignificantDigits;
+
+using namespace Poincare;
+using namespace Calculation;
+
+static constexpr int calculationBufferSize = 10 * (sizeof(::Calculation::Calculation) + ::Calculation::Calculation::k_numberOfExpressions * ::Constant::MaxSerializedExpressionSize + sizeof(::Calculation::Calculation *));
+char calculationBuffer[calculationBufferSize];
+
+
+void assert_store_is(CalculationStore * store, const char * * result) {
+ for (int i = 0; i < store->numberOfCalculations(); i++) {
+ quiz_assert(strcmp(store->calculationAtIndex(i)->inputText(), result[i]) == 0);
+ }
+}
+
+KDCoordinate dummyHeight(::Calculation::Calculation * c, bool expanded) { return 0; }
+
+QUIZ_CASE(calculation_store) {
+ Shared::GlobalContext globalContext;
+ CalculationStore store(calculationBuffer,calculationBufferSize);
+ // Store is now {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
+ const char * result[] = {"9", "8", "7", "6", "5", "4", "3", "2", "1", "0"};
+ for (int i = 0; i < 10; i++) {
+ char text[2] = {(char)(i+'0'), 0};
+ store.push(text, &globalContext, dummyHeight);
+ quiz_assert(store.numberOfCalculations() == i+1);
+ }
+ assert_store_is(&store, result);
+
+ for (int i = 9; i > 0; i = i-2) {
+ store.deleteCalculationAtIndex(i);
+ }
+ // Store is now {9, 7, 5, 3, 1}
+ const char * result2[] = {"9", "7", "5", "3", "1"};
+ assert_store_is(&store, result2);
+
+ store.deleteAll();
+
+ // Checking if the store handles correctly the delete of the oldest calculation when full
+ static int minSize = ::Calculation::Calculation::MinimalSize();
+ char text[2] = {'0', 0};
+ while (store.remainingBufferSize() > minSize) {
+ store.push(text, &globalContext, dummyHeight);
+ }
+ int numberOfCalculations1 = store.numberOfCalculations();
+ /* The buffer is now to full to push a new calculation.
+ * Trying to push a new one should delete the oldest one*/
+ store.push(text, &globalContext, dummyHeight);
+ int numberOfCalculations2 = store.numberOfCalculations();
+ // The numberOfCalculations should be the same
+ quiz_assert(numberOfCalculations1 == numberOfCalculations2);
+ store.deleteAll();
+ quiz_assert(store.remainingBufferSize() == store.bufferSize());
+}
+
+QUIZ_CASE(calculation_ans) {
+ Shared::GlobalContext globalContext;
+ CalculationStore store(calculationBuffer,calculationBufferSize);
+
+ store.push("1+3/4", &globalContext, dummyHeight);
+ store.push("ans+2/3", &globalContext, dummyHeight);
+ Shared::ExpiringPointer<::Calculation::Calculation> lastCalculation = store.calculationAtIndex(0);
+ quiz_assert(lastCalculation->displayOutput(&globalContext) == DisplayOutput::ExactAndApproximate);
+ quiz_assert(strcmp(lastCalculation->exactOutputText(),"29/12") == 0);
+
+ store.push("ans+0.22", &globalContext, dummyHeight);
+ lastCalculation = store.calculationAtIndex(0);
+ quiz_assert(lastCalculation->displayOutput(&globalContext) == DisplayOutput::ExactAndApproximateToggle);
+ quiz_assert(strcmp(lastCalculation->approximateOutputText(NumberOfSignificantDigits::Maximal),"2.6366666666667") == 0);
+
+ store.deleteAll();
+}
+
+void assertCalculationIs(const char * input, DisplayOutput display, EqualSign sign, const char * exactOutput, const char * displayedApproximateOutput, const char * storedApproximateOutput, Context * context, CalculationStore * store) {
+ store->push(input, context, dummyHeight);
+ Shared::ExpiringPointer<::Calculation::Calculation> lastCalculation = store->calculationAtIndex(0);
+ quiz_assert(lastCalculation->displayOutput(context) == display);
+ if (sign != EqualSign::Unknown) {
+ quiz_assert(lastCalculation->exactAndApproximateDisplayedOutputsAreEqual(context) == sign);
+ }
+ if (exactOutput) {
+ quiz_assert_print_if_failure(strcmp(lastCalculation->exactOutputText(), exactOutput) == 0, input);
+ }
+ if (displayedApproximateOutput) {
+ quiz_assert_print_if_failure(strcmp(lastCalculation->approximateOutputText(NumberOfSignificantDigits::UserDefined), displayedApproximateOutput) == 0, input);
+ }
+ if (storedApproximateOutput) {
+ quiz_assert_print_if_failure(strcmp(lastCalculation->approximateOutputText(NumberOfSignificantDigits::Maximal), storedApproximateOutput) == 0, input);
+ }
+ store->deleteAll();
+}
+
+QUIZ_CASE(calculation_significant_digits) {
+ Shared::GlobalContext globalContext;
+ CalculationStore store(calculationBuffer,calculationBufferSize);
+
+ assertCalculationIs("123456789", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "123456789", "1.234568ᴇ8", "123456789", &globalContext, &store);
+ assertCalculationIs("1234567", DisplayOutput::ApproximateOnly, EqualSign::Equal, "1234567", "1234567", "1234567", &globalContext, &store);
+
+}
+
+QUIZ_CASE(calculation_display_exact_approximate) {
+ Shared::GlobalContext globalContext;
+ CalculationStore store(calculationBuffer,calculationBufferSize);
+
+ assertCalculationIs("1/2", DisplayOutput::ExactAndApproximate, EqualSign::Equal, nullptr, nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("1/3", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, nullptr, nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("1/0", DisplayOutput::ApproximateOnly, EqualSign::Unknown, "undef", "undef", "undef", &globalContext, &store);
+ assertCalculationIs("2x-x", DisplayOutput::ApproximateOnly, EqualSign::Unknown, "undef", "undef", "undef", &globalContext, &store);
+ assertCalculationIs("[[1,2,3]]", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("[[1,x,3]]", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "undef", "undef", &globalContext, &store);
+ assertCalculationIs("28^7", DisplayOutput::ExactAndApproximate, EqualSign::Unknown, nullptr, nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("3+√(2)→a", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "√(2)+3", nullptr, nullptr, &globalContext, &store);
+ Ion::Storage::sharedStorage()->recordNamed("a.exp").destroy();
+ assertCalculationIs("3+2→a", DisplayOutput::ApproximateOnly, EqualSign::Equal, "5", "5", "5", &globalContext, &store);
+ Ion::Storage::sharedStorage()->recordNamed("a.exp").destroy();
+ assertCalculationIs("3→a", DisplayOutput::ApproximateOnly, EqualSign::Equal, "3", "3", "3", &globalContext, &store);
+ Ion::Storage::sharedStorage()->recordNamed("a.exp").destroy();
+ assertCalculationIs("3+x→f(x)", DisplayOutput::ExactOnly, EqualSign::Unknown, "x+3", nullptr, nullptr, &globalContext, &store);
+ Ion::Storage::sharedStorage()->recordNamed("f.func").destroy();
+ assertCalculationIs("1+1+random()", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("1+1+round(1.343,2)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "3.34", "3.34", &globalContext, &store);
+ assertCalculationIs("randint(2,2)+3", DisplayOutput::ApproximateOnly, EqualSign::Unknown, "5", "5", "5", &globalContext, &store);
+ assertCalculationIs("confidence(0.5,2)+3", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("prediction(0.5,2)+3", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("prediction95(0.5,2)+3", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, nullptr, nullptr, &globalContext, &store);
+
+}
+
+QUIZ_CASE(calculation_symbolic_computation) {
+ Shared::GlobalContext globalContext;
+ CalculationStore store(calculationBuffer,calculationBufferSize);
+
+ assertCalculationIs("x+x+1+3+√(π)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, "undef", "undef", "undef", &globalContext, &store);
+ assertCalculationIs("f(x)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, "undef", "undef", "undef", &globalContext, &store);
+ assertCalculationIs("1+x→f(x)", DisplayOutput::ExactOnly, EqualSign::Unknown, "x+1", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("f(x)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, "undef", "undef", "undef", &globalContext, &store);
+ assertCalculationIs("f(2)", DisplayOutput::ApproximateOnly, EqualSign::Equal, "3", "3", "3", &globalContext, &store);
+ assertCalculationIs("2→x", DisplayOutput::ApproximateOnly, EqualSign::Equal, "2", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("f(x)", DisplayOutput::ApproximateOnly, EqualSign::Equal, "3", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("x+x+1+3+√(π)", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "√(π)+8", nullptr, nullptr, &globalContext, &store);
+
+ Ion::Storage::sharedStorage()->recordNamed("f.func").destroy();
+ Ion::Storage::sharedStorage()->recordNamed("x.exp").destroy();
+}
+
+QUIZ_CASE(calculation_symbolic_computation_and_parametered_expressions) {
+ Shared::GlobalContext globalContext;
+ CalculationStore store(calculationBuffer,calculationBufferSize);
+
+ assertCalculationIs("int((ℯ^(-x))-x^(0.5), x, 0, 3)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, nullptr, nullptr, &globalContext, &store); // Tests a bug with symbolic computation
+ assertCalculationIs("int(x,x,0,2)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "2", "2", &globalContext, &store);
+ assertCalculationIs("sum(x,x,0,2)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "3", "3", &globalContext, &store);
+ assertCalculationIs("product(x,x,1,2)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "2", "2", &globalContext, &store);
+ assertCalculationIs("diff(x^2,x,3)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "6", "6", &globalContext, &store);
+ assertCalculationIs("2→x", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("int(x,x,0,2)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "2", "2", &globalContext, &store);
+ assertCalculationIs("sum(x,x,0,2)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "3", "3", &globalContext, &store);
+ assertCalculationIs("product(x,x,1,2)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "2", "2", &globalContext, &store);
+ assertCalculationIs("diff(x^2,x,3)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "6", "6", &globalContext, &store);
+
+ Ion::Storage::sharedStorage()->recordNamed("x.exp").destroy();
+}
+
+
+QUIZ_CASE(calculation_complex_format) {
+ Shared::GlobalContext globalContext;
+ CalculationStore store(calculationBuffer,calculationBufferSize);
+
+ Poincare::Preferences::sharedPreferences()->setComplexFormat(Poincare::Preferences::ComplexFormat::Real);
+ assertCalculationIs("1+𝐢", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "1+𝐢", "1+𝐢", &globalContext, &store);
+ assertCalculationIs("√(-1)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, "unreal", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("ln(-2)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "unreal", "unreal", &globalContext, &store);
+ assertCalculationIs("√(-1)×√(-1)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "unreal", "unreal", &globalContext, &store);
+ assertCalculationIs("(-8)^(1/3)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "-2", "-2", &globalContext, &store);
+ assertCalculationIs("(-8)^(2/3)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "4", "4", &globalContext, &store);
+ assertCalculationIs("(-2)^(1/4)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "unreal", "unreal", &globalContext, &store);
+
+ Poincare::Preferences::sharedPreferences()->setComplexFormat(Poincare::Preferences::ComplexFormat::Cartesian);
+ assertCalculationIs("1+𝐢", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "1+𝐢", "1+𝐢", &globalContext, &store);
+ assertCalculationIs("√(-1)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "𝐢", "𝐢", &globalContext, &store);
+ assertCalculationIs("ln(-2)", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "ln(-2)", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("√(-1)×√(-1)", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, "-1", "-1", &globalContext, &store);
+ assertCalculationIs("(-8)^(1/3)", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "1+√(3)×𝐢", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("(-8)^(2/3)", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "-2+2×√(3)×𝐢", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("(-2)^(1/4)", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "root(8,4)/2+root(8,4)/2×𝐢", nullptr, nullptr, &globalContext, &store);
+
+ Poincare::Preferences::sharedPreferences()->setComplexFormat(Poincare::Preferences::ComplexFormat::Polar);
+ assertCalculationIs("1+𝐢", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "√(2)×ℯ^\u0012π/4×𝐢\u0013", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("√(-1)", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "ℯ^\u0012π/2×𝐢\u0013", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("ln(-2)", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "ln(-2)", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("√(-1)×√(-1)", DisplayOutput::ExactAndApproximate, EqualSign::Unknown, nullptr, "ℯ^\u00123.141593×𝐢\u0013", "ℯ^\u00123.1415926535898×𝐢\u0013", &globalContext, &store);
+ assertCalculationIs("(-8)^(1/3)", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "2×ℯ^\u0012π/3×𝐢\u0013", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("(-8)^(2/3)", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "4×ℯ^\u0012\u00122×π\u0013/3×𝐢\u0013", nullptr, nullptr, &globalContext, &store);
+ assertCalculationIs("(-2)^(1/4)", DisplayOutput::ExactAndApproximate, EqualSign::Approximation, "root(2,4)×ℯ^\u0012π/4×𝐢\u0013", nullptr, nullptr, &globalContext, &store);
+
+ Poincare::Preferences::sharedPreferences()->setComplexFormat(Poincare::Preferences::ComplexFormat::Cartesian);
+}
diff --git a/apps/code/Makefile b/apps/code/Makefile
new file mode 100644
index 00000000000..de11dcb4c66
--- /dev/null
+++ b/apps/code/Makefile
@@ -0,0 +1,41 @@
+apps += Code::App
+app_headers += apps/code/app.h
+
+app_code_src = $(addprefix apps/code/,\
+ app.cpp \
+ console_controller.cpp \
+ console_edit_cell.cpp \
+ console_line_cell.cpp \
+ console_store.cpp \
+ editor_controller.cpp \
+ editor_view.cpp \
+ helpers.cpp \
+ menu_controller.cpp \
+ python_text_area.cpp \
+ sandbox_controller.cpp \
+ script_name_cell.cpp \
+ script_parameter_controller.cpp \
+)
+
+app_code_test_src = $(addprefix apps/code/,\
+ python_toolbox.cpp \
+ script.cpp \
+ script_node_cell.cpp \
+ script_store.cpp \
+ script_template.cpp \
+ variable_box_empty_controller.cpp \
+ variable_box_controller.cpp \
+)
+
+tests_src += $(addprefix apps/code/test/,\
+ variable_box_controller.cpp\
+)
+
+app_code_src += $(app_code_test_src)
+apps_src += $(app_code_src)
+
+i18n_files += $(call i18n_with_universal_for,code/base)
+i18n_files += $(call i18n_with_universal_for,code/catalog)
+i18n_files += $(call i18n_with_universal_for,code/toolbox)
+
+$(eval $(call depends_on_image,apps/code/app.cpp,apps/code/code_icon.png))
diff --git a/apps/code/app.cpp b/apps/code/app.cpp
new file mode 100644
index 00000000000..63537618c24
--- /dev/null
+++ b/apps/code/app.cpp
@@ -0,0 +1,145 @@
+#include "app.h"
+#include "code_icon.h"
+#include
+#include "helpers.h"
+#include
+
+namespace Code {
+
+I18n::Message App::Descriptor::name() {
+ return I18n::Message::CodeApp;
+}
+
+I18n::Message App::Descriptor::upperName() {
+ return I18n::Message::CodeAppCapital;
+}
+
+const Image * App::Descriptor::icon() {
+ return ImageStore::CodeIcon;
+}
+
+App::Snapshot::Snapshot() :
+#if EPSILON_GETOPT
+ m_lockOnConsole(false),
+#endif
+ m_scriptStore()
+{
+}
+
+App * App::Snapshot::unpack(Container * container) {
+ return new (container->currentAppBuffer()) App(this);
+}
+
+App::Descriptor * App::Snapshot::descriptor() {
+ static Descriptor descriptor;
+ return &descriptor;
+}
+
+ScriptStore * App::Snapshot::scriptStore() {
+ return &m_scriptStore;
+}
+
+#if EPSILON_GETOPT
+bool App::Snapshot::lockOnConsole() const {
+ return m_lockOnConsole;
+}
+
+void App::Snapshot::setOpt(const char * name, const char * value) {
+ if (strcmp(name, "script") == 0) {
+ m_scriptStore.deleteAllScripts();
+ char * separator = const_cast(UTF8Helper::CodePointSearch(value, ':'));
+ if (*separator == 0) {
+ return;
+ }
+ *separator = 0;
+ const char * scriptName = value;
+ /* We include the 0 in the scriptContent to represent the importation
+ * status. It is set to 1 after addScriptFromTemplate. Indeed, this '/0'
+ * char has two goals: ending the scriptName and representing the
+ * importation status; we cannot set it to 1 before adding the script to
+ * storage. */
+ const char * scriptContent = separator;
+ Code::ScriptTemplate script(scriptName, scriptContent);
+ m_scriptStore.addScriptFromTemplate(&script);
+ ScriptStore::ScriptNamed(scriptName).toggleAutoimportationStatus(); // set Importation Status to 1
+ return;
+ }
+ if (strcmp(name, "lock-on-console") == 0) {
+ m_lockOnConsole = true;
+ return;
+ }
+}
+#endif
+
+App::App(Snapshot * snapshot) :
+ Shared::InputEventHandlerDelegateApp(snapshot, &m_codeStackViewController),
+ m_pythonHeap{},
+ m_pythonUser(nullptr),
+ m_consoleController(nullptr, this, snapshot->scriptStore()
+#if EPSILON_GETOPT
+ , snapshot->lockOnConsole()
+#endif
+ ),
+ m_listFooter(&m_codeStackViewController, &m_menuController, &m_menuController, ButtonRowController::Position::Bottom, ButtonRowController::Style::EmbossedGray, ButtonRowController::Size::Large),
+ m_menuController(&m_listFooter, this, snapshot->scriptStore(), &m_listFooter),
+ m_codeStackViewController(&m_modalViewController, &m_listFooter),
+ m_variableBoxController(snapshot->scriptStore())
+{
+ Clipboard::sharedClipboard()->enterPython();
+}
+
+App::~App() {
+ assert(!m_consoleController.inputRunLoopActive());
+ deinitPython();
+ Clipboard::sharedClipboard()->exitPython();
+}
+
+bool App::handleEvent(Ion::Events::Event event) {
+ if (event == Ion::Events::Home && m_consoleController.inputRunLoopActive()) {
+ /* We need to return true here because we want to actually exit from the
+ * input run loop, which requires ending a dispatchEvent cycle. */
+ m_consoleController.terminateInputLoop();
+ if (m_modalViewController.isDisplayingModal()) {
+ m_modalViewController.dismissModalViewController();
+ }
+ return true;
+ }
+ return false;
+}
+
+void App::willExitResponderChain(Responder * nextFirstResponder) {
+ m_menuController.willExitApp();
+}
+
+Toolbox * App::toolboxForInputEventHandler(InputEventHandler * textInput) {
+ return &m_toolbox;
+}
+
+VariableBoxController * App::variableBoxForInputEventHandler(InputEventHandler * textInput) {
+ return &m_variableBoxController;
+}
+
+bool App::textInputDidReceiveEvent(InputEventHandler * textInput, Ion::Events::Event event) {
+ const char * pythonText = Helpers::PythonTextForEvent(event);
+ if (pythonText != nullptr) {
+ textInput->handleEventWithText(pythonText);
+ return true;
+ }
+ return false;
+}
+
+void App::initPythonWithUser(const void * pythonUser) {
+ if (!m_pythonUser) {
+ MicroPython::init(m_pythonHeap, m_pythonHeap + k_pythonHeapSize);
+ }
+ m_pythonUser = pythonUser;
+}
+
+void App::deinitPython() {
+ if (m_pythonUser) {
+ MicroPython::deinit();
+ m_pythonUser = nullptr;
+ }
+}
+
+}
diff --git a/apps/code/app.h b/apps/code/app.h
new file mode 100644
index 00000000000..a9babedce13
--- /dev/null
+++ b/apps/code/app.h
@@ -0,0 +1,98 @@
+#ifndef CODE_APP_H
+#define CODE_APP_H
+
+#include
+#include
+#include "../shared/input_event_handler_delegate_app.h"
+#include "console_controller.h"
+#include "menu_controller.h"
+#include "script_store.h"
+#include "python_toolbox.h"
+#include "variable_box_controller.h"
+#include "../shared/shared_app.h"
+
+namespace Code {
+
+class App : public Shared::InputEventHandlerDelegateApp {
+public:
+ class Descriptor : public Shared::InputEventHandlerDelegateApp::Descriptor {
+ public:
+ I18n::Message name() override;
+ I18n::Message upperName() override;
+ const Image * icon() override;
+ };
+ class Snapshot : public SharedApp::Snapshot {
+ public:
+ Snapshot();
+ App * unpack(Container * container) override;
+ Descriptor * descriptor() override;
+ ScriptStore * scriptStore();
+#if EPSILON_GETOPT
+ bool lockOnConsole() const;
+ void setOpt(const char * name, const char * value) override;
+#endif
+ private:
+#if EPSILON_GETOPT
+ bool m_lockOnConsole;
+#endif
+ ScriptStore m_scriptStore;
+ };
+ static App * app() {
+ return static_cast(Container::activeApp());
+ }
+ ~App();
+ TELEMETRY_ID("Code");
+ bool prepareForExit() override {
+ if (m_consoleController.inputRunLoopActive()) {
+ m_consoleController.terminateInputLoop();
+ return false;
+ }
+ return true;
+ }
+ StackViewController * stackViewController() { return &m_codeStackViewController; }
+ ConsoleController * consoleController() { return &m_consoleController; }
+ MenuController * menuController() { return &m_menuController; }
+
+ /* Responder */
+ bool handleEvent(Ion::Events::Event event) override;
+ void willExitResponderChain(Responder * nextFirstResponder) override;
+
+ /* InputEventHandlerDelegate */
+ Toolbox * toolboxForInputEventHandler(InputEventHandler * textInput) override;
+ VariableBoxController * variableBoxForInputEventHandler(InputEventHandler * textInput) override;
+
+ /* TextInputDelegate */
+ bool textInputDidReceiveEvent(InputEventHandler * textInput, Ion::Events::Event event);
+
+ /* Code::App */
+ // Python delegate
+ bool pythonIsInited() { return m_pythonUser != nullptr; }
+ bool isPythonUser(const void * pythonUser) { return m_pythonUser == pythonUser; }
+ void initPythonWithUser(const void * pythonUser);
+ void deinitPython();
+
+ VariableBoxController * variableBoxController() { return &m_variableBoxController; }
+
+ static constexpr int k_pythonHeapSize = 32768;
+
+private:
+ /* Python delegate:
+ * MicroPython requires a heap. To avoid dynamic allocation, we keep a working
+ * buffer here and we give to controllers that load Python environment. We
+ * also memoize the last Python user to avoid re-initiating MicroPython when
+ * unneeded. */
+ char m_pythonHeap[k_pythonHeapSize];
+ const void * m_pythonUser;
+
+ App(Snapshot * snapshot);
+ ConsoleController m_consoleController;
+ ButtonRowController m_listFooter;
+ MenuController m_menuController;
+ StackViewController m_codeStackViewController;
+ PythonToolbox m_toolbox;
+ VariableBoxController m_variableBoxController;
+};
+
+}
+
+#endif
diff --git a/apps/code/base.de.i18n b/apps/code/base.de.i18n
new file mode 100644
index 00000000000..15a39732e2f
--- /dev/null
+++ b/apps/code/base.de.i18n
@@ -0,0 +1,13 @@
+AddScript = "Skript hinzufügen"
+AllowedCharactersaz09 = "Erlaubte Zeichen: a-z, 0-9, _"
+Autocomplete = "Autovervollständigung"
+AutoImportScript = "Automatischer Import in Konsole"
+BuiltinsAndKeywords = "Native Funktionen und Schlüsselwörter"
+Console = "Interaktive Konsole"
+DeleteScript = "Skript löschen"
+ExecuteScript = "Skript ausführen"
+FunctionsAndVariables = "Funktionen und Variablen"
+ImportedModulesAndScripts = "Importierte Module und Skripte"
+NoWordAvailableHere = "Kein Wort ist hier verfübar."
+ScriptInProgress = "Aktuelle Skript"
+ScriptOptions = "Skriptoptionen"
diff --git a/apps/code/base.en.i18n b/apps/code/base.en.i18n
new file mode 100644
index 00000000000..4dcdfbd3be1
--- /dev/null
+++ b/apps/code/base.en.i18n
@@ -0,0 +1,13 @@
+AddScript = "Add a script"
+AllowedCharactersaz09 = "Allowed characters: a-z, 0-9, _"
+Autocomplete = "Autocomplete"
+AutoImportScript = "Auto import in shell"
+BuiltinsAndKeywords = "Builtins and keywords"
+Console = "Python shell"
+DeleteScript = "Delete script"
+ExecuteScript = "Execute script"
+FunctionsAndVariables = "Functions and variables"
+ImportedModulesAndScripts = "Imported modules and scripts"
+NoWordAvailableHere = "No word available here."
+ScriptInProgress = "Script in progress"
+ScriptOptions = "Script options"
diff --git a/apps/code/base.es.i18n b/apps/code/base.es.i18n
new file mode 100644
index 00000000000..ccceb896fec
--- /dev/null
+++ b/apps/code/base.es.i18n
@@ -0,0 +1,13 @@
+AddScript = "Agregar un archivo"
+AllowedCharactersaz09 = "Caracteres permitidos : a-z, 0-9, _"
+Autocomplete = "Autocompleción"
+AutoImportScript = "Importación auto en intérprete"
+BuiltinsAndKeywords = "Funciones nativas y palabras clave"
+Console = "Interprete de comandos"
+DeleteScript = "Eliminar el archivo"
+ExecuteScript = "Ejecutar el archivo"
+FunctionsAndVariables = "Funciones y variables"
+ImportedModulesAndScripts = "Módulos y archivos importados"
+NoWordAvailableHere = "No hay ninguna palabra disponible aquí."
+ScriptInProgress = "Archivo en curso"
+ScriptOptions = "Opciones del archivo"
diff --git a/apps/code/base.fr.i18n b/apps/code/base.fr.i18n
new file mode 100644
index 00000000000..bd3179ee7a2
--- /dev/null
+++ b/apps/code/base.fr.i18n
@@ -0,0 +1,13 @@
+AddScript = "Ajouter un script"
+AllowedCharactersaz09 = "Caractères autorisés : a-z, 0-9, _"
+Autocomplete = "Auto-complétion"
+AutoImportScript = "Importation auto dans la console"
+BuiltinsAndKeywords = "Fonctions natives et mots-clés"
+Console = "Console d'exécution"
+DeleteScript = "Supprimer le script"
+ExecuteScript = "Exécuter le script"
+FunctionsAndVariables = "Fonctions et variables"
+ImportedModulesAndScripts = "Modules et scripts importés"
+NoWordAvailableHere = "Aucun mot disponible à cet endroit."
+ScriptInProgress = "Script en cours"
+ScriptOptions = "Options de script"
diff --git a/apps/code/base.it.i18n b/apps/code/base.it.i18n
new file mode 100644
index 00000000000..a3d5abcbb8a
--- /dev/null
+++ b/apps/code/base.it.i18n
@@ -0,0 +1,13 @@
+AddScript = "Aggiungere script"
+AllowedCharactersaz09 = "Caratteri consentiti : a-z, 0-9, _"
+Autocomplete = "Autocompletamento"
+AutoImportScript = "Auto importazione nella console"
+BuiltinsAndKeywords = "Funzioni native e parole chiave"
+Console = "Console d'esecuzione"
+DeleteScript = "Eliminare lo script"
+ExecuteScript = "Eseguire lo script"
+FunctionsAndVariables = "Funzioni e variabili"
+ImportedModulesAndScripts = "Moduli e scripts importati"
+NoWordAvailableHere = "Nessuna parola disponibile qui."
+ScriptInProgress = "Script in corso"
+ScriptOptions = "Opzioni dello script"
diff --git a/apps/code/base.nl.i18n b/apps/code/base.nl.i18n
new file mode 100644
index 00000000000..e016172c246
--- /dev/null
+++ b/apps/code/base.nl.i18n
@@ -0,0 +1,13 @@
+AddScript = "Script toevoegen"
+AllowedCharactersaz09 = "Toegestane tekens: a-z, 0-9, _"
+Autocomplete = "Autocomplete"
+AutoImportScript = "Automatisch importeren in shell"
+BuiltinsAndKeywords = "Builtins and keywords"
+Console = "Python shell"
+DeleteScript = "Script verwijderen"
+ExecuteScript = "Script uitvoeren"
+FunctionsAndVariables = "Functies en variabelen"
+ImportedModulesAndScripts = "Imported modules and scripts"
+NoWordAvailableHere = "No word available here."
+ScriptInProgress = "Script in progress"
+ScriptOptions = "Script opties"
diff --git a/apps/code/base.pt.i18n b/apps/code/base.pt.i18n
new file mode 100644
index 00000000000..3a4a8839cea
--- /dev/null
+++ b/apps/code/base.pt.i18n
@@ -0,0 +1,13 @@
+AddScript = "Adicionar um script"
+AllowedCharactersaz09 = "Caracteres permitidos : a-z, 0-9, _"
+Autocomplete = "Preenchimento automático"
+AutoImportScript = "Importação auto no interpretador"
+BuiltinsAndKeywords = "Funções nativas e palavras-chave"
+Console = "Interpretador interativo"
+DeleteScript = "Eliminar o script"
+ExecuteScript = "Executar o script"
+FunctionsAndVariables = "Funções e variáveis"
+ImportedModulesAndScripts = "Módulos e scripts importados"
+NoWordAvailableHere = "Nenhuma palavra disponível aqui."
+ScriptInProgress = "Script em curso"
+ScriptOptions = "Opções de script"
diff --git a/apps/code/base.universal.i18n b/apps/code/base.universal.i18n
new file mode 100644
index 00000000000..dbbf69f5a3b
--- /dev/null
+++ b/apps/code/base.universal.i18n
@@ -0,0 +1,3 @@
+CodeAppCapital = "PYTHON"
+ConsolePrompt = ">>> "
+ScriptParameters = "..."
diff --git a/apps/code/catalog.de.i18n b/apps/code/catalog.de.i18n
new file mode 100644
index 00000000000..ab998c81bc6
--- /dev/null
+++ b/apps/code/catalog.de.i18n
@@ -0,0 +1,197 @@
+PythonPound = "Kommentar"
+PythonPercent = "Modulo"
+Python1J = "Imaginäres i"
+PythonLF = "Zeilenvorschub"
+PythonTab = "Tabulator"
+PythonAmpersand = "Bitweise und"
+PythonSymbolExp = "Bitweise exklusiv oder"
+PythonVerticalBar = "Bitweise oder"
+PythonImag = "Imaginärteil von z"
+PythonReal = "Realteil von z"
+PythonSingleQuote = "Einfaches Anführungszeichen"
+PythonAbs = "Absolute/r Wert/Größe"
+PythonAcos = "Arkuskosinus"
+PythonAcosh = "Hyperbelkosinus"
+PythonAppend = "Add x to the end of the list"
+PythonArrow = "Arrow from (x,y) to (x+dx,y+dy)"
+PythonAsin = "Arkussinus"
+PythonAsinh = "Hyperbelsinus"
+PythonAtan = "Arkustangens"
+PythonAtan2 = "Gib atan(y/x)"
+PythonAtanh = "Hyperbeltangens"
+PythonAxis = "Set axes to (xmin,xmax,ymin,ymax)"
+PythonBar = "Draw a bar plot with x values"
+PythonBin = "Ganzzahl nach binär konvertieren"
+PythonCeil = "Aufrundung"
+PythonChoice = "Zufallszahl aus der Liste"
+PythonClear = "Empty the list"
+PythonCmathFunction = "cmath-Modul-Funktionspräfix"
+PythonColor = "Definiere eine RGB-Farbe"
+PythonColorBlack = "Black color"
+PythonColorBlue = "Blue color"
+PythonColorBrown = "Brown color"
+PythonColorGray = "Gray color"
+PythonColorGreen = "Green color"
+PythonColorOrange = "Orange color"
+PythonColorPink = "Pink color"
+PythonColorPurple = "Purple color"
+PythonColorRed = "Red color"
+PythonColorWhite = "White color"
+PythonColorYellow = "Yellow color"
+PythonComplex = "a+ib zurückgeben"
+PythonCopySign = "Return x with the sign of y"
+PythonCos = "Kosinus"
+PythonCosh = "Hyperbolic cosine"
+PythonCount = "Count the occurrences of x"
+PythonDegrees = "Convert x from radians to degrees"
+PythonDivMod = "Quotient and remainder"
+PythonDrawString = "Display a text from pixel (x,y)"
+PythonErf = "Error function"
+PythonErfc = "Complementary error function"
+PythonEval = "Return the evaluated expression"
+PythonExp = "Exponential function"
+PythonExpm1 = "Compute exp(x)-1"
+PythonFabs = "Absolute value"
+PythonFillRect = "Fill a rectangle at pixel (x,y)"
+PythonFloat = "Convert x to a float"
+PythonFloor = "Floor"
+PythonFmod = "a modulo b"
+PythonFrExp = "Mantissa and exponent of x: (m,e)"
+PythonGamma = "Gamma function"
+PythonGetPixel = "Return pixel (x,y) color"
+PythonGetrandbits = "Integer with k random bits"
+PythonGrid = "Toggle the visibility of the grid"
+PythonHex = "Convert integer to hexadecimal"
+PythonHist = "Draw the histogram of x"
+PythonImportCmath = "Import cmath module"
+PythonImportIon = "Import ion module"
+PythonImportKandinsky = "Import kandinsky module"
+PythonImportRandom = "Import random module"
+PythonImportMath = "Import math module"
+PythonImportMatplotlibPyplot = "Import matplotlib.pyplot module"
+PythonImportTime = "Import time module"
+PythonImportTurtle = "Import turtle module"
+PythonIndex = "Index of the first x occurrence"
+PythonInput = "Prompt a value"
+PythonInsert = "Insert x at index i in the list"
+PythonInt = "Convert x to an integer"
+PythonIonFunction = "ion module function prefix"
+PythonIsFinite = "Check if x is finite"
+PythonIsInfinite = "Check if x is infinity"
+PythonIsNaN = "Check if x is a NaN"
+PythonIsKeyDown = "Return True if the k key is down"
+PythonKandinskyFunction = "kandinsky module function prefix"
+PythonKeyLeft = "LEFT ARROW key"
+PythonKeyUp = "UP ARROW key"
+PythonKeyDown = "DOWN ARROW key"
+PythonKeyRight = "RIGHT ARROW key"
+PythonKeyOk = "OK key"
+PythonKeyBack = "BACK key"
+PythonKeyHome = "HOME key"
+PythonKeyOnOff = "ON/OFF key"
+PythonKeyShift = "SHIFT key"
+PythonKeyAlpha = "ALPHA key"
+PythonKeyXnt = "X,N,T key"
+PythonKeyVar = "VAR key"
+PythonKeyToolbox = "TOOLBOX key"
+PythonKeyBackspace = "BACKSPACE key"
+PythonKeyExp = "EXPONENTIAL key"
+PythonKeyLn = "NATURAL LOGARITHM key"
+PythonKeyLog = "DECIMAL LOGARITHM key"
+PythonKeyImaginary = "IMAGINARY I key"
+PythonKeyComma = "COMMA key"
+PythonKeyPower = "POWER key"
+PythonKeySine = "SINE key"
+PythonKeyCosine = "COSINE key"
+PythonKeyTangent = "TANGENT key"
+PythonKeyPi = "PI key"
+PythonKeySqrt = "SQUARE ROOT key"
+PythonKeySquare = "SQUARE key"
+PythonKeySeven = "7 key"
+PythonKeyEight = "8 key"
+PythonKeyNine = "9 key"
+PythonKeyLeftParenthesis = "LEFT PARENTHESIS key"
+PythonKeyRightParenthesis = "RIGHT PARENTHESIS key"
+PythonKeyFour = "4 key"
+PythonKeyFive = "5 key"
+PythonKeySix = "6 key"
+PythonKeyMultiplication = "MULTIPLICATION key"
+PythonKeyDivision = "DIVISION key"
+PythonKeyOne = "1 key"
+PythonKeyTwo = "2 key"
+PythonKeyThree = "3 key"
+PythonKeyPlus = "PLUS key"
+PythonKeyMinus = "MINUS key"
+PythonKeyZero = "0 key"
+PythonKeyDot = "DOT key"
+PythonKeyEe = "10 POWER X key"
+PythonKeyAns = "ANS key"
+PythonKeyExe = "EXE key"
+PythonLdexp = "Return x*(2**i), inverse of frexp"
+PythonLength = "Length of an object"
+PythonLgamma = "Log-gamma function"
+PythonLog = "Logarithm to base a"
+PythonLog10 = "Logarithm to base 10"
+PythonLog2 = "Logarithm to base 2"
+PythonMathFunction = "math module function prefix"
+PythonMatplotlibPyplotFunction = "matplotlib.pyplot module prefix"
+PythonMax = "Maximum"
+PythonMin = "Minimum"
+PythonModf = "Fractional and integer parts of x"
+PythonMonotonic = "Value of a monotonic clock"
+PythonOct = "Convert integer to octal"
+PythonPhase = "Phase of z"
+PythonPlot = "Plot y versus x as lines"
+PythonPolar = "z in polar coordinates"
+PythonPop = "Remove and return the last item"
+PythonPower = "x raised to the power y"
+PythonPrint = "Print object"
+PythonRadians = "Convert x from degrees to radians"
+PythonRandint = "Random integer in [a,b]"
+PythonRandom = "Floating point number in [0,1["
+PythonRandomFunction = "random module function prefix"
+PythonRandrange = "Random number in range(start,stop)"
+PythonRangeStartStop = "List from start to stop-1"
+PythonRangeStop = "List from 0 to stop-1"
+PythonRect = "Convert to cartesian coordinates"
+PythonRemove = "Remove the first occurrence of x"
+PythonReverse = "Reverse the elements of the list"
+PythonRound = "Round to n digits"
+PythonScatter = "Draw a scatter plot of y versus x"
+PythonSeed = "Initialize random number generator"
+PythonSetPixel = "Color pixel (x,y)"
+PythonShow = "Display the figure"
+PythonSin = "Sine"
+PythonSinh = "Hyperbolic sine"
+PythonSleep = "Suspend the execution for t seconds"
+PythonSort = "Sort the list"
+PythonSqrt = "Square root"
+PythonSum = "Sum the items of a list"
+PythonTan = "Tangent"
+PythonTanh = "Hyperbolic tangent"
+PythonText = "Display a text at (x,y) coordinates"
+PythonTimeFunction = "time module function prefix"
+PythonTrunc = "x truncated to an integer"
+PythonTurtleBackward = "Move backward by x pixels"
+PythonTurtleCircle = "Circle of radius r pixels"
+PythonTurtleColor = "Set the pen color"
+PythonTurtleColorMode = "Set the color mode to 1.0 or 255"
+PythonTurtleForward = "Move forward by x pixels"
+PythonTurtleFunction = "turtle module function prefix"
+PythonTurtleGoto = "Move to (x,y) coordinates"
+PythonTurtleHeading = "Return the current heading"
+PythonTurtleHideturtle = "Hide the turtle"
+PythonTurtleIsdown = "Return True if the pen is down"
+PythonTurtleLeft = "Turn left by a degrees"
+PythonTurtlePendown = "Pull the pen down"
+PythonTurtlePensize = "Set the line thickness to x pixels"
+PythonTurtlePenup = "Pull the pen up"
+PythonTurtlePosition = "Return the current (x,y) location"
+PythonTurtleReset = "Reset the drawing"
+PythonTurtleRight = "Turn right by a degrees"
+PythonTurtleSetheading = "Set the orientation to a degrees"
+PythonTurtleSetposition = "Positionne la tortue"
+PythonTurtleShowturtle = "Show the turtle"
+PythonTurtleSpeed = "Drawing speed between 0 and 10"
+PythonTurtleWrite = "Display a text"
+PythonUniform = "Floating point number in [a,b]"
diff --git a/apps/code/catalog.en.i18n b/apps/code/catalog.en.i18n
new file mode 100644
index 00000000000..eaffc09a7b9
--- /dev/null
+++ b/apps/code/catalog.en.i18n
@@ -0,0 +1,197 @@
+PythonPound = "Comment"
+PythonPercent = "Modulo"
+Python1J = "Imaginary i"
+PythonLF = "Line feed"
+PythonTab = "Tabulation"
+PythonAmpersand = "Bitwise and"
+PythonSymbolExp = "Bitwise exclusive or"
+PythonVerticalBar = "Bitwise or"
+PythonImag = "Imaginary part of z"
+PythonReal = "Real part of z"
+PythonSingleQuote = "Single quote"
+PythonAbs = "Absolute value/Magnitude"
+PythonAcos = "Arc cosine"
+PythonAcosh = "Arc hyperbolic cosine"
+PythonAppend = "Add x to the end of the list"
+PythonArrow = "Arrow from (x,y) to (x+dx,y+dy)"
+PythonAsin = "Arc sine"
+PythonAsinh = "Arc hyperbolic sine"
+PythonAtan = "Arc tangent"
+PythonAtan2 = "Return atan(y/x)"
+PythonAtanh = "Arc hyperbolic tangent"
+PythonAxis = "Set axes to (xmin,xmax,ymin,ymax)"
+PythonBar = "Draw a bar plot with x values"
+PythonBin = "Convert integer to binary"
+PythonCeil = "Ceiling"
+PythonChoice = "Random number in the list"
+PythonClear = "Empty the list"
+PythonCmathFunction = "cmath module function prefix"
+PythonColor = "Define a rgb color"
+PythonColorBlack = "Black color"
+PythonColorBlue = "Blue color"
+PythonColorBrown = "Brown color"
+PythonColorGray = "Gray color"
+PythonColorGreen = "Green color"
+PythonColorOrange = "Orange color"
+PythonColorPink = "Pink color"
+PythonColorPurple = "Purple color"
+PythonColorRed = "Red color"
+PythonColorWhite = "White color"
+PythonColorYellow = "Yellow color"
+PythonComplex = "Return a+ib"
+PythonCopySign = "Return x with the sign of y"
+PythonCos = "Cosine"
+PythonCosh = "Hyperbolic cosine"
+PythonCount = "Count the occurrences of x"
+PythonDegrees = "Convert x from radians to degrees"
+PythonDivMod = "Quotient and remainder"
+PythonDrawString = "Display a text from pixel (x,y)"
+PythonErf = "Error function"
+PythonErfc = "Complementary error function"
+PythonEval = "Return the evaluated expression"
+PythonExp = "Exponential function"
+PythonExpm1 = "Compute exp(x)-1"
+PythonFabs = "Absolute value"
+PythonFillRect = "Fill a rectangle at pixel (x,y)"
+PythonFloat = "Convert x to a float"
+PythonFloor = "Floor"
+PythonFmod = "a modulo b"
+PythonFrExp = "Mantissa and exponent of x: (m,e)"
+PythonGamma = "Gamma function"
+PythonGetPixel = "Return pixel (x,y) color"
+PythonGetrandbits = "Integer with k random bits"
+PythonGrid = "Toggle the visibility of the grid"
+PythonHex = "Convert integer to hexadecimal"
+PythonHist = "Draw the histogram of x"
+PythonImportCmath = "Import cmath module"
+PythonImportIon = "Import ion module"
+PythonImportKandinsky = "Import kandinsky module"
+PythonImportRandom = "Import random module"
+PythonImportMath = "Import math module"
+PythonImportMatplotlibPyplot = "Import matplotlib.pyplot module"
+PythonImportTime = "Import time module"
+PythonImportTurtle = "Import turtle module"
+PythonIndex = "Index of the first x occurrence"
+PythonInput = "Prompt a value"
+PythonInsert = "Insert x at index i in the list"
+PythonInt = "Convert x to an integer"
+PythonIonFunction = "ion module function prefix"
+PythonIsFinite = "Check if x is finite"
+PythonIsInfinite = "Check if x is infinity"
+PythonIsKeyDown = "Return True if the k key is down"
+PythonIsNaN = "Check if x is a NaN"
+PythonKandinskyFunction = "kandinsky module function prefix"
+PythonKeyLeft = "LEFT ARROW key"
+PythonKeyUp = "UP ARROW key"
+PythonKeyDown = "DOWN ARROW key"
+PythonKeyRight = "RIGHT ARROW key"
+PythonKeyOk = "OK key"
+PythonKeyBack = "BACK key"
+PythonKeyHome = "HOME key"
+PythonKeyOnOff = "ON/OFF key"
+PythonKeyShift = "SHIFT key"
+PythonKeyAlpha = "ALPHA key"
+PythonKeyXnt = "X,N,T key"
+PythonKeyVar = "VAR key"
+PythonKeyToolbox = "TOOLBOX key"
+PythonKeyBackspace = "BACKSPACE key"
+PythonKeyExp = "EXPONENTIAL key"
+PythonKeyLn = "NATURAL LOGARITHM key"
+PythonKeyLog = "DECIMAL LOGARITHM key"
+PythonKeyImaginary = "IMAGINARY I key"
+PythonKeyComma = "COMMA key"
+PythonKeyPower = "POWER key"
+PythonKeySine = "SINE key"
+PythonKeyCosine = "COSINE key"
+PythonKeyTangent = "TANGENT key"
+PythonKeyPi = "PI key"
+PythonKeySqrt = "SQUARE ROOT key"
+PythonKeySquare = "SQUARE key"
+PythonKeySeven = "7 key"
+PythonKeyEight = "8 key"
+PythonKeyNine = "9 key"
+PythonKeyLeftParenthesis = "LEFT PARENTHESIS key"
+PythonKeyRightParenthesis = "RIGHT PARENTHESIS key"
+PythonKeyFour = "4 key"
+PythonKeyFive = "5 key"
+PythonKeySix = "6 key"
+PythonKeyMultiplication = "MULTIPLICATION key"
+PythonKeyDivision = "DIVISION key"
+PythonKeyOne = "1 key"
+PythonKeyTwo = "2 key"
+PythonKeyThree = "3 key"
+PythonKeyPlus = "PLUS key"
+PythonKeyMinus = "MINUS key"
+PythonKeyZero = "0 key"
+PythonKeyDot = "DOT key"
+PythonKeyEe = "10 POWER X key"
+PythonKeyAns = "ANS key"
+PythonKeyExe = "EXE key"
+PythonLdexp = "Return x*(2**i), inverse of frexp"
+PythonLength = "Length of an object"
+PythonLgamma = "Log-gamma function"
+PythonLog = "Logarithm to base a"
+PythonLog10 = "Logarithm to base 10"
+PythonLog2 = "Logarithm to base 2"
+PythonMathFunction = "math module function prefix"
+PythonMatplotlibPyplotFunction = "matplotlib.pyplot module prefix"
+PythonMax = "Maximum"
+PythonMin = "Minimum"
+PythonModf = "Fractional and integer parts of x"
+PythonMonotonic = "Value of a monotonic clock"
+PythonOct = "Convert integer to octal"
+PythonPhase = "Phase of z"
+PythonPlot = "Plot y versus x as lines"
+PythonPolar = "z in polar coordinates"
+PythonPop = "Remove and return the last item"
+PythonPower = "x raised to the power y"
+PythonPrint = "Print object"
+PythonRadians = "Convert x from degrees to radians"
+PythonRandint = "Random integer in [a,b]"
+PythonRandom = "Floating point number in [0,1["
+PythonRandomFunction = "random module function prefix"
+PythonRandrange = "Random number in range(start,stop)"
+PythonRangeStartStop = "List from start to stop-1"
+PythonRangeStop = "List from 0 to stop-1"
+PythonRect = "Convert to cartesian coordinates"
+PythonRemove = "Remove the first occurrence of x"
+PythonReverse = "Reverse the elements of the list"
+PythonRound = "Round to n digits"
+PythonScatter = "Draw a scatter plot of y versus x"
+PythonSeed = "Initialize random number generator"
+PythonSetPixel = "Color pixel (x,y)"
+PythonShow = "Display the figure"
+PythonSin = "Sine"
+PythonSinh = "Hyperbolic sine"
+PythonSleep = "Suspend the execution for t seconds"
+PythonSort = "Sort the list"
+PythonSqrt = "Square root"
+PythonSum = "Sum the items of a list"
+PythonTan = "Tangent"
+PythonTanh = "Hyperbolic tangent"
+PythonText = "Display a text at (x,y) coordinates"
+PythonTimeFunction = "time module function prefix"
+PythonTrunc = "x truncated to an integer"
+PythonTurtleBackward = "Move backward by x pixels"
+PythonTurtleCircle = "Circle of radius r pixels"
+PythonTurtleColor = "Set the pen color"
+PythonTurtleColorMode = "Set the color mode to 1.0 or 255"
+PythonTurtleForward = "Move forward by x pixels"
+PythonTurtleFunction = "turtle module function prefix"
+PythonTurtleGoto = "Move to (x,y) coordinates"
+PythonTurtleHeading = "Return the current heading"
+PythonTurtleHideturtle = "Hide the turtle"
+PythonTurtleIsdown = "Return True if the pen is down"
+PythonTurtleLeft = "Turn left by a degrees"
+PythonTurtlePendown = "Pull the pen down"
+PythonTurtlePensize = "Set the line thickness to x pixels"
+PythonTurtlePenup = "Pull the pen up"
+PythonTurtlePosition = "Return the current (x,y) location"
+PythonTurtleReset = "Reset the drawing"
+PythonTurtleRight = "Turn right by a degrees"
+PythonTurtleSetheading = "Set the orientation to a degrees"
+PythonTurtleSetposition = "Positionne la tortue"
+PythonTurtleShowturtle = "Show the turtle"
+PythonTurtleSpeed = "Drawing speed between 0 and 10"
+PythonTurtleWrite = "Display a text"
+PythonUniform = "Floating point number in [a,b]"
diff --git a/apps/code/catalog.es.i18n b/apps/code/catalog.es.i18n
new file mode 100644
index 00000000000..eaffc09a7b9
--- /dev/null
+++ b/apps/code/catalog.es.i18n
@@ -0,0 +1,197 @@
+PythonPound = "Comment"
+PythonPercent = "Modulo"
+Python1J = "Imaginary i"
+PythonLF = "Line feed"
+PythonTab = "Tabulation"
+PythonAmpersand = "Bitwise and"
+PythonSymbolExp = "Bitwise exclusive or"
+PythonVerticalBar = "Bitwise or"
+PythonImag = "Imaginary part of z"
+PythonReal = "Real part of z"
+PythonSingleQuote = "Single quote"
+PythonAbs = "Absolute value/Magnitude"
+PythonAcos = "Arc cosine"
+PythonAcosh = "Arc hyperbolic cosine"
+PythonAppend = "Add x to the end of the list"
+PythonArrow = "Arrow from (x,y) to (x+dx,y+dy)"
+PythonAsin = "Arc sine"
+PythonAsinh = "Arc hyperbolic sine"
+PythonAtan = "Arc tangent"
+PythonAtan2 = "Return atan(y/x)"
+PythonAtanh = "Arc hyperbolic tangent"
+PythonAxis = "Set axes to (xmin,xmax,ymin,ymax)"
+PythonBar = "Draw a bar plot with x values"
+PythonBin = "Convert integer to binary"
+PythonCeil = "Ceiling"
+PythonChoice = "Random number in the list"
+PythonClear = "Empty the list"
+PythonCmathFunction = "cmath module function prefix"
+PythonColor = "Define a rgb color"
+PythonColorBlack = "Black color"
+PythonColorBlue = "Blue color"
+PythonColorBrown = "Brown color"
+PythonColorGray = "Gray color"
+PythonColorGreen = "Green color"
+PythonColorOrange = "Orange color"
+PythonColorPink = "Pink color"
+PythonColorPurple = "Purple color"
+PythonColorRed = "Red color"
+PythonColorWhite = "White color"
+PythonColorYellow = "Yellow color"
+PythonComplex = "Return a+ib"
+PythonCopySign = "Return x with the sign of y"
+PythonCos = "Cosine"
+PythonCosh = "Hyperbolic cosine"
+PythonCount = "Count the occurrences of x"
+PythonDegrees = "Convert x from radians to degrees"
+PythonDivMod = "Quotient and remainder"
+PythonDrawString = "Display a text from pixel (x,y)"
+PythonErf = "Error function"
+PythonErfc = "Complementary error function"
+PythonEval = "Return the evaluated expression"
+PythonExp = "Exponential function"
+PythonExpm1 = "Compute exp(x)-1"
+PythonFabs = "Absolute value"
+PythonFillRect = "Fill a rectangle at pixel (x,y)"
+PythonFloat = "Convert x to a float"
+PythonFloor = "Floor"
+PythonFmod = "a modulo b"
+PythonFrExp = "Mantissa and exponent of x: (m,e)"
+PythonGamma = "Gamma function"
+PythonGetPixel = "Return pixel (x,y) color"
+PythonGetrandbits = "Integer with k random bits"
+PythonGrid = "Toggle the visibility of the grid"
+PythonHex = "Convert integer to hexadecimal"
+PythonHist = "Draw the histogram of x"
+PythonImportCmath = "Import cmath module"
+PythonImportIon = "Import ion module"
+PythonImportKandinsky = "Import kandinsky module"
+PythonImportRandom = "Import random module"
+PythonImportMath = "Import math module"
+PythonImportMatplotlibPyplot = "Import matplotlib.pyplot module"
+PythonImportTime = "Import time module"
+PythonImportTurtle = "Import turtle module"
+PythonIndex = "Index of the first x occurrence"
+PythonInput = "Prompt a value"
+PythonInsert = "Insert x at index i in the list"
+PythonInt = "Convert x to an integer"
+PythonIonFunction = "ion module function prefix"
+PythonIsFinite = "Check if x is finite"
+PythonIsInfinite = "Check if x is infinity"
+PythonIsKeyDown = "Return True if the k key is down"
+PythonIsNaN = "Check if x is a NaN"
+PythonKandinskyFunction = "kandinsky module function prefix"
+PythonKeyLeft = "LEFT ARROW key"
+PythonKeyUp = "UP ARROW key"
+PythonKeyDown = "DOWN ARROW key"
+PythonKeyRight = "RIGHT ARROW key"
+PythonKeyOk = "OK key"
+PythonKeyBack = "BACK key"
+PythonKeyHome = "HOME key"
+PythonKeyOnOff = "ON/OFF key"
+PythonKeyShift = "SHIFT key"
+PythonKeyAlpha = "ALPHA key"
+PythonKeyXnt = "X,N,T key"
+PythonKeyVar = "VAR key"
+PythonKeyToolbox = "TOOLBOX key"
+PythonKeyBackspace = "BACKSPACE key"
+PythonKeyExp = "EXPONENTIAL key"
+PythonKeyLn = "NATURAL LOGARITHM key"
+PythonKeyLog = "DECIMAL LOGARITHM key"
+PythonKeyImaginary = "IMAGINARY I key"
+PythonKeyComma = "COMMA key"
+PythonKeyPower = "POWER key"
+PythonKeySine = "SINE key"
+PythonKeyCosine = "COSINE key"
+PythonKeyTangent = "TANGENT key"
+PythonKeyPi = "PI key"
+PythonKeySqrt = "SQUARE ROOT key"
+PythonKeySquare = "SQUARE key"
+PythonKeySeven = "7 key"
+PythonKeyEight = "8 key"
+PythonKeyNine = "9 key"
+PythonKeyLeftParenthesis = "LEFT PARENTHESIS key"
+PythonKeyRightParenthesis = "RIGHT PARENTHESIS key"
+PythonKeyFour = "4 key"
+PythonKeyFive = "5 key"
+PythonKeySix = "6 key"
+PythonKeyMultiplication = "MULTIPLICATION key"
+PythonKeyDivision = "DIVISION key"
+PythonKeyOne = "1 key"
+PythonKeyTwo = "2 key"
+PythonKeyThree = "3 key"
+PythonKeyPlus = "PLUS key"
+PythonKeyMinus = "MINUS key"
+PythonKeyZero = "0 key"
+PythonKeyDot = "DOT key"
+PythonKeyEe = "10 POWER X key"
+PythonKeyAns = "ANS key"
+PythonKeyExe = "EXE key"
+PythonLdexp = "Return x*(2**i), inverse of frexp"
+PythonLength = "Length of an object"
+PythonLgamma = "Log-gamma function"
+PythonLog = "Logarithm to base a"
+PythonLog10 = "Logarithm to base 10"
+PythonLog2 = "Logarithm to base 2"
+PythonMathFunction = "math module function prefix"
+PythonMatplotlibPyplotFunction = "matplotlib.pyplot module prefix"
+PythonMax = "Maximum"
+PythonMin = "Minimum"
+PythonModf = "Fractional and integer parts of x"
+PythonMonotonic = "Value of a monotonic clock"
+PythonOct = "Convert integer to octal"
+PythonPhase = "Phase of z"
+PythonPlot = "Plot y versus x as lines"
+PythonPolar = "z in polar coordinates"
+PythonPop = "Remove and return the last item"
+PythonPower = "x raised to the power y"
+PythonPrint = "Print object"
+PythonRadians = "Convert x from degrees to radians"
+PythonRandint = "Random integer in [a,b]"
+PythonRandom = "Floating point number in [0,1["
+PythonRandomFunction = "random module function prefix"
+PythonRandrange = "Random number in range(start,stop)"
+PythonRangeStartStop = "List from start to stop-1"
+PythonRangeStop = "List from 0 to stop-1"
+PythonRect = "Convert to cartesian coordinates"
+PythonRemove = "Remove the first occurrence of x"
+PythonReverse = "Reverse the elements of the list"
+PythonRound = "Round to n digits"
+PythonScatter = "Draw a scatter plot of y versus x"
+PythonSeed = "Initialize random number generator"
+PythonSetPixel = "Color pixel (x,y)"
+PythonShow = "Display the figure"
+PythonSin = "Sine"
+PythonSinh = "Hyperbolic sine"
+PythonSleep = "Suspend the execution for t seconds"
+PythonSort = "Sort the list"
+PythonSqrt = "Square root"
+PythonSum = "Sum the items of a list"
+PythonTan = "Tangent"
+PythonTanh = "Hyperbolic tangent"
+PythonText = "Display a text at (x,y) coordinates"
+PythonTimeFunction = "time module function prefix"
+PythonTrunc = "x truncated to an integer"
+PythonTurtleBackward = "Move backward by x pixels"
+PythonTurtleCircle = "Circle of radius r pixels"
+PythonTurtleColor = "Set the pen color"
+PythonTurtleColorMode = "Set the color mode to 1.0 or 255"
+PythonTurtleForward = "Move forward by x pixels"
+PythonTurtleFunction = "turtle module function prefix"
+PythonTurtleGoto = "Move to (x,y) coordinates"
+PythonTurtleHeading = "Return the current heading"
+PythonTurtleHideturtle = "Hide the turtle"
+PythonTurtleIsdown = "Return True if the pen is down"
+PythonTurtleLeft = "Turn left by a degrees"
+PythonTurtlePendown = "Pull the pen down"
+PythonTurtlePensize = "Set the line thickness to x pixels"
+PythonTurtlePenup = "Pull the pen up"
+PythonTurtlePosition = "Return the current (x,y) location"
+PythonTurtleReset = "Reset the drawing"
+PythonTurtleRight = "Turn right by a degrees"
+PythonTurtleSetheading = "Set the orientation to a degrees"
+PythonTurtleSetposition = "Positionne la tortue"
+PythonTurtleShowturtle = "Show the turtle"
+PythonTurtleSpeed = "Drawing speed between 0 and 10"
+PythonTurtleWrite = "Display a text"
+PythonUniform = "Floating point number in [a,b]"
diff --git a/apps/code/catalog.fr.i18n b/apps/code/catalog.fr.i18n
new file mode 100644
index 00000000000..f98473ed823
--- /dev/null
+++ b/apps/code/catalog.fr.i18n
@@ -0,0 +1,197 @@
+PythonPound = "Commentaire"
+PythonPercent = "Modulo"
+Python1J = "i complexe"
+PythonLF = "Saut à la ligne"
+PythonTab = "Tabulation"
+PythonAmpersand = "Et logique"
+PythonSymbolExp = "Ou exclusif logique"
+PythonVerticalBar = "Ou logique"
+PythonImag = "Partie imaginaire de z"
+PythonReal = "Partie réelle de z"
+PythonSingleQuote = "Apostrophe"
+PythonAbs = "Valeur absolue/Module"
+PythonAcos = "Arc cosinus"
+PythonAcosh = "Arc cosinus hyperbolique"
+PythonAppend = "Insère x à la fin de la liste"
+PythonArrow = "Flèche de (x,y) à (x+dx,y+dy)"
+PythonAsin = "Arc sinus"
+PythonAsinh = "Arc sinus hyperbolique"
+PythonAtan = "Arc tangente"
+PythonAtan2 = "Calcul de atan(y/x)"
+PythonAtanh = "Arc tangente hyperbolique"
+PythonAxis = "Met les axes (xmin,xmax,ymin,ymax)"
+PythonBar = "Diagramme en barres de la liste x"
+PythonBin = "Conversion d'un entier en binaire"
+PythonCeil = "Plafond"
+PythonChoice = "Nombre aléatoire dans la liste"
+PythonClear = "Vide la liste"
+PythonCmathFunction = "Préfixe fonction du module cmath"
+PythonColor = "Définit une couleur rvb"
+PythonColorBlack = "Couleur noire"
+PythonColorBlue = "Couleur bleue"
+PythonColorBrown = "Couleur marron"
+PythonColorGray = "Couleur grise"
+PythonColorGreen = "Couleur verte"
+PythonColorOrange = "Couleur orange"
+PythonColorPink = "Couleur rose"
+PythonColorPurple = "Couleur violette"
+PythonColorRed = "Couleur rouge"
+PythonColorWhite = "Couleur blanche"
+PythonColorYellow = "Couleur jaune"
+PythonComplex = "Renvoie a+ib"
+PythonCopySign = "Renvoie x avec le signe de y"
+PythonCos = "Cosinus"
+PythonCosh = "Cosinus hyperbolique"
+PythonCount = "Compte les occurrences de x"
+PythonDegrees = "Conversion de radians en degrés"
+PythonDivMod = "Quotient et reste"
+PythonDrawString = "Affiche un texte au pixel (x,y)"
+PythonErf = "Fonction d'erreur"
+PythonErfc = "Fonction d'erreur complémentaire"
+PythonEval = "Evalue l'expression en argument "
+PythonExp = "Fonction exponentielle"
+PythonExpm1 = "Calcul de exp(x)-1"
+PythonFabs = "Valeur absolue"
+PythonFillRect = "Remplit un rectangle"
+PythonFloat = "Conversion en flottant"
+PythonFloor = "Partie entière"
+PythonFmod = "a modulo b"
+PythonFrExp = "Mantisse et exposant de x : (m,e)"
+PythonGamma = "Fonction gamma"
+PythonGetPixel = "Renvoie la couleur du pixel (x,y)"
+PythonGetrandbits = "Nombre aléatoire sur k bits"
+PythonGrid = "Affiche ou masque la grille"
+PythonHex = "Conversion entier en hexadécimal"
+PythonHist = "Histogramme de la liste x"
+PythonImportCmath = "Importation du module cmath"
+PythonImportIon = "Importation du module ion"
+PythonImportKandinsky = "Importation du module kandinsky"
+PythonImportRandom = "Importation du module random"
+PythonImportMath = "Importation du module math"
+PythonImportMatplotlibPyplot = "Importation de matplotlib.pyplot"
+PythonImportTurtle = "Importation du module turtle"
+PythonImportTime = "Importation du module time"
+PythonIndex = "Indice première occurrence de x"
+PythonInput = "Entrer une valeur"
+PythonInsert = "Insère x en i-ème position"
+PythonInt = "Conversion en entier"
+PythonIonFunction = "Préfixe fonction module ion"
+PythonIsFinite = "Teste si x est fini"
+PythonIsInfinite = "Teste si x est infini"
+PythonIsKeyDown = "Renvoie True si touche k enfoncée"
+PythonIsNaN = "Teste si x est NaN"
+PythonKandinskyFunction = "Préfixe fonction module kandinsky"
+PythonKeyLeft = "Touche FLECHE GAUCHE"
+PythonKeyUp = "Touche FLECHE HAUT"
+PythonKeyDown = "Touche FLECHE BAS"
+PythonKeyRight = "Touche FLECHE DROITE"
+PythonKeyOk = "Touche OK"
+PythonKeyBack = "Touche RETOUR"
+PythonKeyHome = "Touche HOME"
+PythonKeyOnOff = "Touche ON/OFF"
+PythonKeyShift = "Touche SHIFT"
+PythonKeyAlpha = "Touche ALPHA"
+PythonKeyXnt = "Touche X,N,T"
+PythonKeyVar = "Touche VAR"
+PythonKeyToolbox = "Touche BOITE A OUTILS"
+PythonKeyBackspace = "Touche EFFACER"
+PythonKeyExp = "Touche EXPONENTIELLE"
+PythonKeyLn = "Touche LOGARITHME NEPERIEN"
+PythonKeyLog = "Touche LOGARITHME DECIMAL"
+PythonKeyImaginary = "Touche I IMAGINAIRE"
+PythonKeyComma = "Touche VIRGULE"
+PythonKeyPower = "Touche PUISSANCE"
+PythonKeySine = "Touche SINUS"
+PythonKeyCosine = "Touche COSINUS"
+PythonKeyTangent = "Touche TANGENTE"
+PythonKeyPi = "Touche PI"
+PythonKeySqrt = "Touche RACINE CARREE"
+PythonKeySquare = "Touche CARRE"
+PythonKeySeven = "Touche 7"
+PythonKeyEight = "Touche 8"
+PythonKeyNine = "Touche 9"
+PythonKeyLeftParenthesis = "Touche PARENTHESE GAUCHE"
+PythonKeyRightParenthesis = "Touche PARENTHESE DROITE"
+PythonKeyFour = "Touche 4"
+PythonKeyFive = "Touche 5"
+PythonKeySix = "Touche 6"
+PythonKeyMultiplication = "Touche MULTIPLICATION"
+PythonKeyDivision = "Touche DIVISION"
+PythonKeyOne = "Touche 1"
+PythonKeyTwo = "Touche 2"
+PythonKeyThree = "Touche 3"
+PythonKeyPlus = "Touche PLUS"
+PythonKeyMinus = "Touche MOINS"
+PythonKeyZero = "Touche 0"
+PythonKeyDot = "Touche POINT"
+PythonKeyEe = "Touche 10 PUISSANCE X"
+PythonKeyAns = "Touche ANS"
+PythonKeyExe = "Touche EXE"
+PythonLdexp = "Inverse de frexp : x*(2**i)"
+PythonLength = "Longueur d'un objet"
+PythonLgamma = "Logarithme de la fonction gamma"
+PythonLog = "Logarithme de base a"
+PythonLog10 = "Logarithme décimal"
+PythonLog2 = "Logarithme de base 2"
+PythonMathFunction = "Préfixe fonction du module math"
+PythonMatplotlibPyplotFunction = "Préfixe du module matplotlib.pyplot"
+PythonMax = "Maximum"
+PythonMin = "Minimum"
+PythonModf = "Parties fractionnaire et entière"
+PythonMonotonic = "Renvoie la valeur de l'horloge"
+PythonOct = "Conversion en octal"
+PythonPhase = "Argument de z"
+PythonPlot = "Trace y en fonction de x"
+PythonPolar = "Conversion en polaire"
+PythonPop = "Supprime le dernier élément"
+PythonPower = "x à la puissance y"
+PythonPrint = "Affiche l'objet"
+PythonRadians = "Conversion de degrés en radians"
+PythonRandint = "Entier aléatoire dans [a,b]"
+PythonRandom = "Nombre décimal dans [0,1["
+PythonRandomFunction = "Préfixe fonction du module random"
+PythonRandrange = "Nombre dans range(start,stop)"
+PythonRangeStartStop = "Liste de start à stop-1"
+PythonRangeStop = "Liste de 0 à stop-1"
+PythonRect = "Conversion en algébrique"
+PythonRemove = "Supprime le premier x de la liste"
+PythonReverse = "Inverse les éléments de la liste"
+PythonRound = "Arrondi à n décimales"
+PythonScatter = "Nuage des points (x,y)"
+PythonSeed = "Initialiser générateur aléatoire"
+PythonSetPixel = "Colore le pixel (x,y)"
+PythonShow = "Affiche la figure"
+PythonSin = "Sinus"
+PythonSinh = "Sinus hyperbolique"
+PythonSleep = "Suspend l'exécution t secondes"
+PythonSort = "Trie la liste"
+PythonSqrt = "Racine carrée"
+PythonSum = "Somme des éléments de la liste"
+PythonTan = "Tangente"
+PythonTanh = "Tangente hyperbolique"
+PythonText = "Affiche un texte en (x,y)"
+PythonTimeFunction = "Préfixe fonction module time"
+PythonTrunc = "Troncature entière"
+PythonTurtleBackward = "Recule de x pixels"
+PythonTurtleCircle = "Cercle de rayon r pixels"
+PythonTurtleColor = "Modifie la couleur du tracé"
+PythonTurtleColorMode = "Met le mode de couleur à 1.0 ou 255"
+PythonTurtleForward = "Avance de x pixels"
+PythonTurtleFunction = "Préfixe fonction du module turtle"
+PythonTurtleGoto = "Va au point de coordonnées (x,y)"
+PythonTurtleHeading = "Renvoie l'orientation actuelle"
+PythonTurtleHideturtle = "Masque la tortue"
+PythonTurtleIsdown = "True si le crayon est abaissé"
+PythonTurtleLeft = "Pivote de a degrés vers la gauche"
+PythonTurtlePendown = "Abaisse le crayon"
+PythonTurtlePensize = "Taille du tracé en pixels"
+PythonTurtlePenup = "Relève le crayon"
+PythonTurtlePosition = "Renvoie la position (x,y)"
+PythonTurtleReset = "Réinitialise le dessin"
+PythonTurtleRight = "Pivote de a degrés vers la droite"
+PythonTurtleSetheading = "Met un cap de a degrés"
+PythonTurtleSetposition = "Positionne la tortue"
+PythonTurtleShowturtle = "Affiche la tortue"
+PythonTurtleSpeed = "Vitesse du tracé entre 0 et 10"
+PythonTurtleWrite = "Affiche un texte"
+PythonUniform = "Nombre décimal dans [a,b]"
diff --git a/apps/code/catalog.it.i18n b/apps/code/catalog.it.i18n
new file mode 100644
index 00000000000..51223cbdcfb
--- /dev/null
+++ b/apps/code/catalog.it.i18n
@@ -0,0 +1,197 @@
+PythonPound = "Commento"
+PythonPercent = "Modulo"
+Python1J = "Unità immaginaria"
+PythonLF = "Nuova riga"
+PythonTab = "Tabulazione"
+PythonAmpersand = "Congiunzione"
+PythonSymbolExp = "Disgiunzione esclusiva"
+PythonVerticalBar = "Disgiunzione"
+PythonImag = "Parte immaginaria di z"
+PythonReal = "Parte reale di z"
+PythonSingleQuote = "Apostrofo"
+PythonAbs = "Valore assoluto/Modulo"
+PythonAcos = "Coseno d'arco"
+PythonAcosh = "Coseno iperbolico inverso"
+PythonAppend = "Inserisce x alla fine della lista"
+PythonArrow = "Freccia da (x,y) a (x+dx,y+dy)"
+PythonAsin = "Arco sinusoidale"
+PythonAsinh = "Arco sinusoidale iperbolico"
+PythonAtan = "Arco tangente"
+PythonAtan2 = "Calcolo di atan(y/x)"
+PythonAtanh = "Arco tangente iperbolico"
+PythonAxis = "Imposta assi (xmin,xmax,ymin,ymax)"
+PythonBar = "Grafico a barre con x valori"
+PythonBin = "Converte un intero in binario"
+PythonCeil = "Parte intera superiore"
+PythonChoice = "Numero aleatorio nella lista"
+PythonClear = "Svuota la lista"
+PythonCmathFunction = "Funz. prefissata modulo cmath"
+PythonColor = "Definisci un colore rvb"
+PythonColorBlack = "Colore nero"
+PythonColorBlue = "Colore blu"
+PythonColorBrown = "Colore marrone"
+PythonColorGray = "Colore grigio"
+PythonColorGreen = "Colore verde"
+PythonColorOrange = "Colore arancione"
+PythonColorPink = "Colore rosa"
+PythonColorPurple = "Colore viola"
+PythonColorRed = "Colore rosso"
+PythonColorWhite = "Colore bianco"
+PythonColorYellow = "Colore giallo"
+PythonComplex = "Restituisce a+ib"
+PythonCopySign = "Restituisce x con segno di y"
+PythonCos = "Coseno"
+PythonCosh = "Coseno iperbolico"
+PythonCount = "Conta le ricorrenze di x"
+PythonDegrees = "Conversione di radianti in gradi"
+PythonDivMod = "Quoziente e resto"
+PythonDrawString = "Visualizza il testo dal pixel x,y"
+PythonErf = "Funzione d'errore"
+PythonErfc = "Funzione d'errore complementare"
+PythonEval = "Valuta l'espressione nell'argomento "
+PythonExp = "Funzione esponenziale"
+PythonExpm1 = "Calcola exp(x)-1"
+PythonFabs = "Valore assoluto"
+PythonFillRect = "Riempie un rettangolo"
+PythonFloat = "Conversione in flottanti"
+PythonFloor = "Parte intera"
+PythonFmod = "a modulo b"
+PythonFrExp = "Mantissa ed esponente di x : (m,e)"
+PythonGamma = "Funzione gamma"
+PythonGetPixel = "Restituisce colore del pixel(x,y)"
+PythonGetrandbits = "Numero aleatorio con k bit"
+PythonGrid = "Attiva la visibilità della griglia"
+PythonHex = "Conversione intero in esadecimale"
+PythonHist = "Disegna l'istogramma di x"
+PythonImportCmath = "Importa modulo cmath"
+PythonImportIon = "Importa modulo ion"
+PythonImportKandinsky = "Importa modulo kandinsky"
+PythonImportRandom = "Importa modulo random"
+PythonImportMath = "Importa modulo math"
+PythonImportMatplotlibPyplot = "Importa modulo matplotlib.pyplot"
+PythonImportTurtle = "Importa del modulo turtle"
+PythonImportTime = "Importa del modulo time"
+PythonIndex = "Indice prima occorrenza di x"
+PythonInput = "Inserire un valore"
+PythonInsert = "Inserire x in posizione i-esima"
+PythonInt = "Conversione in intero"
+PythonIonFunction = "Prefisso di funzione modulo ion"
+PythonIsFinite = "Testa se x è finito"
+PythonIsInfinite = "Testa se x est infinito"
+PythonIsKeyDown = "Restituisce True premendo tasto k"
+PythonIsNaN = "Testa se x è NaN"
+PythonKandinskyFunction = "Prefisso funzione modulo kandinsky"
+PythonKeyLeft = "Tasto FRECCIA SINISTRA"
+PythonKeyUp = "Tasto FRECCIA ALTO"
+PythonKeyDown = "Tasto FRECCIA BASSO"
+PythonKeyRight = "Tasto FRECCIA DESTRA"
+PythonKeyOk = "Tasto OK"
+PythonKeyBack = "Tasto INDIETRO"
+PythonKeyHome = "Tasto CASA"
+PythonKeyOnOff = "Tasto ON/OFF"
+PythonKeyShift = "Tasto SHIFT"
+PythonKeyAlpha = "Tasto ALPHA"
+PythonKeyXnt = "Tasto X,N,T"
+PythonKeyVar = "Tasto VAR"
+PythonKeyToolbox = "Tasto TOOLBOX"
+PythonKeyBackspace = "Tasto CANCELLA"
+PythonKeyExp = "Tasto ESPONENZIALE"
+PythonKeyLn = "Tasto LOGARITMO NEPERIANO"
+PythonKeyLog = "Tasto LOGARITMO DECIMALE"
+PythonKeyImaginary = "Tasto I IMMAGINE"
+PythonKeyComma = "Tasto VIRGOLA"
+PythonKeyPower = "Tasto POTENZA"
+PythonKeySine = "Tasto SENO"
+PythonKeyCosine = "Tasto COSENO"
+PythonKeyTangent = "Tasto TANGENTE"
+PythonKeyPi = "Tasto PI"
+PythonKeySqrt = "Tasto RADICE QUADRATA"
+PythonKeySquare = "Tasto QUADRATO"
+PythonKeySeven = "Tasto 7"
+PythonKeyEight = "Tasto 8"
+PythonKeyNine = "Tasto 9"
+PythonKeyLeftParenthesis = "Tasto PARENTESI SINISTRA"
+PythonKeyRightParenthesis = "Tasto PARENTESI DESTRA"
+PythonKeyFour = "Tasto 4"
+PythonKeyFive = "Tasto 5"
+PythonKeySix = "Tasto 6"
+PythonKeyMultiplication = "Tasto MOLTIPLICAZIONE"
+PythonKeyDivision = "Tasto DIVISIONE"
+PythonKeyOne = "Tasto 1"
+PythonKeyTwo = "Tasto 2"
+PythonKeyThree = "Tasto 3"
+PythonKeyPlus = "Tasto PIÙ"
+PythonKeyMinus = "Tasto MENO"
+PythonKeyZero = "Tasto 0"
+PythonKeyDot = "Tasto PUNTO"
+PythonKeyEe = "Tasto 10 POTENZA X"
+PythonKeyAns = "Tasto ANS"
+PythonKeyExe = "Tasto EXE"
+PythonLdexp = "Inversa di frexp : x*(2**i)"
+PythonLength = "Longhezza di un oggetto"
+PythonLgamma = "Logaritmo della funzione gamma"
+PythonLog = "Logaritmo di base a"
+PythonLog10 = "Logaritmo decimale"
+PythonLog2 = "Logaritmo di base 2"
+PythonMathFunction = "Prefisso funzione del modulo math"
+PythonMatplotlibPyplotFunction = "Prefisso modulo matplotlib.pyplot"
+PythonMax = "Massimo"
+PythonMin = "Minimo"
+PythonModf = "Parti frazionarie e intere"
+PythonMonotonic = "Restituisce il valore dell'orologio"
+PythonOct = "Conversione in ottale"
+PythonPhase = "Argomento di z"
+PythonPlot = "Disegna y in f. di x come linee"
+PythonPolar = "Conversione in polare"
+PythonPop = "Cancella l'ultimo elemento"
+PythonPower = "x alla potenza y"
+PythonPrint = "Visualizza l'oggetto"
+PythonRadians = "Conversione da gradi a radianti"
+PythonRandint = "Intero aleatorio in [a,b]"
+PythonRandom = "Numero aleatorio in [0,1["
+PythonRandomFunction = "Prefisso funzione modulo casuale"
+PythonRandrange = "Numero dentro il range(start, stop)"
+PythonRangeStartStop = "Lista da start a stop-1"
+PythonRangeStop = "Lista da 0 a stop-1"
+PythonRect = "Converte in coordinate algebriche"
+PythonRemove = "Cancella la prima x dalla lista"
+PythonReverse = "Inverte gli elementi della lista"
+PythonRound = "Arrotondato a n cifre decimali"
+PythonScatter = "Diagramma dispersione y in f. di x"
+PythonSeed = "Inizializza il generatore random"
+PythonSetPixel = "Colora il pixel (x,y)"
+PythonShow = "Mostra la figura"
+PythonSin = "Seno"
+PythonSinh = "Seno iperbolico"
+PythonSleep = "Sospende l'esecuzione t secondi"
+PythonSort = "Ordina l'elenco"
+PythonSqrt = "Radice quadrata"
+PythonSum = "Somma degli elementi della lista"
+PythonTan = "Tangente"
+PythonTanh = "Tangente iperbolica"
+PythonText = "Mostra un testo in (x,y)"
+PythonTimeFunction = "Prefisso funzione modulo time"
+PythonTrunc = "Troncamento intero"
+PythonTurtleBackward = "Indietreggia di x pixels"
+PythonTurtleCircle = "Cerchio di raggio r pixel"
+PythonTurtleColor = "Modifica il colore del tratto"
+PythonTurtleColorMode = "Imposta modalità colore a 1.0 o 255"
+PythonTurtleForward = "Avanza di x pixel"
+PythonTurtleFunction = "Prefisso funzione modello turtle"
+PythonTurtleGoto = "Spostati alle coordinate (x,y)"
+PythonTurtleHeading = "Restituisce l'orientamento attuale"
+PythonTurtleHideturtle = "Nascondi la tartaruga"
+PythonTurtleIsdown = "True se la penna è abbassata"
+PythonTurtleLeft = "Ruota di a gradi a sinistra"
+PythonTurtlePendown = "Abbassa la penna"
+PythonTurtlePensize = "Dimensione del tratto in pixel"
+PythonTurtlePenup = "Solleva la penna"
+PythonTurtlePosition = "Fornisce posizione corrente (x,y)"
+PythonTurtleReset = "Azzera il disegno"
+PythonTurtleRight = "Ruota di a gradi a destra"
+PythonTurtleSetheading = "Imposta l'orientamento per a gradi"
+PythonTurtleSetposition = "Posiziona la tartaruga"
+PythonTurtleShowturtle = "Mostra la tartaruga"
+PythonTurtleSpeed = "Velocità di disegno (x tra 0 e 10)"
+PythonTurtleWrite = "Mostra un testo"
+PythonUniform = "Numero decimale tra [a,b]"
diff --git a/apps/code/catalog.nl.i18n b/apps/code/catalog.nl.i18n
new file mode 100644
index 00000000000..84be6987829
--- /dev/null
+++ b/apps/code/catalog.nl.i18n
@@ -0,0 +1,197 @@
+PythonPound = "Opmerkingen"
+PythonPercent = "Modulo"
+Python1J = "Imaginaire i"
+PythonLF = "Nieuwe regel"
+PythonTab = "Tabulatie"
+PythonAmpersand = "Bitsgewijze en"
+PythonSymbolExp = "Bitsgewijze exclusieve of"
+PythonVerticalBar = "Bitsgewijze of"
+PythonImag = "Imaginair deel van z"
+PythonReal = "Reëel deel van z"
+PythonSingleQuote = "Enkele aanhalingstekens"
+PythonAbs = "Absolute waarde"
+PythonAcos = "Arccosinus"
+PythonAcosh = "Arccosinus hyperbolicus"
+PythonAppend = "Voeg x toe aan het eind van je lijst"
+PythonArrow = "Pijl van (x,y) naar (x+dx,y+dy)"
+PythonAsin = "Arcsinus"
+PythonAsinh = "Arcsinus hyperbolicus"
+PythonAtan = "Arctangens"
+PythonAtan2 = "Geeft atan(y/x)"
+PythonAtanh = "Arctangens hyperbolicus"
+PythonAxis = "Stel de assen in (xmin,xmax,ymin,ymax)"
+PythonBar = "Teken staafdiagram met x-waarden"
+PythonBin = "Zet integer om in een binair getal"
+PythonCeil = "Plafond"
+PythonChoice = "Geeft willek. getal van de lijst"
+PythonClear = "Lijst leegmaken"
+PythonCmathFunction = "cmath module voorvoegsel"
+PythonColor = "Definieer een rgb kleur"
+PythonColorBlack = "Zwarte kleur"
+PythonColorBlue = "Blauwe kleur"
+PythonColorBrown = "Bruine kleur"
+PythonColorGray = "Grijze kleur"
+PythonColorGreen = "Groene kleur"
+PythonColorOrange = "Oranje kleur"
+PythonColorPink = "Roze kleur"
+PythonColorPurple = "Paarse kleur"
+PythonColorRed = "Rode kleur"
+PythonColorWhite = "Witte kleur"
+PythonColorYellow = "Gele kleur"
+PythonComplex = "Geeft a+ib"
+PythonCopySign = "Geeft x met het teken van y"
+PythonCos = "Cosinus"
+PythonCosh = "Cosinus hyperbolicus"
+PythonCount = "Tel voorkomen van x"
+PythonDegrees = "Zet x om van radialen naar graden"
+PythonDivMod = "Quotiënt en rest"
+PythonDrawString = "Geef een tekst weer van pixel (x,y)"
+PythonErf = "Error functie"
+PythonErfc = "Complementaire error functie"
+PythonEval = "Geef de geëvalueerde uitdrukking"
+PythonExp = "Exponentiële functie"
+PythonExpm1 = "Bereken exp(x)-1"
+PythonFabs = "Absolute waarde"
+PythonFillRect = "Vul een rechthoek bij pixel (x,y)"
+PythonFloat = "Zet x om in een float"
+PythonFloor = "Vloer"
+PythonFmod = "a modulo b"
+PythonFrExp = "Mantisse en exponent van x: (m,e)"
+PythonGamma = "Gammafunctie"
+PythonGetPixel = "Geef pixel (x,y) kleur (rgb)"
+PythonGetrandbits = "Integer met k willekeurige bits"
+PythonGrid = "Verander zichtbaarheid raster"
+PythonHex = "Zet integer om in hexadecimaal"
+PythonHist = "Teken het histogram van x"
+PythonImportCmath = "Importeer cmath module"
+PythonImportIon = "Importeer ion module"
+PythonImportKandinsky = "Importeer kandinsky module"
+PythonImportRandom = "Importeer random module"
+PythonImportMath = "Importeer math module"
+PythonImportMatplotlibPyplot = "Importeer matplotlib.pyplot module"
+PythonImportTime = "Importeer time module"
+PythonImportTurtle = "Importeer turtle module"
+PythonIndex = "Index van de eerste x aanwezigheden"
+PythonInput = "Wijs een waarde toe"
+PythonInsert = "Voeg x toe aan index i in de lijst"
+PythonInt = "Zet x om in een integer"
+PythonIonFunction = "ion module voorvoegsel"
+PythonIsFinite = "Controleer of x eindig is"
+PythonIsInfinite = "Controleer of x oneindig is"
+PythonIsKeyDown = "Geef True als k toets omlaag is"
+PythonIsNaN = "Controleer of x geen getal is"
+PythonKandinskyFunction = "kandinsky module voorvoegsel"
+PythonKeyLeft = "PIJL NAAR LINKS toets"
+PythonKeyUp = "PIJL OMHOOG toets"
+PythonKeyDown = "PIJL OMLAAG toets"
+PythonKeyRight = "PIJL NAAR RECHTS toets"
+PythonKeyOk = "OK toets"
+PythonKeyBack = "TERUG toets"
+PythonKeyHome = "HOME toets"
+PythonKeyOnOff = "AAN/UIT toets"
+PythonKeyShift = "SHIFT toets"
+PythonKeyAlpha = "ALPHA toets"
+PythonKeyXnt = "X,N,T toets"
+PythonKeyVar = "VAR toets"
+PythonKeyToolbox = "TOOLBOX toets"
+PythonKeyBackspace = "BACKSPACE toets"
+PythonKeyExp = "EXPONENTIEEL toets"
+PythonKeyLn = "NATUURLIJKE LOGARITME toets"
+PythonKeyLog = "BRIGGSE LOGARITME toets"
+PythonKeyImaginary = "IMAGINAIRE I toets"
+PythonKeyComma = "KOMMA toets"
+PythonKeyPower = "MACHT toets"
+PythonKeySine = "SINUS toets"
+PythonKeyCosine = "COSINUS toets"
+PythonKeyTangent = "TANGENS toets"
+PythonKeyPi = "PI toets"
+PythonKeySqrt = "VIERKANTSWORTEL toets"
+PythonKeySquare = "KWADRAAT toets"
+PythonKeySeven = "7 toets"
+PythonKeyEight = "8 toets"
+PythonKeyNine = "9 toets"
+PythonKeyLeftParenthesis = "HAAKJE OPENEN toets"
+PythonKeyRightParenthesis = "HAAKJE SLUITEN toets"
+PythonKeyFour = "4 toets"
+PythonKeyFive = "5 toets"
+PythonKeySix = "6 toets"
+PythonKeyMultiplication = "VERMENIGVULDIGEN toets"
+PythonKeyDivision = "DELEN toets"
+PythonKeyOne = "1 toets"
+PythonKeyTwo = "2 toets"
+PythonKeyThree = "3 toets"
+PythonKeyPlus = "PLUS toets"
+PythonKeyMinus = "MIN toets"
+PythonKeyZero = "0 toets"
+PythonKeyDot = "PUNT toets"
+PythonKeyEe = "10 TOT DE MACHT X toets"
+PythonKeyAns = "ANS toets"
+PythonKeyExe = "EXE toets"
+PythonLdexp = "Geeft x*(2**i), inversie van frexp"
+PythonLength = "Lengte van een object"
+PythonLgamma = "Log-gammafunctie"
+PythonLog = "Logaritme met grondgetal a"
+PythonLog10 = "Logaritme met grondgetal 10"
+PythonLog2 = "Logaritme met grondgetal 2"
+PythonMathFunction = "math module voorvoegsel"
+PythonMatplotlibPyplotFunction = "matplotlib.pyplot module prefix"
+PythonMax = "Maximum"
+PythonMin = "Minimum"
+PythonModf = "Fractionele en gehele delen van x"
+PythonMonotonic = "Waarde van een monotone klok"
+PythonOct = "Integer omzetten naar octaal"
+PythonPhase = "Fase van z in radialen"
+PythonPlot = "Plot y versus x als lijnen"
+PythonPolar = "z in poolcoördinaten"
+PythonPop = "Verwijder en breng het laatste item terug"
+PythonPower = "x tot de macht y"
+PythonPrint = "Print object"
+PythonRadians = "Zet x om van graden naar radialen"
+PythonRandint = "Geeft willek. integer in [a,b]"
+PythonRandom = "Een willekeurig getal in [0,1)"
+PythonRandomFunction = "random module voorvoegsel"
+PythonRandrange = "Willek. getal in range(start, stop)"
+PythonRangeStartStop = "Lijst van start tot stop-1"
+PythonRangeStop = "Lijst van 0 tot stop-1"
+PythonRect = "z in cartesiaanse coördinaten"
+PythonRemove = "Verwijder het eerste voorkomen van x"
+PythonReverse = "Keer de elementen van de lijst om"
+PythonRound = "Rond af op n cijfers"
+PythonScatter = "Teken scatterplot van y versus x"
+PythonSeed = "Start willek. getallengenerator"
+PythonSetPixel = "Kleur pixel (x,y)"
+PythonShow = "Figuur weergeven"
+PythonSin= "Sinus"
+PythonSinh = "Sinus hyperbolicus"
+PythonSleep = "Stel executie voor t seconden uit"
+PythonSort = "Sorteer de lijst"
+PythonSqrt = "Vierkantswortel"
+PythonSum = "Sommeer de items van een lijst"
+PythonTan = "Tangens"
+PythonTanh = "Tangens hyperbolicus"
+PythonText = "Geef tekst weer op coördinaten (x,y)"
+PythonTimeFunction = "time module voorvoegsel"
+PythonTrunc = "x afgeknot tot een integer"
+PythonTurtleBackward = "Ga achterwaarts met x pixels"
+PythonTurtleCircle = "Cirkel van straal r pixels"
+PythonTurtleColor = "Stel de kleur van de pen in"
+PythonTurtleColorMode = "Stel de kleurmodus in op 1.0 of 255"
+PythonTurtleForward = "Ga voorwaarts met x pixels"
+PythonTurtleFunction = "turtle module voorvoegsel"
+PythonTurtleGoto = "Verplaats naar (x,y) coordinaten"
+PythonTurtleHeading = "Ga terug naar de huidige koers"
+PythonTurtleHideturtle = "Verberg de schildpad"
+PythonTurtleIsdown = "Geeft True als pen naar beneden is"
+PythonTurtleLeft = "Ga linksaf met a graden"
+PythonTurtlePendown = "Zet de pen naar beneden"
+PythonTurtlePensize = "Stel de lijndikte in op x pixels"
+PythonTurtlePenup = "Zet de pen omhoog"
+PythonTurtlePosition = "Zet huidige (x,y) locatie terug"
+PythonTurtleReset = "Reset de tekening"
+PythonTurtleRight = "Ga rechtsaf met a graden"
+PythonTurtleSetheading = "Zet de oriëntatie op a graden"
+PythonTurtleSetposition = "Plaats de schildpad"
+PythonTurtleShowturtle = "Laat de schildpad zien"
+PythonTurtleSpeed = "Tekensnelheid tussen 0 and 10"
+PythonTurtleWrite = "Display a text"
+PythonUniform = "Decimaal getal in [a,b]"
diff --git a/apps/code/catalog.pt.i18n b/apps/code/catalog.pt.i18n
new file mode 100644
index 00000000000..4f50cadaab4
--- /dev/null
+++ b/apps/code/catalog.pt.i18n
@@ -0,0 +1,197 @@
+PythonPound = "Comentário"
+PythonPercent = "Módulo"
+Python1J = "i Complexo"
+PythonLF = "Nova linha"
+PythonTab = "Tabulação"
+PythonAmpersand = "Operador binário and"
+PythonSymbolExp = "Operador binário exclusivo or"
+PythonVerticalBar = "Operador binário or"
+PythonSingleQuote = "Apóstrofo"
+PythonImag = "Parte imaginária de z"
+PythonReal = "Parte real de z"
+PythonAbs = "Valor absoluto/módulo"
+PythonAcos = "Arco cosseno"
+PythonAcosh = "Arco cosseno hiperbólico"
+PythonAppend = "Adicionar x no fim da lista"
+PythonArrow = "Seta de (x,y) para (x+dx,y+dy)"
+PythonAsin = "Arco seno"
+PythonAsinh = "Arco seno hiperbólico"
+PythonAtan = "Arco tangente"
+PythonAtan2 = "Cálculo de atan(y/x)"
+PythonAtanh = "Arco tangente hiperbólica"
+PythonAxis = "Definir eixos (xmin,xmax,ymin,ymax)"
+PythonBar = "Gráfico de barras com valores de x"
+PythonBin = "Converter número inteiro em binário"
+PythonCeil = "Teto"
+PythonChoice = "Número aleatório na lista"
+PythonClear = "Esvaziar a lista"
+PythonCmathFunction = "Prefixo da função do módulo cmath"
+PythonColor = "Define uma cor rgb"
+PythonColorBlack = "Cor preta"
+PythonColorBlue = "Cor azul"
+PythonColorBrown = "Cor castanha"
+PythonColorGray = "Cor cinzenta"
+PythonColorGreen = "Cor verde"
+PythonColorOrange = "Cor laranja"
+PythonColorPink = "Cor rosa"
+PythonColorPurple = "Cor roxa"
+PythonColorRed = "Cor vermelha"
+PythonColorWhite = "Cor branca"
+PythonColorYellow = "Cor amarela"
+PythonComplex = "Devolve a+ib"
+PythonCopySign = "Devolve x com o sinal de y"
+PythonCos = "Cosseno"
+PythonCosh = "Cosseno hiperbólico"
+PythonCount = "Contar as ocorrências de x"
+PythonDegrees = "Converter x de radianos para graus"
+PythonDivMod = "Quociente e resto"
+PythonDrawString = "Mostrar o texto do pixel (x,y)"
+PythonErf = "Função erro"
+PythonErfc = "Função erro complementar"
+PythonEval = "Devolve a expressão avaliada"
+PythonExp = "Função exponencial"
+PythonExpm1 = "Calcular exp(x)-1"
+PythonFabs = "Valor absoluto"
+PythonFillRect = "Preencher um retângulo em (x,y)"
+PythonFloat = "Converter x num flutuante"
+PythonFloor = "Parte inteira"
+PythonFmod = "a módulo b"
+PythonFrExp = "Coeficiente e expoente de x: (m, e)"
+PythonGamma = "Função gama"
+PythonGetPixel = "Devolve a cor do pixel (x,y)"
+PythonGetrandbits = "Número inteiro aleatório com k bits"
+PythonGrid = "Alterar visibilidade da grelha"
+PythonHex = "Converter inteiro em hexadecimal"
+PythonHist = "Desenhar o histograma de x"
+PythonImportCmath = "Importar módulo cmath"
+PythonImportIon = "Importar módulo ion"
+PythonImportKandinsky = "Importar módulo kandinsky"
+PythonImportRandom = "Importar módulo random"
+PythonImportMath = "Importar módulo math"
+PythonImportMatplotlibPyplot = "Importar módulo matplotlib.pyplot"
+PythonImportTime = "Importar módulo time"
+PythonImportTurtle = "Importar módulo turtle"
+PythonIndex = "Índice da primeira ocorrência de x"
+PythonInput = "Adicionar um valor"
+PythonInsert = "Inserir x no índice i na lista"
+PythonInt = "Converter x num número inteiro"
+PythonIonFunction = "Prefixo da função do módulo ion"
+PythonIsFinite = "Verificar se x é finito"
+PythonIsInfinite = "Verificar se x é infinito"
+PythonIsKeyDown = "Devolve True se tecla k pressionada"
+PythonIsNaN = "Verificar se x é um NaN"
+PythonKandinskyFunction = "Prefixo da função do módulo kandinsky"
+PythonKeyLeft = "tecla SETA ESQUERDA"
+PythonKeyUp = "tecla SETA CIMA "
+PythonKeyDown = "tecla SETA BAIXO"
+PythonKeyRight = "tecla SETA DIREITA"
+PythonKeyOk = "tecla OK"
+PythonKeyBack = "tecla VOLTAR"
+PythonKeyHome = "tecla HOME"
+PythonKeyOnOff = "tecla ON/OFF"
+PythonKeyShift = "tecla SHIFT"
+PythonKeyAlpha = "tecla ALPHA"
+PythonKeyXnt = "tecla X,N,T"
+PythonKeyVar = "tecla VAR"
+PythonKeyToolbox = "tecla CAIXA DE FERRAMENTAS"
+PythonKeyBackspace = "tecla APAGAR"
+PythonKeyExp = "tecla EXPONENCIAL"
+PythonKeyLn = "tecla LOGARITMO NATURAL"
+PythonKeyLog = "tecla LOGARITMO DECIMAL"
+PythonKeyImaginary = "tecla I IMAGINÁRIO"
+PythonKeyComma = "tecla VÍRGULA"
+PythonKeyPower = "tecla EXPOENTE"
+PythonKeySine = "tecla SENO"
+PythonKeyCosine = "tecla COSSENO"
+PythonKeyTangent = "tecla TANGENTE"
+PythonKeyPi = "tecla PI"
+PythonKeySqrt = "tecla RAIZ QUADRADA"
+PythonKeySquare = "tecla AO QUADRADO"
+PythonKeySeven = "tecla 7"
+PythonKeyEight = "tecla 8"
+PythonKeyNine = "tecla 9"
+PythonKeyLeftParenthesis = "tecla PARÊNTESE ESQUERDO"
+PythonKeyRightParenthesis = "tecla PARÊNTESE DIREITO"
+PythonKeyFour = "tecla 4"
+PythonKeyFive = "tecla 5"
+PythonKeySix = "tecla 6"
+PythonKeyMultiplication = "tecla MULTIPLICAÇÃO"
+PythonKeyDivision = "tecla DIVISÃO"
+PythonKeyOne = "tecla 1"
+PythonKeyTwo = "tecla 2"
+PythonKeyThree = "tecla 3"
+PythonKeyPlus = "tecla MAIS"
+PythonKeyMinus = "tecla MENOS"
+PythonKeyZero = "tecla 0"
+PythonKeyDot = "tecla PONTO"
+PythonKeyEe = "tecla 10 expoente X"
+PythonKeyAns = "tecla ANS"
+PythonKeyExe = "tecla EXE"
+PythonLdexp = "Devolve x*(2**i), inverso de frexp"
+PythonLength = "Comprimento de um objeto"
+PythonLgamma = "Logaritmo da função gama"
+PythonLog = "Logaritmo de base a"
+PythonLog10 = "Logaritmo de base 10"
+PythonLog2 = "Logaritmo de base 2"
+PythonMathFunction = "Prefixo da função do módulo math"
+PythonMatplotlibPyplotFunction = "Prefixo do módulo matplotlib.pyplot"
+PythonMax = "Máximo"
+PythonMin = "Mínimo"
+PythonModf = "Partes inteira e frácionária de x"
+PythonMonotonic = "Devolve o valor do relógio"
+PythonOct = "Converter número inteiro em octal"
+PythonPhase = "Argumento de z"
+PythonPlot = "Desenhar y em função de x"
+PythonPolar = "z em coordenadas polares"
+PythonPop = "Remover o último item"
+PythonPower = "x levantado a y"
+PythonPrint = "Mostrar o objeto"
+PythonRadians = "Converter x de graus para radianos"
+PythonRandint = "Número inteiro aleatório em [a,b]"
+PythonRandom = "Número decimal em [0,1["
+PythonRandomFunction = "Prefixo da função do módulo random"
+PythonRandrange = "Número aleatório em [start,stop-1]"
+PythonRangeStartStop = "Lista de start a stop-1"
+PythonRangeStop = "Lista de 0 a stop-1"
+PythonRect = "Converter para coordenadas cartesianas"
+PythonRemove = "Remover a primeira ocorrência de x"
+PythonReverse = "Inverter os elementos da lista"
+PythonRound = "Arredondar para n dígitos"
+PythonScatter = "Gráfico de dispersão (x,y)"
+PythonSeed = "Iniciar gerador aleatório"
+PythonSetPixel = "Cor do pixel (x,y)"
+PythonShow = "Mostrar a figura"
+PythonSin = "Seno"
+PythonSinh = "Seno hiperbólico"
+PythonSleep = "Suspender a execução por t segundos"
+PythonSort = "Ordenar a lista"
+PythonSqrt = "Raiz quadrada"
+PythonSum = "Soma dos itens da lista"
+PythonTan = "Tangente"
+PythonTanh = "Tangente hiperbólica"
+PythonText = "Mostrar um texto em (x,y)"
+PythonTimeFunction = "Prefixo da função do módulo time"
+PythonTrunc = "x truncado a um número inteiro"
+PythonTurtleBackward = "Recuar x pixels"
+PythonTurtleCircle = "Circunferência de raio r pixels"
+PythonTurtleColor = "Definir a cor da caneta"
+PythonTurtleColorMode = "Define modo de cor para 1.0 ou 255"
+PythonTurtleForward = "Avançar x pixels"
+PythonTurtleFunction = "Prefixo da função do módulo turtle"
+PythonTurtleGoto = "Ir paras as coordenadas (x,y)"
+PythonTurtleHeading = "Voltar para a orientação atual"
+PythonTurtleHideturtle = "Esconder o turtle"
+PythonTurtleIsdown = "True se a caneta está pressionada"
+PythonTurtleLeft = "Vira à esquerda por a graus"
+PythonTurtlePendown = "Puxar a caneta para baixo"
+PythonTurtlePensize = "Definir a espessura para x pixels"
+PythonTurtlePenup = "Puxar a caneta para cima"
+PythonTurtlePosition = "Devolve a posição atual (x,y)"
+PythonTurtleReset = "Reiniciar o desenho"
+PythonTurtleRight = "Virar à esquerda por a graus"
+PythonTurtleSetheading = "Definir a orientação por a graus"
+PythonTurtleSetposition = "Positionne la tortue"
+PythonTurtleShowturtle = "Mostrar o turtle"
+PythonTurtleSpeed = "Velocidade do desenho entre 0 e 10"
+PythonTurtleWrite = "Mostrar um texto"
+PythonUniform = "Número decimal em [a,b]"
diff --git a/apps/code/catalog.universal.i18n b/apps/code/catalog.universal.i18n
new file mode 100644
index 00000000000..ff0cfed7a3f
--- /dev/null
+++ b/apps/code/catalog.universal.i18n
@@ -0,0 +1,235 @@
+PythonCommandAmpersand = "&"
+PythonCommandLF = "\\n"
+PythonCommandPercent = "%"
+PythonCommandPound = "#"
+PythonCommandSingleQuote = "'x'"
+PythonCommandSymbolExp = "^"
+PythonCommandTab = "\\t"
+PythonCommandVerticalBar = "|"
+PythonCommand1J = "1j"
+PythonCommandAbs = "abs(x)"
+PythonCommandAcos = "acos(x)"
+PythonCommandAcosh = "acosh(x)"
+PythonCommandAppend = "list.append(x)"
+PythonCommandAppendWithoutArg = ".append(\x11)"
+PythonCommandArrow = "arrow(x,y,dx,dy)"
+PythonCommandAsin = "asin(x)"
+PythonCommandAsinh = "asinh(x)"
+PythonCommandAtan = "atan(x)"
+PythonCommandAtan2 = "atan2(y,x)"
+PythonCommandAtanh = "atanh(x)"
+PythonCommandAxis = "axis((xmin,xmax,ymin,ymax))"
+PythonCommandAxisWithoutArg = "axis(\x11)"
+PythonCommandBar = "bar(x,height)"
+PythonCommandBin = "bin(x)"
+PythonCommandCeil = "ceil(x)"
+PythonCommandChoice = "choice(list)"
+PythonCommandClear = "list.clear()"
+PythonCommandClearWithoutArg = ".clear()"
+PythonCommandCmathFunction = "cmath.function"
+PythonCommandCmathFunctionWithoutArg = "cmath.\x11"
+PythonCommandColor = "color(r,g,b)"
+PythonCommandColorBlack = "'black'"
+PythonCommandColorBlue = "'blue'"
+PythonCommandColorBrown = "'brown'"
+PythonCommandColorGray = "'gray'"
+PythonCommandColorGreen = "'green'"
+PythonCommandColorOrange = "'orange'"
+PythonCommandColorPink = "'pink'"
+PythonCommandColorPurple = "'purple'"
+PythonCommandColorRed = "'red'"
+PythonCommandColorWhite = "'white'"
+PythonCommandColorYellow = "'yellow'"
+PythonCommandComplex = "complex(a,b)"
+PythonCommandConstantPi = "pi"
+PythonCommandCopySign = "copysign(x,y)"
+PythonCommandCos = "cos(x)"
+PythonCommandCosComplex = "cos(z)"
+PythonCommandCosh = "cosh(x)"
+PythonCommandCount = "list.count(x)"
+PythonCommandCountWithoutArg = ".count(\x11)"
+PythonCommandDegrees = "degrees(x)"
+PythonCommandDivMod = "divmod(a,b)"
+PythonCommandDrawString = "draw_string(\"text\",x,y)"
+PythonCommandConstantE = "e"
+PythonCommandErf = "erf(x)"
+PythonCommandErfc = "erfc(x)"
+PythonCommandEval = "eval(\"expression\")"
+PythonCommandExp = "exp(x)"
+PythonCommandExpComplex = "exp(z)"
+PythonCommandExpm1 = "expm1(x)"
+PythonCommandFabs = "fabs(x)"
+PythonCommandFillRect = "fill_rect(x,y,width,height,color)"
+PythonCommandFloat = "float(x)"
+PythonCommandFloor = "floor(x)"
+PythonCommandFmod = "fmod(a,b)"
+PythonCommandFrExp = "frexp(x)"
+PythonCommandGamma = "gamma(x)"
+PythonCommandGetPixel = "get_pixel(x,y)"
+PythonCommandGetrandbits = "getrandbits(k)"
+PythonCommandGrid = "grid()"
+PythonCommandHex = "hex(x)"
+PythonCommandHist = "hist(x,bins)"
+PythonCommandImag = "z.imag"
+PythonCommandImagWithoutArg = ".imag"
+PythonCommandImportFromCmath = "from cmath import *"
+PythonCommandImportFromIon = "from ion import *"
+PythonCommandImportFromKandinsky = "from kandinsky import *"
+PythonCommandImportFromMath = "from math import *"
+PythonCommandImportFromMatplotlibPyplot = "from matplotlib.pyplot import *"
+PythonCommandImportFromRandom = "from random import *"
+PythonCommandImportFromTime = "from time import *"
+PythonCommandImportFromTurtle = "from turtle import *"
+PythonCommandImportCmath = "import cmath"
+PythonCommandImportIon = "import ion"
+PythonCommandImportKandinsky = "import kandinsky"
+PythonCommandImportMath = "import math"
+PythonCommandImportMatplotlibPyplot = "import matplotlib.pyplot"
+PythonCommandImportRandom = "import random"
+PythonCommandImportTime = "import time"
+PythonCommandImportTurtle = "import turtle"
+PythonCommandIndex = "list.index(x)"
+PythonCommandIndexWithoutArg = ".index(\x11)"
+PythonCommandInput = "input(\"text\")"
+PythonCommandInsert = "list.insert(i,x)"
+PythonCommandInsertWithoutArg = ".insert(\x11,)"
+PythonCommandInt = "int(x)"
+PythonCommandIonFunction = "ion.function"
+PythonCommandIonFunctionWithoutArg = "ion.\x11"
+PythonCommandIsFinite = "isfinite(x)"
+PythonCommandIsInfinite = "isinf(x)"
+PythonCommandIsNaN = "isnan(x)"
+PythonCommandKandinskyFunction = "kandinsky.function"
+PythonCommandKandinskyFunctionWithoutArg = "kandinsky.\x11"
+PythonCommandKeyLeft = "KEY_LEFT"
+PythonCommandKeyUp = "KEY_UP"
+PythonCommandKeyDown = "KEY_DOWN"
+PythonCommandKeyRight = "KEY_RIGHT"
+PythonCommandKeyOk = "KEY_OK"
+PythonCommandKeyBack = "KEY_BACK"
+PythonCommandKeyHome = "KEY_HOME"
+PythonCommandKeyOnOff = "KEY_ONOFF"
+PythonCommandKeyShift = "KEY_SHIFT"
+PythonCommandKeyAlpha = "KEY_ALPHA"
+PythonCommandKeyXnt = "KEY_XNT"
+PythonCommandKeyVar = "KEY_VAR"
+PythonCommandKeyToolbox = "KEY_TOOLBOX"
+PythonCommandKeyBackspace = "KEY_BACKSPACE"
+PythonCommandKeyExp = "KEY_EXP"
+PythonCommandKeyLn = "KEY_LN"
+PythonCommandKeyLog = "KEY_LOG"
+PythonCommandKeyImaginary = "KEY_IMAGINARY"
+PythonCommandKeyComma = "KEY_COMMA"
+PythonCommandKeyPower = "KEY_POWER"
+PythonCommandKeySine = "KEY_SINE"
+PythonCommandKeyCosine = "KEY_COSINE"
+PythonCommandKeyTangent = "KEY_TANGENT"
+PythonCommandKeyPi = "KEY_PI"
+PythonCommandKeySqrt = "KEY_SQRT"
+PythonCommandKeySquare = "KEY_SQUARE"
+PythonCommandKeySeven = "KEY_SEVEN"
+PythonCommandKeyEight = "KEY_EIGHT"
+PythonCommandKeyNine = "KEY_NINE"
+PythonCommandKeyLeftParenthesis = "KEY_LEFTPARENTHESIS"
+PythonCommandKeyRightParenthesis = "KEY_RIGHTPARENTHESIS"
+PythonCommandKeyFour = "KEY_FOUR"
+PythonCommandKeyFive = "KEY_FIVE"
+PythonCommandKeySix = "KEY_SIX"
+PythonCommandKeyMultiplication = "KEY_MULTIPLICATION"
+PythonCommandKeyDivision = "KEY_DIVISION"
+PythonCommandKeyOne = "KEY_ONE"
+PythonCommandKeyTwo = "KEY_TWO"
+PythonCommandKeyThree = "KEY_THREE"
+PythonCommandKeyPlus = "KEY_PLUS"
+PythonCommandKeyMinus = "KEY_MINUS"
+PythonCommandKeyZero = "KEY_ZERO"
+PythonCommandKeyDot = "KEY_DOT"
+PythonCommandKeyEe = "KEY_EE"
+PythonCommandKeyAns = "KEY_ANS"
+PythonCommandKeyExe = "KEY_EXE"
+PythonCommandIsKeyDown = "keydown(k)"
+PythonCommandLdexp = "ldexp(x,i)"
+PythonCommandLength = "len(object)"
+PythonCommandLgamma = "lgamma(x)"
+PythonCommandLog = "log(x,a)"
+PythonCommandLog10 = "log10(x)"
+PythonCommandLog2 = "log2(x)"
+PythonCommandLogComplex = "log(z,a)"
+PythonCommandMathFunction = "math.function"
+PythonCommandMathFunctionWithoutArg = "math.\x11"
+PythonCommandMatplotlibPyplotFunction = "matplotlib.pyplot.function"
+PythonCommandMatplotlibPyplotFunctionWithoutArg = "matplotlib.pyplot.\x11"
+PythonCommandMax = "max(list)"
+PythonCommandMin = "min(list)"
+PythonCommandModf = "modf(x)"
+PythonCommandMonotonic = "monotonic()"
+PythonCommandOct = "oct(x)"
+PythonCommandPhase = "phase(z)"
+PythonCommandPlot = "plot(x,y,color)"
+PythonCommandPolar = "polar(z)"
+PythonCommandPop = "list.pop()"
+PythonCommandPopWithoutArg = ".pop()"
+PythonCommandPower = "pow(x,y)"
+PythonCommandPrint = "print(object)"
+PythonCommandRadians = "radians(x)"
+PythonCommandRandint = "randint(a,b)"
+PythonCommandRandom = "random()"
+PythonCommandRandomFunction = "random.function"
+PythonCommandRandomFunctionWithoutArg = "random.\x11"
+PythonCommandRandrange = "randrange(start,stop)"
+PythonCommandRangeStartStop = "range(start,stop)"
+PythonCommandRangeStop = "range(stop)"
+PythonCommandReal = "z.real"
+PythonCommandRealWithoutArg = ".real"
+PythonCommandRect = "rect(r,arg)"
+PythonCommandRemove = "list.remove(x)"
+PythonCommandRemoveWithoutArg = ".remove(\x11)"
+PythonCommandReverse = "list.reverse()"
+PythonCommandReverseWithoutArg = ".reverse()"
+PythonCommandRound = "round(x,n)"
+PythonCommandScatter = "scatter(x,y)"
+PythonCommandSeed = "seed(x)"
+PythonCommandSetPixel = "set_pixel(x,y,color)"
+PythonCommandShow = "show()"
+PythonCommandSin = "sin(x)"
+PythonCommandSinComplex = "sin(z)"
+PythonCommandSinh = "sinh(x)"
+PythonCommandSleep = "sleep(t)"
+PythonCommandSort = "list.sort()"
+PythonCommandSortWithoutArg = ".sort()"
+PythonCommandSorted = "sorted(list)"
+PythonCommandSqrt = "sqrt(x)"
+PythonCommandSqrtComplex = "sqrt(z)"
+PythonCommandSum = "sum(list)"
+PythonCommandTan = "tan(x)"
+PythonCommandTanh = "tanh(x)"
+PythonCommandText = "text(x,y,\"text\")"
+PythonCommandTimeFunction = "time.function"
+PythonCommandTimeFunctionWithoutArg = "time.\x11"
+PythonCommandTrunc = "trunc(x)"
+PythonCommandTurtleFunction = "turtle.function"
+PythonCommandTurtleFunctionWithoutArg = "turtle.\x11"
+PythonCommandUniform = "uniform(a,b)"
+PythonConstantE = "2.718281828459045"
+PythonConstantPi = "3.141592653589793"
+PythonTurtleCommandBackward = "backward(x)"
+PythonTurtleCommandCircle = "circle(r)"
+PythonTurtleCommandColor = "color('c')"
+PythonTurtleCommandColorMode = "colormode(x)"
+PythonTurtleCommandForward = "forward(x)"
+PythonTurtleCommandGoto = "goto(x,y)"
+PythonTurtleCommandHeading = "heading()"
+PythonTurtleCommandHideturtle = "hideturtle()"
+PythonTurtleCommandIsdown= "isdown()"
+PythonTurtleCommandLeft = "left(a)"
+PythonTurtleCommandPendown = "pendown()"
+PythonTurtleCommandPensize = "pensize(x)"
+PythonTurtleCommandPenup = "penup()"
+PythonTurtleCommandPosition = "position()"
+PythonTurtleCommandReset = "reset()"
+PythonTurtleCommandRight = "right(a)"
+PythonTurtleCommandSetheading = "setheading(a)"
+PythonTurtleCommandSetposition = "setposition(x,[y])"
+PythonTurtleCommandShowturtle = "showturtle()"
+PythonTurtleCommandSpeed = "speed(x)"
+PythonTurtleCommandWrite = "write(\"text\")"
diff --git a/apps/code/code_icon.png b/apps/code/code_icon.png
new file mode 100644
index 00000000000..5f4d2d1623c
Binary files /dev/null and b/apps/code/code_icon.png differ
diff --git a/apps/code/console_controller.cpp b/apps/code/console_controller.cpp
new file mode 100644
index 00000000000..ac0dc330d55
--- /dev/null
+++ b/apps/code/console_controller.cpp
@@ -0,0 +1,542 @@
+#include "console_controller.h"
+#include "app.h"
+#include "script.h"
+#include "variable_box_controller.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+extern "C" {
+#include
+}
+
+namespace Code {
+
+static const char * sStandardPromptText = ">>> ";
+
+ConsoleController::ConsoleController(Responder * parentResponder, App * pythonDelegate, ScriptStore * scriptStore
+#if EPSILON_GETOPT
+ , bool lockOnConsole
+#endif
+ ) :
+ ViewController(parentResponder),
+ SelectableTableViewDataSource(),
+ TextFieldDelegate(),
+ MicroPython::ExecutionEnvironment(),
+ m_pythonDelegate(pythonDelegate),
+ m_importScriptsWhenViewAppears(false),
+ m_selectableTableView(this, this, this, this),
+ m_editCell(this, this, this),
+ m_scriptStore(scriptStore),
+ m_sandboxController(this),
+ m_inputRunLoopActive(false)
+#if EPSILON_GETOPT
+ , m_locked(lockOnConsole)
+#endif
+{
+ m_selectableTableView.setMargins(0, Metric::CommonRightMargin, 0, Metric::TitleBarExternHorizontalMargin);
+ m_selectableTableView.setBackgroundColor(KDColorWhite);
+ m_editCell.setPrompt(sStandardPromptText);
+ for (int i = 0; i < k_numberOfLineCells; i++) {
+ m_cells[i].setParentResponder(&m_selectableTableView);
+ }
+}
+
+bool ConsoleController::loadPythonEnvironment() {
+ if (!m_pythonDelegate->isPythonUser(this)) {
+ m_scriptStore->clearConsoleFetchInformation();
+ emptyOutputAccumulationBuffer();
+ m_pythonDelegate->initPythonWithUser(this);
+ MicroPython::registerScriptProvider(m_scriptStore);
+ m_importScriptsWhenViewAppears = m_autoImportScripts;
+ }
+ return true;
+}
+
+void ConsoleController::unloadPythonEnvironment() {
+ if (!m_pythonDelegate->isPythonUser(nullptr)) {
+ m_consoleStore.startNewSession();
+ m_pythonDelegate->deinitPython();
+ }
+}
+
+void ConsoleController::autoImport() {
+ for (int i = 0; i < m_scriptStore->numberOfScripts(); i++) {
+ autoImportScript(m_scriptStore->scriptAtIndex(i));
+ }
+}
+
+void ConsoleController::runAndPrintForCommand(const char * command) {
+ const char * storedCommand = m_consoleStore.pushCommand(command);
+ assert(m_outputAccumulationBuffer[0] == '\0');
+
+ // Draw the console before running the code
+ m_editCell.setText("");
+ m_editCell.setPrompt("");
+ refreshPrintOutput();
+
+ runCode(storedCommand);
+
+ m_editCell.setPrompt(sStandardPromptText);
+ m_editCell.setEditing(true);
+
+ flushOutputAccumulationBufferToStore();
+ m_consoleStore.deleteLastLineIfEmpty();
+}
+
+void ConsoleController::terminateInputLoop() {
+ assert(m_inputRunLoopActive);
+ m_inputRunLoopActive = false;
+ interrupt();
+}
+
+const char * ConsoleController::inputText(const char * prompt) {
+ AppsContainer * appsContainer = AppsContainer::sharedAppsContainer();
+ m_inputRunLoopActive = true;
+
+ // Hide the sandbox if it is displayed
+ hideAnyDisplayedViewController();
+
+ const char * promptText = prompt;
+ char * s = const_cast(prompt);
+
+ if (promptText != nullptr) {
+ /* Set the prompt text. If the prompt text has a '\n', put the prompt text in
+ * the history until the last '\n', and put the remaining prompt text in the
+ * edit cell's prompt. */
+ char * lastCarriageReturn = nullptr;
+ while (*s != 0) {
+ if (*s == '\n') {
+ lastCarriageReturn = s;
+ }
+ s++;
+ }
+ if (lastCarriageReturn != nullptr) {
+ printText(prompt, lastCarriageReturn-prompt+1);
+ promptText = lastCarriageReturn+1;
+ }
+ }
+
+ const char * previousPrompt = m_editCell.promptText();
+ m_editCell.setPrompt(promptText);
+
+ /* The user will input some text that is stored in the edit cell. When the
+ * input is finished, we want to clear that cell and return the input text.
+ * We choose to shift the input in the edit cell and put a null char in first
+ * position, so that the cell seems cleared but we can still use it to store
+ * the input.
+ * To do so, we need to reduce the cell buffer size by one, so that the input
+ * can be shifted afterwards, even if it has maxSize.
+ *
+ * Illustration of a input sequence:
+ * | | | | | | | | | <- the edit cell buffer
+ * |0| | | | | | |X| <- clear and reduce the size
+ * |a|0| | | | | |X| <- user input
+ * |a|b|0| | | | |X| <- user input
+ * |a|b|c|0| | | |X| <- user input
+ * |a|b|c|d|0| | |X| <- last user input
+ * | |a|b|c|d|0| | | <- increase the buffer size and shift the user input by one
+ * |0|a|b|c|d|0| | | <- put a zero in first position: the edit cell seems empty
+ */
+
+ m_editCell.clearAndReduceSize();
+
+ // Reload the history
+ reloadData(true);
+ appsContainer->redrawWindow();
+
+ // Launch a new input loop
+ appsContainer->runWhile([](void * a){
+ ConsoleController * c = static_cast(a);
+ return c->inputRunLoopActive();
+ }, this);
+
+ // Print the prompt and the input text
+ if (promptText != nullptr) {
+ printText(promptText, s - promptText);
+ }
+ const char * text = m_editCell.text();
+ size_t textSize = strlen(text);
+ printText(text, textSize);
+ flushOutputAccumulationBufferToStore();
+
+ // Clear the edit cell and return the input
+ text = m_editCell.shiftCurrentTextAndClear();
+ m_editCell.setPrompt(previousPrompt);
+ refreshPrintOutput();
+
+ return text;
+}
+
+void ConsoleController::viewWillAppear() {
+ ViewController::viewWillAppear();
+ loadPythonEnvironment();
+ if (m_importScriptsWhenViewAppears) {
+ m_importScriptsWhenViewAppears = false;
+ autoImport();
+ }
+
+ reloadData(true);
+}
+
+void ConsoleController::didBecomeFirstResponder() {
+ if (!isDisplayingViewController()) {
+ Container::activeApp()->setFirstResponder(&m_editCell);
+ } else {
+ /* A view controller might be displayed: for example, when pushing the
+ * console on the stack controller, we auto-import scripts during the
+ * 'viewWillAppear' and then we set the console as first responder. The
+ * sandbox or the matplotlib controller might have been pushed in the
+ * auto-import. */
+ Container::activeApp()->setFirstResponder(stackViewController()->topViewController());
+ }
+}
+
+bool ConsoleController::handleEvent(Ion::Events::Event event) {
+ if (event == Ion::Events::OK || event == Ion::Events::EXE) {
+ if (m_consoleStore.numberOfLines() > 0 && m_selectableTableView.selectedRow() < m_consoleStore.numberOfLines()) {
+ const char * text = m_consoleStore.lineAtIndex(m_selectableTableView.selectedRow()).text();
+ m_editCell.setEditing(true);
+ m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines());
+ Container::activeApp()->setFirstResponder(&m_editCell);
+ return m_editCell.insertText(text);
+ }
+ } else if (event == Ion::Events::Clear) {
+ m_selectableTableView.deselectTable();
+ m_consoleStore.clear();
+ m_selectableTableView.reloadData();
+ m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines());
+ return true;
+ } else if (event == Ion::Events::Backspace) {
+ int selectedRow = m_selectableTableView.selectedRow();
+ assert(selectedRow >= 0 && selectedRow < m_consoleStore.numberOfLines());
+ m_selectableTableView.deselectTable();
+ int firstDeletedLineIndex = m_consoleStore.deleteCommandAndResultsAtIndex(selectedRow);
+ m_selectableTableView.reloadData();
+ m_selectableTableView.selectCellAtLocation(0, firstDeletedLineIndex);
+ return true;
+ }
+#if EPSILON_GETOPT
+ if (m_locked && (event == Ion::Events::Home || event == Ion::Events::Back)) {
+ if (m_inputRunLoopActive) {
+ terminateInputLoop();
+ }
+ return true;
+ }
+#endif
+ return false;
+}
+
+int ConsoleController::numberOfRows() const {
+ return m_consoleStore.numberOfLines()+1;
+}
+
+KDCoordinate ConsoleController::rowHeight(int j) {
+ return GlobalPreferences::sharedGlobalPreferences()->font()->glyphSize().height();
+}
+
+KDCoordinate ConsoleController::cumulatedHeightFromIndex(int j) {
+ return j*rowHeight(0);
+}
+
+int ConsoleController::indexFromCumulatedHeight(KDCoordinate offsetY ){
+ return offsetY/rowHeight(0);
+}
+
+HighlightCell * ConsoleController::reusableCell(int index, int type) {
+ assert(index >= 0);
+ if (type == LineCellType) {
+ assert(index < k_numberOfLineCells);
+ return m_cells+index;
+ } else {
+ assert(type == EditCellType);
+ assert(index == 0);
+ return &m_editCell;
+ }
+}
+
+int ConsoleController::reusableCellCount(int type) {
+ if (type == LineCellType) {
+ return k_numberOfLineCells;
+ } else {
+ return 1;
+ }
+}
+
+int ConsoleController::typeAtLocation(int i, int j) {
+ assert(i == 0);
+ assert(j >= 0);
+ if (j < m_consoleStore.numberOfLines()) {
+ return LineCellType;
+ } else {
+ assert(j == m_consoleStore.numberOfLines());
+ return EditCellType;
+ }
+}
+
+void ConsoleController::willDisplayCellAtLocation(HighlightCell * cell, int i, int j) {
+ assert(i == 0);
+ if (j < m_consoleStore.numberOfLines()) {
+ static_cast(cell)->setLine(m_consoleStore.lineAtIndex(j));
+ }
+}
+
+void ConsoleController::tableViewDidChangeSelectionAndDidScroll(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY, bool withinTemporarySelection) {
+ if (withinTemporarySelection) {
+ return;
+ }
+ if (t->selectedRow() == m_consoleStore.numberOfLines()) {
+ m_editCell.setEditing(true);
+ return;
+ }
+ if (t->selectedRow()>-1) {
+ if (previousSelectedCellY > -1 && previousSelectedCellY < m_consoleStore.numberOfLines()) {
+ // Reset the scroll of the previous cell
+ ConsoleLineCell * previousCell = (ConsoleLineCell *)(t->cellAtLocation(previousSelectedCellX, previousSelectedCellY));
+ if (previousCell) {
+ previousCell->reloadCell();
+ }
+ }
+ ConsoleLineCell * selectedCell = (ConsoleLineCell *)(t->selectedCell());
+ if (selectedCell) {
+ selectedCell->reloadCell();
+ }
+ }
+}
+
+bool ConsoleController::textFieldShouldFinishEditing(TextField * textField, Ion::Events::Event event) {
+ assert(textField->isEditing());
+ return (textField->draftTextLength() > 0
+ && (event == Ion::Events::OK || event == Ion::Events::EXE));
+}
+
+bool ConsoleController::textFieldDidReceiveEvent(TextField * textField, Ion::Events::Event event) {
+ if (m_inputRunLoopActive
+ && (event == Ion::Events::Up
+ || event == Ion::Events::OK
+ || event == Ion::Events::EXE))
+ {
+ m_inputRunLoopActive = false;
+ /* We need to return true here because we want to actually exit from the
+ * input run loop, which requires ending a dispatchEvent cycle. */
+ return true;
+ }
+ if (event == Ion::Events::Up) {
+ if (m_consoleStore.numberOfLines() > 0 && m_selectableTableView.selectedRow() == m_consoleStore.numberOfLines()) {
+ m_editCell.setEditing(false);
+ m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines()-1);
+ return true;
+ }
+ }
+ return App::app()->textInputDidReceiveEvent(textField, event);
+}
+
+bool ConsoleController::textFieldDidFinishEditing(TextField * textField, const char * text, Ion::Events::Event event) {
+ if (m_inputRunLoopActive) {
+ m_inputRunLoopActive = false;
+ return false;
+ }
+ telemetryReportEvent("Console", text);
+ runAndPrintForCommand(text);
+ if (!isDisplayingViewController()) {
+ reloadData(true);
+ }
+ return true;
+}
+
+bool ConsoleController::textFieldDidAbortEditing(TextField * textField) {
+ if (m_inputRunLoopActive) {
+ m_inputRunLoopActive = false;
+ } else {
+#if EPSILON_GETOPT
+ /* In order to lock the console controller, we disable poping controllers
+ * below the console controller included. The stack should only hold:
+ * - the menu controller
+ * - the console controller
+ * The depth of the stack controller must always be above or equal to 2. */
+ if (!m_locked || stackViewController()->depth() > 2) {
+#endif
+ stackViewController()->pop();
+#if EPSILON_GETOPT
+ } else {
+ textField->setEditing(true);
+ }
+#endif
+ }
+ return true;
+}
+
+VariableBoxController * ConsoleController::variableBoxForInputEventHandler(InputEventHandler * textInput) {
+ VariableBoxController * varBox = App::app()->variableBoxController();
+ varBox->loadVariablesImportedFromScripts();
+ varBox->setTitle(I18n::Message::FunctionsAndVariables);
+ varBox->setDisplaySubtitles(false);
+ return varBox;
+}
+
+void ConsoleController::resetSandbox() {
+ if (stackViewController()->topViewController() != sandbox()) {
+ return;
+ }
+ m_sandboxController.reset();
+}
+
+void ConsoleController::displayViewController(ViewController * controller) {
+ if (stackViewController()->topViewController() == controller) {
+ return;
+ }
+ hideAnyDisplayedViewController();
+ stackViewController()->push(controller);
+}
+
+void ConsoleController::hideAnyDisplayedViewController() {
+ if (!isDisplayingViewController()) {
+ return;
+ }
+ stackViewController()->pop();
+}
+
+bool ConsoleController::isDisplayingViewController() {
+ /* The StackViewController model state is the best way to know wether the
+ * console is displaying a View Controller (Sandbox or Matplotlib). Indeed,
+ * keeping a boolean or a pointer raises the issue of when updating it - when
+ * 'viewWillAppear' or when 'didEnterResponderChain' - in both cases, the
+ * state would be wrong at some point... */
+ return stackViewController()->depth() > 2;
+}
+
+void ConsoleController::refreshPrintOutput() {
+ if (!isDisplayingViewController()) {
+ reloadData(false);
+ AppsContainer::sharedAppsContainer()->redrawWindow();
+ }
+}
+
+void ConsoleController::reloadData(bool isEditing) {
+ m_selectableTableView.reloadData();
+ m_selectableTableView.selectCellAtLocation(0, m_consoleStore.numberOfLines());
+ if (isEditing) {
+ m_editCell.setEditing(true);
+ m_editCell.setText("");
+ } else {
+ m_editCell.setEditing(false);
+ }
+}
+
+/* printText is called by the Python machine.
+ * The text argument is not always null-terminated. */
+void ConsoleController::printText(const char * text, size_t length) {
+ size_t textCutIndex = firstNewLineCharIndex(text, length);
+ if (textCutIndex >= length) {
+ /* If there is no new line in text, just append it to the output
+ * accumulation buffer. */
+ appendTextToOutputAccumulationBuffer(text, length);
+ } else {
+ if (textCutIndex < length - 1) {
+ /* If there is a new line in the middle of the text, we have to store at
+ * least two new console lines in the console store. */
+ printText(text, textCutIndex + 1);
+ printText(&text[textCutIndex+1], length - (textCutIndex + 1));
+ return;
+ }
+ /* There is a new line at the end of the text, we have to store the line in
+ * the console store. */
+ assert(textCutIndex == length - 1);
+ appendTextToOutputAccumulationBuffer(text, length-1);
+ flushOutputAccumulationBufferToStore();
+ micropython_port_vm_hook_refresh_print();
+ }
+}
+
+void ConsoleController::autoImportScript(Script script, bool force) {
+ /* The sandbox might be displayed, for instance if we are auto-importing
+ * several scripts that draw at importation. In this case, we want to remove
+ * the sandbox. */
+ hideAnyDisplayedViewController();
+
+ if (script.autoImportationStatus() || force) {
+ // Step 1 - Create the command "from scriptName import *".
+
+ assert(strlen(k_importCommand1) + strlen(script.fullName()) - strlen(ScriptStore::k_scriptExtension) - 1 + strlen(k_importCommand2) + 1 <= k_maxImportCommandSize);
+ char command[k_maxImportCommandSize];
+
+ // Copy "from "
+ size_t currentChar = strlcpy(command, k_importCommand1, k_maxImportCommandSize);
+ const char * scriptName = script.fullName();
+
+ /* Copy the script name without the extension ".py". The '.' is overwritten
+ * by the null terminating char. */
+ int copySizeWithNullTerminatingZero = std::min(k_maxImportCommandSize - currentChar, strlen(scriptName) - strlen(ScriptStore::k_scriptExtension));
+ assert(copySizeWithNullTerminatingZero >= 0);
+ assert(copySizeWithNullTerminatingZero <= k_maxImportCommandSize - currentChar);
+ strlcpy(command+currentChar, scriptName, copySizeWithNullTerminatingZero);
+ currentChar += copySizeWithNullTerminatingZero-1;
+
+ // Copy " import *"
+ assert(k_maxImportCommandSize >= currentChar);
+ strlcpy(command+currentChar, k_importCommand2, k_maxImportCommandSize - currentChar);
+
+ // Step 2 - Run the command
+ runAndPrintForCommand(command);
+ }
+ if (!isDisplayingViewController() && force) {
+ reloadData(true);
+ }
+}
+
+void ConsoleController::flushOutputAccumulationBufferToStore() {
+ m_consoleStore.pushResult(m_outputAccumulationBuffer);
+ emptyOutputAccumulationBuffer();
+}
+
+void ConsoleController::appendTextToOutputAccumulationBuffer(const char * text, size_t length) {
+ int endOfAccumulatedText = strlen(m_outputAccumulationBuffer);
+ int spaceLeft = k_outputAccumulationBufferSize - endOfAccumulatedText;
+ if (spaceLeft > (int)length) {
+ memcpy(&m_outputAccumulationBuffer[endOfAccumulatedText], text, length);
+ return;
+ }
+ /* The text to append is too long for the buffer. We need to split it in
+ * chunks. We take special care not to break in the middle of code points! */
+ int maxAppendedTextLength = spaceLeft-1; // we keep the last char to null-terminate the buffer
+ int appendedTextLength = 0;
+ UTF8Decoder decoder(text);
+ while (decoder.stringPosition() - text <= maxAppendedTextLength) {
+ appendedTextLength = decoder.stringPosition() - text;
+ decoder.nextCodePoint();
+ }
+ memcpy(&m_outputAccumulationBuffer[endOfAccumulatedText], text, appendedTextLength);
+ // The last char of m_outputAccumulationBuffer is kept to 0 to ensure a null-terminated text.
+ assert(endOfAccumulatedText+appendedTextLength < k_outputAccumulationBufferSize);
+ m_outputAccumulationBuffer[endOfAccumulatedText+appendedTextLength] = 0;
+ flushOutputAccumulationBufferToStore();
+ appendTextToOutputAccumulationBuffer(&text[appendedTextLength], length - appendedTextLength);
+}
+
+// TODO: is it really needed? Maybe discard to optimize?
+void ConsoleController::emptyOutputAccumulationBuffer() {
+ for (int i = 0; i < k_outputAccumulationBufferSize; i++) {
+ m_outputAccumulationBuffer[i] = 0;
+ }
+}
+
+size_t ConsoleController::firstNewLineCharIndex(const char * text, size_t length) {
+ size_t index = 0;
+ while (index < length) {
+ if (text[index] == '\n') {
+ return index;
+ }
+ index++;
+ }
+ return index;
+}
+
+StackViewController * ConsoleController::stackViewController() {
+ return static_cast(parentResponder());
+}
+
+}
diff --git a/apps/code/console_controller.h b/apps/code/console_controller.h
new file mode 100644
index 00000000000..ff79e7b51b3
--- /dev/null
+++ b/apps/code/console_controller.h
@@ -0,0 +1,119 @@
+#ifndef CODE_CONSOLE_CONTROLLER_H
+#define CODE_CONSOLE_CONTROLLER_H
+
+#include
+#include
+
+#include "console_edit_cell.h"
+#include "console_line_cell.h"
+#include "console_store.h"
+#include "sandbox_controller.h"
+#include "script_store.h"
+#include "variable_box_controller.h"
+#include "../shared/input_event_handler_delegate.h"
+
+namespace Code {
+
+class App;
+
+class ConsoleController : public ViewController, public ListViewDataSource, public SelectableTableViewDataSource, public SelectableTableViewDelegate, public TextFieldDelegate, public Shared::InputEventHandlerDelegate, public MicroPython::ExecutionEnvironment {
+public:
+ ConsoleController(Responder * parentResponder, App * pythonDelegate, ScriptStore * scriptStore
+#if EPSILON_GETOPT
+ , bool m_lockOnConsole
+#endif
+ );
+
+ bool loadPythonEnvironment();
+ void unloadPythonEnvironment();
+
+ void setAutoImport(bool autoImport) { m_autoImportScripts = autoImport; }
+ void autoImport();
+ void autoImportScript(Script script, bool force = false);
+ void runAndPrintForCommand(const char * command);
+ bool inputRunLoopActive() const { return m_inputRunLoopActive; }
+ void terminateInputLoop();
+
+ // ViewController
+ View * view() override { return &m_selectableTableView; }
+ void viewWillAppear() override;
+ void didBecomeFirstResponder() override;
+ bool handleEvent(Ion::Events::Event event) override;
+ ViewController::DisplayParameter displayParameter() override { return ViewController::DisplayParameter::WantsMaximumSpace; }
+ TELEMETRY_ID("Console");
+
+ // ListViewDataSource
+ int numberOfRows() const override;
+ KDCoordinate rowHeight(int j) override;
+ KDCoordinate cumulatedHeightFromIndex(int j) override;
+ int indexFromCumulatedHeight(KDCoordinate offsetY) override;
+ HighlightCell * reusableCell(int index, int type) override;
+ int reusableCellCount(int type) override;
+ int typeAtLocation(int i, int j) override;
+ void willDisplayCellAtLocation(HighlightCell * cell, int i, int j) override;
+
+ // SelectableTableViewDelegate
+ void tableViewDidChangeSelectionAndDidScroll(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY, bool withinTemporarySelection) override;
+
+ // TextFieldDelegate
+ bool textFieldShouldFinishEditing(TextField * textField, Ion::Events::Event event) override;
+ bool textFieldDidReceiveEvent(TextField * textField, Ion::Events::Event event) override;
+ bool textFieldDidFinishEditing(TextField * textField, const char * text, Ion::Events::Event event) override;
+ bool textFieldDidAbortEditing(TextField * textField) override;
+
+ // InputEventHandlerDelegate
+ VariableBoxController * variableBoxForInputEventHandler(InputEventHandler * textInput) override;
+
+ // MicroPython::ExecutionEnvironment
+ ViewController * sandbox() override { return &m_sandboxController; }
+ void resetSandbox() override;
+ void displayViewController(ViewController * controller) override;
+ void hideAnyDisplayedViewController() override;
+ void refreshPrintOutput() override;
+ void printText(const char * text, size_t length) override;
+ const char * inputText(const char * prompt) override;
+
+#if EPSILON_GETOPT
+ bool locked() const {
+ return m_locked;
+ }
+#endif
+private:
+ static constexpr const char * k_importCommand1 = "from ";
+ static constexpr const char * k_importCommand2 = " import *";
+ static constexpr size_t k_maxImportCommandSize = 5 + 9 + TextField::maxBufferSize(); // strlen(k_importCommand1) + strlen(k_importCommand2) + TextField::maxBufferSize()
+ static constexpr int LineCellType = 0;
+ static constexpr int EditCellType = 1;
+ static constexpr int k_numberOfLineCells = (Ion::Display::Height - Metric::TitleBarHeight) / 14 + 2; // 14 = KDFont::SmallFont->glyphSize().height()
+ // k_numberOfLineCells = (240 - 18)/14 ~ 15.9. The 0.1 cell can be above and below the 15 other cells so we add +2 cells.
+ static constexpr int k_outputAccumulationBufferSize = 100;
+ bool isDisplayingViewController();
+ void reloadData(bool isEditing);
+ void flushOutputAccumulationBufferToStore();
+ void appendTextToOutputAccumulationBuffer(const char * text, size_t length);
+ void emptyOutputAccumulationBuffer();
+ size_t firstNewLineCharIndex(const char * text, size_t length);
+ StackViewController * stackViewController();
+ App * m_pythonDelegate;
+ bool m_importScriptsWhenViewAppears;
+ ConsoleStore m_consoleStore;
+ SelectableTableView m_selectableTableView;
+ ConsoleLineCell m_cells[k_numberOfLineCells];
+ ConsoleEditCell m_editCell;
+ char m_outputAccumulationBuffer[k_outputAccumulationBufferSize];
+ /* The Python machine might call printText several times to print a single
+ * string. We thus use m_outputAccumulationBuffer to store and concatenate the
+ * different strings until a new line char appears in the text. When this
+ * happens, or when m_outputAccumulationBuffer is full, we create a new
+ * ConsoleLine in the ConsoleStore and empty m_outputAccumulationBuffer. */
+ ScriptStore * m_scriptStore;
+ SandboxController m_sandboxController;
+ bool m_inputRunLoopActive;
+ bool m_autoImportScripts;
+#if EPSILON_GETOPT
+ bool m_locked;
+#endif
+};
+}
+
+#endif
diff --git a/apps/code/console_edit_cell.cpp b/apps/code/console_edit_cell.cpp
new file mode 100644
index 00000000000..f82d5d5962a
--- /dev/null
+++ b/apps/code/console_edit_cell.cpp
@@ -0,0 +1,79 @@
+#include "console_edit_cell.h"
+#include "console_controller.h"
+#include
+#include
+#include
+#include
+#include
+
+namespace Code {
+
+ConsoleEditCell::ConsoleEditCell(Responder * parentResponder, InputEventHandlerDelegate * inputEventHandlerDelegate, TextFieldDelegate * delegate) :
+ HighlightCell(),
+ Responder(parentResponder),
+ m_promptView(GlobalPreferences::sharedGlobalPreferences()->font(), nullptr, 0, 0.5),
+ m_textField(this, nullptr, TextField::maxBufferSize(), TextField::maxBufferSize(), inputEventHandlerDelegate, delegate, GlobalPreferences::sharedGlobalPreferences()->font())
+{
+}
+
+int ConsoleEditCell::numberOfSubviews() const {
+ return 2;
+}
+
+View * ConsoleEditCell::subviewAtIndex(int index) {
+ assert(index == 0 || index ==1);
+ if (index == 0) {
+ return &m_promptView;
+ } else {
+ return &m_textField;
+ }
+}
+
+void ConsoleEditCell::layoutSubviews(bool force) {
+ KDSize promptSize = m_promptView.minimalSizeForOptimalDisplay();
+ m_promptView.setFrame(KDRect(KDPointZero, promptSize.width(), bounds().height()), force);
+ m_textField.setFrame(KDRect(KDPoint(promptSize.width(), KDCoordinate(0)), bounds().width() - promptSize.width(), bounds().height()), force);
+}
+
+void ConsoleEditCell::didBecomeFirstResponder() {
+ Container::activeApp()->setFirstResponder(&m_textField);
+}
+
+void ConsoleEditCell::setEditing(bool isEditing) {
+ m_textField.setEditing(isEditing);
+}
+
+void ConsoleEditCell::setText(const char * text) {
+ m_textField.setText(text);
+}
+
+void ConsoleEditCell::setPrompt(const char * prompt) {
+ m_promptView.setText(prompt);
+ layoutSubviews();
+}
+
+bool ConsoleEditCell::insertText(const char * text) {
+ return m_textField.handleEventWithText(text);
+}
+
+void ConsoleEditCell::clearAndReduceSize() {
+ setText("");
+ size_t previousBufferSize = m_textField.draftTextBufferSize();
+ assert(previousBufferSize > 1);
+ m_textField.setDraftTextBufferSize(previousBufferSize - 1);
+}
+
+const char * ConsoleEditCell::shiftCurrentTextAndClear() {
+ size_t previousBufferSize = m_textField.draftTextBufferSize();
+ m_textField.setDraftTextBufferSize(previousBufferSize + 1);
+ char * textFieldBuffer = const_cast(m_textField.text());
+ char * newTextPosition = textFieldBuffer + 1;
+ assert(previousBufferSize > 0);
+ size_t copyLength = std::min(previousBufferSize - 1, strlen(textFieldBuffer));
+ memmove(newTextPosition, textFieldBuffer, copyLength);
+ newTextPosition[copyLength] = 0;
+ textFieldBuffer[0] = 0;
+ return newTextPosition;
+}
+
+}
diff --git a/apps/code/console_edit_cell.h b/apps/code/console_edit_cell.h
new file mode 100644
index 00000000000..3c076f9a1e7
--- /dev/null
+++ b/apps/code/console_edit_cell.h
@@ -0,0 +1,45 @@
+#ifndef CODE_EDIT_CELL_H
+#define CODE_EDIT_CELL_H
+
+#include
+#include
+#include
+#include
+#include
+
+namespace Code {
+
+class ConsoleEditCell : public HighlightCell, public Responder {
+public:
+ ConsoleEditCell(Responder * parentResponder = nullptr, InputEventHandlerDelegate * inputEventHandlerDelegate = nullptr, TextFieldDelegate * delegate = nullptr);
+
+ // View
+ int numberOfSubviews() const override;
+ View * subviewAtIndex(int index) override;
+ void layoutSubviews(bool force = false) override;
+
+ // Responder
+ void didBecomeFirstResponder() override;
+
+ /* HighlightCell */
+ Responder * responder() override {
+ return this;
+ }
+
+ // Edit cell
+ void setEditing(bool isEditing);
+ const char * text() const override { return m_textField.text(); }
+ void setText(const char * text);
+ bool insertText(const char * text);
+ void setPrompt(const char * prompt);
+ const char * promptText() const { return m_promptView.text(); }
+ void clearAndReduceSize();
+ const char * shiftCurrentTextAndClear();
+private:
+ PointerTextView m_promptView;
+ TextField m_textField;
+};
+
+}
+
+#endif
diff --git a/apps/code/console_line.h b/apps/code/console_line.h
new file mode 100644
index 00000000000..ee857c64bb3
--- /dev/null
+++ b/apps/code/console_line.h
@@ -0,0 +1,33 @@
+#ifndef CODE_CONSOLE_LINE_H
+#define CODE_CONSOLE_LINE_H
+
+#include
+
+namespace Code {
+
+class ConsoleLine {
+public:
+ enum class Type {
+ CurrentSessionCommand = 0,
+ CurrentSessionResult = 1,
+ PreviousSessionCommand = 2,
+ PreviousSessionResult = 3
+ };
+ ConsoleLine(Type type = Type::CurrentSessionCommand, const char * text = nullptr) :
+ m_type(type), m_text(text) {}
+ Type type() const { return m_type; }
+ const char * text() const { return m_text; }
+ bool isFromCurrentSession() const { return m_type == Type::CurrentSessionCommand || m_type == Type::CurrentSessionResult; }
+ bool isCommand() const { return m_type == Type::CurrentSessionCommand || m_type == Type::PreviousSessionCommand; }
+ bool isResult() const { return m_type == Type::CurrentSessionResult || m_type == Type::PreviousSessionResult; }
+ static inline size_t sizeOfConsoleLine(size_t textLength) {
+ return 1 + textLength + 1; // Marker, text, null termination
+ }
+private:
+ Type m_type;
+ const char * m_text;
+};
+
+}
+
+#endif
diff --git a/apps/code/console_line_cell.cpp b/apps/code/console_line_cell.cpp
new file mode 100644
index 00000000000..99042762be0
--- /dev/null
+++ b/apps/code/console_line_cell.cpp
@@ -0,0 +1,97 @@
+#include "console_line_cell.h"
+#include "console_controller.h"
+#include
+#include
+#include
+#include
+
+namespace Code {
+
+ConsoleLineCell::ScrollableConsoleLineView::ConsoleLineView::ConsoleLineView() :
+ HighlightCell(),
+ m_line(nullptr)
+{
+}
+
+void ConsoleLineCell::ScrollableConsoleLineView::ConsoleLineView::setLine(ConsoleLine * line) {
+ m_line = line;
+}
+
+void ConsoleLineCell::ScrollableConsoleLineView::ConsoleLineView::drawRect(KDContext * ctx, KDRect rect) const {
+ ctx->fillRect(bounds(), KDColorWhite);
+ ctx->drawString(m_line->text(), KDPointZero, GlobalPreferences::sharedGlobalPreferences()->font(), textColor(m_line), isHighlighted()? Palette::Select : KDColorWhite);
+}
+
+KDSize ConsoleLineCell::ScrollableConsoleLineView::ConsoleLineView::minimalSizeForOptimalDisplay() const {
+ return GlobalPreferences::sharedGlobalPreferences()->font()->stringSize(m_line->text());
+}
+
+ConsoleLineCell::ScrollableConsoleLineView::ScrollableConsoleLineView(Responder * parentResponder) :
+ ScrollableView(parentResponder, &m_consoleLineView, this),
+ m_consoleLineView()
+{
+}
+
+ConsoleLineCell::ConsoleLineCell(Responder * parentResponder) :
+ HighlightCell(),
+ Responder(parentResponder),
+ m_promptView(GlobalPreferences::sharedGlobalPreferences()->font(), I18n::Message::ConsolePrompt, 0, 0.5),
+ m_scrollableView(this),
+ m_line()
+{
+}
+
+void ConsoleLineCell::setLine(ConsoleLine line) {
+ m_line = line;
+ m_scrollableView.consoleLineView()->setLine(&m_line);
+ m_promptView.setTextColor(textColor(&m_line));
+ reloadCell();
+}
+
+void ConsoleLineCell::setHighlighted(bool highlight) {
+ HighlightCell::setHighlighted(highlight);
+ m_scrollableView.consoleLineView()->setHighlighted(highlight);
+}
+
+void ConsoleLineCell::reloadCell() {
+ layoutSubviews();
+ HighlightCell::reloadCell();
+ m_scrollableView.reloadScroll();
+}
+
+int ConsoleLineCell::numberOfSubviews() const {
+ if (m_line.isCommand()) {
+ return 2;
+ }
+ assert(m_line.isResult());
+ return 1;
+}
+
+View * ConsoleLineCell::subviewAtIndex(int index) {
+ if (m_line.isCommand()) {
+ assert(index >= 0 && index < 2);
+ View * views[] = {&m_promptView, &m_scrollableView};
+ return views[index];
+ }
+ assert(m_line.isResult());
+ assert(index == 0);
+ return &m_scrollableView;
+}
+
+void ConsoleLineCell::layoutSubviews(bool force) {
+ if (m_line.isCommand()) {
+ KDSize promptSize = GlobalPreferences::sharedGlobalPreferences()->font()->stringSize(I18n::translate(I18n::Message::ConsolePrompt));
+ m_promptView.setFrame(KDRect(KDPointZero, promptSize.width(), bounds().height()), force);
+ m_scrollableView.setFrame(KDRect(KDPoint(promptSize.width(), 0), bounds().width() - promptSize.width(), bounds().height()), force);
+ return;
+ }
+ assert(m_line.isResult());
+ m_promptView.setFrame(KDRectZero, force);
+ m_scrollableView.setFrame(bounds(), force);
+}
+
+void ConsoleLineCell::didBecomeFirstResponder() {
+ Container::activeApp()->setFirstResponder(&m_scrollableView);
+}
+
+}
diff --git a/apps/code/console_line_cell.h b/apps/code/console_line_cell.h
new file mode 100644
index 00000000000..fc49f61876f
--- /dev/null
+++ b/apps/code/console_line_cell.h
@@ -0,0 +1,65 @@
+#ifndef CODE_CONSOLE_LINE_CELL_H
+#define CODE_CONSOLE_LINE_CELL_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "console_line.h"
+
+namespace Code {
+
+class ConsoleLineCell : public HighlightCell, public Responder {
+public:
+ ConsoleLineCell(Responder * parentResponder = nullptr);
+ void setLine(ConsoleLine line);
+
+ /* HighlightCell */
+ void setHighlighted(bool highlight) override;
+ void reloadCell() override;
+ Responder * responder() override {
+ return this;
+ }
+ const char * text() const override {
+ return m_line.text();
+ }
+ /* View */
+ int numberOfSubviews() const override;
+ View * subviewAtIndex(int index) override;
+ void layoutSubviews(bool force = false) override;
+
+ /* Responder */
+ void didBecomeFirstResponder() override;
+private:
+ class ScrollableConsoleLineView : public ScrollableView, public ScrollViewDataSource {
+ public:
+ class ConsoleLineView : public HighlightCell {
+ public:
+ ConsoleLineView();
+ void setLine(ConsoleLine * line);
+ void drawRect(KDContext * ctx, KDRect rect) const override;
+ KDSize minimalSizeForOptimalDisplay() const override;
+ private:
+ ConsoleLine * m_line;
+ };
+
+ ScrollableConsoleLineView(Responder * parentResponder);
+ ConsoleLineView * consoleLineView() { return &m_consoleLineView; }
+ private:
+ ConsoleLineView m_consoleLineView;
+ };
+ static KDColor textColor(ConsoleLine * line) {
+ return line->isFromCurrentSession() ? KDColorBlack : Palette::GrayDark;
+ }
+ MessageTextView m_promptView;
+ ScrollableConsoleLineView m_scrollableView;
+ ConsoleLine m_line;
+};
+
+}
+
+#endif
diff --git a/apps/code/console_store.cpp b/apps/code/console_store.cpp
new file mode 100644
index 00000000000..e4ff3fa35ed
--- /dev/null
+++ b/apps/code/console_store.cpp
@@ -0,0 +1,188 @@
+#include "console_store.h"
+#include
+#include
+
+namespace Code {
+
+void ConsoleStore::startNewSession() {
+ if (k_historySize < 1) {
+ return;
+ }
+
+ m_history[0] = makePrevious(m_history[0]);
+
+ for (size_t i = 0; i < k_historySize - 1; i++) {
+ if (m_history[i] == 0) {
+ if (m_history[i+1] == 0) {
+ return ;
+ }
+ m_history[i+1] = makePrevious(m_history[i+1]);
+ }
+ }
+}
+
+ConsoleLine ConsoleStore::lineAtIndex(int i) const {
+ assert(i >= 0 && i < numberOfLines());
+ int currentLineIndex = 0;
+ for (size_t j=0; j= 0 && index < numberOfLinesAtStart);
+ int indexOfLineToDelete = index;
+ while (indexOfLineToDelete < numberOfLinesAtStart - 1) {
+ if (lineAtIndex(indexOfLineToDelete + 1).isCommand()) {
+ break;
+ }
+ indexOfLineToDelete++;
+ }
+ ConsoleLine lineToDelete = lineAtIndex(indexOfLineToDelete);
+ while (indexOfLineToDelete > 0 && !lineAtIndex(indexOfLineToDelete).isCommand()) {
+ deleteLineAtIndex(indexOfLineToDelete);
+ indexOfLineToDelete--;
+ lineToDelete = lineAtIndex(indexOfLineToDelete);
+ }
+ deleteLineAtIndex(indexOfLineToDelete);
+ return indexOfLineToDelete;
+}
+
+const char * ConsoleStore::push(const char marker, const char * text) {
+ size_t textLength = strlen(text);
+ if (ConsoleLine::sizeOfConsoleLine(textLength) > k_historySize - 1) {
+ textLength = k_historySize - 1 - 1 - 1; // Marker, null termination and null marker.
+ }
+ size_t i = indexOfNullMarker();
+ // If needed, make room for the text we want to push.
+ while (i + ConsoleLine::sizeOfConsoleLine(textLength) > k_historySize - 1) {
+ deleteFirstLine();
+ i = indexOfNullMarker();
+ }
+ m_history[i] = marker;
+ strlcpy(&m_history[i+1], text, std::min(k_historySize-(i+1),textLength+1));
+ m_history[i+1+textLength+1] = 0;
+ return &m_history[i+1];
+}
+
+ConsoleLine::Type ConsoleStore::lineTypeForMarker(char marker) const {
+ assert(marker == CurrentSessionCommandMarker || marker == CurrentSessionResultMarker || marker == PreviousSessionCommandMarker || marker == PreviousSessionResultMarker);
+ return static_cast(marker-1);
+}
+
+size_t ConsoleStore::indexOfNullMarker() const {
+ if (m_history[0] == 0) {
+ return 0;
+ }
+ for (size_t i=0; i=0 && index < numberOfLines());
+ int currentLineIndex = 0;
+ for (size_t i = 0; i < k_historySize - 1; i++) {
+ if (m_history[i] == 0) {
+ currentLineIndex++;
+ continue;
+ }
+ if (currentLineIndex == index) {
+ size_t nextLineStart = i;
+ while (m_history[nextLineStart] != 0 && nextLineStart < k_historySize - 2) {
+ nextLineStart++;
+ }
+ nextLineStart++;
+ if (nextLineStart > k_historySize - 1) {
+ return;
+ }
+ memmove(&m_history[i], &m_history[nextLineStart], (k_historySize - 1) - nextLineStart + 1);
+ return;
+ }
+ }
+}
+
+void ConsoleStore::deleteFirstLine() {
+ if (m_history[0] == 0) {
+ return;
+ }
+ int secondLineMarkerIndex = 1;
+ while (m_history[secondLineMarkerIndex] != 0) {
+ secondLineMarkerIndex++;
+ }
+ secondLineMarkerIndex++;
+ for (size_t i=0; i
+#include
+
+namespace Code {
+
+class ConsoleStore {
+public:
+ ConsoleStore() : m_history{0} {}
+ void clear() { assert(k_historySize > 0); m_history[0] = 0; }
+ void startNewSession();
+ ConsoleLine lineAtIndex(int i) const;
+ int numberOfLines() const;
+ const char * pushCommand(const char * text);
+ void pushResult(const char * text);
+ void deleteLastLineIfEmpty();
+ int deleteCommandAndResultsAtIndex(int index);
+private:
+ static constexpr char CurrentSessionCommandMarker = 0x01;
+ static constexpr char CurrentSessionResultMarker = 0x02;
+ static constexpr char PreviousSessionCommandMarker = 0x03;
+ static constexpr char PreviousSessionResultMarker = 0x04;
+ static constexpr size_t k_historySize = 1024;
+ static char makePrevious(char marker) {
+ if (marker == CurrentSessionCommandMarker || marker == CurrentSessionResultMarker) {
+ return marker + 0x02;
+ }
+ return marker;
+ }
+ const char * push(const char marker, const char * text);
+ ConsoleLine::Type lineTypeForMarker(char marker) const;
+ size_t indexOfNullMarker() const;
+ void deleteLineAtIndex(int index);
+ void deleteFirstLine();
+ /* When there is no room left to store a new ConsoleLine, we have to delete
+ * old ConsoleLines. deleteFirstLine() deletes the first ConsoleLine of
+ * m_history and shifts the rest of the ConsoleLines towards the beginning of
+ * m_history. */
+ void deleteLastLine();
+ char m_history[k_historySize];
+ /* The m_history variable sequentially stores an array of ConsoleLine objects.
+ * Each ConsoleLine is stored as follow:
+ * - First, a char that says whether the ConsoleLine is a Command or a Result
+ * - Then, the text content of the ConsoleLine
+ * - Last but not least, a null byte.
+ * The buffer ends whenever the marker char is null. */
+};
+
+}
+
+#endif
diff --git a/apps/code/editor_controller.cpp b/apps/code/editor_controller.cpp
new file mode 100644
index 00000000000..39348a1d412
--- /dev/null
+++ b/apps/code/editor_controller.cpp
@@ -0,0 +1,163 @@
+#include "editor_controller.h"
+#include "menu_controller.h"
+#include "script_parameter_controller.h"
+#include "app.h"
+#include
+#include
+
+using namespace Shared;
+
+namespace Code {
+
+EditorController::EditorController(MenuController * menuController, App * pythonDelegate) :
+ ViewController(nullptr),
+ m_editorView(this, pythonDelegate),
+ m_script(Ion::Storage::Record()),
+ m_scriptIndex(-1),
+ m_menuController(menuController)
+{
+ m_editorView.setTextAreaDelegates(this, this);
+}
+
+void EditorController::setScript(Script script, int scriptIndex) {
+ m_script = script;
+ m_scriptIndex = scriptIndex;
+
+ /* We edit the script directly in the storage buffer. We thus put all the
+ * storage available space at the end of the current edited script and we set
+ * its size.
+ *
+ * |****|****|m_script|****|**********|¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨|
+ * available space
+ * is transformed to:
+ *
+ * |****|****|m_script|¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨|****|**********|
+ * available space
+ *
+ * */
+
+ Ion::Storage::sharedStorage()->putAvailableSpaceAtEndOfRecord(m_script);
+ m_editorView.setText(const_cast(m_script.content()), m_script.contentSize());
+}
+
+void EditorController::willExitApp() {
+ cleanStorageEmptySpace();
+}
+
+// TODO: this should be done in textAreaDidFinishEditing maybe??
+bool EditorController::handleEvent(Ion::Events::Event event) {
+ if (event == Ion::Events::OK || event == Ion::Events::Back || event == Ion::Events::Home || event == Ion::Events::USBEnumeration) {
+ /* Exit the edition on USB enumeration, because the storage needs to be in a
+ * "clean" state (with all records packed at the beginning of the storage) */
+ cleanStorageEmptySpace();
+ stackController()->pop();
+ return event != Ion::Events::Home && event != Ion::Events::USBEnumeration;
+ }
+ return false;
+}
+
+void EditorController::didBecomeFirstResponder() {
+ Container::activeApp()->setFirstResponder(&m_editorView);
+}
+
+void EditorController::viewWillAppear() {
+ ViewController::viewWillAppear();
+ m_editorView.loadSyntaxHighlighter();
+ m_editorView.setCursorLocation(m_editorView.text() + strlen(m_editorView.text()));
+}
+
+void EditorController::viewDidDisappear() {
+ m_editorView.resetSelection();
+ m_menuController->scriptContentEditionDidFinish();
+}
+
+bool EditorController::textAreaDidReceiveEvent(TextArea * textArea, Ion::Events::Event event) {
+ if (App::app()->textInputDidReceiveEvent(textArea, event)) {
+ return true;
+ }
+ if (event == Ion::Events::EXE) {
+ textArea->handleEventWithText("\n", true, false);
+ return true;
+ }
+
+ if (event == Ion::Events::Backspace && textArea->selectionIsEmpty()) {
+ /* If the cursor is on the left of the text of a line, backspace one
+ * indentation space at a time. */
+ const char * text = textArea->text();
+ const char * cursorLocation = textArea->cursorLocation();
+ const char * firstNonSpace = UTF8Helper::NotCodePointSearch(text, ' ', true, cursorLocation);
+ assert(firstNonSpace >= text);
+ bool cursorIsPrecededOnTheLineBySpacesOnly = false;
+ size_t numberOfSpaces = cursorLocation - firstNonSpace;
+ if (UTF8Helper::CodePointIs(firstNonSpace, '\n')) {
+ cursorIsPrecededOnTheLineBySpacesOnly = true;
+ numberOfSpaces -= UTF8Decoder::CharSizeOfCodePoint('\n');
+ } else if (firstNonSpace == text) {
+ cursorIsPrecededOnTheLineBySpacesOnly = true;
+ }
+ numberOfSpaces = numberOfSpaces / UTF8Decoder::CharSizeOfCodePoint(' ');
+ if (cursorIsPrecededOnTheLineBySpacesOnly && numberOfSpaces >= TextArea::k_indentationSpaces) {
+ for (int i = 0; i < TextArea::k_indentationSpaces; i++) {
+ textArea->removePreviousGlyph();
+ }
+ return true;
+ }
+ } else if (event == Ion::Events::Space) {
+ /* If the cursor is on the left of the text of a line, a space triggers an
+ * indentation. */
+ const char * text = textArea->text();
+ const char * firstNonSpace = UTF8Helper::NotCodePointSearch(text, ' ', true, textArea->cursorLocation());
+ assert(firstNonSpace >= text);
+ if (UTF8Helper::CodePointIs(firstNonSpace, '\n')) {
+ assert(UTF8Decoder::CharSizeOfCodePoint(' ') == 1);
+ char indentationBuffer[TextArea::k_indentationSpaces+1];
+ for (int i = 0; i < TextArea::k_indentationSpaces; i++) {
+ indentationBuffer[i] = ' ';
+ }
+ indentationBuffer[TextArea::k_indentationSpaces] = 0;
+ textArea->handleEventWithText(indentationBuffer);
+ return true;
+ }
+ }
+ return false;
+}
+
+VariableBoxController * EditorController::variableBoxForInputEventHandler(InputEventHandler * textInput) {
+ VariableBoxController * varBox = App::app()->variableBoxController();
+ /* If the editor should be autocompleting an identifier, the variable box has
+ * already been loaded. We check shouldAutocomplete and not isAutocompleting,
+ * because the autocompletion result might be empty. */
+ const char * beginningOfAutocompletion = nullptr;
+ const char * cursor = nullptr;
+ PythonTextArea::AutocompletionType autocompType = m_editorView.autocompletionType(&beginningOfAutocompletion, &cursor);
+ if (autocompType == PythonTextArea::AutocompletionType::NoIdentifier) {
+ varBox->loadFunctionsAndVariables(m_scriptIndex, nullptr, 0);
+ } else if (autocompType == PythonTextArea::AutocompletionType::MiddleOfIdentifier) {
+ varBox->empty();
+ } else {
+ assert(autocompType == PythonTextArea::AutocompletionType::EndOfIdentifier);
+ assert(beginningOfAutocompletion != nullptr && cursor != nullptr);
+ assert(cursor > beginningOfAutocompletion);
+ varBox->loadFunctionsAndVariables(m_scriptIndex, beginningOfAutocompletion, cursor - beginningOfAutocompletion);
+ }
+ varBox->setTitle(I18n::Message::Autocomplete);
+ varBox->setDisplaySubtitles(true);
+ return varBox;
+}
+
+StackViewController * EditorController::stackController() {
+ return static_cast(parentResponder());
+}
+
+void EditorController::cleanStorageEmptySpace() {
+ if (m_script.isNull() || !Ion::Storage::sharedStorage()->hasRecord(m_script)) {
+ return;
+ }
+ Ion::Storage::Record::Data scriptValue = m_script.value();
+ Ion::Storage::sharedStorage()->getAvailableSpaceFromEndOfRecord(
+ m_script,
+ scriptValue.size - Script::StatusSize() - (strlen(m_script.content()) + 1)); // TODO optimize number of script fetches
+}
+
+
+}
diff --git a/apps/code/editor_controller.h b/apps/code/editor_controller.h
new file mode 100644
index 00000000000..4cd32c1edc8
--- /dev/null
+++ b/apps/code/editor_controller.h
@@ -0,0 +1,50 @@
+#ifndef CODE_EDITOR_CONTROLLER_H
+#define CODE_EDITOR_CONTROLLER_H
+
+#include
+#include "script.h"
+#include "editor_view.h"
+#include "variable_box_controller.h"
+#include "../shared/input_event_handler_delegate.h"
+
+namespace Code {
+
+class MenuController;
+class ScriptParameterController;
+class App;
+
+class EditorController : public ViewController, public TextAreaDelegate, public Shared::InputEventHandlerDelegate {
+public:
+ EditorController(MenuController * menuController, App * pythonDelegate);
+ void setScript(Script script, int scriptIndex);
+ int scriptIndex() const { return m_scriptIndex; }
+ void willExitApp();
+
+ /* ViewController */
+ View * view() override { return &m_editorView; }
+ bool handleEvent(Ion::Events::Event event) override;
+ void didBecomeFirstResponder() override;
+ void viewWillAppear() override;
+ void viewDidDisappear() override;
+ ViewController::DisplayParameter displayParameter() override { return ViewController::DisplayParameter::WantsMaximumSpace; }
+ TELEMETRY_ID("Editor");
+
+ /* TextAreaDelegate */
+ bool textAreaDidReceiveEvent(TextArea * textArea, Ion::Events::Event event) override;
+
+ /* InputEventHandlerDelegate */
+ VariableBoxController * variableBoxForInputEventHandler(InputEventHandler * textInput) override;
+
+private:
+ void cleanStorageEmptySpace();
+ StackViewController * stackController();
+ EditorView m_editorView;
+ Script m_script;
+ int m_scriptIndex;
+ MenuController * m_menuController;
+};
+
+}
+
+#endif
+
diff --git a/apps/code/editor_view.cpp b/apps/code/editor_view.cpp
new file mode 100644
index 00000000000..9ec48d7eb74
--- /dev/null
+++ b/apps/code/editor_view.cpp
@@ -0,0 +1,108 @@
+#include "editor_view.h"
+#include
+#include
+#include
+#include
+
+namespace Code {
+
+/* EditorView */
+
+EditorView::EditorView(Responder * parentResponder, App * pythonDelegate) :
+ Responder(parentResponder),
+ View(),
+ m_textArea(parentResponder, pythonDelegate, GlobalPreferences::sharedGlobalPreferences()->font()),
+ m_gutterView(GlobalPreferences::sharedGlobalPreferences()->font())
+{
+ m_textArea.setScrollViewDelegate(this);
+}
+
+bool EditorView::isAutocompleting() const {
+ return m_textArea.isAutocompleting();
+}
+
+void EditorView::resetSelection() {
+ m_textArea.resetSelection();
+}
+
+void EditorView::scrollViewDidChangeOffset(ScrollViewDataSource * scrollViewDataSource) {
+ m_gutterView.setOffset(scrollViewDataSource->offset().y());
+}
+
+View * EditorView::subviewAtIndex(int index) {
+ if (index == 0) {
+ return &m_textArea;
+ }
+ assert(index == 1);
+ return &m_gutterView;
+}
+
+void EditorView::didBecomeFirstResponder() {
+ Container::activeApp()->setFirstResponder(&m_textArea);
+}
+
+void EditorView::layoutSubviews(bool force) {
+ m_gutterView.setOffset(0);
+ KDCoordinate gutterWidth = m_gutterView.minimalSizeForOptimalDisplay().width();
+ m_gutterView.setFrame(KDRect(0, 0, gutterWidth, bounds().height()), force);
+
+ m_textArea.setFrame(KDRect(
+ gutterWidth,
+ 0,
+ bounds().width()-gutterWidth,
+ bounds().height()),
+ force);
+}
+
+/* EditorView::GutterView */
+
+void EditorView::GutterView::drawRect(KDContext * ctx, KDRect rect) const {
+ KDColor textColor = Palette::BlueishGray;
+ KDColor backgroundColor = KDColor::RGB24(0xE4E6E7);
+
+ ctx->fillRect(rect, backgroundColor);
+
+ KDSize glyphSize = m_font->glyphSize();
+
+ KDCoordinate firstLine = m_offset / glyphSize.height();
+ KDCoordinate firstLinePixelOffset = m_offset - firstLine * glyphSize.height();
+
+ char lineNumber[k_lineNumberCharLength];
+ int numberOfLines = bounds().height() / glyphSize.height() + 1;
+ for (int i=0; i= 10) {
+ line.serialize(lineNumber, k_lineNumberCharLength);
+ } else {
+ // Add a leading "0"
+ lineNumber[0] = '0';
+ line.serialize(lineNumber + 1, k_lineNumberCharLength - 1);
+ }
+ KDCoordinate leftPadding = (2 - strlen(lineNumber)) * glyphSize.width();
+ ctx->drawString(
+ lineNumber,
+ KDPoint(k_margin + leftPadding, i*glyphSize.height() - firstLinePixelOffset),
+ m_font,
+ textColor,
+ backgroundColor
+ );
+ }
+}
+
+void EditorView::GutterView::setOffset(KDCoordinate offset) {
+ if (m_offset == offset) {
+ return;
+ }
+ m_offset = offset;
+ markRectAsDirty(bounds());
+}
+
+
+KDSize EditorView::GutterView::minimalSizeForOptimalDisplay() const {
+ int numberOfChars = 2; // TODO: Could be computed
+ return KDSize(2 * k_margin + numberOfChars * m_font->glyphSize().width(), 0);
+}
+
+}
diff --git a/apps/code/editor_view.h b/apps/code/editor_view.h
new file mode 100644
index 00000000000..547f7340b53
--- /dev/null
+++ b/apps/code/editor_view.h
@@ -0,0 +1,56 @@
+#ifndef CODE_EDITOR_VIEW_H
+#define CODE_EDITOR_VIEW_H
+
+#include
+#include "python_text_area.h"
+
+namespace Code {
+
+class EditorView : public Responder, public View, public ScrollViewDelegate {
+public:
+ EditorView(Responder * parentResponder, App * pythonDelegate);
+ PythonTextArea::AutocompletionType autocompletionType(const char ** autocompletionBeginning, const char ** autocompletionEnd) const { return m_textArea.autocompletionType(nullptr, autocompletionBeginning, autocompletionEnd); }
+ bool isAutocompleting() const;
+ void resetSelection();
+ void setTextAreaDelegates(InputEventHandlerDelegate * inputEventHandlerDelegate, TextAreaDelegate * delegate) {
+ m_textArea.setDelegates(inputEventHandlerDelegate, delegate);
+ }
+ const char * text() const { return m_textArea.text(); }
+ void setText(char * textBuffer, size_t textBufferSize) {
+ m_textArea.setText(textBuffer, textBufferSize);
+ }
+ const char * cursorLocation() {
+ return m_textArea.cursorLocation();
+ }
+ bool setCursorLocation(const char * location) {
+ return m_textArea.setCursorLocation(location);
+ }
+ void loadSyntaxHighlighter() { m_textArea.loadSyntaxHighlighter(); };
+ void unloadSyntaxHighlighter() { m_textArea.unloadSyntaxHighlighter(); };
+ void scrollViewDidChangeOffset(ScrollViewDataSource * scrollViewDataSource) override;
+ void didBecomeFirstResponder() override;
+private:
+ int numberOfSubviews() const override { return 2; }
+ View * subviewAtIndex(int index) override;
+ void layoutSubviews(bool force = false) override;
+
+ class GutterView : public View {
+ public:
+ GutterView(const KDFont * font) : View(), m_font(font), m_offset(0) {}
+ void drawRect(KDContext * ctx, KDRect rect) const override;
+ void setOffset(KDCoordinate offset);
+ KDSize minimalSizeForOptimalDisplay() const override;
+ private:
+ static constexpr KDCoordinate k_margin = 2;
+ static constexpr int k_lineNumberCharLength = 3;
+ const KDFont * m_font;
+ KDCoordinate m_offset;
+ };
+
+ PythonTextArea m_textArea;
+ GutterView m_gutterView;
+};
+
+}
+
+#endif
diff --git a/apps/code/helpers.cpp b/apps/code/helpers.cpp
new file mode 100644
index 00000000000..18250e77f47
--- /dev/null
+++ b/apps/code/helpers.cpp
@@ -0,0 +1,20 @@
+#include "helpers.h"
+#include
+
+namespace Code {
+namespace Helpers {
+
+const char * PythonTextForEvent(Ion::Events::Event event) {
+ for (size_t i=0; i
+
+namespace Code {
+namespace Helpers {
+
+const char * PythonTextForEvent(Ion::Events::Event event);
+
+}
+}
+
+#endif
diff --git a/apps/code/menu_controller.cpp b/apps/code/menu_controller.cpp
new file mode 100644
index 00000000000..0857a0499b9
--- /dev/null
+++ b/apps/code/menu_controller.cpp
@@ -0,0 +1,419 @@
+#include "menu_controller.h"
+#include "app.h"
+#include
+#include "../apps_container.h"
+#include
+#include
+#include
+#include
+
+namespace Code {
+
+MenuController::MenuController(Responder * parentResponder, App * pythonDelegate, ScriptStore * scriptStore, ButtonRowController * footer) :
+ ViewController(parentResponder),
+ ButtonRowDelegate(nullptr, footer),
+ m_scriptStore(scriptStore),
+ m_addNewScriptCell(),
+ m_consoleButton(this, I18n::Message::Console, Invocation([](void * context, void * sender) {
+ MenuController * menu = (MenuController *)context;
+ menu->consoleController()->setAutoImport(true);
+ menu->stackViewController()->push(menu->consoleController());
+ return true;
+ }, this), KDFont::LargeFont),
+ m_selectableTableView(this, this, this, this),
+ m_scriptParameterController(nullptr, I18n::Message::ScriptOptions, this),
+ m_editorController(this, pythonDelegate),
+ m_reloadConsoleWhenBecomingFirstResponder(false),
+ m_shouldDisplayAddScriptRow(true)
+{
+ m_selectableTableView.setMargins(0);
+ m_selectableTableView.setDecoratorType(ScrollView::Decorator::Type::None);
+ m_addNewScriptCell.setMessage(I18n::Message::AddScript);
+ for (int i = 0; i < k_maxNumberOfDisplayableScriptCells; i++) {
+ m_scriptCells[i].setParentResponder(&m_selectableTableView);
+ m_scriptCells[i].textField()->setDelegates(nullptr, this);
+ }
+}
+
+ConsoleController * MenuController::consoleController() {
+ return App::app()->consoleController();
+}
+
+StackViewController * MenuController::stackViewController() {
+ return static_cast(parentResponder()->parentResponder());
+}
+
+void MenuController::willExitResponderChain(Responder * nextFirstResponder) {
+ int selectedRow = m_selectableTableView.selectedRow();
+ int selectedColumn = m_selectableTableView.selectedColumn();
+ if (selectedRow >= 0 && selectedRow < m_scriptStore->numberOfScripts() && selectedColumn == 0) {
+ TextField * tf = static_cast(m_selectableTableView.selectedCell())->textField();
+ if (tf->isEditing()) {
+ tf->setEditing(false);
+ privateTextFieldDidAbortEditing(tf, false);
+ }
+ }
+}
+
+void MenuController::didBecomeFirstResponder() {
+ if (m_reloadConsoleWhenBecomingFirstResponder) {
+ reloadConsole();
+ }
+ if (footer()->selectedButton() == 0) {
+ assert(m_selectableTableView.selectedRow() < 0);
+ Container::activeApp()->setFirstResponder(&m_consoleButton);
+ return;
+ }
+ if (m_selectableTableView.selectedRow() < 0) {
+ m_selectableTableView.selectCellAtLocation(0,0);
+ }
+ assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts() + 1);
+ Container::activeApp()->setFirstResponder(&m_selectableTableView);
+#if EPSILON_GETOPT
+ if (consoleController()->locked()) {
+ consoleController()->setAutoImport(true);
+ stackViewController()->push(consoleController());
+ return;
+ }
+#endif
+}
+
+void MenuController::viewWillAppear() {
+ ViewController::viewWillAppear();
+ updateAddScriptRowDisplay();
+}
+
+bool MenuController::handleEvent(Ion::Events::Event event) {
+ if (event == Ion::Events::Down) {
+ m_selectableTableView.deselectTable();
+ footer()->setSelectedButton(0);
+ return true;
+ }
+ if (event == Ion::Events::Up) {
+ if (footer()->selectedButton() == 0) {
+ footer()->setSelectedButton(-1);
+ m_selectableTableView.selectCellAtLocation(0, numberOfRows()-1);
+ Container::activeApp()->setFirstResponder(&m_selectableTableView);
+ return true;
+ }
+ }
+ if (event == Ion::Events::OK || event == Ion::Events::EXE) {
+ int selectedRow = m_selectableTableView.selectedRow();
+ int selectedColumn = m_selectableTableView.selectedColumn();
+ if (selectedRow >= 0 && selectedRow < m_scriptStore->numberOfScripts()) {
+ if (selectedColumn == 1) {
+ configureScript();
+ return true;
+ }
+ assert(selectedColumn == 0);
+ editScriptAtIndex(selectedRow);
+ return true;
+ } else if (m_shouldDisplayAddScriptRow
+ && selectedColumn == 0
+ && selectedRow == m_scriptStore->numberOfScripts())
+ {
+ addScript();
+ return true;
+ }
+ }
+ return false;
+}
+
+void MenuController::renameSelectedScript() {
+ assert(m_selectableTableView.selectedRow() >= 0);
+ assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts());
+ AppsContainer::sharedAppsContainer()->setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus::AlphaLock);
+ m_selectableTableView.selectCellAtLocation(0, (m_selectableTableView.selectedRow()));
+ ScriptNameCell * myCell = static_cast(m_selectableTableView.selectedCell());
+ Container::activeApp()->setFirstResponder(myCell);
+ myCell->setHighlighted(false);
+ TextField * tf = myCell->textField();
+ const char * previousText = tf->text();
+ tf->setEditing(true);
+ tf->setText(previousText);
+ tf->setCursorLocation(tf->text() + strlen(previousText));
+}
+
+void MenuController::deleteScript(Script script) {
+ assert(!script.isNull());
+ script.destroy();
+ updateAddScriptRowDisplay();
+}
+
+void MenuController::reloadConsole() {
+ consoleController()->unloadPythonEnvironment();
+ m_reloadConsoleWhenBecomingFirstResponder = false;
+}
+
+void MenuController::openConsoleWithScript(Script script) {
+ reloadConsole();
+ consoleController()->setAutoImport(false);
+ stackViewController()->push(consoleController());
+ consoleController()->autoImportScript(script, true);
+ m_reloadConsoleWhenBecomingFirstResponder = true;
+}
+
+void MenuController::scriptContentEditionDidFinish() {
+ reloadConsole();
+}
+
+void MenuController::willExitApp() {
+ m_editorController.willExitApp();
+}
+
+int MenuController::numberOfRows() const {
+ return m_scriptStore->numberOfScripts() + m_shouldDisplayAddScriptRow;
+}
+
+void MenuController::willDisplayCellAtLocation(HighlightCell * cell, int i, int j) {
+ if (i == 0 && j < m_scriptStore->numberOfScripts()) {
+ willDisplayScriptTitleCellForIndex(cell, j);
+ }
+ static_cast(cell)->setEven(j%2 == 0);
+ cell->setHighlighted(i == selectedColumn() && j == selectedRow());
+}
+
+KDCoordinate MenuController::columnWidth(int i) {
+ switch (i) {
+ case 0:
+ return m_selectableTableView.bounds().width()-k_parametersColumnWidth;
+ case 1:
+ return k_parametersColumnWidth;
+ default:
+ assert(false);
+ return 0;
+ }
+}
+
+KDCoordinate MenuController::cumulatedWidthFromIndex(int i) {
+ switch (i) {
+ case 0:
+ return 0;
+ case 1:
+ return m_selectableTableView.bounds().width()-k_parametersColumnWidth;
+ case 2:
+ return m_selectableTableView.bounds().width();
+ default:
+ assert(false);
+ return 0;
+ }
+}
+
+KDCoordinate MenuController::cumulatedHeightFromIndex(int j) {
+ return Metric::StoreRowHeight * j;
+}
+
+int MenuController::indexFromCumulatedWidth(KDCoordinate offsetX) {
+ if (offsetX <= m_selectableTableView.bounds().width()-k_parametersColumnWidth) {
+ return 0;
+ }
+ if (offsetX <= m_selectableTableView.bounds().width()) {
+ return 1;
+ }
+ else {
+ return 2;
+ }
+ assert(false);
+ return 0;
+}
+
+int MenuController::indexFromCumulatedHeight(KDCoordinate offsetY) {
+ if (Metric::StoreRowHeight == 0) {
+ return 0;
+ }
+ return (offsetY - 1) / Metric::StoreRowHeight;
+}
+
+HighlightCell * MenuController::reusableCell(int index, int type) {
+ assert(index >= 0);
+ if (type == ScriptCellType) {
+ assert(index >=0 && index < k_maxNumberOfDisplayableScriptCells);
+ return &m_scriptCells[index];
+ }
+ if (type == ScriptParameterCellType) {
+ assert(index >=0 && index < k_maxNumberOfDisplayableScriptCells);
+ return &m_scriptParameterCells[index];
+ }
+ if (type == AddScriptCellType) {
+ assert(index == 0);
+ return &m_addNewScriptCell;
+ }
+ if(type == EmptyCellType) {
+ return &m_emptyCell;
+ }
+ assert(false);
+ return nullptr;
+}
+
+int MenuController::reusableCellCount(int type) {
+ if (type == AddScriptCellType) {
+ return 1;
+ }
+ if (type == ScriptCellType || type == ScriptParameterCellType) {
+ return k_maxNumberOfDisplayableScriptCells;
+ }
+ if (type == EmptyCellType) {
+ return 1;
+ }
+ assert(false);
+ return 0;
+}
+
+int MenuController::typeAtLocation(int i, int j) {
+ assert(i >= 0 && i < numberOfColumns());
+ assert(j >= 0 && j < numberOfRows());
+ if (i == 0) {
+ if (j == numberOfRows()-1 && m_shouldDisplayAddScriptRow) {
+ return AddScriptCellType;
+ }
+ return ScriptCellType;
+ }
+ assert(i == 1);
+ if (j == numberOfRows()-1 && m_shouldDisplayAddScriptRow) {
+ return EmptyCellType;
+ }
+ return ScriptParameterCellType;
+}
+
+void MenuController::willDisplayScriptTitleCellForIndex(HighlightCell * cell, int index) {
+ assert(index >= 0 && index < m_scriptStore->numberOfScripts());
+ (static_cast(cell))->textField()->setText(m_scriptStore->scriptAtIndex(index).fullName());
+}
+
+void MenuController::tableViewDidChangeSelection(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY, bool withinTemporarySelection) {
+ if (selectedRow() == numberOfRows() - 1 && selectedColumn() == 1 && m_shouldDisplayAddScriptRow) {
+ t->selectCellAtLocation(0, numberOfRows()-1);
+ }
+}
+
+bool MenuController::textFieldShouldFinishEditing(TextField * textField, Ion::Events::Event event) {
+ return event == Ion::Events::OK || event == Ion::Events::EXE
+ || event == Ion::Events::Down || event == Ion::Events::Up;
+}
+
+bool MenuController::textFieldDidFinishEditing(TextField * textField, const char * text, Ion::Events::Event event) {
+ const char * newName;
+ static constexpr int bufferSize = Script::k_defaultScriptNameMaxSize + 1 + ScriptStore::k_scriptExtensionLength; //"script99" + "." + "py"
+ char numberedDefaultName[bufferSize];
+
+ if (strlen(text) > 1 + strlen(ScriptStore::k_scriptExtension)) {
+ newName = text;
+ } else {
+ // The user entered an empty name. Use a numbered default script name.
+ bool foundDefaultName = Script::DefaultName(numberedDefaultName, Script::k_defaultScriptNameMaxSize);
+ int defaultNameLength = strlen(numberedDefaultName);
+ assert(UTF8Decoder::CharSizeOfCodePoint('.') == 1);
+ numberedDefaultName[defaultNameLength++] = '.';
+ assert(defaultNameLength < bufferSize);
+ strlcpy(numberedDefaultName + defaultNameLength, ScriptStore::k_scriptExtension, bufferSize - defaultNameLength);
+ /* If there are already scripts named script1.py, script2.py,... until
+ * Script::k_maxNumberOfDefaultScriptNames, we want to write the last tried
+ * default name and let the user modify it. */
+ if (!foundDefaultName) {
+ textField->setText(numberedDefaultName);
+ textField->setCursorLocation(textField->draftTextBuffer() + defaultNameLength);
+ }
+ newName = const_cast(numberedDefaultName);
+ }
+ Script::ErrorStatus error = Script::nameCompliant(newName) ? m_scriptStore->scriptAtIndex(m_selectableTableView.selectedRow()).setName(newName) : Script::ErrorStatus::NonCompliantName;
+ if (error == Script::ErrorStatus::None) {
+ updateAddScriptRowDisplay();
+ textField->setText(newName);
+ int currentRow = m_selectableTableView.selectedRow();
+ if (event == Ion::Events::Down && currentRow < numberOfRows() - 1) {
+ m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), currentRow + 1);
+ } else if (event == Ion::Events::Up && currentRow > 0) {
+ m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), currentRow - 1);
+ }
+ m_selectableTableView.selectedCell()->setHighlighted(true);
+ reloadConsole();
+ Container::activeApp()->setFirstResponder(&m_selectableTableView);
+ AppsContainer::sharedAppsContainer()->setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus::Default);
+ return true;
+ } else if (error == Script::ErrorStatus::NameTaken) {
+ Container::activeApp()->displayWarning(I18n::Message::NameTaken);
+ } else if (error == Script::ErrorStatus::NonCompliantName) {
+ Container::activeApp()->displayWarning(I18n::Message::AllowedCharactersaz09, I18n::Message::NameCannotStartWithNumber);
+ } else {
+ assert(error == Script::ErrorStatus::NotEnoughSpaceAvailable);
+ Container::activeApp()->displayWarning(I18n::Message::NameTooLong);
+ }
+ return false;
+}
+
+bool MenuController::textFieldDidHandleEvent(TextField * textField, bool returnValue, bool textSizeDidChange) {
+ int scriptExtensionLength = 1 + strlen(ScriptStore::k_scriptExtension);
+ if (textField->isEditing()) {
+ const char * maxPointerLocation = textField->text() + textField->draftTextLength() - scriptExtensionLength;
+ if (textField->cursorLocation() > maxPointerLocation) {
+ textField->setCursorLocation(maxPointerLocation);
+ }
+ }
+ return returnValue;
+}
+
+void MenuController::addScript() {
+ Script::ErrorStatus error = m_scriptStore->addNewScript();
+ if (error == Script::ErrorStatus::None) {
+ updateAddScriptRowDisplay();
+ renameSelectedScript();
+ return;
+ }
+ assert(false); // Adding a new script is called when !m_scriptStore.isFull() which guarantees that the available space in the storage is big enough
+}
+
+void MenuController::configureScript() {
+ assert(m_selectableTableView.selectedRow() >= 0);
+ assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts());
+ m_scriptParameterController.setScript(m_scriptStore->scriptAtIndex(m_selectableTableView.selectedRow()));
+ stackViewController()->push(&m_scriptParameterController);
+}
+
+void MenuController::editScriptAtIndex(int scriptIndex) {
+ assert(scriptIndex >=0 && scriptIndex < m_scriptStore->numberOfScripts());
+ Script script = m_scriptStore->scriptAtIndex(scriptIndex);
+ m_editorController.setScript(script, scriptIndex);
+ stackViewController()->push(&m_editorController);
+}
+
+void MenuController::updateAddScriptRowDisplay() {
+ m_shouldDisplayAddScriptRow = !m_scriptStore->isFull();
+ m_selectableTableView.reloadData();
+}
+
+bool MenuController::privateTextFieldDidAbortEditing(TextField * textField, bool menuControllerStaysInResponderChain) {
+ /* If menuControllerStaysInResponderChain is false, we do not want to use
+ * methods that might call setFirstResponder, because we might be in the
+ * middle of another setFirstResponder call. */
+ Script script = m_scriptStore->scriptAtIndex(m_selectableTableView.selectedRow());
+ const char * scriptName = script.fullName();
+ if (strlen(scriptName) <= 1 + strlen(ScriptStore::k_scriptExtension)) {
+ // The previous text was an empty name. Use a numbered default script name.
+ char numberedDefaultName[Script::k_defaultScriptNameMaxSize];
+ bool foundDefaultName = Script::DefaultName(numberedDefaultName, Script::k_defaultScriptNameMaxSize);
+ if (!foundDefaultName) {
+ // If we did not find a default name, delete the script
+ deleteScript(script);
+ return true;
+ }
+ Script::ErrorStatus error = script.setBaseNameWithExtension(numberedDefaultName, ScriptStore::k_scriptExtension);
+ scriptName = m_scriptStore->scriptAtIndex(m_selectableTableView.selectedRow()).fullName();
+ /* Because we use the numbered default name, the name should not be
+ * already taken. Plus, the script could be added only if the storage has
+ * enough available space to add a script named 'script99.py' */
+ (void) error; // Silence the "variable unused" warning if assertions are not enabled
+ assert(error == Script::ErrorStatus::None);
+ if (menuControllerStaysInResponderChain) {
+ updateAddScriptRowDisplay();
+ }
+ }
+ textField->setText(scriptName);
+ if (menuControllerStaysInResponderChain) {
+ m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), m_selectableTableView.selectedRow());
+ Container::activeApp()->setFirstResponder(&m_selectableTableView);
+ }
+ AppsContainer::sharedAppsContainer()->setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus::Default);
+ return true;
+}
+
+}
diff --git a/apps/code/menu_controller.h b/apps/code/menu_controller.h
new file mode 100644
index 00000000000..6d87672adca
--- /dev/null
+++ b/apps/code/menu_controller.h
@@ -0,0 +1,98 @@
+#ifndef CODE_MENU_CONTROLLER_H
+#define CODE_MENU_CONTROLLER_H
+
+#include
+#include "console_controller.h"
+#include "editor_controller.h"
+#include "script_name_cell.h"
+#include "script_parameter_controller.h"
+#include "script_store.h"
+
+namespace Code {
+
+class ScriptParameterController;
+
+class MenuController : public ViewController, public TableViewDataSource, public SelectableTableViewDataSource, public SelectableTableViewDelegate, public TextFieldDelegate, public ButtonRowDelegate {
+public:
+ MenuController(Responder * parentResponder, App * pythonDelegate, ScriptStore * scriptStore, ButtonRowController * footer);
+ ConsoleController * consoleController();
+ StackViewController * stackViewController();
+ void willExitResponderChain(Responder * nextFirstResponder) override;
+ void renameSelectedScript();
+ void deleteScript(Script script);
+ void reloadConsole();
+ void openConsoleWithScript(Script script);
+ void scriptContentEditionDidFinish();
+ void willExitApp();
+ int editedScriptIndex() const { return m_editorController.scriptIndex(); }
+
+ /* ViewController */
+ View * view() override { return &m_selectableTableView; }
+ bool handleEvent(Ion::Events::Event event) override;
+ void didBecomeFirstResponder() override;
+ void viewWillAppear() override;
+ TELEMETRY_ID("Menu");
+
+ /* TableViewDataSource */
+ int numberOfRows() const override;
+ int numberOfColumns() const override { return 2; }
+ void willDisplayCellAtLocation(HighlightCell * cell, int i, int j) override;
+ KDCoordinate columnWidth(int i) override;
+ KDCoordinate rowHeight(int j) override { return Metric::StoreRowHeight; }
+ KDCoordinate cumulatedWidthFromIndex(int i) override;
+ KDCoordinate cumulatedHeightFromIndex(int j) override;
+ int indexFromCumulatedWidth(KDCoordinate offsetX) override;
+ int indexFromCumulatedHeight(KDCoordinate offsetY) override;
+ HighlightCell * reusableCell(int index, int type) override;
+ int reusableCellCount(int type) override;
+ int typeAtLocation(int i, int j) override;
+ void willDisplayScriptTitleCellForIndex(HighlightCell * cell, int index);
+
+ /* SelectableTableViewDelegate */
+ void tableViewDidChangeSelection(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY, bool withinTemporarySelection) override;
+
+ /* TextFieldDelegate */
+ bool textFieldShouldFinishEditing(TextField * textField, Ion::Events::Event event) override;
+ bool textFieldDidReceiveEvent(TextField * textField, Ion::Events::Event event) override { return false; }
+ bool textFieldDidFinishEditing(TextField * textField, const char * text, Ion::Events::Event event) override;
+ bool textFieldDidAbortEditing(TextField * textField) override {
+ return privateTextFieldDidAbortEditing(textField, true);
+ }
+ bool textFieldDidHandleEvent(TextField * textField, bool returnValue, bool textSizeDidChange) override;
+
+ /* ButtonRowDelegate */
+ int numberOfButtons(ButtonRowController::Position position) const override { return 1; }
+ Button * buttonAtIndex(int index, ButtonRowController::Position position) const override {
+ assert(index == 0);
+ return const_cast