diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..30b227d0415 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,14 @@ +--- +name: Bug report +about: Omega is not working like it should? Let us know! +title: '' +labels: Bug, Triage +assignees: '' + +--- + +#### Describe the bug + + +#### Environment + - Omega Version: {go to settings > about > Omega Version and type the version here} diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..3ba13e0cec6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000000..eee03aa6ac5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,10 @@ +--- +name: Feature request +about: Suggest an idea for an improvement of Omega +title: '' +labels: Feature, Triage +assignees: '' + +--- + +#### What I want to see in the next version of Omega diff --git a/.github/ISSUE_TEMPLATE/other.md b/.github/ISSUE_TEMPLATE/other.md new file mode 100644 index 00000000000..38b47c71fbd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other.md @@ -0,0 +1,10 @@ +--- +name: Other +about: A question? A problem? ... +title: '' +labels: Triage +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/problems-during-installation.md b/.github/ISSUE_TEMPLATE/problems-during-installation.md new file mode 100644 index 00000000000..347b6d9574a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/problems-during-installation.md @@ -0,0 +1,19 @@ +--- +name: Problems during installation +about: Need help to install Omega? +title: '' +labels: Installation issue, Triage +assignees: '' + +--- + +#### Describe the problem + + +#### Logs +``` +Copy/paste the logs here (If you have some) +``` + +#### Environment + - Omega Version: {go to settings > about > Omega Version and type the version here} diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml new file mode 100644 index 00000000000..ef955c806c5 --- /dev/null +++ b/.github/workflows/ci-docker.yml @@ -0,0 +1,12 @@ +name: Docker Image CI +on: [pull_request, push] + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Build the Docker image + run: docker build . --file docker/Dockerfile --tag omega:$(date +%s) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml new file mode 100644 index 00000000000..c60b1180786 --- /dev/null +++ b/.github/workflows/ci-workflow.yml @@ -0,0 +1,191 @@ +name: Continuous integration +#on: [pull_request, push] +on: + push: + pull_request: + workflow_dispatch: + inputs: + triggerIos: + description: 'Run iOS tests' + required: true + default: 'no' + triggerMacos: + description: 'Run macOS tests' + required: true + default: 'no' + trigger3DS: + description: 'Run 3DS tests' + required: true + default: 'no' + +jobs: + nintendo_3ds: + if: github.event.inputs.trigger3DS == 'yes' + runs-on: ubuntu-latest + steps: + - run: wget https://github.com/devkitPro/pacman/releases/download/v1.0.2/devkitpro-pacman.amd64.deb -O /tmp/devkitpro-pacman.deb + - run: yes | sudo dpkg -i /tmp/devkitpro-pacman.deb + - run: yes | sudo dkp-pacman -Syu --needed devkitARM 3dstools libctru + - run: echo ::set-env name=DEVKITPRO::/opt/devkitpro + - run: echo ::set-env name=DEVKITARM::/opt/devkitpro/devkitARM + - run: echo ::set-env name=PATH::$DEVKITPRO/tools/bin:$DEVKITARM/bin:$PATH + + - uses: actions/checkout@v1 + with: + submodules: true + - run: make -j2 PLATFORM=simulator TARGET=3ds + - run: make -j2 PLATFORM=simulator TARGET=3ds epsilon.cia + - uses: actions/upload-artifact@master + with: + name: epsilon-3ds.3dsx + path: output/release/simulator/3ds/epsilon.3dsx + - uses: actions/upload-artifact@master + with: + name: epsilon-3ds.cia + path: output/release/simulator/3ds/epsilon.cia + android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: 'recursive' + - run: make -j2 PLATFORM=simulator TARGET=android + - 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-q2 + - uses: actions/checkout@v2 + with: + submodules: 'recursive' + - run: mkdir final-output + - run: make -j2 MODEL=n0100 EPSILON_I18N=en output/release/device/n0100/epsilon.onboarding.two_binaries + - run: mv output/release/device/n0100/epsilon.onboarding.internal.bin final-output/epsilon.onboarding.internal.en.bin + - run: rm output/release/device/n0100/apps/i18n.o output/release/device/n0100/apps/i18n.cpp + - run: make -j2 MODEL=n0100 EPSILON_I18N=fr output/release/device/n0100/epsilon.onboarding.two_binaries + - run: mv output/release/device/n0100/epsilon.onboarding.internal.bin final-output/epsilon.onboarding.internal.fr.bin + - run: rm output/release/device/n0100/apps/i18n.o output/release/device/n0100/apps/i18n.cpp + - run: make -j2 MODEL=n0100 EPSILON_I18N=nl output/release/device/n0100/epsilon.onboarding.two_binaries + - run: mv output/release/device/n0100/epsilon.onboarding.internal.bin final-output/epsilon.onboarding.internal.nl.bin + - run: rm output/release/device/n0100/apps/i18n.o output/release/device/n0100/apps/i18n.cpp + - run: make -j2 MODEL=n0100 EPSILON_I18N=pt output/release/device/n0100/epsilon.onboarding.two_binaries + - run: mv output/release/device/n0100/epsilon.onboarding.internal.bin final-output/epsilon.onboarding.internal.pt.bin + - run: rm output/release/device/n0100/apps/i18n.o output/release/device/n0100/apps/i18n.cpp + - run: make -j2 MODEL=n0100 EPSILON_I18N=it output/release/device/n0100/epsilon.onboarding.two_binaries + - run: mv output/release/device/n0100/epsilon.onboarding.internal.bin final-output/epsilon.onboarding.internal.it.bin + - run: rm output/release/device/n0100/apps/i18n.o output/release/device/n0100/apps/i18n.cpp + - run: make -j2 MODEL=n0100 EPSILON_I18N=de output/release/device/n0100/epsilon.onboarding.two_binaries + - run: mv output/release/device/n0100/epsilon.onboarding.internal.bin final-output/epsilon.onboarding.internal.de.bin + - run: rm output/release/device/n0100/apps/i18n.o output/release/device/n0100/apps/i18n.cpp + - run: make -j2 MODEL=n0100 EPSILON_I18N=es output/release/device/n0100/epsilon.onboarding.two_binaries + - run: mv output/release/device/n0100/epsilon.onboarding.internal.bin final-output/epsilon.onboarding.internal.es.bin + - run: rm output/release/device/n0100/apps/i18n.o output/release/device/n0100/apps/i18n.cpp + - run: make -j2 MODEL=n0100 EPSILON_I18N=hu output/release/device/n0100/epsilon.onboarding.two_binaries + - run: mv output/release/device/n0100/epsilon.onboarding.internal.bin final-output/epsilon.onboarding.internal.hu.bin + - run: rm output/release/device/n0100/apps/i18n.o output/release/device/n0100/apps/i18n.cpp + - run: make -j2 MODEL=n0100 output/release/device/n0100/flasher.light.bin + - run: mv output/release/device/n0100/flasher.light.bin final-output/flasher.light.bin + - run: find final-output/ -type f -exec bash -c "shasum -a 256 -b {} > {}.sha256" \; + - run: tar cvfz binpack-n0100.tgz final-output/* + - uses: actions/upload-artifact@master + with: + name: epsilon-binpack-n0100.tgz + path: binpack-n0100.tgz + 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-q2 + - uses: actions/checkout@v2 + with: + submodules: 'recursive' + - run: make -j2 epsilon.dfu + - run: make -j2 epsilon.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 binpack + - run: cp output/release/device/n0110/binpack-n0110-`git rev-parse HEAD | head -c 7`.tgz output/release/device/n0110/binpack-n0110.tgz + - uses: actions/upload-artifact@master + with: + name: epsilon-binpack-n0110.tgz + path: output/release/device/n0110/binpack-n0110.tgz + windows: + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + steps: + - uses: msys2/setup-msys2@v2 + - uses: actions/checkout@v2 + with: + submodules: 'recursive' + - 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 + - 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: latest-upstream + - uses: actions/checkout@v2 + with: + submodules: 'recursive' + - run: make -j2 PLATFORM=simulator TARGET=web + - 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 + with: + submodules: 'recursive' + - run: make -j2 PLATFORM=simulator + - 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 + with: + submodules: 'recursive' + - run: make -j2 PLATFORM=simulator + - 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 + with: + submodules: 'recursive' + - run: make -j2 PLATFORM=simulator TARGET=ios EPSILON_TELEMETRY=0 + - 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/metric-workflow.yml b/.github/workflows/metric-workflow.yml new file mode 100644 index 00000000000..0cca6ba5360 --- /dev/null +++ b/.github/workflows/metric-workflow.yml @@ -0,0 +1,41 @@ +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-q2 + - name: Checkout PR base + uses: actions/checkout@v2 + with: + submodules: recursive + 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: + submodules: recursive + 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 --custom 'Total (RAM)' .data .bss --custom 'Total (ROM)' .text .rodata .data --escape)" + - name: Add comment + uses: actions/github-script@v3.0.0 + with: + github-token: ${{ secrets.OMEGA_ROBOT_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/.github/workflows/unit-workflow.yml b/.github/workflows/unit-workflow.yml new file mode 100644 index 00000000000..e4076aa63cc --- /dev/null +++ b/.github/workflows/unit-workflow.yml @@ -0,0 +1,14 @@ +name: Unit tests +on: [pull_request_target] + +jobs: + units: + 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 + with: + submodules: 'recursive' + - run: make -j2 PLATFORM=simulator test.headless.bin + - run: output/release/simulator/linux/test.headless.bin + diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..8645c08d24b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/output/ +/build/artifacts/ +build/device/**/*.pyc +epsilon.elf +epsilon.map +.vscode +.DS_Store +.gradle diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000000..d80d898590a --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,26 @@ +image: gcc + +stages: + - build + +job:build: + stage: build + before_script: + - "echo 'deb http://httpredir.debian.org/debian jessie main contrib \n deb-src http://httpredir.debian.org/debian jessie main contrib \n deb http://httpredir.debian.org/debian jessie-updates main contrib \n deb-src http://httpredir.debian.org/debian jessie-updates main contrib \n deb http://security.debian.org/ jessie/updates main contrib \n deb-src http://security.debian.org/ jessie/updates main contrib ' > /etc/apt/source.list" + - "apt-get update" + - "apt -y install build-essential git imagemagick libx11-dev libxext-dev libfreetype6-dev libpng-dev libjpeg-dev pkg-config fltk1.3-dev gcc-arm-none-eabi nodejs npm" + - "git submodule update --init --recursive" + - "git clone https://github.com/RedGl0w/omega-auto-increment" + - "cd omega-auto-increment" + - "npm i request exeq" + - "PrivateToken=$PrivateToken node index.js" + - "cd .." + script: + - make clean + - make MODEL=n0100 epsilon.bin + artifacts: + paths: + - output/release/device/n0100/epsilon.bin + - omega-auto-increment/version.txt + name: artifact:build:simulator + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000000..02641c8d556 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "apps/rpn"] + path = apps/rpn + url = https://github.com/Omega-Numworks/Omega-RPN.git +[submodule "apps/atomic"] + path = apps/atomic + url = https://github.com/Omega-Numworks/Omega-Atomic.git diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..7540a495a46 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at getomega.pro@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000000..1852d7fef18 --- /dev/null +++ b/LICENSE.md @@ -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..f83a5922f12 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,188 @@ -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/pimp.mak +include build/defaults.mak +include build/platform.$(PLATFORM).mak +include build/toolchain.$(TOOLCHAIN).mak +include build/variants.mak +include build/helpers.mk + +ifeq (${MODEL}, n0110) + apps_list = ${EPSILON_APPS} +else + apps_list = $(foreach i, ${EPSILON_APPS}, $(if $(filter external, $(i)),,$(i))) +endif + +ifdef FORCE_EXTERNAL + apps_list = ${EPSILON_APPS} +endif + +ifdef HOME_DISPLAY_EXTERNALS + ifneq ($(filter external,$(apps_list)),) + SFLAGS += -DHOME_DISPLAY_EXTERNALS + else + $(warning HOME_DISPLAY_EXTERNALS is set but external isn't included, ignoring flag.) + endif +endif + +.PHONY: info +info: + @echo "EPSILON_VERSION = $(EPSILON_VERSION)" + @echo "EPSILON_APPS = $(EPSILON_APPS)" + @echo "EPSILON_I18N = $(EPSILON_I18N)" + @echo "THEME_NAME = $(THEME_NAME)" + @echo "THEME_REPO = $(THEME_REPO)" + @echo "BUILD_DIR = $(BUILD_DIR)" + @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" + @echo " make PLATFORM=simulator TARGET=3ds" + +.PHONY: doc +doc: + @echo "DOXYGEN" + @mkdir -p output/doc/ + $(Q) doxygen build/doc/Doxyfile + +.PHONY: print-% +print-%: + @echo $* = $($*) + @echo $*\'s origin is $(origin $*) + +# 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) +# Make palette.h a dep for every source-file. +# This ensures that the theming engine works correctly. +$(call object_for,$(all_src)): $(BUILD_DIR)/escher/palette.h $(BUILD_DIR)/apps/i18n.h + +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: cleanall +cleanall: + @echo "CLEANALL" + $(Q) rm -rf output + +.PHONY: cowsay_% +cowsay_%: + @echo " -------" + @echo "| $(*F) |" + @echo " -------" + @echo " \\ ^__^" + @echo " \\ (oo)\\_______" + @echo " (__)\\ )\\/\\" + @echo " ||----w |" + @echo " || ||" + +.PHONY: clena +clena: cowsay_CLENA clean + +.PHONY: compile +compile: output/$(BUILD_TYPE)/simulator/$(HOST)/epsilon.$(EXE) + +.PHONY: cleanandcompile +cleanandcompile: + ${MAKE} cleanall + ${MAKE} compile + +.PHONY: start +start: + @echo "INFO Starting output/$(BUILD_TYPE)/simulator/$(HOST)/epsilon.$(EXE)" + @$(Q) output/$(BUILD_TYPE)/simulator/$(HOST)/epsilon.$(EXE) -v -%.o: %.cpp - clang++ $(CXXFLAGS) -c $< -o $@ +.PHONY: clean_run +clean_run: cleanandcompile + ${MAKE} start +.PHONY: run +run: compile + ${MAKE} start diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 00000000000..d7b1093d4b7 --- /dev/null +++ b/README.fr.md @@ -0,0 +1,177 @@ +

+ +

+ cc by-nc-sa 4.0 + Issues +
+ Discord +

+ +> Don't understand french ? speak english ? here's the [english README](./README.md) ! + +## À propos + +Omega est un fork d'Epsilon, l'OS de Numworks tournant sur les calculatrices du même nom, qui apporte beaucoup de fonctionnalités en plus. Omega est fait pour ceux qui aimeraient ajouter certaines fonctionnalités ayant été rejetées par Numworks à leurs calculatrices (pour des raisons 100% compréhensibles !). [Essayez en ligne](https://getomega.web.app/simulator). + +### Quelques fonctionnalités supplémentaires +- Retour du calcul littéral +- Une application RPN +- Application Externes +- Des thèmes +- Python amélioré (module os, méthode open...) +- Un tableau périodique et toutes les masses molaires des éléments dans la toolbox +- *Ainsi que d'autres à découvrir...* [Changelogs complets](https://github.com/Omega-Numworks/Omega/wiki/Changelog) | [Fonctionnalités princpales & captures d'écran](https://github.com/Omega-Numworks/Omega/wiki/Main-features). + +## Installation + +### Automatique + +Vous pouvez installer Omega automatiquement depuis [notre site](https://getomega.web.app/) sur la page "installer". + +

Omega Banner Discord

+ +### Manuelle + +Tout d'abord, suivez **la première étape** [ici](https://www.numworks.com/resources/engineering/software/build/), puis : + +
+ Modèle n0100 + +(note : vous pouvez changer `EPSILON_I18N=fr` en `en`, `nl`, `pt`, `it`, `de`, `es` ou `hu`). + +```bash +git clone --recursive https://github.com/Omega-Numworks/Omega.git +cd Omega +git checkout omega-master +make MODEL=n0100 clean +make MODEL=n0100 EPSILON_I18N=fr OMEGA_USERNAME="{Votre nom ici, 15 caractères max}" -j4 +make MODEL=n0100 epsilon_flash +``` + +Important : N'oubliez pas l'argument `--recursive`, Omega a besoin de sous-modules. +Vous pouvez aussi changer le nombre de processus parallèles pendant la compilation en changeant la valeur suivant `-j`. + +
+ +
+ Modèle n0110 + +```bash +git clone --recursive https://github.com/Omega-Numworks/Omega.git +cd Omega +git checkout omega-master +make clean +make OMEGA_USERNAME="{Votre nom ici, 15 caractères max}" -j4 +make epsilon_flash +``` + +Important : N'oubliez pas l'argument `--recursive`, Omega a besoin de sous-modules. +Vous pouvez aussi changer le nombre de processus parallèles pendant la compilation en changeant la valeur suivant `-j`. + +
+ +
+ Fichiers binaires + +Ces fichiers peuvent être utilisés pour distribuer Omega (pour que tout le monde puisse le flasher via [Webdfu_Numworks](https://ti-planet.github.io/webdfu_numworks/)). + +```bash +git clone --recursive https://github.com/Omega-Numworks/Omega.git +cd Omega +git checkout omega-master +make clean +make MODEL=n0100 OMEGA_USERNAME="" -j8 +make MODEL=n0100 OMEGA_USERNAME="" binpack -j8 +make OMEGA_USERNAME="" -j8 +make OMEGA_USERNAME="" binpack -j8 +``` + +Important : N'oubliez pas l'argument `--recursive`, Omega a besoin de sous-modules. +Vous pouvez aussi changer le nombre de processus parallèles pendant la compilation en changeant la valeur suivant `-j`. + +
+ +
+ Simulateur web + +D'abord, installez emsdk : + +```bash +git clone https://github.com/emscripten-core/emsdk.git +cd emsdk +./emsdk install latest-fastcomp +./emsdk activate latest-fastcomp +source emsdk_env.sh +``` + +Puis, compilez Omega : + +```bash +git clone --recursive https://github.com/Omega-Numworks/Omega.git +cd Omega +git checkout omega-master +make clean +make PLATFORM=simulator TARGET=web OMEGA_USERNAME="{Votre nom ici, 15 caractères max}" -j4 +``` + +Le simulateur se trouve dans `output/release/simulator/web/simulator.zip` + +Important : N'oubliez pas l'argument `--recursive`, Omega a besoin de sous-modules. +Vous pouvez aussi changer le nombre de processus parallèles pendant la compilation en changeant la valeur suivant `-j`. + +
+ +
+ Simulateur 3DS + +Vous aurez besoin de devkitPro et de devkitARM disponible dans votre `$PATH` (instructions [ici](https://devkitpro.org/wiki/Getting_Started) (en anglais)) + +```bash +git clone --recursive https://github.com/Omega-Numworks/Omega.git +cd Omega +git checkout --recursive omega-dev +make PLATFORM=simulator TARGET=3ds -j +``` + +Vous pouvez ensuite copier epsilon.3dsx sur une carte SD pour l'exécuter depuis le HBC ou utiliser 3dslink pour le lancer via le réseau : + +```bash +3dslink output/release/simulator/3ds/epsilon.3dsx -a +``` + +
+ +Si vous avez besoin d'aide, n'hésitez pas à rejoindre notre serveur discord : https://discord.gg/X2TWhh9 + +

Omega Banner Discord

+--- + +## Contribution + +Pour contribuer, merci de lire le [Wiki](https://github.com/Omega-Numworks/Omega/wiki/Contributing) + +## Nos autres projets + +* [Omega Themes](https://github.com/Omega-Numworks/Omega-Themes) +* [Omega Website](https://github.com/Omega-Numworks/Omega-Website) +* [Omega RPN `APP`](https://github.com/Omega-Numworks/Omega-RPN) +* [Omega Atomic `APP`](https://github.com/Omega-Numworks/Omega-Atomic) +* [Omega Design](https://github.com/Omega-Numworks/Omega-Design) +* [Omega Discord Bot](https://github.com/Omega-Numworks/Omega-Discord-Bot) +* [Omega App Template `BETA`](https://github.com/Omega-Numworks/Omega-App-Template) +* [External Apps](https://github.com/Omega-Numworks/External-Apps) + +## À propos d'Epsilon + +Omega est un fork d'Epsilon, un système d'exploitation performant pour calculatrices graphiques. Il inclut huit applications pour les mathématiques de lycée et d'études supérieurs + +Vous pouvez essayer Epsilon depuis votre navigateur sur le [simulateur en ligne](https://www.numworks.com/simulator/). + +## Licence + +NumWorks est une marque déposée de NumWorks SAS, 24 Rue Godot de Mauroy, 75009 Paris, France. +Nintendo est Nintendo 3DS sont des marques déposées de Nintendo of America Inc, 4600 150th Ave NE, Redmond, WA 98052, Etats-Unis. +NumWorks SAS et Nintendo of America Inc ne sont en aucun cas associés avec ce projet. + +* NumWorks Epsilon est disponible sous [Lisense CC BY-NC-SA](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode). +* Omega est disponible sous [Lisense CC BY-NC-SA](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode). diff --git a/README.md b/README.md new file mode 100644 index 00000000000..c9b6597d405 --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +

+ +

+ cc by-nc-sa 4.0 + Issues +
+ Discord +

+ +> Vous ne comprenez pas l'anglais ? vous êtes francophone ? Regardez le [*LISEZ-MOI* français](./README.fr.md) ! + +## About + +Omega is a fork of Numworks' Epsilon, the OS that runs on their calculator, which brings many features to it. Omega is for the people who want to add features to the calculator, but cannot because they have been rejected by Numworks (for reasons that are 100% understandable!). [Try it online](https://getomega.web.app/simulator). + +### Some new features +- Adding symbolic calculation back into the calculator +- An app for RPN +- Exernal apps +- A theme engine +- New python features (os module, open method...) +- A periodic table app + all of the molar masses for the elements in the toolbox +- *And much more to discover...* [Complete changelog](https://github.com/Omega-Numworks/Omega/wiki/Changelog) | [Main new features + screenshots](https://github.com/Omega-Numworks/Omega/wiki/Main-features). + +## Installation + +### Automatic + +You can install Omega automatically on our website [here](https://getomega.web.app/) in the "install" page. + +

Omega Banner Discord

+ +### Manual + +First of all, follow **step 1** [here](https://www.numworks.com/resources/engineering/software/build/). Then: + +
+ Model n0100 + +(note: you can change the `EPSILON_I18N=en` flag to `fr`, `nl`, `pt`, `it`, `de`, `es` or `hu`). + +```bash +git clone --recursive https://github.com/Omega-Numworks/Omega.git +cd Omega +git checkout omega-master +make MODEL=n0100 clean +make MODEL=n0100 EPSILON_I18N=en OMEGA_USERNAME="{Your name, max 15 characters}" -j4 +make MODEL=n0100 epsilon_flash +``` + +Important: Don't forget the `--recursive` tag, because Omega relies on submodules. +Also, you can change the number of processes that run in parallel during the build by changing the value of the `-j` flag. + +
+ +
+ Model n0110 + +```bash +git clone --recursive https://github.com/Omega-Numworks/Omega.git +cd Omega +git checkout omega-master +make clean +make OMEGA_USERNAME="{Your name, max 15 characters}" -j4 +make epsilon_flash +``` + +Important: Don't forget the `--recursive` tag, because Omega relies on submodules. +Also, you can change the number of processes that run in parallel during the build by changing the value of the `-j` flag. + +
+ +
+ Bin files + +These can be used to distribute Omega (so that it can be flashed by anyone with [Webdfu_Numworks](https://ti-planet.github.io/webdfu_numworks/)). + +```bash +git clone --recursive https://github.com/Omega-Numworks/Omega.git +cd Omega +git checkout omega-master +make clean +make MODEL=n0100 OMEGA_USERNAME="" -j8 +make MODEL=n0100 OMEGA_USERNAME="" binpack -j8 +make OMEGA_USERNAME="" -j8 +make OMEGA_USERNAME="" binpack -j8 +``` + +Important: Don't forget the `--recursive` tag, because Omega relies on submodules. +Also, you can change the number of processes that run in parallel during the build by changing the value of the `-j` flag. + +
+ +
+ Web simulator + +First, install emsdk : + +```bash +git clone https://github.com/emscripten-core/emsdk.git +cd emsdk +./emsdk install latest-fastcomp +./emsdk activate latest-fastcomp +source emsdk_env.sh +``` + +Then, compile Omega : + +```bash +git clone --recursive https://github.com/Omega-Numworks/Omega.git +cd Omega +git checkout omega-master +make clean +make PLATFORM=simulator TARGET=web OMEGA_USERNAME="{Your name, max 15 characters}" -j4 +``` + +The simulator is now in `output/release/simulator/web/simulator.zip` + +Important: Don't forget the `--recursive` tag, because Omega relies on submodules. +Also, you can change the number of processes that run in parallel during the build by changing the value of the `-j` flag. + +
+ +
+ 3DS Simulator + +You need devkitPro and devkitARM installed and in your path (instructions [here](https://devkitpro.org/wiki/Getting_Started)) + +```bash +git clone --recursive https://github.com/Omega-Numworks/Omega.git +cd Omega +git checkout --recursive omega-dev +make PLATFORM=simulator TARGET=3ds -j +``` +You can then put epsilon.3dsx on a SD card to run it from the HBC or use 3dslink to launch it over the network: + +```bash +3dslink output/release/simulator/3ds/epsilon.3dsx -a <3DS' IP ADDRESS> +``` + +
+ +If you need help, you can join our Discord server here : https://discord.gg/X2TWhh9 + +

Omega Banner Discord

+--- + +## Contributing + +To contribute, please refer to the [Wiki](https://github.com/Omega-Numworks/Omega/wiki/Contributing) + +## Related repositories + +* [Omega Themes](https://github.com/Omega-Numworks/Omega-Themes) +* [Omega Website](https://github.com/Omega-Numworks/Omega-Website) +* [Omega RPN `APP`](https://github.com/Omega-Numworks/Omega-RPN) +* [Omega Atomic `APP`](https://github.com/Omega-Numworks/Omega-Atomic) +* [Omega Design](https://github.com/Omega-Numworks/Omega-Design) +* [Omega Discord Bot](https://github.com/Omega-Numworks/Omega-Discord-Bot) +* [Omega App Template `BETA`](https://github.com/Omega-Numworks/Omega-App-Template) +* [External Apps](https://github.com/Omega-Numworks/External-Apps) + +## About Epsilon + +Omega is a fork of Epsilon, 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/). + +## License + +NumWorks is a registered trademark of NumWorks SAS, 24 Rue Godot de Mauroy, 75009 Paris, France. +Nintendo and Nintendo 3DS are registered trademarks of Nintendo of America Inc, 4600 150th Ave NE, Redmond, WA 98052, USA. +NumWorks SAS and Nintendo of America Inc aren't associated in any shape or form with this project. + +* NumWorks Epsilon is released under a [CC BY-NC-SA License](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode). +* Omega is released under a [CC BY-NC-SA License](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode). 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..69f88165e38 --- /dev/null +++ b/apps/Makefile @@ -0,0 +1,139 @@ +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,${apps_list},${eval include apps/$(i)/Makefile}) + +app_equals = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))) +# We list all the apps that are missing +apps_missing = $(foreach i, ${apps_list}, $(if $(call app_equals, apps/$(i)/Makefile, $(wildcard apps/$(i)/Makefile)),, $(i) ) ) + +# If the two doesn't match, we got an error. +ifneq ($(strip $(apps_missing)),) + miss_modules = 0 + + # Check if the missing apps are one that are supposed to be submodules + $(foreach i, $(SUBMODULES_APPS), $(if $(call app_equals, $(filter $(i), $(apps_missing)), $(i)), $(eval miss_modules=1))) + + ifeq ($(miss_modules), 1) + PLS_IGNORE := $(shell >&2 printf "\nSome submodules apps seem to be missing. To download them, assumming you git clone'd the repo, do\n") + PLS_IGNORE := $(shell >&2 printf " git submodule init\n") + PLS_IGNORE := $(shell >&2 printf " git submodule update\n\n") + endif + $(error Missing apps: $(strip $(apps_missing))) +endif + +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 \ + clock_timer.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_non_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,${apps_list},"$(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) -DOMEGA_USERNAME="$(OMEGA_USERNAME)" + +# 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)/apps/home/apps_layout.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 \ +) + +ifeq ($(THEME_REPO),local) +$(foreach img,$(image_list), $(eval $(call rule_for, \ + ICON, \ + $(img), \ + $(addprefix themes/themes/local/, $(addsuffix .json, $(THEME_NAME))), \ + $$(PYTHON) themes/themes_manager.py -i $(THEME_REPO) $(THEME_NAME) $$@ $(BUILD_DIR)/, \ + global \ +))) +else +$(foreach img,$(image_list), $(eval $(call rule_for, \ + ICON, \ + $(img), \ + $(addsuffix /escher/palette.h, $(BUILD_DIR)), \ + $$(PYTHON) themes/themes_manager.py -i $(THEME_REPO) $(THEME_NAME) $$@ $(BUILD_DIR)/, \ + global \ +))) +endif 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..0bbe77d117c --- /dev/null +++ b/apps/apps_container.cpp @@ -0,0 +1,443 @@ +#include "apps_container.h" +#include "apps_container_storage.h" +#include "global_preferences.h" +#include "exam_mode_configuration.h" +#include +#include +#include +#include +#include + +extern "C" { +#include +} + +using namespace Poincare; +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_clockTimer(ClockTimer(this)), + 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 */ + + /* + * This can be run in Omega, since it uses WebASM. + */ +// #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 { + if (KDIonContext::sharedContext()->zoomEnabled) { + bool changedZoom = true; + + if (event == Ion::Events::ShiftOne) { + KDIonContext::sharedContext()->zoomPosition = 0; + } else if (event == Ion::Events::ShiftTwo) { + KDIonContext::sharedContext()->zoomPosition = 1; + } else if (event == Ion::Events::ShiftThree) { + KDIonContext::sharedContext()->zoomPosition = 2; + } else if (event == Ion::Events::ShiftFour) { + KDIonContext::sharedContext()->zoomPosition = 3; + } else if (event == Ion::Events::ShiftFive) { + KDIonContext::sharedContext()->zoomPosition = 4; + } else if (event == Ion::Events::ShiftSix) { + KDIonContext::sharedContext()->zoomPosition = 5; + } else if (event == Ion::Events::ShiftSeven) { + KDIonContext::sharedContext()->zoomPosition = 6; + } else if (event == Ion::Events::ShiftEight) { + KDIonContext::sharedContext()->zoomPosition = 7; + } else if (event == Ion::Events::ShiftNine) { + KDIonContext::sharedContext()->zoomPosition = 8; + } else { + changedZoom = false; + } + if (changedZoom) { + KDIonContext::sharedContext()->updatePostProcessingEffects(); + redrawWindow(true); + return true; + } + } + 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::ShiftHome) { + switchTo(appSnapshotAtIndex(1)); + return true; + } + if (event == Ion::Events::OnOff) { + suspend(true); + return true; + } + if (event == Ion::Events::BrightnessPlus || event == Ion::Events::BrightnessMinus) { + int delta = Ion::Backlight::MaxBrightness/GlobalPreferences::NumberOfBrightnessStates; + int direction = (event == Ion::Events::BrightnessPlus) ? Ion::Backlight::NumberOfStepsPerShortcut*delta : -delta*Ion::Backlight::NumberOfStepsPerShortcut; + GlobalPreferences::sharedGlobalPreferences()->setBrightnessLevel(GlobalPreferences::sharedGlobalPreferences()->brightnessLevel()+direction); + } + 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::updateClock() { + return m_window.updateClock(); +} + +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(bool force) { + m_window.redraw(force); +} + +void AppsContainer::activateExamMode(GlobalPreferences::ExamMode examMode) { + assert(examMode != GlobalPreferences::ExamMode::Off && examMode != GlobalPreferences::ExamMode::Unknown); + reset(); + Ion::LED::setColor(KDColorRed); + /* The Dutch exam mode LED is supposed to be orange but we can only make + * blink "pure" colors: with RGB leds on or off (as the PWM is used for + * blinking). The closest "pure" color is Yellow. Moreover, Orange LED is + * already used when the battery is charging. Using yellow, we can assert + * that the yellow LED only means that Dutch exam mode is on and avoid + * confusing states when the battery is charging and states when the Dutch + * exam mode is on. */ + // Ion::LED::setColor(examMode == GlobalPreferences::ExamMode::Dutch ? KDColorYellow : KDColorRed); + 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 4; +} + +Timer * AppsContainer::containerTimerAtIndex(int i) { + Timer * timers[4] = {&m_batteryTimer, &m_suspendTimer, &m_backlightDimmingTimer, &m_clockTimer}; + 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..ab945b6efec --- /dev/null +++ b/apps/apps_container.h @@ -0,0 +1,88 @@ +#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 "clock_timer.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 updateClock(); + bool updateBatteryState(); + void refreshPreferences(); + void reloadTitleBarView(); + void displayExamModePopUp(GlobalPreferences::ExamMode mode); + void shutdownDueToLowBattery(); + void setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus newStatus); + OnBoarding::PromptController * promptController(); + void redrawWindow(bool force = false); + 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; + ClockTimer m_clockTimer; + 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..613acc99d9a --- /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::AccentText}; + +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..e837b592f27 --- /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::AccentText}; + +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..057152d9c53 --- /dev/null +++ b/apps/apps_window.cpp @@ -0,0 +1,79 @@ +#include "apps_window.h" +#include +extern "C" { +#include +} +#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::updateClock() { + Ion::RTC::DateTime dateTime = Ion::RTC::dateTime(); + return m_titleBarView.setClock(dateTime.tm_hour, dateTime.tm_min, Ion::RTC::mode() != Ion::RTC::Mode::Disabled); +} + +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..9b930a8e5f6 --- /dev/null +++ b/apps/apps_window.h @@ -0,0 +1,27 @@ +#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 updateClock(); + 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/atomic b/apps/atomic new file mode 160000 index 00000000000..69f7a06ba53 --- /dev/null +++ b/apps/atomic @@ -0,0 +1 @@ +Subproject commit 69f7a06ba53eaae9ba2ab8f0c0318e2f77455f4d 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..28ab8d9794e --- /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), Palette::Battery); + + // 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::BatteryInCharge); + KDRect frame((k_batteryWidth-k_flashWidth)/2, 0, k_flashWidth, k_flashHeight); + KDColor flashWorkingBuffer[BatteryView::k_flashHeight*BatteryView::k_flashWidth]; + ctx->blendRectWithMask(frame, Palette::Battery, (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::BatteryLow); + ctx->fillRect(KDRect(3*k_elementWidth+k_separatorThickness, 0, k_batteryWidth-5*k_elementWidth-2*k_separatorThickness, k_batteryHeight), Palette::BatteryInCharge); + } 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), Palette::Battery); + ctx->fillRect(KDRect(batteryInsideX+middleChargeWidth, 0, middleChargeWidth, k_batteryHeight), Palette::BatteryInCharge); + } else { + assert(m_chargeState == Ion::Battery::Charge::FULL); + // Full but not plugged: Full battery + ctx->fillRect(KDRect(batteryInsideX, 0, batteryInsideWidth, k_batteryHeight), Palette::Battery); + 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::Toolbar, (const uint8_t *)tickMask, tickWorkingBuffer); + } + } + + // Draw the right part + ctx->fillRect(KDRect(k_batteryWidth-2*k_elementWidth, 0, k_elementWidth, k_batteryHeight), Palette::Battery); + ctx->fillRect(KDRect(k_batteryWidth-k_elementWidth, (k_batteryHeight-k_capHeight)/2, k_elementWidth, k_capHeight), Palette::Battery); +} + +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..fe38bf9105a --- /dev/null +++ b/apps/calculation/additional_outputs/complex_graph_cell.cpp @@ -0,0 +1,94 @@ +#include "complex_graph_cell.h" +#include + +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, Palette::BackgroundApps); + + // 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::SecondaryText, 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::SecondaryText, false); + + // Draw dashed segment to indicate real and imaginary + drawHorizontalOrVerticalSegment(ctx, rect, Axis::Vertical, real, 0.0f, imag, Palette::CalculationTrigoAndComplexForeground, 1, 3); + drawHorizontalOrVerticalSegment(ctx, rect, Axis::Horizontal, imag, 0.0f, real, Palette::CalculationTrigoAndComplexForeground, 1, 3); + + // Draw complex position on the plan + drawDot(ctx, rect, real, imag, Palette::CalculationTrigoAndComplexForeground, Size::Large); + + // Draw labels + // 're(z)' label + drawLabel(ctx, rect, real, 0.0f, "re(z)", Palette::CalculationTrigoAndComplexForeground, CurveView::RelativePosition::None, imag >= 0.0f ? CurveView::RelativePosition::Before : CurveView::RelativePosition::After); + // 'im(z)' label + drawLabel(ctx, rect, 0.0f, imag, "im(z)", Palette::CalculationTrigoAndComplexForeground, 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::CalculationTrigoAndComplexForeground, 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::CalculationTrigoAndComplexForeground, 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..359fbe46bde --- /dev/null +++ b/apps/calculation/additional_outputs/expression_with_equal_sign_view.h @@ -0,0 +1,27 @@ +#ifndef CALCULATION_EXPRESSION_WITH_EQUAL_SIGN_VIEW_H +#define CALCULATION_EXPRESSION_WITH_EQUAL_SIGN_VIEW_H + +#include +#include +#include +#include + +namespace Calculation { + +class ExpressionWithEqualSignView : public ExpressionView { +public: + ExpressionWithEqualSignView() : + m_equalSign(KDFont::LargeFont, I18n::Message::Equal, 0.5f, 0.5f, Palette::PrimaryText) + {} + 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..20249c7d808 --- /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::ListCellBorder); +} + +} 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..ea2b17d3c29 --- /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, Palette::ToolboxHeaderText, Palette::ToolboxHeaderBackground, Palette::ToolboxHeaderBorder), + 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..95ad648235d --- /dev/null +++ b/apps/calculation/additional_outputs/scrollable_three_expressions_cell.h @@ -0,0 +1,81 @@ +#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" +#include + +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(Palette::BackgroundApps); + } + 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 Palette::BackgroundApps; } + 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..636ce588a80 --- /dev/null +++ b/apps/calculation/additional_outputs/trigonometry_graph_cell.cpp @@ -0,0 +1,38 @@ +#include "trigonometry_graph_cell.h" +#include + +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, Palette::BackgroundApps); + 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::SecondaryText, false); + // Draw dashed segment to indicate sine and cosine + drawHorizontalOrVerticalSegment(ctx, rect, Axis::Vertical, c, 0.0f, s, Palette::CalculationTrigoAndComplexForeground, 1, 3); + drawHorizontalOrVerticalSegment(ctx, rect, Axis::Horizontal, s, 0.0f, c, Palette::CalculationTrigoAndComplexForeground, 1, 3); + // Draw angle position on the circle + drawDot(ctx, rect, c, s, Palette::CalculationTrigoAndComplexForeground, 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::CalculationTrigoAndComplexForeground, c >= 0.0f ? CurveView::RelativePosition::Before : CurveView::RelativePosition::After, CurveView::RelativePosition::None); + drawLabel(ctx, rect, c, 0.0f, "cos(θ)", Palette::CalculationTrigoAndComplexForeground, 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..2c5c97d3c87 --- /dev/null +++ b/apps/calculation/app.cpp @@ -0,0 +1,88 @@ +#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; +} + +App::Descriptor::ExaminationLevel App::Descriptor::examinationLevel() { + return App::Descriptor::ExaminationLevel::Strict; +} + +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..975989ffa36 --- /dev/null +++ b/apps/calculation/app.h @@ -0,0 +1,58 @@ +#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; + App::Descriptor::ExaminationLevel examinationLevel() 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..967c0905063 --- /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" 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.hu.i18n b/apps/calculation/base.hu.i18n new file mode 100644 index 00000000000..39397adee2f --- /dev/null +++ b/apps/calculation/base.hu.i18n @@ -0,0 +1,14 @@ +CalculApp = "Számolás" +CalculAppCapital = "SZÁMOLÁS" +AdditionalResults = "További eredmények" +DecimalBase = "Decimális" +HexadecimalBase = "Hexadecimális" +BinaryBase = "Bináris" +PrimeFactors = "Alapvetö tényezök" +MixedFraction = "Vegyes frakció" +EuclideanDivision = "Euklideszi osztás" +AdditionalDeterminant = "Meghatározó" +AdditionalInverse = "inverz" +AdditionalRowEchelonForm = "Sor echelon forma" +AdditionalReducedRowEchelonForm = "Csökkentett sorú Echelon forma" +AdditionalTrace = "Nyomkövetés" 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..c293cbbe841 --- /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) + || strcmp(approximateOutputText(NumberOfSignificantDigits::Maximal), Undefined::Name()) == 0; +} + +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..e0dcc12f161 --- /dev/null +++ b/apps/calculation/calculation_store.cpp @@ -0,0 +1,226 @@ +#include "calculation_store.h" +#include "../shared/poincare_helpers.h" +#include "../global_preferences.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, GlobalPreferences::sharedGlobalPreferences()->isInExamModeSymbolic() ? Poincare::ExpressionNode::SymbolicComputation::ReplaceAllDefinedSymbolsWithDefinition : 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..71799403c26 --- /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::ListCellBackgroundSelected); + } 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..38d7e22f18c --- /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 ? Palette::CalculationBackgroundEven : Palette::CalculationBackgroundOdd; } + 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.py b/apps/calculation/test.py new file mode 100644 index 00000000000..84fb10c01f4 --- /dev/null +++ b/apps/calculation/test.py @@ -0,0 +1,11 @@ +import sys, tty +def command_line(): + tty.setraw(sys.stdin) + while True: + char = sys.stdin.read(1) + if ord(char) == 3: # CTRL-C + break; + print(ord(char)) + sys.stdout.write(u"\u001b[1000D") # Move all the way left + +command_line(); diff --git a/apps/calculation/test/calculation_store.cpp b/apps/calculation/test/calculation_store.cpp new file mode 100644 index 00000000000..b73a5143316 --- /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::ExactOnly, EqualSign::Unknown, "undef", "undef", "undef", &globalContext, &store); + assertCalculationIs("2x-x", DisplayOutput::ExactOnly, EqualSign::Unknown, "x", "undef", "undef", &globalContext, &store); + assertCalculationIs("[[1,2,3]]", DisplayOutput::ApproximateOnly, EqualSign::Unknown, nullptr, nullptr, nullptr, &globalContext, &store); + assertCalculationIs("[[1,x,3]]", DisplayOutput::ExactAndApproximate, EqualSign::Unknown, nullptr, "[[1,undef,3]]", "[[1,undef,3]]", &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::ExactOnly, EqualSign::Unknown, nullptr, nullptr, nullptr, &globalContext, &store); + assertCalculationIs("prediction(0.5,2)+3", DisplayOutput::ExactOnly, EqualSign::Unknown, nullptr, nullptr, nullptr, &globalContext, &store); + assertCalculationIs("prediction95(0.5,2)+3", DisplayOutput::ExactOnly, 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::ExactOnly, EqualSign::Unknown, "2×x+√(π)+4", "undef", "undef", &globalContext, &store); + assertCalculationIs("f(x)", DisplayOutput::ExactOnly, EqualSign::Unknown, "f×x", "undef", "undef", &globalContext, &store); + assertCalculationIs("1+x→f(x)", DisplayOutput::ExactOnly, EqualSign::Unknown, "x+1", nullptr, nullptr, &globalContext, &store); + assertCalculationIs("f(x)", DisplayOutput::ExactOnly, EqualSign::Unknown, "x+1", "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/clock_timer.cpp b/apps/clock_timer.cpp new file mode 100644 index 00000000000..e06a1e6b206 --- /dev/null +++ b/apps/clock_timer.cpp @@ -0,0 +1,12 @@ +#include "clock_timer.h" +#include "apps_container.h" + +ClockTimer::ClockTimer(AppsContainer * container) : + Timer(1), + m_container(container) +{ +} + +bool ClockTimer::fire() { + return m_container->updateClock(); +} diff --git a/apps/clock_timer.h b/apps/clock_timer.h new file mode 100644 index 00000000000..985af96e95d --- /dev/null +++ b/apps/clock_timer.h @@ -0,0 +1,16 @@ +#ifndef APPS_CLOCK_TIMER_H +#define APPS_CLOCK_TIMER_H + +#include + +class AppsContainer; + +class ClockTimer : public Timer { +public: + ClockTimer(AppsContainer * container); +private: + bool fire() override; + AppsContainer * m_container; +}; + +#endif 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..4db851b222b --- /dev/null +++ b/apps/code/app.cpp @@ -0,0 +1,154 @@ +#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; +} + +App::Descriptor::ExaminationLevel App::Descriptor::examinationLevel() { + return App::Descriptor::ExaminationLevel::Basic; +} + +const Image * App::Descriptor::icon() { + return ImageStore::CodeIcon; +} + +App::Snapshot::Snapshot() : +#if EPSILON_GETOPT + m_lockOnConsole(false), + m_hasBeenWiped(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) { + if (!m_hasBeenWiped) { + m_hasBeenWiped = true; + 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..494f5c0f8c7 --- /dev/null +++ b/apps/code/app.h @@ -0,0 +1,100 @@ +#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; + App::Descriptor::ExaminationLevel examinationLevel() 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; + bool m_hasBeenWiped; +#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 = 100000; + +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..40cf856fa34 --- /dev/null +++ b/apps/code/base.de.i18n @@ -0,0 +1,15 @@ +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" +DuplicateScript = "Skript duplizieren" +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" +ScriptSize = "Script size" diff --git a/apps/code/base.en.i18n b/apps/code/base.en.i18n new file mode 100644 index 00000000000..082bcfd7961 --- /dev/null +++ b/apps/code/base.en.i18n @@ -0,0 +1,15 @@ +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" +DuplicateScript = "Duplicate 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" +ScriptSize = "Script size" diff --git a/apps/code/base.es.i18n b/apps/code/base.es.i18n new file mode 100644 index 00000000000..f3db58624a2 --- /dev/null +++ b/apps/code/base.es.i18n @@ -0,0 +1,15 @@ +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" +DuplicateScript = "Duplicar el guión" +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" +ScriptSize = "Tamaño del script" diff --git a/apps/code/base.fr.i18n b/apps/code/base.fr.i18n new file mode 100644 index 00000000000..46d7109ad38 --- /dev/null +++ b/apps/code/base.fr.i18n @@ -0,0 +1,15 @@ +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" +DuplicateScript = "Dupliquer 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" +ScriptSize = "Taille du script" diff --git a/apps/code/base.hu.i18n b/apps/code/base.hu.i18n new file mode 100644 index 00000000000..07d0e297fb1 --- /dev/null +++ b/apps/code/base.hu.i18n @@ -0,0 +1,15 @@ +AddScript = "Script hozzáadása" +AllowedCharactersaz09 = "Engedélyezett karakterek: a-z, 0-9, _" +Autocomplete = "Önkiegészítés" +AutoImportScript = "Script automata importálása" +BuiltinsAndKeywords = "Beépített fonkciók és szókincs" +Console = "Konzol" +DeleteScript = "Script törlése" +DuplicateScript = "Script másolása" +ExecuteScript = "Script indítása" +FunctionsAndVariables = "Fonktiók és változók" +ImportedModulesAndScripts = "Importált scriptek és modulok" +NoWordAvailableHere = "Nincs rendelkezésre álló szó." +ScriptInProgress = "Script müködésben" +ScriptOptions = "Script beállítások" +ScriptSize = "Script mérete" diff --git a/apps/code/base.it.i18n b/apps/code/base.it.i18n new file mode 100644 index 00000000000..cd99347a262 --- /dev/null +++ b/apps/code/base.it.i18n @@ -0,0 +1,15 @@ +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" +DuplicateScript = "Duplicate 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" +ScriptSize = "Script Size" diff --git a/apps/code/base.nl.i18n b/apps/code/base.nl.i18n new file mode 100644 index 00000000000..769bdbb7203 --- /dev/null +++ b/apps/code/base.nl.i18n @@ -0,0 +1,15 @@ +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" +DuplicateScript = "Duplicate script" +ExecuteScript = "Script uitvoeren" +FunctionsAndVariables = "Functies en variabelen" +ImportedModulesAndScripts = "Imported modules and scripts" +NoWordAvailableHere = "No word available here." +ScriptInProgress = "Script in progress" +ScriptOptions = "Script opties" +ScriptSize = "Script Size" diff --git a/apps/code/base.pt.i18n b/apps/code/base.pt.i18n new file mode 100644 index 00000000000..889c0fa3566 --- /dev/null +++ b/apps/code/base.pt.i18n @@ -0,0 +1,15 @@ +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" +DuplicateScript = "Duplicar 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" +ScriptSize = "Script Size" 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..f1c208068f7 --- /dev/null +++ b/apps/code/catalog.de.i18n @@ -0,0 +1,222 @@ +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 = "Hängt x an das Ende der Liste" +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 = "Leere die Liste" +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 = "x mit dem Vorzeichen von y" +PythonCos = "Kosinus" +PythonCosh = "Hyperbolic cosine" +PythonCount = "Zählt wie oft x vorkommt" +PythonDegrees = "x von Radian zu Grad umwandeln" +PythonDivMod = "Quotient und Rest" +PythonDrawString = "Schreibt Text bei (x,y)" +PythonErf = "Fehlerfunktion" +PythonErfc = "Complementary error function" +PythonEval = "Return the evaluated expression" +PythonExp = "Exponentialfunktion" +PythonExpm1 = "Berechne exp(x)-1" +PythonFabs = "Absoluter Wert" +PythonFillRect = "Malt ein Rechteck bei Pixel (x,y)" +PythonFloat = "Wandelt x zu float um" +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 = "Ganzzahl zu Hexadecimal" +PythonHist = "Draw the histogram of x" +PythonImportCmath = "cmath Modul importieren" +PythonImportIon = "ion Modul importieren" +PythonImportKandinsky = "kandinsky Modul importieren" +PythonImportRandom = "random Modul importieren" +PythonImportMath = "math Modul importieren" +PythonImportMatplotlibPyplot = "Import matplotlib.pyplot module" +PythonImportOs = "os Modul importieren" +PythonOsUname = "Informieren Sie sich über das System" +PythonOsRemove = "Datei namens Dateiname entfernen" +PythonOsRename = "Datei mit altem Namen in neuen Namen umbenennen" +PythonOsListdir = "Dateien im Speicher auflisten" +PythonImportTime = "time Modul importieren" +PythonImportTurtle = "turtle Modul importieren" +PythonIndex = "Index, bei dem x zuerst vorkommt" +PythonInput = "Eingabeaufforderung" +PythonInsert = "x bei index i in der Liste einsetzen" +PythonInt = "x zu Ganzzahl" +PythonIonFunction = "ion module function prefix" +PythonIsFinite = "Prüft ob x endlich ist" +PythonIsInfinite = "Prüft ob x unendlich ist" +PythonIsNaN = "Prüft ob x NaN ist" +PythonIsKeyDown = "true wenn k gedrückt ist" +PythonKandinskyFunction = "kandinsky module function prefix" +PythonKeyLeft = "Linke Pfeiltaste" +PythonKeyUp = "Pfeiltaste nach oben" +PythonKeyDown = "Pfeiltaste nach unten" +PythonKeyRight = "Rechte Pfeiltaste" +PythonKeyOk = "OK Taste" +PythonKeyBack = "ZURÜCK Taste" +PythonKeyHome = "HOME Taste" +PythonKeyOnOff = "AN/AUS Taste" +PythonKeyShift = "SHIFT Taste" +PythonKeyAlpha = "ALPHA Taste" +PythonKeyXnt = "X,N,T Taste" +PythonKeyVar = "VAR Taste" +PythonKeyToolbox = "WERKZEUGBOX Taste" +PythonKeyBackspace = "LÖSCHEN Taste" +PythonKeyExp = "EXPONENTIAL Taste" +PythonKeyLn = "NATURAL LOGARITHM Taste" +PythonKeyLog = "DECIMAL LOGARITHM Taste" +PythonKeyImaginary = "IMAGINÄRES I Taste" +PythonKeyComma = "KOMMA Taste" +PythonKeyPower = "HOCH Taste" +PythonKeySine = "SINUS Taste" +PythonKeyCosine = "COSINUS Taste" +PythonKeyTangent = "TANGENZ Taste" +PythonKeyPi = "PI Taste" +PythonKeySqrt = "WURZEL Taste" +PythonKeySquare = "QUADRAT Taste" +PythonKeySeven = "7 Taste" +PythonKeyEight = "8 Taste" +PythonKeyNine = "9 Taste" +PythonKeyLeftParenthesis = "LEFT PARENTHESIS key" +PythonKeyRightParenthesis = "RIGHT PARENTHESIS key" +PythonKeyFour = "4 Taste" +PythonKeyFive = "5 Taste" +PythonKeySix = "6 Taste" +PythonKeyMultiplication = "MULTIPLIKATIONSTASTE" +PythonKeyDivision = "DIVISIONSTASTE" +PythonKeyOne = "1 Taste" +PythonKeyTwo = "2 Taste" +PythonKeyThree = "3 Taste" +PythonKeyPlus = "PLUS Taste" +PythonKeyMinus = "MINUS Taste" +PythonKeyZero = "0 Taste" +PythonKeyDot = "PUNKT Taste" +PythonKeyEe = "10 HOCH X Taste" +PythonKeyAns = "ANS Taste" +PythonKeyExe = "EXE Taste" +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 = "Wurzel" +PythonSum = "Sum the items of a list" +PythonTan = "Tangens" +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 = "Stiftfarbe setzen" +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]" +PythonImportTime = "Import time module" +PythonTimePrefix = "time module function prefix" +PythonTimeSleep = "Wait for n second" +PythonMonotonic = "Return monotonic time" +PythonFileOpen = "Öffnet eine Datei" +PythonFileSeekable = "Ist eine Datei durchsuchbar?" +PythonFileSeek = "Dateicursor verschieben" +PythonFileTell = "Cursorposition der Datei abrufen" +PythonFileClose = "Schließt eine Datei" +PythonFileClosed = "Wenn Datei geschlossen wurde" +PythonFileRead = "Bis zu size Bytes lesen" +PythonFileWrite = "Schreibe b in die Datei" +PythonFileReadline = "Lies eine Zeile" +PythonFileReadlines = "Liest eine Liste von Zeilen" +PythonFileTruncate = "Größe der Datei ändern" +PythonFileWritelines = "Schreibt eine Liste von Zeilen" +PythonFileName = "Dateiname" +PythonFileMode = "Dateiöffnungsmodus" +PythonFileReadable = "Ist die Datei lesbar?" +PythonFileWritable = "Ist die Datei beschreibbar?" diff --git a/apps/code/catalog.en.i18n b/apps/code/catalog.en.i18n new file mode 100644 index 00000000000..cd115b9aa5b --- /dev/null +++ b/apps/code/catalog.en.i18n @@ -0,0 +1,222 @@ +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]" +PythonImportTime = "Import time module" +PythonImportOs = "Import os module" +PythonOsUname = "Get infos about the system" +PythonOsRemove = "Remove file named filename" +PythonOsRename = "Rename file oldname to newname" +PythonOsListdir = "List files in memory" +PythonTimePrefix = "time module function prefix" +PythonTimeSleep = "Wait for n second" +PythonMonotonic = "Return monotonic time" +PythonFileOpen = "Opens a file" +PythonFileSeekable = "Tells if seek can be used on a file" +PythonFileSeek = "Move file's cursor" +PythonFileTell = "Get file's cursor location" +PythonFileClose = "Closes a file" +PythonFileClosed = "True if file was closed" +PythonFileRead = "Read up to size bytes" +PythonFileWrite = "Write b into file" +PythonFileReadline = "Reads a line or up to size bytes" +PythonFileReadlines = "Reads a list of lines" +PythonFileTruncate = "Resize the file to size" +PythonFileWritelines = "Writes a list of lines" +PythonFileName = "Contains file's name" +PythonFileMode = "Contains file's open mode" +PythonFileReadable = "Tells if read can be used on a file" +PythonFileWritable = "Tells if write can be used on a file" diff --git a/apps/code/catalog.es.i18n b/apps/code/catalog.es.i18n new file mode 100644 index 00000000000..ee8b58f598b --- /dev/null +++ b/apps/code/catalog.es.i18n @@ -0,0 +1,222 @@ +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]" +PythonImportTime = "Import time module" +PythonImportOs = "Import os module" +PythonOsUname = " Información del sistema " +PythonOsRemove = "Eliminar un archivo" +PythonOsRename = "Renombrar archivo" +PythonOsListdir = "Archivos de la lista" +PythonTimePrefix = "time module function prefix" +PythonTimeSleep = "Esperar n segundos" +PythonMonotonic = "Tiempo monótono de retorno" +PythonFileOpen = "Opens a file" +PythonFileSeekable = "Tells if seek can be used on a file" +PythonFileSeek = "Move file's internal cursor" +PythonFileTell = "Get file's internal cursor location" +PythonFileClose = "Closes a file" +PythonFileClosed = "True if file was closed" +PythonFileRead = "Read up to size bytes" +PythonFileWrite = "Write b into file" +PythonFileReadline = "Reads a line or up to size bytes" +PythonFileReadlines = "Reads a list of lines" +PythonFileTruncate = "Resize the file to size" +PythonFileWritelines = "Writes a list of lines" +PythonFileName = "Contains file's name" +PythonFileMode = "Contains file's open mode" +PythonFileReadable = "Tells if read can be used on a file" +PythonFileWritable = "Tells if write can be used on a file" diff --git a/apps/code/catalog.fr.i18n b/apps/code/catalog.fr.i18n new file mode 100644 index 00000000000..558c09cdc2a --- /dev/null +++ b/apps/code/catalog.fr.i18n @@ -0,0 +1,222 @@ +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]" +PythonImportTime = "Importation du module temps" +PythonImportOs = "Importation du module os" +PythonOsUname = "Donne des infos sur le système" +PythonOsRemove = "Supprime le fichier nommé filename" +PythonOsRename = "Renomme oldname en newname" +PythonOsListdir = "Liste les fichiers" +PythonTimePrefix = "Préfixe fonction du module temps" +PythonTimeSleep = "Attendre n secondes" +PythonMonotonic = "Retourne le temps monotone" +PythonFileOpen = "Ouvre un fichier" +PythonFileSeekable = "Indique si seek peut être utilisé" +PythonFileSeek = "Déplace le curseur interne" +PythonFileTell = "Donne la posititon du curseur" +PythonFileClose = "Ferme un fichier" +PythonFileClosed = "True si le fichier a été fermé" +PythonFileRead = "Lit jusqu'à size bytes" +PythonFileWrite = "Écrit b dans le fichier" +PythonFileReadline = "Lit une ligne ou jusqu'à size bytes" +PythonFileReadlines = "Lit une liste de lignes" +PythonFileTruncate = "Redimensionne le fichier" +PythonFileWritelines = "Écrit une liste de lignes" +PythonFileName = "Nom du fichier" +PythonFileMode = "Mode d'ouverture du fichier" +PythonFileReadable = "Indique si read peut être utilisé" +PythonFileWritable = "Indique si write peut être utilisé" diff --git a/apps/code/catalog.hu.i18n b/apps/code/catalog.hu.i18n new file mode 100644 index 00000000000..6871a7dcfbe --- /dev/null +++ b/apps/code/catalog.hu.i18n @@ -0,0 +1,222 @@ +PythonPound = "Megjegyzés" +PythonPercent = "Modulo" +Python1J = "Képzeletbeli i" +PythonLF = "Enter" +PythonTab = "Táblázat" +PythonAmpersand = "Logikus és" +PythonSymbolExp = "logikus exkluzív vagy pedig" +PythonVerticalBar = "logikus vagy pedig" +PythonImag = "z képzeletbeli része" +PythonReal = "z valódi része" +PythonSingleQuote = "apostróf" +PythonAbs = "Abszolút érték/nagyság" +PythonAcos = "Ív (arc) koszinusz" +PythonAcosh = "Hiperbolikus arc koszinusz" +PythonAppend = "Lista végére hozzáadni x-et" +PythonArrow = "(x,y) nyíla (x+dx,y+dy) nyílához" +PythonAsin = "Ív (arc) szinusz" +PythonAsinh = "Hiperbolikus ív (arc) szinusz" +PythonAtan = "Ív (arc) érintö (tan)" +PythonAtan2 = "atan(y/x) sámolása" +PythonAtanh = "Hiperbolikus ív (arc) érintö (atan)" +PythonAxis = "Tengelyeket (xmin,xmax,ymin,ymax)-ra állitani" +PythonBar = "Az x lista oszlopdiagramja" +PythonBin = "Egész szám konvertálása binárisra" +PythonCeil = "Mennyezet" +PythonChoice = "Véletlenszerü szám a listában" +PythonClear = "A lista ürítése" +PythonCmathFunction = "cmath modul funkció elötag" +PythonColor = "Rgb (pzk) szín allítása" +PythonColorBlack = "Fekete szín" +PythonColorBlue = "Kék szín" +PythonColorBrown = "Barna szín" +PythonColorGreen = "Zöld szín" +PythonColorGray = "Szürke szín" +PythonColorOrange = "Narancssárga szín" +PythonColorPink = "Rózsaszín szín" +PythonColorPurple = "Lila szín" +PythonColorRed = "Piros szín" +PythonColorWhite = "Fehér szín" +PythonColorYellow = "Sárga szín" +PythonComplex = "A + ib visszaadása" +PythonCopySign = "X visszaadása y jelével" +PythonCos = "Koszinusz" +PythonCosh = "Hiperbolikus koszinusz" +PythonCount = "Számolja az x elöfordulását" +PythonDegrees = "x konvertálása radiánokrol fokokra" +PythonDivMod = "Hányados és maradék" +PythonDrawString = "Szöveg megjelenítése (x, y)-en" +PythonErf = "Hiba funkció" +PythonErfc = "Kiegészítö hiba funkció" +PythonEval = "Visszaadja az értékelt kifejezést" +PythonExp = "Exponenciális függvény" +PythonExpm1 = "exp(x)-1 sámitása" +PythonFabs = "Abszolút érték" +PythonFillRect = "Téglalap töltése" +PythonFloat = "Konvertálása tizedes számra" +PythonFloor = "Egész része" +PythonFmod = "a modulo b" +PythonFrExp = "X mantissája és kiállítója" +PythonGamma = "Gamma funkció" +PythonGetPixel = "Visszatéríti (x,y) színét" +PythonGetrandbits = "Váletlenszám visszatérítése k biten" +PythonGrid = "Rács megjelenítése/elrejtése" +PythonHex = "Decimális szám konvertálása hexadecimális számra" +PythonHist = "x hisztográmiája" +PythonImportCmath = "cmath modul importálása" +PythonImportIon = "Ion modul importálása" +PythonImportKandinsky = "Kandinsky modul importálása" +PythonImportRandom = "Véletlenszerü modul importálása" +PythonImportMath = "math modul importálása" +PythonImportMatplotlibPyplot = "matplotlib.pyplot modul importálása" +PythonImportTurtle = "turtle modul importálása" +PythonImportTime = "time modul importálása" +PythonIndex = "Az elsö x esemény indexe" +PythonInput = "Irjon egy értéket (számot)" +PythonInsert = "x-et i. pozícióra helyezze a listában" +PythonInt = "egész számra konvertálás" +PythonIonFunction = "ion modul funkció elötag" +PythonIsFinite = "x véges-e" +PythonIsInfinite = "x végtelen-e" +PythonIsKeyDown = "True-t válaszol ha a k gomb le van nyomva" +PythonIsNaN = "Ellenörizze hogy x nem NaN" +PythonKandinskyFunction = "kandinsky modul funkció elötag" +PythonKeyLeft = "BAL NYÍL gomb" +PythonKeyUp = "FEL NYÍL gomb" +PythonKeyDown = "LE NYÍL gomb" +PythonKeyRight = "JOBB NYÍL gomb" +PythonKeyOk = "OK gomb" +PythonKeyBack = "VISSZA gomb" +PythonKeyHome = "HOME gomb" +PythonKeyOnOff = "ON/OFF gomb" +PythonKeyShift = "SHIFT gomb" +PythonKeyAlpha = "ALPHA gomb" +PythonKeyXnt = "X,N,T gomb" +PythonKeyVar = "VAR gomb" +PythonKeyToolbox = "TOOLBOX gomb" +PythonKeyBackspace = "TÖRLÉS gomb" +PythonKeyExp = "EXPONENCIÁLIS gomb" +PythonKeyLn = "NEPERIAI LOGARITHM gomb" +PythonKeyLog = "DECIMÁLIS LOGARITHM gomb" +PythonKeyImaginary = "KÉPZELETBELI I gomb" +PythonKeyComma = "VESSZÖ gomb" +PythonKeyPower = "KIÁLLÍTÓ gomb" +PythonKeySine = "SZINUSZ gomb" +PythonKeyCosine = "KOSZINUSZ gomb" +PythonKeyTangent = "TANGENS gomb" +PythonKeyPi = "PI gomb" +PythonKeySqrt = "NÉGYZETGYÖK gomb" +PythonKeySquare = "NÉGYZET gomb" +PythonKeySeven = "7 gomb" +PythonKeyEight = "8 gomb" +PythonKeyNine = "9 gomb" +PythonKeyLeftParenthesis = "BAL ZÁRÓJEL gomb" +PythonKeyRightParenthesis = "JOB ZÁRÓJEL gomb" +PythonKeyFour = "4 gomb" +PythonKeyFive = "5 gomb" +PythonKeySix = "6 gomb" +PythonKeyMultiplication = "SZOZÁS gomb" +PythonKeyDivision = "OSZTÁS gomb" +PythonKeyOne = "1 gomb" +PythonKeyTwo = "2 gomb" +PythonKeyThree = "3 gomb" +PythonKeyPlus = "PLUS gomb" +PythonKeyMinus = "MINUS gomb" +PythonKeyZero = "0 gomb" +PythonKeyDot = "PONT gomb" +PythonKeyEe = "10 X KITEVÖ gomb" +PythonKeyAns = "ANS gomb" +PythonKeyExe = "EXE gomb" +PythonLdexp = "frexp ellentéte : x*(2**i)" +PythonLength = "Egy targy hossza" +PythonLgamma = "Gamma funkció logaritmusa" +PythonLog = "a alapú logaritmus" +PythonLog10 = "Decimális logaritmus" +PythonLog2 = "Bináris logaritmus" +PythonMathFunction = "math modul funkció elötag" +PythonMatplotlibPyplotFunction = "matplotlib.pyplot elötag" +PythonMax = "Maximum" +PythonMin = "Minimum" +PythonModf = "x-nek tört és egész részei" +PythonMonotonic = "Az óra értékét adja vissza" +PythonOct = "Decimális szám konvertálása octális számra" +PythonPhase = "z fázisa" +PythonPlot = "y-t jelöli x függvényében" +PythonPolar = "Verctorizálni" +PythonPop = "Az utolsó elemet el törölni" +PythonPower = "x y. kitevö" +PythonPrint = "Ki irni a elemeket" +PythonRadians = "Fokról radiánra konvertálni" +PythonRandint = "Véletlen egész szám [a;b] -ban" +PythonRandom = "Decimális szám [0;1] -ban" +PythonRandomFunction = "random modul funkció elötag" +PythonRandrange = "Véletlen szám range(start,stop)-ban" +PythonRangeStartStop = "start-tol stop-ig listája" +PythonRangeStop = "0 tol stop-ig lista" +PythonRect = "Algebrai számra konvertálni" +PythonRemove = "Elsö x elöfordulását törolni" +PythonReverse = "A lista elemeit megfordítani (másik irány)" +PythonRound = "N számjegyre kerekítni" +PythonScatter = "(x,y) halmaza" +PythonSeed = "Inicializálni a véletlenszám-választót" +PythonSetPixel = "Az (x,y) pixel-t ki szinezni" +PythonShow = "Mutassa az ábrát" +PythonSin = "Szinusz" +PythonSinh = "Hiperbolikus szinusz" +PythonSleep = "t másodpercre meg állitani a programmot" +PythonSort = "A listát rendezni" +PythonSqrt = "Négyzetgyök" +PythonSum = "Összeadni a lista elemeit" +PythonTan = "Érintö (tan)" +PythonTanh = "Hiperbolikus érintö (tan)" +PythonText = "(x,y) nél egy szöveget irni" +PythonTimeFunction = "time funkció elötag" +PythonTrunc = "Egész csonka (?)" +PythonTurtleBackward = "x pixelt hátra" +PythonTurtleCircle = "r pixel sugarú kört rajzolni" +PythonTurtleColor = "Toll szinét beállitani" +PythonTurtleColorMode = "Szin módot 1.0-ra vagy 255-ra állitani" +PythonTurtleForward = "x pixelt elölre" +PythonTurtleFunction = "turtle modul funkció elötag" +PythonTurtleGoto = "Menjen a (x,y) koordinátákra" +PythonTurtleHeading = "Visszaadja az aktuális irányt" +PythonTurtleHideturtle = "A teknös elrejtése" +PythonTurtleIsdown = "True-t válaszol ha a toll irás pozícióban van" +PythonTurtleLeft = "a fokkot forduljon balra" +PythonTurtlePendown = "Húzza le a tollat" +PythonTurtlePensize = "Állítsa a vonalvastagságot x pixelre" +PythonTurtlePenup = "Húzza fel a tollat" +PythonTurtlePosition = "Az aktuális (x,y) pozíciót visszaadása" +PythonTurtleReset = "Visszaállitani a rajzot (torléssel)" +PythonTurtleRight = "a fokkot forduljon jobbra" +PythonTurtleSetheading = "a fokokra állítja be az irányt" +PythonTurtleSetposition = "A teknös pozicioját allitja" +PythonTurtleShowturtle = "A teknöst meg mutatni" +PythonTurtleSpeed = "Rajzolási sebesség 0 és 10 között" +PythonTurtleWrite = "Szöveg irás" +PythonUniform = "Lebegöpontos szám [a,b] -ban" +PythonImportTime = "time modul importálása" +PythonTimePrefix = "time funkció elötag" +PythonTimeSleep = "n másodpercet várni" +PythonMonotonic = "Meg fordítani a monoton idö" +PythonFileOpen = "Fájl megnyitása" +PythonFileSeekable = "Seek-et lehete használni" +PythonFileSeek = "A kurzort áthelyezni" +PythonFileTell = "Visszaadja a kurzor helye" +PythonFileClose = "Bezárni egy fájlt" +PythonFileClosed = "True ha a fájl bezárva" +PythonFileRead = "Olvas 16 bájtig" +PythonFileWrite = "b-t irjon a fájlba" +PythonFileReadline = "Olvas egy sort vagy 16 bájtig" +PythonFileReadlines = "Olvas több sort" +PythonFileTruncate = "A fájl átméretezése" +PythonFileWritelines = "Irjon több sort" +PythonFileName = "A fájl neve" +PythonFileMode = "A fájl nyitott módja" +PythonFileReadable = "read-et lehete használni" +PythonFileWritable = "write-ot lehete használni" +PythonImportOs = "os modul importálása" +PythonOsUname = "Rendszer informaciók" +PythonOsRemove = "Fájl törlése" +PythonOsRename = "Fájl átnevezése" +PythonOsListdir = "Fájlok listája" diff --git a/apps/code/catalog.it.i18n b/apps/code/catalog.it.i18n new file mode 100644 index 00000000000..27a7ea40845 --- /dev/null +++ b/apps/code/catalog.it.i18n @@ -0,0 +1,222 @@ +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" +PythonImportOs = "Importa modulo os" +PythonOsUname = "Ottieni informazioni sul sistema" +PythonOsRemove = "Rimuovere un file" +PythonOsRename = "Rinomina file" +PythonOsListdir = "Elenca file" +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]" +PythonImportTime = "Import time module" +PythonTimePrefix = "time module function prefix" +PythonTimeSleep = "Wait for n second" +PythonMonotonic = "Return monotonic time" +PythonFileOpen = "Opens a file" +PythonFileSeekable = "Tells if seek can be used on a file" +PythonFileSeek = "Move file's cursor" +PythonFileTell = "Get file's cursor location" +PythonFileClose = "Closes a file" +PythonFileClosed = "True if file was closed" +PythonFileRead = "Read up to size bytes" +PythonFileWrite = "Write b into file" +PythonFileReadline = "Reads a line or up to size bytes" +PythonFileReadlines = "Reads a list of lines" +PythonFileTruncate = "Resize the file to size" +PythonFileWritelines = "Writes a list of lines" +PythonFileName = "Contains file's name" +PythonFileMode = "Contains file's open mode" +PythonFileReadable = "Tells if read can be used on a file" +PythonFileWritable = "Tells if write can be used on a file" diff --git a/apps/code/catalog.nl.i18n b/apps/code/catalog.nl.i18n new file mode 100644 index 00000000000..b3ec4225723 --- /dev/null +++ b/apps/code/catalog.nl.i18n @@ -0,0 +1,222 @@ +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" +PythonImportOs = "Importeer os module" +PythonOsUname = " Krijg systeeminfo" +PythonOsRemove = "Een bestand verwijderen" +PythonOsRename = "Hernoem bestand" +PythonOsListdir = "Lijstbestanden" +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]" +PythonImportTime = "Import time module" +PythonTimePrefix = "time module function prefix" +PythonTimeSleep = "Wait for n second" +PythonMonotonic = "Return monotonic time" +PythonFileOpen = "Opens a file" +PythonFileSeekable = "Tells if seek can be used on a file" +PythonFileSeek = "Move file's cursor" +PythonFileTell = "Get file's cursor location" +PythonFileClose = "Closes a file" +PythonFileClosed = "True if file was closed" +PythonFileRead = "Read up to size bytes" +PythonFileWrite = "Write b into file" +PythonFileReadline = "Reads a line or up to size bytes" +PythonFileReadlines = "Reads a list of lines" +PythonFileTruncate = "Resize the file to size" +PythonFileWritelines = "Writes a list of lines" +PythonFileName = "Contains file's name" +PythonFileMode = "Contains file's open mode" +PythonFileReadable = "Tells if read can be used on a file" +PythonFileWritable = "Tells if write can be used on a file" diff --git a/apps/code/catalog.pt.i18n b/apps/code/catalog.pt.i18n new file mode 100644 index 00000000000..de29d19d465 --- /dev/null +++ b/apps/code/catalog.pt.i18n @@ -0,0 +1,222 @@ +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]" +PythonImportTime = "Import time module" +PythonImportOs = "Import os module" +PythonOsUname = " Obter informações do sistema" +PythonOsRemove = "Remover um ficheiro" +PythonOsRename = "Renomear ficheiro" +PythonOsListdir = "Listar ficheiros" +PythonTimePrefix = "time module function prefix" +PythonTimeSleep = "Wait for n second" +PythonMonotonic = "Return monotonic time" +PythonFileOpen = "Opens a file" +PythonFileSeekable = "Tells if seek can be used on a file" +PythonFileSeek = "Move file's cursor" +PythonFileTell = "Get file's cursor location" +PythonFileClose = "Closes a file" +PythonFileClosed = "True if file was closed" +PythonFileRead = "Read up to size bytes" +PythonFileWrite = "Write b into file" +PythonFileReadline = "Reads a line or up to size bytes" +PythonFileReadlines = "Reads a list of lines" +PythonFileTruncate = "Resize the file to size" +PythonFileWritelines = "Writes a list of lines" +PythonFileName = "Contains file's name" +PythonFileMode = "Contains file's open mode" +PythonFileReadable = "Tells if read can be used on a file" +PythonFileWritable = "Tells if write can be used on a file" diff --git a/apps/code/catalog.universal.i18n b/apps/code/catalog.universal.i18n new file mode 100644 index 00000000000..001c5834909 --- /dev/null +++ b/apps/code/catalog.universal.i18n @@ -0,0 +1,282 @@ +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" +PythonCommandImportOs = "import os" +PythonCommandImportFromOs = "from os import *" +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" +PythonOsCommandUname = "uname()" +PythonOsCommandRemove = "remove(filename)" +PythonOsCommandRename = "rename(oldname, newname)" +PythonOsCommandRemoveWithoutArg = "remove(\x11)" +PythonOsCommandRenameWithoutArg = "rename(\x11,)" +PythonOsCommandListdir = "listdir()" +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)" +PythonTurtleCommandWhite = "'white'" +PythonTurtleCommandYellow = "'yellow'" +PythonTimeModule = "time" +PythonTimeCommandImportFrom = "from time import *" +PythonTimeCommandSleep = "sleep()" +PythonTimeCommandSleepDemo = "sleep(n)" +PythonTimeCommandMonotonic = "monotonic()" +PythonCommandFileOpen = "open(name, [mode])" +PythonCommandFileOpenWithoutArg = "open(\x11)" +PythonCommandFileSeek = "file.seek(offset, [whence])" +PythonCommandFileSeekWithoutArg = ".seek(\x11)" +PythonCommandFileTell = "file.tell()" +PythonCommandFileTellWithoutArg = ".tell()" +PythonCommandFileSeekable = "file.seekable()" +PythonCommandFileSeekableWithoutArg = ".seekable()" +PythonCommandFileClose = "file.close()" +PythonCommandFileCloseWithoutArg = ".close()" +PythonCommandFileClosed = "file.closed" +PythonCommandFileClosedWithoutArg = ".closed" +PythonCommandFileRead = "file.read([size])" +PythonCommandFileReadWithoutArg = ".read(\x11)" +PythonCommandFileWrite = "file.write(b)" +PythonCommandFileWriteWithoutArg = ".write(\x11)" +PythonCommandFileReadline = "file.readline([size])" +PythonCommandFileReadlineWithoutArg = ".readline(\x11)" +PythonCommandFileReadlines = "file.readlines([hint])" +PythonCommandFileReadlinesWithoutArg = ".readlines(\x11)" +PythonCommandFileTruncate = "file.truncate([size])" +PythonCommandFileTruncateWithoutArg = ".truncate(\x11)" +PythonCommandFileWritelines = "file.writelines(lines)" +PythonCommandFileWritelinesWithoutArg = ".writelines(\x11)" +PythonCommandFileName = "file.name" +PythonCommandFileNameWithoutArg = ".name" +PythonCommandFileMode = "file.mode" +PythonCommandFileModeWithoutArg = ".mode" +PythonCommandFileReadable = "file.readable()" +PythonCommandFileReadableWithoutArg = ".readable()" +PythonCommandFileWritable = "file.writable()" +PythonCommandFileWritableWithoutArg = ".writable()" +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..c550d19f39e --- /dev/null +++ b/apps/code/console_controller.cpp @@ -0,0 +1,543 @@ +#include "console_controller.h" +#include "app.h" +#include "script.h" +#include "variable_box_controller.h" +#include +#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(Palette::CodeBackground); + 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..d4fa256f68a --- /dev/null +++ b/apps/code/console_controller.h @@ -0,0 +1,120 @@ +#ifndef CODE_CONSOLE_CONTROLLER_H +#define CODE_CONSOLE_CONTROLLER_H + +#include +#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..18f16ba5582 --- /dev/null +++ b/apps/code/console_edit_cell.h @@ -0,0 +1,46 @@ +#ifndef CODE_EDIT_CELL_H +#define CODE_EDIT_CELL_H + +#include +#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..3cad12787ad --- /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(), Palette::CodeBackground); + ctx->drawString(m_line->text(), KDPointZero, GlobalPreferences::sharedGlobalPreferences()->font(), textColor(m_line), isHighlighted()? Palette::Select : Palette::BackgroundApps); +} + +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..b6c32d6d3ea --- /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() ? Palette::CodeText : Palette::SecondaryText; + } + 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..ffc2a77685b --- /dev/null +++ b/apps/code/editor_controller.cpp @@ -0,0 +1,164 @@ +#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..2541612d5be --- /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::PrimaryText; + KDColor backgroundColor = Palette::CodeGutterViewBackground; + + 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 * Poincare::Preferences::sharedPreferences()->KDPythonFont()->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..a857fc539a2 --- /dev/null +++ b/apps/code/menu_controller.cpp @@ -0,0 +1,432 @@ +#include "menu_controller.h" +#include "app.h" +#include +#include "../apps_container.h" +#include +#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::duplicateScript(Script script) { + assert(!script.isNull()); + + // Clone here + char buffer[10]; + Script::DefaultName(buffer, 10); + + Ion::Storage::sharedStorage()->createRecordWithExtension(buffer, Code::ScriptStore::k_scriptExtension, script.value().buffer, script.value().size); + + updateAddScriptRowDisplay(); +} + +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..72bb6313013 --- /dev/null +++ b/apps/code/menu_controller.h @@ -0,0 +1,99 @@ +#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 duplicateScript(Script script); + 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