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 @@ +NumWorks Epsilon logo + +[![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