├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── data ├── Application.css ├── icons │ ├── 32 │ │ └── spreadsheet.svg │ ├── 48 │ │ └── spreadsheet.svg │ ├── 64 │ │ └── spreadsheet.svg │ └── 128 │ │ └── spreadsheet.svg ├── meson.build ├── spreadsheet.appdata.xml.in ├── spreadsheet.desktop.in ├── spreadsheet.gresource.xml └── spreadsheet.gschema.xml ├── io.github.elework.spreadsheet.yml ├── meson.build ├── po ├── LINGUAS ├── POTFILES ├── README.md ├── extra │ ├── LINGUAS │ ├── POTFILES │ ├── extra.pot │ ├── fr.po │ ├── meson.build │ └── nl.po ├── fr.po ├── io.github.elework.spreadsheet.pot ├── meson.build └── nl.po ├── screen.png └── src ├── AlphabetGenerator.vala ├── App.vala ├── Config.vala.in ├── Functions ├── Basic.vala └── Geometry.vala ├── Models ├── Cell.vala ├── CellStyle.vala ├── FontStyle.vala ├── Function.vala ├── HistoryAction.vala ├── Page.vala └── Spreadsheet.vala ├── Services ├── CSV │ ├── CSVGrammar.vala │ ├── CSVParser.vala │ └── CSVWriter.vala ├── Formula │ ├── AST │ │ ├── CallExpression.vala │ │ ├── CellReference.vala │ │ ├── Expression.vala │ │ ├── NumberExpression.vala │ │ └── TextExpression.vala │ ├── FormulaGrammar.vala │ └── FormulaParser.vala ├── FuncSearchList.vala ├── HistoryManager.vala └── Parsing │ ├── Evaluator.vala │ ├── Grammar.vala │ ├── Lexer.vala │ ├── Parser.vala │ └── Token.vala ├── UI ├── MainWindow.vala └── TitleBar.vala └── Widgets ├── ActionBar.vala ├── FunctionPresenter.vala ├── Sheet.vala └── StyleModal.vala /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig 2 | root = true 3 | 4 | # elementary defaults 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = tab 9 | indent_style = space 10 | insert_final_newline = true 11 | max_line_length = 80 12 | tab_width = 4 13 | 14 | # Markup files 15 | [{*.html,*.xml,*.xml.in,*.yml}] 16 | tab_width = 2 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | flatpak: 7 | name: Flatpak 8 | runs-on: ubuntu-22.04 9 | 10 | strategy: 11 | matrix: 12 | arch: [x86_64, aarch64] 13 | # Don't fail the whole workflow if one architecture fails 14 | fail-fast: false 15 | 16 | container: 17 | image: ghcr.io/elementary/flatpak-platform/runtime:8-${{ matrix.arch }} 18 | options: --privileged 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up QEMU for aarch64 emulation 25 | if: ${{ matrix.arch != 'x86_64' }} 26 | uses: docker/setup-qemu-action@v3 27 | with: 28 | platforms: arm64 29 | 30 | - name: Build 31 | uses: flatpak/flatpak-github-actions/flatpak-builder@v6 32 | with: 33 | bundle: spreadsheet.flatpak 34 | manifest-path: io.github.elework.spreadsheet.yml 35 | run-tests: true 36 | repository-name: appcenter 37 | repository-url: https://flatpak.elementary.io/repo.flatpakrepo 38 | cache-key: "flatpak-builder-${{ github.sha }}" 39 | arch: ${{ matrix.arch }} 40 | 41 | lint: 42 | name: Lint 43 | runs-on: ubuntu-22.04 44 | 45 | container: 46 | image: valalang/lint 47 | 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v4 51 | 52 | - name: Lint 53 | run: io.elementary.vala-lint -d . 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .flatpak-builder 3 | build* 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Baptiste Gelez 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spreadsheet 2 | 3 | Spreadsheet is a spreadsheet app built with Vala and GTK, and especially for elementary OS. 4 | 5 | ![Screenshot](screen.png) 6 | 7 | It was originally developed by [Gelez](https://github.com/elegaanz), who wrote: 8 | 9 | > One day I was lost on the Internet, I found this great [mockup](https://www.deviantart.com/bassultra/art/Spreadsheet-363147552) of a spreadsheet app, and I decided to make it real. 10 | 11 | The goal of this project is to build a spreadsheet app that perfectly fits in elementary OS. Only CSV files are supported at this moment. 12 | 13 | ## Building and Installation 14 | 15 | You'll need the following dependencies: 16 | 17 | * libgee-0.8-dev 18 | * libgranite-dev (>= 5.4.0) 19 | * libgtk-3-dev (>= 3.22) 20 | * libhandy-1-dev (>= 1.2.0) 21 | * meson (>= 0.59.0) 22 | * valac 23 | 24 | On elementary OS (or any distribution with `apt`), you can get them with the following command: 25 | 26 | sudo apt install valac libgranite-dev meson 27 | 28 | Then clone the project and go to its root directory. Run `meson build` to configure the build environment. Change to the build directory and run `ninja` to build 29 | 30 | meson build --prefix=/usr 31 | cd build 32 | ninja 33 | 34 | To install, use `ninja install`, then execute with `io.github.elework.spreadsheet` 35 | 36 | sudo ninja install 37 | io.github.elework.spreadsheet 38 | 39 | ## Contributing 40 | 41 | There are many ways you can contribute, even if you don't know how to code. 42 | 43 | ### Reporting Bugs or Suggesting Improvements 44 | 45 | Simply [create a new issue](https://github.com/elework/Spreadsheet/issues/new) describing your problem and how to reproduce or your suggestion. If you are not used to do, [this section](https://docs.elementary.io/contributor-guide/feedback/reporting-issues) is for you. 46 | 47 | ### Writing Some Code 48 | 49 | Before coding, fork the project and build it as explained above. 50 | 51 | We use Vala, as many other elementary OS apps, so it would be better if you know a bit about it, but you don't have to be an expert. 52 | 53 | Before writing some code, let the others know on what you'll be working. The best way to do that is to go to the related issue (or create one if any related issue doesn't exist yet), and to say that you are working on it. Then start a new branch on your fork, based on `master` (and be sure master is up-to-date). You can start coding. 54 | 55 | We follow the [coding style of elementary OS](https://docs.elementary.io/develop/writing-apps/code-style) and [its Human Interface Guidelines](https://docs.elementary.io/hig#human-interface-guidelines) in our code, please try to respect them. But there are two differences: 56 | 57 | * We also name our namespaces after the folder they are in (e.g. `Spreadsheet.Services.Formula.AST` is in `src/Services/Formula/AST`) 58 | * We don't put the GPL in every file, since the project is licensed under the MIT license 59 | 60 | ### Translating This App 61 | 62 | We accept translations through Pull Requests. If you're not sure how to do, [the guideline I made](po/README.md) might be helpful. 63 | -------------------------------------------------------------------------------- /data/Application.css: -------------------------------------------------------------------------------- 1 | .toggle-button { 2 | background: none; 3 | } 4 | 5 | .func-list-button { 6 | font-style: italic; 7 | font-weight: bold; 8 | } 9 | 10 | label.h4 { 11 | margin: 0; 12 | } 13 | -------------------------------------------------------------------------------- /data/icons/128/spreadsheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 37 | 41 | 52 | 53 | 55 | 57 | 61 | 65 | 66 | 68 | 72 | 76 | 80 | 84 | 85 | 87 | 91 | 95 | 96 | 98 | 102 | 106 | 107 | 109 | 113 | 117 | 121 | 122 | 124 | 128 | 132 | 133 | 143 | 153 | 162 | 171 | 181 | 189 | 191 | 195 | 199 | 200 | 209 | 210 | 212 | 213 | 215 | image/svg+xml 216 | 218 | 219 | 220 | 221 | 222 | 227 | 231 | 235 | 242 | 250 | 257 | 258 | 259 | 268 | 277 | 286 | 294 | 302 | 310 | 318 | 326 | 334 | 342 | 350 | 358 | 359 | -------------------------------------------------------------------------------- /data/icons/32/spreadsheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 37 | 41 | 52 | 53 | 55 | 57 | 61 | 65 | 66 | 75 | 77 | 81 | 85 | 86 | 95 | 97 | 101 | 105 | 109 | 113 | 114 | 124 | 126 | 130 | 134 | 135 | 145 | 147 | 151 | 155 | 156 | 165 | 167 | 171 | 175 | 179 | 180 | 188 | 189 | 191 | 192 | 194 | image/svg+xml 195 | 197 | 198 | 199 | 200 | 201 | 205 | 209 | 216 | 224 | 231 | 232 | 233 | 242 | 251 | 260 | 268 | 276 | 284 | 292 | 300 | 308 | 316 | 324 | 332 | 333 | -------------------------------------------------------------------------------- /data/icons/48/spreadsheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 37 | 41 | 52 | 53 | 55 | 57 | 61 | 65 | 66 | 75 | 77 | 81 | 85 | 86 | 95 | 97 | 101 | 105 | 109 | 113 | 114 | 124 | 126 | 130 | 134 | 135 | 145 | 147 | 151 | 155 | 156 | 158 | 162 | 166 | 170 | 171 | 180 | 188 | 189 | 191 | 192 | 194 | image/svg+xml 195 | 197 | 198 | 199 | 200 | 201 | 205 | 212 | 220 | 227 | 228 | 237 | 246 | 255 | 263 | 271 | 279 | 287 | 295 | 303 | 311 | 319 | 327 | 328 | -------------------------------------------------------------------------------- /data/icons/64/spreadsheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 39 | 50 | 54 | 55 | 57 | 59 | 63 | 67 | 68 | 77 | 79 | 83 | 87 | 88 | 97 | 99 | 103 | 107 | 111 | 115 | 116 | 126 | 128 | 132 | 136 | 137 | 147 | 149 | 153 | 157 | 158 | 160 | 164 | 168 | 172 | 173 | 182 | 190 | 191 | 193 | 194 | 196 | image/svg+xml 197 | 199 | 200 | 201 | 202 | 203 | 207 | 214 | 222 | 229 | 230 | 239 | 248 | 257 | 265 | 273 | 281 | 289 | 297 | 305 | 313 | 321 | 329 | 330 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | icon_sizes = ['32', '48', '64', '128'] 2 | 3 | install_data( 4 | file_name + '.gschema.xml', 5 | rename: meson.project_name() + '.gschema.xml', 6 | install_dir: get_option('datadir') / 'glib-2.0' / 'schemas' 7 | ) 8 | 9 | foreach i : icon_sizes 10 | install_data( 11 | 'icons' / i / file_name + '.svg', 12 | rename: meson.project_name() + '.svg', 13 | install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i / 'apps' 14 | ) 15 | endforeach 16 | 17 | i18n.merge_file( 18 | input: file_name + '.desktop.in', 19 | output: meson.project_name() + '.desktop', 20 | po_dir: meson.project_source_root() / 'po' / 'extra', 21 | type: 'desktop', 22 | install: true, 23 | install_dir: get_option('datadir') / 'applications' 24 | ) 25 | 26 | i18n.merge_file( 27 | input: file_name + '.appdata.xml.in', 28 | output: meson.project_name() + '.appdata.xml', 29 | po_dir: meson.project_source_root() / 'po' / 'extra', 30 | install: true, 31 | install_dir: get_option('datadir') / 'metainfo' 32 | ) 33 | -------------------------------------------------------------------------------- /data/spreadsheet.appdata.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | io.github.elework.spreadsheet 5 | io.github.elework.spreadsheet.desktop 6 | CC0-1.0 7 | MIT 8 | Spreadsheet 9 | Create simple and beautiful spreadsheets 10 | 11 |

12 | A spreadsheet app that lets you create simple and beautiful spreadsheets. 13 |

14 |

Features include:

15 |
    16 |
  • Keyboard shortcuts for better experience. You can use your accustomed keyboard shortcuts e.g. Ctrl + Z to undo, Ctrl + Shift + Z to redo, etc.
  • 17 |
  • Save your works automatically. Your sheets are saved automatically when created or changed.
  • 18 |
19 |
20 | 21 | 22 | https://raw.githubusercontent.com/elework/Spreadsheet/master/screen.png 23 | 24 | 25 | 26 | 27 | none 28 | none 29 | none 30 | none 31 | none 32 | none 33 | none 34 | none 35 | none 36 | none 37 | none 38 | none 39 | none 40 | none 41 | none 42 | none 43 | none 44 | none 45 | none 46 | none 47 | none 48 | mild 49 | none 50 | none 51 | none 52 | none 53 | none 54 | 55 | 56 | Spreadsheet Developers 57 | https://github.com/elework/Spreadsheet 58 | https://github.com/elework/Spreadsheet/issues 59 | https://github.com/elework/Spreadsheet/discussions 60 | 61 | 62 | #206b00 63 | #ffffff 64 | 65 |
66 | -------------------------------------------------------------------------------- /data/spreadsheet.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Spreadsheet 3 | GenericName=Spreadsheet App 4 | Comment=Create simple and beautiful spreadsheets 5 | Categories=Office;Spreadsheet; 6 | Exec=io.github.elework.spreadsheet %U 7 | Icon=io.github.elework.spreadsheet 8 | Terminal=false 9 | Type=Application 10 | Keywords=Office;Book; 11 | MimeType=text/csv; 12 | -------------------------------------------------------------------------------- /data/spreadsheet.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Application.css 5 | 6 | 7 | -------------------------------------------------------------------------------- /data/spreadsheet.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | (-1, -1) 6 | Window position 7 | Position of last closed window 8 | 9 | 10 | (800, 700) 11 | Window size 12 | Size of last closed window 13 | 14 | 15 | false 16 | Open window maximized 17 | If true, window opens maximized 18 | 19 | 20 | [] 21 | Recent files 22 | Files that are most recently opened or saved 23 | 24 | 25 | 100 26 | Zoom level 27 | Zoom level of the sheet 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /io.github.elework.spreadsheet.yml: -------------------------------------------------------------------------------- 1 | id: io.github.elework.spreadsheet 2 | runtime: io.elementary.Platform 3 | runtime-version: '8' 4 | sdk: io.elementary.Sdk 5 | command: io.github.elework.spreadsheet 6 | finish-args: 7 | - '--share=ipc' 8 | - '--socket=wayland' 9 | - '--socket=fallback-x11' 10 | - '--device=dri' 11 | - '--filesystem=xdg-documents' 12 | modules: 13 | - name: spreadsheet 14 | buildsystem: meson 15 | sources: 16 | - type: dir 17 | path: . 18 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'io.github.elework.spreadsheet', 3 | 'vala', 'c', 4 | version: '0.1.0', 5 | meson_version: '>=0.59.0' 6 | ) 7 | 8 | file_name = 'spreadsheet' 9 | 10 | i18n = import('i18n') 11 | add_global_arguments( 12 | '-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()), 13 | language:'c' 14 | ) 15 | 16 | config_data = configuration_data() 17 | config_data.set_quoted('LOCALEDIR', get_option('prefix') / get_option('localedir')) 18 | config_data.set_quoted('GETTEXT_PACKAGE', meson.project_name()) 19 | config_file = configure_file( 20 | input: 'src' / 'Config.vala.in', 21 | output: '@BASENAME@', 22 | configuration: config_data 23 | ) 24 | 25 | gnome = import('gnome') 26 | asresources = gnome.compile_resources( 27 | 'as-resources', 28 | 'data' / file_name + '.gresource.xml', 29 | source_dir: 'data', 30 | c_name: 'as' 31 | ) 32 | 33 | gnome.post_install( 34 | glib_compile_schemas: true, 35 | gtk_update_icon_cache: true, 36 | update_desktop_database: true 37 | ) 38 | 39 | sources = files( 40 | 'src' / 'AlphabetGenerator.vala', 41 | 'src' / 'App.vala', 42 | 'src' / 'Functions' / 'Basic.vala', 43 | 'src' / 'Functions' / 'Geometry.vala', 44 | 'src' / 'Models' / 'Cell.vala', 45 | 'src' / 'Models' / 'CellStyle.vala', 46 | 'src' / 'Models' / 'FontStyle.vala', 47 | 'src' / 'Models' / 'Function.vala', 48 | 'src' / 'Models' / 'HistoryAction.vala', 49 | 'src' / 'Models' / 'Page.vala', 50 | 'src' / 'Models' / 'Spreadsheet.vala', 51 | 'src' / 'Services' / 'CSV' / 'CSVGrammar.vala', 52 | 'src' / 'Services' / 'CSV' / 'CSVParser.vala', 53 | 'src' / 'Services' / 'CSV' / 'CSVWriter.vala', 54 | 'src' / 'Services' / 'Formula' / 'AST' / 'CallExpression.vala', 55 | 'src' / 'Services' / 'Formula' / 'AST' / 'CellReference.vala', 56 | 'src' / 'Services' / 'Formula' / 'AST' / 'Expression.vala', 57 | 'src' / 'Services' / 'Formula' / 'AST' / 'NumberExpression.vala', 58 | 'src' / 'Services' / 'Formula' / 'AST' / 'TextExpression.vala', 59 | 'src' / 'Services' / 'Formula' / 'FormulaGrammar.vala', 60 | 'src' / 'Services' / 'Formula' / 'FormulaParser.vala', 61 | 'src' / 'Services' / 'Parsing' / 'Evaluator.vala', 62 | 'src' / 'Services' / 'Parsing' / 'Grammar.vala', 63 | 'src' / 'Services' / 'Parsing' / 'Lexer.vala', 64 | 'src' / 'Services' / 'Parsing' / 'Parser.vala', 65 | 'src' / 'Services' / 'Parsing' / 'Token.vala', 66 | 'src' / 'Services' / 'FuncSearchList.vala', 67 | 'src' / 'Services' / 'HistoryManager.vala', 68 | 'src' / 'UI' / 'MainWindow.vala', 69 | 'src' / 'UI' / 'TitleBar.vala', 70 | 'src' / 'Widgets' / 'ActionBar.vala', 71 | 'src' / 'Widgets' / 'FunctionPresenter.vala', 72 | 'src' / 'Widgets' / 'Sheet.vala', 73 | 'src' / 'Widgets' / 'StyleModal.vala' 74 | ) 75 | 76 | executable(meson.project_name(), 77 | asresources, 78 | config_file, 79 | sources, 80 | dependencies: [ 81 | dependency('gee-0.8'), 82 | dependency('glib-2.0'), 83 | dependency('gobject-2.0'), 84 | dependency('granite', version: '>=5.4.0'), 85 | dependency('gtk+-3.0', version: '>=3.22'), 86 | dependency('libhandy-1', version: '>=1.2.0'), 87 | dependency('pango') 88 | ], 89 | install: true, 90 | link_args: ['-lm'] 91 | ) 92 | 93 | subdir('data') 94 | subdir('po') 95 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | fr 2 | nl 3 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | src/App.vala 2 | src/Models/Function.vala 3 | src/UI/MainWindow.vala 4 | src/UI/TitleBar.vala 5 | src/Widgets/ActionBar.vala 6 | src/Widgets/StyleModal.vala 7 | -------------------------------------------------------------------------------- /po/README.md: -------------------------------------------------------------------------------- 1 | # How to Add a Translation of This App 2 | 3 | ## Fork and Clone the Repository 4 | 5 | First of all, fork this repository on GitHub. Next, clone the forked repository to local: 6 | 7 | git clone https://github.com/your-username/Spreadsheet.git 8 | 9 | ## Add Your Language Code to LINGUAS Files 10 | 11 | Search for your language code (e.g. en = English, zh_CN = Chinese Simplified). See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes if needed. Then add it to `po/LINGUAS` and `po/extra/LINGUAS`. Please make sure language codes sort alphabetically. For example, if a LINGUAS file contains `ko`, `mr` and `zh_TW`, its content should be sorted like this: 12 | 13 | ko 14 | mr 15 | zh_TW 16 | 17 | ## Translate .po Files 18 | 19 | Now what you've been waiting for! Copy `po/io.github.elework.spreadsheet.pot` and name `po/.po` and copy `po/extra/extra.pot` and name `po/extra/.po`. Then translate these created .po files using a .po file editor of your choice (e.g. Poedit). The former file contains strings for the app itself and the latter is for metadata files (.appdata.xml and .desktop files). 20 | 21 | ## Commit Your Translation Works 22 | 23 | After saving the .po files, open a terminal in the folder you've cloned this repository in and type: 24 | 25 | git checkout -b add-translation 26 | 27 | Then add the .po files of your language and LINGUAS files. **Do not add other files!** 28 | 29 | git add po/LINGUAS po/extra/LINGUAS po/.po po/extra/.po 30 | 31 | Next, create a commit and push it to your cloned repository on GitHub: 32 | 33 | git commit -m "Add translation" 34 | git push origin add-translation 35 | 36 | Type your GitHub username and password if asked. 37 | 38 | Finally, open your cloned repository on GitHub, select "Compare & Pull Request", and create a new pull request. 39 | 40 | And that's all! I'll check whether there is no problem in your pull request and if so I'll approve and merge your pull request! Your translation is released every time I release a new version of the app to AppCenter, so it is not always reflected when your pull request is merged. Please be patient. 41 | 42 | # How to Update an Existing Translation 43 | 44 | You can also create a pull request that updates existing translations. In this case, you don't have to edit LINGUAS files. Open existing .po files with any .po file editor and commit them when completed. 45 | 46 | # Note 47 | 48 | * **If you find some issue (e.g. typo) in the source strings, create another pull request that fixes it. Do NOT fix it in your translation pull request.** If you don't know how to fix it, create a new issue about it. I'll fix it. 49 | * **If you would like to translate the app into multiple languages, please make separated PRs per languages.** It's not a good thing to include translations of more than 2 languages in your one pull request. 50 | * Edit and commit only `po/LINGUAS`, `po/extra/LINGUAS`, `po/.po` and `po/extra/.po`. Do NOT include other files in your pull request. 51 | 52 | # References 53 | 54 | This file was created by referring the following reference: 55 | 56 | * https://github.com/lainsce/notejot/blob/master/po/README.md 57 | -------------------------------------------------------------------------------- /po/extra/LINGUAS: -------------------------------------------------------------------------------- 1 | fr 2 | nl 3 | -------------------------------------------------------------------------------- /po/extra/POTFILES: -------------------------------------------------------------------------------- 1 | data/spreadsheet.desktop.in 2 | data/spreadsheet.appdata.xml.in 3 | -------------------------------------------------------------------------------- /po/extra/extra.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: extra\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-12-31 17:40+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: data/spreadsheet.desktop.in:3 data/spreadsheet.appdata.xml.in:8 21 | msgid "Spreadsheet" 22 | msgstr "" 23 | 24 | #: data/spreadsheet.desktop.in:4 25 | msgid "Spreadsheet App" 26 | msgstr "" 27 | 28 | #: data/spreadsheet.desktop.in:5 data/spreadsheet.appdata.xml.in:9 29 | msgid "Create simple and beautiful spreadsheets" 30 | msgstr "" 31 | 32 | #: data/spreadsheet.desktop.in:11 33 | msgid "Office;Book;" 34 | msgstr "" 35 | 36 | #: data/spreadsheet.appdata.xml.in:11 37 | msgid "" 38 | "A spreadsheet app that lets you create simple and beautiful spreadsheets." 39 | msgstr "" 40 | 41 | #: data/spreadsheet.appdata.xml.in:14 42 | msgid "Features include:" 43 | msgstr "" 44 | 45 | #: data/spreadsheet.appdata.xml.in:16 46 | msgid "" 47 | "Keyboard shortcuts for better experience. You can use your accustomed " 48 | "keyboard shortcuts e.g. Ctrl + Z to undo, Ctrl + Shift + Z to redo, etc." 49 | msgstr "" 50 | 51 | #: data/spreadsheet.appdata.xml.in:17 52 | msgid "" 53 | "Save your works automatically. Your sheets are saved automatically when " 54 | "created or changed." 55 | msgstr "" 56 | 57 | #: data/spreadsheet.appdata.xml.in:56 58 | msgid "Spreadsheet Developers" 59 | msgstr "" 60 | -------------------------------------------------------------------------------- /po/extra/fr.po: -------------------------------------------------------------------------------- 1 | # French translations for extra package. 2 | # Copyright (C) 2019 THE extra'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # NathanBnm, 2019. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: extra\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-12-31 17:40+0900\n" 11 | "PO-Revision-Date: 2019-06-22 16:01+0200\n" 12 | "Last-Translator: NathaBnm\n" 13 | "Language-Team: none\n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: data/spreadsheet.desktop.in:3 data/spreadsheet.appdata.xml.in:8 21 | msgid "Spreadsheet" 22 | msgstr "Spreadsheet" 23 | 24 | #: data/spreadsheet.desktop.in:4 25 | msgid "Spreadsheet App" 26 | msgstr "Application Spreadsheet" 27 | 28 | #: data/spreadsheet.desktop.in:5 data/spreadsheet.appdata.xml.in:9 29 | msgid "Create simple and beautiful spreadsheets" 30 | msgstr "Créez des feuilles de calcul simples et belles" 31 | 32 | #: data/spreadsheet.desktop.in:11 33 | msgid "Office;Book;" 34 | msgstr "Bureautique;Livre;" 35 | 36 | #: data/spreadsheet.appdata.xml.in:11 37 | msgid "" 38 | "A spreadsheet app that lets you create simple and beautiful spreadsheets." 39 | msgstr "" 40 | "Une application tableur qui vous permet de créer des feuilles de calcul " 41 | "simples et belles." 42 | 43 | #: data/spreadsheet.appdata.xml.in:14 44 | msgid "Features include:" 45 | msgstr "Fonctionnalités incluses :" 46 | 47 | #: data/spreadsheet.appdata.xml.in:16 48 | msgid "" 49 | "Keyboard shortcuts for better experience. You can use your accustomed " 50 | "keyboard shortcuts e.g. Ctrl + Z to undo, Ctrl + Shift + Z to redo, etc." 51 | msgstr "" 52 | "Raccourcis clavier pour une meilleure expérience. Vous pouvez utiliser vos " 53 | "raccourcis clavier habituels comme par exemple CTRL + Z pour annuler, CTRL + " 54 | "Maj + Z pour rétablir etc." 55 | 56 | #: data/spreadsheet.appdata.xml.in:17 57 | msgid "" 58 | "Save your works automatically. Your sheets are saved automatically when " 59 | "created or changed." 60 | msgstr "" 61 | "Enregistrement automatique de votre travail. Vos feuilles de calcul sont " 62 | "automatiquement enregistrées lors de leur création ou modification." 63 | 64 | #: data/spreadsheet.appdata.xml.in:56 65 | msgid "Spreadsheet Developers" 66 | msgstr "Les développeurs de Spreadsheet" 67 | 68 | #~ msgid "com.github.elework.spreadsheet" 69 | #~ msgstr "com.github.elework.spreadsheet" 70 | -------------------------------------------------------------------------------- /po/extra/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('extra', 2 | args: [ 3 | '--directory=' + meson.project_source_root(), 4 | '--from-code=UTF-8' 5 | ], 6 | install: false 7 | ) 8 | -------------------------------------------------------------------------------- /po/extra/nl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the extra package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: extra\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-12-31 17:40+0900\n" 11 | "PO-Revision-Date: 2020-09-19 13:23+0200\n" 12 | "Last-Translator: Heimen Stoffels \n" 13 | "Language-Team: \n" 14 | "Language: nl\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 2.4.1\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: data/spreadsheet.desktop.in:3 data/spreadsheet.appdata.xml.in:8 22 | msgid "Spreadsheet" 23 | msgstr "Rekenblad" 24 | 25 | #: data/spreadsheet.desktop.in:4 26 | msgid "Spreadsheet App" 27 | msgstr "Rekenbladtoepassing" 28 | 29 | #: data/spreadsheet.desktop.in:5 data/spreadsheet.appdata.xml.in:9 30 | msgid "Create simple and beautiful spreadsheets" 31 | msgstr "Maak eenvoudige, maar mooie rekenbladen" 32 | 33 | #: data/spreadsheet.desktop.in:11 34 | msgid "Office;Book;" 35 | msgstr "Kantoor;Boek;Blad;Rekenen;Excel;" 36 | 37 | #: data/spreadsheet.appdata.xml.in:11 38 | msgid "" 39 | "A spreadsheet app that lets you create simple and beautiful spreadsheets." 40 | msgstr "" 41 | "Een rekenbladtoepassing waarmee je eenvoudige, maar mooie rekenbladen kunt " 42 | "maken." 43 | 44 | #: data/spreadsheet.appdata.xml.in:14 45 | msgid "Features include:" 46 | msgstr "Kenmerken:" 47 | 48 | #: data/spreadsheet.appdata.xml.in:16 49 | msgid "" 50 | "Keyboard shortcuts for better experience. You can use your accustomed " 51 | "keyboard shortcuts e.g. Ctrl + Z to undo, Ctrl + Shift + Z to redo, etc." 52 | msgstr "" 53 | "Je kunt de bekende sneltoetsen gebruiken, zoals Ctrl + Z om ongedaan te " 54 | "maken, Ctrl + Shift + Z om opnieuw uit te voeren, etc." 55 | 56 | #: data/spreadsheet.appdata.xml.in:17 57 | msgid "" 58 | "Save your works automatically. Your sheets are saved automatically when " 59 | "created or changed." 60 | msgstr "Je werkt wordt automatisch opgeslagen als je iets maakt of bewerkt." 61 | 62 | #: data/spreadsheet.appdata.xml.in:56 63 | msgid "Spreadsheet Developers" 64 | msgstr "Rekenbladontwikkelaars" 65 | 66 | #~ msgid "com.github.elework.spreadsheet" 67 | #~ msgstr "com.github.elework.spreadsheet" 68 | -------------------------------------------------------------------------------- /po/fr.po: -------------------------------------------------------------------------------- 1 | # French translations for com.github.elework.spreadsheet package. 2 | # Copyright (C) 2019 THE com.github.elework.spreadsheet'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the com.github.elework.spreadsheet package. 4 | # NathanBnm, 2019. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: com.github.elework.spreadsheet\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-12-31 17:40+0900\n" 11 | "PO-Revision-Date: 2019-06-22 16:00+0200\n" 12 | "Last-Translator: NathanBnm\n" 13 | "Language-Team: none\n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: src/App.vala:29 21 | msgid "Add numbers" 22 | msgstr "Ajoute des nombres" 23 | 24 | #: src/App.vala:30 25 | msgid "Multiply numbers" 26 | msgstr "Multiplie des nombres" 27 | 28 | #: src/App.vala:31 29 | msgid "Divide numbers" 30 | msgstr "Divise des nombres" 31 | 32 | #: src/App.vala:32 33 | #, fuzzy 34 | msgid "Subtract numbers" 35 | msgstr "Soustrait des nombres" 36 | 37 | #: src/App.vala:33 38 | msgid "Gives the modulo of numbers" 39 | msgstr "Donne le modulo de deux nombres" 40 | 41 | #: src/App.vala:35 42 | msgid "Elevate a number to the power of a second one" 43 | msgstr "Met un nombre à la puissance d'un autre" 44 | 45 | #: src/App.vala:36 46 | msgid "The square root of a number" 47 | msgstr "La racine carrée d'un nombre" 48 | 49 | #: src/App.vala:37 50 | msgid "Rounds a number to the nearest integer" 51 | msgstr "Arrondi un nombre à l'entier le plus proche" 52 | 53 | #: src/App.vala:38 54 | msgid "Removes the decimal part of a number" 55 | msgstr "Retire la partie décimale d'un nombre" 56 | 57 | #: src/App.vala:39 58 | msgid "Return the smallest value" 59 | msgstr "Renvoie la plus petite valeur" 60 | 61 | #: src/App.vala:40 62 | msgid "Return the biggest value" 63 | msgstr "Renvoie la plus grande valeur" 64 | 65 | #: src/App.vala:41 66 | msgid "Gives the mean of a list of numbers" 67 | msgstr "Donne la moyenne d'une liste de nombres" 68 | 69 | #: src/App.vala:43 70 | #, fuzzy 71 | msgid "Gives the cosine of a number (in radians)" 72 | msgstr "Donne le cosinus d'un nombre (en radians)" 73 | 74 | #: src/App.vala:44 75 | #, fuzzy 76 | msgid "Gives the sine of an angle (in radians)" 77 | msgstr "Donne le sinus d'un angle (en radians)" 78 | 79 | #: src/App.vala:45 80 | msgid "Gives the tangent of a number (in radians)" 81 | msgstr "Donne la tangente d'un nombre (en radians)" 82 | 83 | #: src/App.vala:46 84 | #, fuzzy 85 | msgid "Gives the arc cosine of a number" 86 | msgstr "Donne l'arc cosinus d'un nombre" 87 | 88 | #: src/App.vala:47 89 | #, fuzzy 90 | msgid "Gives the arc sine of a number" 91 | msgstr "Donne l'arc sinus d'un nombre" 92 | 93 | #: src/App.vala:48 94 | #, fuzzy 95 | msgid "Gives the arc tangent of a number" 96 | msgstr "Donne l'arc tangente d'un nombre" 97 | 98 | #: src/Models/Function.vala:6 99 | msgid "No documentation" 100 | msgstr "" 101 | 102 | #: src/UI/MainWindow.vala:50 103 | msgid "Not saved yet" 104 | msgstr "Pas encore enregistré" 105 | 106 | #: src/UI/MainWindow.vala:172 src/UI/MainWindow.vala:502 107 | msgid "Spreadsheet" 108 | msgstr "Spreadsheet" 109 | 110 | #: src/UI/MainWindow.vala:172 111 | msgid "Start something new, or continue what you have been working on." 112 | msgstr "Commencez quelque chose de nouveau, ou continuez votre travail." 113 | 114 | #: src/UI/MainWindow.vala:173 115 | msgid "New Sheet" 116 | msgstr "Nouvelle feuille" 117 | 118 | #: src/UI/MainWindow.vala:173 119 | msgid "Create an empty sheet" 120 | msgstr "Créer une feuille vierge" 121 | 122 | #: src/UI/MainWindow.vala:174 123 | msgid "Open File" 124 | msgstr "Ouvrir un fichier" 125 | 126 | #: src/UI/MainWindow.vala:174 127 | msgid "Choose a saved file" 128 | msgstr "Sélectionner un fichier enregistré" 129 | 130 | #: src/UI/MainWindow.vala:240 131 | msgid "Insert functions to a selected cell" 132 | msgstr "Insérer des fonctions dans la cellule sélectionnée" 133 | 134 | #: src/UI/MainWindow.vala:244 135 | msgid "Click to insert numbers or functions to a selected cell" 136 | msgstr "" 137 | "Cliquez pour insérer des nombres ou des fonctions dans la cellule " 138 | "sélectionnée" 139 | 140 | #: src/UI/MainWindow.vala:276 141 | msgid "Search functions" 142 | msgstr "Rechercher des fonctions" 143 | 144 | #: src/UI/MainWindow.vala:304 145 | msgid "Set colors to letters in a selected cell" 146 | msgstr "Définir la couleur de police de la cellule sélectionnée" 147 | 148 | #: src/UI/MainWindow.vala:378 149 | #, c-format 150 | msgid "Untitled Spreadsheet %i" 151 | msgstr "Classeur sans titre %i" 152 | 153 | #: src/UI/MainWindow.vala:386 154 | msgid "Sheet 1" 155 | msgstr "Feuille 1" 156 | 157 | #: src/UI/MainWindow.vala:403 src/UI/TitleBar.vala:25 158 | msgid "Open a file" 159 | msgstr "Ouvrir un fichier" 160 | 161 | #: src/UI/MainWindow.vala:403 162 | msgid "_Open" 163 | msgstr "_Ouvrir" 164 | 165 | #: src/UI/MainWindow.vala:403 src/UI/MainWindow.vala:436 166 | msgid "_Cancel" 167 | msgstr "_Annuler" 168 | 169 | #: src/UI/MainWindow.vala:408 src/UI/MainWindow.vala:441 170 | msgid "CSV files" 171 | msgstr "Fichiers CSV" 172 | 173 | #: src/UI/MainWindow.vala:436 174 | msgid "Save your work" 175 | msgstr "Enregistrez votre travail" 176 | 177 | #: src/UI/MainWindow.vala:436 178 | msgid "_Save" 179 | msgstr "_Enregistrer" 180 | 181 | #: src/UI/MainWindow.vala:518 182 | #, fuzzy 183 | msgid "Recent files" 184 | msgstr "Ouvrir un fichier" 185 | 186 | #: src/UI/TitleBar.vala:17 187 | msgid "Open another window" 188 | msgstr "" 189 | 190 | #: src/UI/TitleBar.vala:33 191 | msgid "Save this file with a different name" 192 | msgstr "Enregistrer ce fichier avec un nom différent" 193 | 194 | #: src/UI/TitleBar.vala:41 195 | msgid "Redo" 196 | msgstr "Rétablir" 197 | 198 | #: src/UI/TitleBar.vala:49 199 | msgid "Undo" 200 | msgstr "Annuler" 201 | 202 | #: src/Widgets/ActionBar.vala:26 203 | msgid "Zoom in/out the sheet" 204 | msgstr "" 205 | 206 | #: src/Widgets/ActionBar.vala:33 207 | msgid "Reset to the default zoom level" 208 | msgstr "" 209 | 210 | #: src/Widgets/StyleModal.vala:4 211 | msgid "Font" 212 | msgstr "Police" 213 | 214 | #: src/Widgets/StyleModal.vala:5 215 | msgid "Cell" 216 | msgstr "Cellule" 217 | 218 | #: src/Widgets/StyleModal.vala:22 219 | msgid "Bold" 220 | msgstr "" 221 | 222 | #: src/Widgets/StyleModal.vala:28 223 | msgid "Italic" 224 | msgstr "" 225 | 226 | #: src/Widgets/StyleModal.vala:34 227 | msgid "Underline" 228 | msgstr "" 229 | 230 | #: src/Widgets/StyleModal.vala:40 231 | msgid "Strikethrough" 232 | msgstr "" 233 | 234 | #: src/Widgets/StyleModal.vala:58 235 | msgid "Set font color of a selected cell" 236 | msgstr "Définit la couleur de police de la cellule sélectionnée" 237 | 238 | #: src/Widgets/StyleModal.vala:64 239 | msgid "Reset font color of a selected cell to black" 240 | msgstr "Réinitialise la couleur de police de la cellule sélectionnée en noir" 241 | 242 | #: src/Widgets/StyleModal.vala:68 243 | msgid "Style" 244 | msgstr "" 245 | 246 | #: src/Widgets/StyleModal.vala:70 247 | msgid "Color" 248 | msgstr "Couleur" 249 | 250 | #: src/Widgets/StyleModal.vala:101 251 | msgid "Set fill color of a selected cell" 252 | msgstr "Définir la couleur de remplissage de la cellule sélectionnée" 253 | 254 | #: src/Widgets/StyleModal.vala:106 255 | msgid "Remove fill color of a selected cell" 256 | msgstr "Supprime la couleur de remplissage de la cellule sélectionnée" 257 | 258 | #: src/Widgets/StyleModal.vala:111 259 | msgid "Set stroke color of a selected cell" 260 | msgstr "Définit la couleur de bordure de la cellule sélectionnée" 261 | 262 | #: src/Widgets/StyleModal.vala:116 263 | msgid "Set the border width of a selected cell" 264 | msgstr "Définir la largeur de bordure de la cellule sélectionnée" 265 | 266 | #: src/Widgets/StyleModal.vala:121 267 | msgid "Remove stroke color of a selected cell" 268 | msgstr "Supprimer la couleur de border de la cellule sélectionnée" 269 | 270 | #: src/Widgets/StyleModal.vala:132 271 | msgid "Fill" 272 | msgstr "Remplissage" 273 | 274 | #: src/Widgets/StyleModal.vala:135 275 | msgid "Stroke" 276 | msgstr "Bordure" 277 | 278 | #~ msgid "Open Last File" 279 | #~ msgstr "Ouvrir le dernier fichier" 280 | 281 | #~ msgid "Continue working on foo.xlsx" 282 | #~ msgstr "Continuer à travailler sur foo.xlsx" 283 | 284 | #~ msgid "Create a new empty file" 285 | #~ msgstr "Créer un nouveau fichier vierge" 286 | -------------------------------------------------------------------------------- /po/io.github.elework.spreadsheet.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the io.github.elework.spreadsheet package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: io.github.elework.spreadsheet\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-12-31 17:40+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/App.vala:29 21 | msgid "Add numbers" 22 | msgstr "" 23 | 24 | #: src/App.vala:30 25 | msgid "Multiply numbers" 26 | msgstr "" 27 | 28 | #: src/App.vala:31 29 | msgid "Divide numbers" 30 | msgstr "" 31 | 32 | #: src/App.vala:32 33 | msgid "Subtract numbers" 34 | msgstr "" 35 | 36 | #: src/App.vala:33 37 | msgid "Gives the modulo of numbers" 38 | msgstr "" 39 | 40 | #: src/App.vala:35 41 | msgid "Elevate a number to the power of a second one" 42 | msgstr "" 43 | 44 | #: src/App.vala:36 45 | msgid "The square root of a number" 46 | msgstr "" 47 | 48 | #: src/App.vala:37 49 | msgid "Rounds a number to the nearest integer" 50 | msgstr "" 51 | 52 | #: src/App.vala:38 53 | msgid "Removes the decimal part of a number" 54 | msgstr "" 55 | 56 | #: src/App.vala:39 57 | msgid "Return the smallest value" 58 | msgstr "" 59 | 60 | #: src/App.vala:40 61 | msgid "Return the biggest value" 62 | msgstr "" 63 | 64 | #: src/App.vala:41 65 | msgid "Gives the mean of a list of numbers" 66 | msgstr "" 67 | 68 | #: src/App.vala:43 69 | msgid "Gives the cosine of a number (in radians)" 70 | msgstr "" 71 | 72 | #: src/App.vala:44 73 | msgid "Gives the sine of an angle (in radians)" 74 | msgstr "" 75 | 76 | #: src/App.vala:45 77 | msgid "Gives the tangent of a number (in radians)" 78 | msgstr "" 79 | 80 | #: src/App.vala:46 81 | msgid "Gives the arc cosine of a number" 82 | msgstr "" 83 | 84 | #: src/App.vala:47 85 | msgid "Gives the arc sine of a number" 86 | msgstr "" 87 | 88 | #: src/App.vala:48 89 | msgid "Gives the arc tangent of a number" 90 | msgstr "" 91 | 92 | #: src/Models/Function.vala:6 93 | msgid "No documentation" 94 | msgstr "" 95 | 96 | #: src/UI/MainWindow.vala:50 97 | msgid "Not saved yet" 98 | msgstr "" 99 | 100 | #: src/UI/MainWindow.vala:172 src/UI/MainWindow.vala:502 101 | msgid "Spreadsheet" 102 | msgstr "" 103 | 104 | #: src/UI/MainWindow.vala:172 105 | msgid "Start something new, or continue what you have been working on." 106 | msgstr "" 107 | 108 | #: src/UI/MainWindow.vala:173 109 | msgid "New Sheet" 110 | msgstr "" 111 | 112 | #: src/UI/MainWindow.vala:173 113 | msgid "Create an empty sheet" 114 | msgstr "" 115 | 116 | #: src/UI/MainWindow.vala:174 117 | msgid "Open File" 118 | msgstr "" 119 | 120 | #: src/UI/MainWindow.vala:174 121 | msgid "Choose a saved file" 122 | msgstr "" 123 | 124 | #: src/UI/MainWindow.vala:240 125 | msgid "Insert functions to a selected cell" 126 | msgstr "" 127 | 128 | #: src/UI/MainWindow.vala:244 129 | msgid "Click to insert numbers or functions to a selected cell" 130 | msgstr "" 131 | 132 | #: src/UI/MainWindow.vala:276 133 | msgid "Search functions" 134 | msgstr "" 135 | 136 | #: src/UI/MainWindow.vala:304 137 | msgid "Set colors to letters in a selected cell" 138 | msgstr "" 139 | 140 | #: src/UI/MainWindow.vala:378 141 | #, c-format 142 | msgid "Untitled Spreadsheet %i" 143 | msgstr "" 144 | 145 | #: src/UI/MainWindow.vala:386 146 | msgid "Sheet 1" 147 | msgstr "" 148 | 149 | #: src/UI/MainWindow.vala:403 src/UI/TitleBar.vala:25 150 | msgid "Open a file" 151 | msgstr "" 152 | 153 | #: src/UI/MainWindow.vala:403 154 | msgid "_Open" 155 | msgstr "" 156 | 157 | #: src/UI/MainWindow.vala:403 src/UI/MainWindow.vala:436 158 | msgid "_Cancel" 159 | msgstr "" 160 | 161 | #: src/UI/MainWindow.vala:408 src/UI/MainWindow.vala:441 162 | msgid "CSV files" 163 | msgstr "" 164 | 165 | #: src/UI/MainWindow.vala:436 166 | msgid "Save your work" 167 | msgstr "" 168 | 169 | #: src/UI/MainWindow.vala:436 170 | msgid "_Save" 171 | msgstr "" 172 | 173 | #: src/UI/MainWindow.vala:518 174 | msgid "Recent files" 175 | msgstr "" 176 | 177 | #: src/UI/TitleBar.vala:17 178 | msgid "Open another window" 179 | msgstr "" 180 | 181 | #: src/UI/TitleBar.vala:33 182 | msgid "Save this file with a different name" 183 | msgstr "" 184 | 185 | #: src/UI/TitleBar.vala:41 186 | msgid "Redo" 187 | msgstr "" 188 | 189 | #: src/UI/TitleBar.vala:49 190 | msgid "Undo" 191 | msgstr "" 192 | 193 | #: src/Widgets/ActionBar.vala:26 194 | msgid "Zoom in/out the sheet" 195 | msgstr "" 196 | 197 | #: src/Widgets/ActionBar.vala:33 198 | msgid "Reset to the default zoom level" 199 | msgstr "" 200 | 201 | #: src/Widgets/StyleModal.vala:4 202 | msgid "Font" 203 | msgstr "" 204 | 205 | #: src/Widgets/StyleModal.vala:5 206 | msgid "Cell" 207 | msgstr "" 208 | 209 | #: src/Widgets/StyleModal.vala:22 210 | msgid "Bold" 211 | msgstr "" 212 | 213 | #: src/Widgets/StyleModal.vala:28 214 | msgid "Italic" 215 | msgstr "" 216 | 217 | #: src/Widgets/StyleModal.vala:34 218 | msgid "Underline" 219 | msgstr "" 220 | 221 | #: src/Widgets/StyleModal.vala:40 222 | msgid "Strikethrough" 223 | msgstr "" 224 | 225 | #: src/Widgets/StyleModal.vala:58 226 | msgid "Set font color of a selected cell" 227 | msgstr "" 228 | 229 | #: src/Widgets/StyleModal.vala:64 230 | msgid "Reset font color of a selected cell to black" 231 | msgstr "" 232 | 233 | #: src/Widgets/StyleModal.vala:68 234 | msgid "Style" 235 | msgstr "" 236 | 237 | #: src/Widgets/StyleModal.vala:70 238 | msgid "Color" 239 | msgstr "" 240 | 241 | #: src/Widgets/StyleModal.vala:101 242 | msgid "Set fill color of a selected cell" 243 | msgstr "" 244 | 245 | #: src/Widgets/StyleModal.vala:106 246 | msgid "Remove fill color of a selected cell" 247 | msgstr "" 248 | 249 | #: src/Widgets/StyleModal.vala:111 250 | msgid "Set stroke color of a selected cell" 251 | msgstr "" 252 | 253 | #: src/Widgets/StyleModal.vala:116 254 | msgid "Set the border width of a selected cell" 255 | msgstr "" 256 | 257 | #: src/Widgets/StyleModal.vala:121 258 | msgid "Remove stroke color of a selected cell" 259 | msgstr "" 260 | 261 | #: src/Widgets/StyleModal.vala:132 262 | msgid "Fill" 263 | msgstr "" 264 | 265 | #: src/Widgets/StyleModal.vala:135 266 | msgid "Stroke" 267 | msgstr "" 268 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext(meson.project_name(), 2 | args: [ 3 | '--directory=' + meson.project_source_root(), 4 | '--from-code=UTF-8' 5 | ] 6 | ) 7 | 8 | subdir('extra') 9 | -------------------------------------------------------------------------------- /po/nl.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the com.github.elework.spreadsheet package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: com.github.elework.spreadsheet\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-12-31 17:40+0900\n" 11 | "PO-Revision-Date: 2020-09-19 13:20+0200\n" 12 | "Last-Translator: Heimen Stoffels \n" 13 | "Language-Team: \n" 14 | "Language: nl\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 2.4.1\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: src/App.vala:29 22 | msgid "Add numbers" 23 | msgstr "Getallen toevoegen" 24 | 25 | #: src/App.vala:30 26 | msgid "Multiply numbers" 27 | msgstr "Getallen vermenigvuldigen" 28 | 29 | #: src/App.vala:31 30 | msgid "Divide numbers" 31 | msgstr "Getallen delen" 32 | 33 | #: src/App.vala:32 34 | msgid "Subtract numbers" 35 | msgstr "Getallen aftrekken" 36 | 37 | #: src/App.vala:33 38 | msgid "Gives the modulo of numbers" 39 | msgstr "Toon de modulaire berekening" 40 | 41 | #: src/App.vala:35 42 | msgid "Elevate a number to the power of a second one" 43 | msgstr "Verhoog het getal van het ene met het andere" 44 | 45 | #: src/App.vala:36 46 | msgid "The square root of a number" 47 | msgstr "Toon de vierkantswortel van een getal" 48 | 49 | #: src/App.vala:37 50 | msgid "Rounds a number to the nearest integer" 51 | msgstr "Rond een getal af naar het hoogste gehele getal" 52 | 53 | #: src/App.vala:38 54 | msgid "Removes the decimal part of a number" 55 | msgstr "Verwijder het decimale gedeelte van een getal" 56 | 57 | #: src/App.vala:39 58 | msgid "Return the smallest value" 59 | msgstr "Bereken de laagste waarde" 60 | 61 | #: src/App.vala:40 62 | msgid "Return the biggest value" 63 | msgstr "Bereken de hoogste waarde" 64 | 65 | #: src/App.vala:41 66 | msgid "Gives the mean of a list of numbers" 67 | msgstr "Toon het totaal van een lijst met getallen" 68 | 69 | #: src/App.vala:43 70 | msgid "Gives the cosine of a number (in radians)" 71 | msgstr "Toon de cosinus van een getal (in straal)" 72 | 73 | #: src/App.vala:44 74 | msgid "Gives the sine of an angle (in radians)" 75 | msgstr "Toon de sinus van een hoek (in straal)" 76 | 77 | #: src/App.vala:45 78 | msgid "Gives the tangent of a number (in radians)" 79 | msgstr "Toon de tangent van een getal (in straal)" 80 | 81 | #: src/App.vala:46 82 | msgid "Gives the arc cosine of a number" 83 | msgstr "Toon de curvecosinus van een getal (in straal)" 84 | 85 | #: src/App.vala:47 86 | msgid "Gives the arc sine of a number" 87 | msgstr "Toon de curvesinus van een getal" 88 | 89 | #: src/App.vala:48 90 | msgid "Gives the arc tangent of a number" 91 | msgstr "Toon de curvetangent van een getal" 92 | 93 | #: src/Models/Function.vala:6 94 | msgid "No documentation" 95 | msgstr "" 96 | 97 | #: src/UI/MainWindow.vala:50 98 | msgid "Not saved yet" 99 | msgstr "Nog niet opgeslagen" 100 | 101 | #: src/UI/MainWindow.vala:172 src/UI/MainWindow.vala:502 102 | msgid "Spreadsheet" 103 | msgstr "Rekenblad" 104 | 105 | #: src/UI/MainWindow.vala:172 106 | msgid "Start something new, or continue what you have been working on." 107 | msgstr "Maak een nieuw werkblad of ga door met je vorige werk." 108 | 109 | #: src/UI/MainWindow.vala:173 110 | msgid "New Sheet" 111 | msgstr "Nieuw blad" 112 | 113 | #: src/UI/MainWindow.vala:173 114 | msgid "Create an empty sheet" 115 | msgstr "Voeg een blanco rekenblad toe" 116 | 117 | #: src/UI/MainWindow.vala:174 118 | msgid "Open File" 119 | msgstr "Bestand openen" 120 | 121 | #: src/UI/MainWindow.vala:174 122 | msgid "Choose a saved file" 123 | msgstr "Kies een opgeslagen bestand" 124 | 125 | #: src/UI/MainWindow.vala:240 126 | msgid "Insert functions to a selected cell" 127 | msgstr "Functies invoegen in geselecteerde cel" 128 | 129 | #: src/UI/MainWindow.vala:244 130 | msgid "Click to insert numbers or functions to a selected cell" 131 | msgstr "Klik om getallen of functies in te voegen in de geselecteerde cel" 132 | 133 | #: src/UI/MainWindow.vala:276 134 | msgid "Search functions" 135 | msgstr "Functies zoeken" 136 | 137 | #: src/UI/MainWindow.vala:304 138 | msgid "Set colors to letters in a selected cell" 139 | msgstr "Letters inkleuren in geselecteerde cel" 140 | 141 | #: src/UI/MainWindow.vala:378 142 | #, c-format 143 | msgid "Untitled Spreadsheet %i" 144 | msgstr "Naamloos rekenblad %i" 145 | 146 | #: src/UI/MainWindow.vala:386 147 | msgid "Sheet 1" 148 | msgstr "Rekenblad 1" 149 | 150 | #: src/UI/MainWindow.vala:403 src/UI/TitleBar.vala:25 151 | msgid "Open a file" 152 | msgstr "Open een bestand" 153 | 154 | #: src/UI/MainWindow.vala:403 155 | msgid "_Open" 156 | msgstr "_Openen" 157 | 158 | #: src/UI/MainWindow.vala:403 src/UI/MainWindow.vala:436 159 | msgid "_Cancel" 160 | msgstr "Ann_uleren" 161 | 162 | #: src/UI/MainWindow.vala:408 src/UI/MainWindow.vala:441 163 | msgid "CSV files" 164 | msgstr "CSV-bestanden" 165 | 166 | #: src/UI/MainWindow.vala:436 167 | msgid "Save your work" 168 | msgstr "Sla je werk op" 169 | 170 | #: src/UI/MainWindow.vala:436 171 | msgid "_Save" 172 | msgstr "Op_slaan" 173 | 174 | #: src/UI/MainWindow.vala:518 175 | #, fuzzy 176 | msgid "Recent files" 177 | msgstr "Open een bestand" 178 | 179 | #: src/UI/TitleBar.vala:17 180 | msgid "Open another window" 181 | msgstr "Nieuw venster openen" 182 | 183 | #: src/UI/TitleBar.vala:33 184 | msgid "Save this file with a different name" 185 | msgstr "Sla dit bestand op onder een andere naam" 186 | 187 | #: src/UI/TitleBar.vala:41 188 | msgid "Redo" 189 | msgstr "Opnieuw uitvoeren" 190 | 191 | #: src/UI/TitleBar.vala:49 192 | msgid "Undo" 193 | msgstr "Ongedaan maken" 194 | 195 | #: src/Widgets/ActionBar.vala:26 196 | msgid "Zoom in/out the sheet" 197 | msgstr "" 198 | 199 | #: src/Widgets/ActionBar.vala:33 200 | msgid "Reset to the default zoom level" 201 | msgstr "" 202 | 203 | #: src/Widgets/StyleModal.vala:4 204 | msgid "Font" 205 | msgstr "Lettertype" 206 | 207 | #: src/Widgets/StyleModal.vala:5 208 | msgid "Cell" 209 | msgstr "Cel" 210 | 211 | #: src/Widgets/StyleModal.vala:22 212 | msgid "Bold" 213 | msgstr "Vetgedrukt" 214 | 215 | #: src/Widgets/StyleModal.vala:28 216 | msgid "Italic" 217 | msgstr "Cursief" 218 | 219 | #: src/Widgets/StyleModal.vala:34 220 | msgid "Underline" 221 | msgstr "Onderstrepen" 222 | 223 | #: src/Widgets/StyleModal.vala:40 224 | msgid "Strikethrough" 225 | msgstr "Doorhalen" 226 | 227 | #: src/Widgets/StyleModal.vala:58 228 | msgid "Set font color of a selected cell" 229 | msgstr "Letterkleur instellen in geselecteerde cel" 230 | 231 | #: src/Widgets/StyleModal.vala:64 232 | msgid "Reset font color of a selected cell to black" 233 | msgstr "Standaard letterkleur herstellen in geselecteerde cel" 234 | 235 | #: src/Widgets/StyleModal.vala:68 236 | msgid "Style" 237 | msgstr "Stijl" 238 | 239 | #: src/Widgets/StyleModal.vala:70 240 | msgid "Color" 241 | msgstr "Kleur" 242 | 243 | #: src/Widgets/StyleModal.vala:101 244 | msgid "Set fill color of a selected cell" 245 | msgstr "Opvulkleur van de geselecteerde cel instellen" 246 | 247 | #: src/Widgets/StyleModal.vala:106 248 | msgid "Remove fill color of a selected cell" 249 | msgstr "Opvulkleur van de geselecteerde cel wissen" 250 | 251 | #: src/Widgets/StyleModal.vala:111 252 | msgid "Set stroke color of a selected cell" 253 | msgstr "Penseelkleur van de geselecteerde cel instellen" 254 | 255 | #: src/Widgets/StyleModal.vala:116 256 | msgid "Set the border width of a selected cell" 257 | msgstr "Randdikte van de geselecteerde cel instellen" 258 | 259 | #: src/Widgets/StyleModal.vala:121 260 | msgid "Remove stroke color of a selected cell" 261 | msgstr "Penseelkleur van de geselecteerde cel wissen" 262 | 263 | #: src/Widgets/StyleModal.vala:132 264 | msgid "Fill" 265 | msgstr "Opvullen" 266 | 267 | #: src/Widgets/StyleModal.vala:135 268 | msgid "Stroke" 269 | msgstr "Penseelstreek" 270 | -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elework/Spreadsheet/605b8c1d081f85a61b4ac3eab36a586695d01afb/screen.png -------------------------------------------------------------------------------- /src/AlphabetGenerator.vala: -------------------------------------------------------------------------------- 1 | using Gee; 2 | 3 | /** 4 | * Generates identifier using the alphabet and an index. 5 | * 6 | * 0 -> A 7 | * 1 -> B 8 | * ... 9 | * 25 -> Z 10 | * 26 -> AA 11 | * 27 -> AB 12 | * ... 13 | * 701 -> ZZ 14 | * 702 -> AAA 15 | * 703 -> AAB 16 | */ 17 | public class Spreadsheet.AlphabetGenerator : Object { 18 | 19 | private const string[] ALPHABET = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", 20 | "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" }; 21 | 22 | public uint limit { get; construct set; } 23 | public uint index { get; construct set; } 24 | 25 | public AlphabetGenerator (uint limit = 26, uint start_at = 0) requires (limit > start_at) { 26 | Object (limit: limit, index: start_at); 27 | } 28 | 29 | public AlphabetGenerator iterator () { 30 | return this; 31 | } 32 | 33 | public int index_of (string letters) { 34 | int res = 0; 35 | int i = 0; 36 | foreach (char letter in letters.to_utf8 ()) { 37 | int power = letters.length - i; 38 | int index_in_alphabet = new ArrayList.wrap (ALPHABET).index_of (letter.to_string ()); 39 | res += (int)Math.pow (index_in_alphabet, power); 40 | i++; 41 | } 42 | return res; 43 | } 44 | 45 | public string get_at (uint index) { 46 | if (index >= ALPHABET.length) { 47 | return get_at ((index / ALPHABET.length) - 1) + get_at (index % ALPHABET.length); 48 | } else { 49 | return ALPHABET[index]; 50 | } 51 | } 52 | 53 | public new string get () { 54 | var res = get_at (index); 55 | index++; 56 | return res; 57 | } 58 | 59 | public bool next () { 60 | return index < limit; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/App.vala: -------------------------------------------------------------------------------- 1 | using Gee; 2 | using Spreadsheet.Services; 3 | using Spreadsheet.UI; 4 | using Spreadsheet.Models; 5 | 6 | public class Spreadsheet.App : Gtk.Application { 7 | public static GLib.Settings settings; 8 | private MainWindow window; 9 | 10 | public static ArrayList functions { get; set; default = new ArrayList (); } 11 | 12 | public static int main (string[] args) { 13 | return new App ().run (args); 14 | } 15 | 16 | static construct { 17 | settings = new Settings ("io.github.elework.spreadsheet"); 18 | } 19 | 20 | construct { 21 | application_id = "io.github.elework.spreadsheet"; 22 | flags = ApplicationFlags.HANDLES_OPEN; 23 | 24 | Intl.setlocale (LocaleCategory.ALL, ""); 25 | Intl.bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); 26 | Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); 27 | Intl.textdomain (GETTEXT_PACKAGE); 28 | 29 | functions.add (new Function ("sum", Functions.sum, _("Add numbers"))); 30 | functions.add (new Function ("mul", Functions.mul, _("Multiply numbers"))); 31 | functions.add (new Function ("div", Functions.div, _("Divide numbers"))); 32 | functions.add (new Function ("sub", Functions.sub, _("Subtract numbers"))); 33 | functions.add (new Function ("mod", Functions.mod, _("Gives the modulo of numbers"))); 34 | 35 | functions.add (new Function ("pow", Functions.pow, _("Elevate a number to the power of a second one"))); 36 | functions.add (new Function ("sqrt", Functions.sqrt, _("The square root of a number"))); 37 | functions.add (new Function ("round", Functions.round, _("Rounds a number to the nearest integer"))); 38 | functions.add (new Function ("floor", Functions.floor, _("Removes the decimal part of a number"))); 39 | functions.add (new Function ("min", Functions.min, _("Return the smallest value"))); 40 | functions.add (new Function ("max", Functions.max, _("Return the biggest value"))); 41 | functions.add (new Function ("mean", Functions.mean, _("Gives the mean of a list of numbers"))); 42 | 43 | functions.add (new Function ("cos", Functions.cos, _("Gives the cosine of a number (in radians)"))); 44 | functions.add (new Function ("sin", Functions.sin, _("Gives the sine of an angle (in radians)"))); 45 | functions.add (new Function ("tan", Functions.tan, _("Gives the tangent of a number (in radians)"))); 46 | functions.add (new Function ("arccos", Functions.arccos, _("Gives the arc cosine of a number"))); 47 | functions.add (new Function ("arcsin", Functions.arcsin, _("Gives the arc sine of a number"))); 48 | functions.add (new Function ("arctan", Functions.arctan, _("Gives the arc tangent of a number"))); 49 | } 50 | 51 | protected override void open (File[] csv_files, string hint) { 52 | if (csv_files.length == 0) { 53 | return; 54 | } 55 | 56 | setup_shortcuts (); 57 | 58 | foreach (var csv_file in csv_files) { 59 | new_window (); 60 | 61 | try { 62 | var file = new Spreadsheet.Services.CSV.CSVParser.from_file (csv_file.get_path ()).parse (); 63 | window.file = file; 64 | window.header.set_buttons_visibility (true); 65 | window.show_all (); 66 | window.app_stack.set_visible_child_name ("app"); 67 | } catch (Spreadsheet.Services.Parsing.ParserError err) { 68 | debug ("Error: " + err.message); 69 | } 70 | } 71 | } 72 | 73 | protected override void activate () { 74 | new_window (); 75 | setup_shortcuts (); 76 | } 77 | 78 | private void setup_shortcuts () { 79 | var back_action = new SimpleAction ("back", null); 80 | add_action (back_action); 81 | set_accels_for_action ("app.back", {"Home"}); 82 | back_action.activate.connect (() => { 83 | var active_window = get_windows ().nth_data (0) as MainWindow; 84 | if (active_window != null && active_window.app_stack.visible_child_name == "app") { 85 | active_window.show_welcome (); 86 | } 87 | }); 88 | 89 | var new_action = new SimpleAction ("new", null); 90 | add_action (new_action); 91 | set_accels_for_action ("app.new", {"n"}); 92 | new_action.activate.connect (() => { 93 | new_window (); 94 | }); 95 | 96 | var open_action = new SimpleAction ("open", null); 97 | add_action (open_action); 98 | set_accels_for_action ("app.open", {"o"}); 99 | open_action.activate.connect (() => { 100 | var active_window = get_windows ().nth_data (0) as MainWindow; 101 | if (active_window != null) { 102 | active_window.open_sheet (); 103 | } 104 | }); 105 | 106 | var quit_action = new SimpleAction ("quit", null); 107 | add_action (quit_action); 108 | set_accels_for_action ("app.quit", {"q"}); 109 | quit_action.activate.connect (() => { 110 | var active_window = get_windows ().nth_data (0) as MainWindow; 111 | if (active_window != null) { 112 | active_window.destroy (); 113 | } 114 | }); 115 | 116 | var save_action = new SimpleAction ("save", null); 117 | add_action (save_action); 118 | set_accels_for_action ("app.save", {"s"}); 119 | save_action.activate.connect (() => { 120 | var active_window = get_windows ().nth_data (0) as MainWindow; 121 | if (active_window != null && active_window.app_stack.visible_child_name == "app") { 122 | active_window.save_sheet (); 123 | } 124 | }); 125 | 126 | var save_as_action = new SimpleAction ("save_as", null); 127 | add_action (save_as_action); 128 | set_accels_for_action ("app.save_as", {"s"}); 129 | save_as_action.activate.connect (() => { 130 | var active_window = get_windows ().nth_data (0) as MainWindow; 131 | if (active_window != null && active_window.app_stack.visible_child_name == "app") { 132 | active_window.save_as_sheet (); 133 | } 134 | }); 135 | 136 | var undo_action = new SimpleAction ("undo", null); 137 | add_action (undo_action); 138 | set_accels_for_action ("app.undo", {"z"}); 139 | undo_action.activate.connect (() => { 140 | var active_window = get_windows ().nth_data (0) as MainWindow; 141 | if (active_window != null && active_window.app_stack.visible_child_name == "app" && active_window.history_manager.can_undo ()) { 142 | active_window.undo_sheet (); 143 | } 144 | }); 145 | 146 | var redo_action = new SimpleAction ("redo", null); 147 | add_action (redo_action); 148 | set_accels_for_action ("app.redo", {"z"}); 149 | redo_action.activate.connect (() => { 150 | var active_window = get_windows ().nth_data (0) as MainWindow; 151 | if (active_window != null && active_window.app_stack.visible_child_name == "app" && active_window.history_manager.can_redo ()) { 152 | active_window.redo_sheet (); 153 | } 154 | }); 155 | 156 | var focus_expression_action = new SimpleAction ("focus_expression", null); 157 | add_action (focus_expression_action); 158 | set_accels_for_action ("app.focus_expression", {"F2"}); 159 | focus_expression_action.activate.connect (() => { 160 | var active_window = get_windows ().nth_data (0) as MainWindow; 161 | if (active_window != null && active_window.app_stack.visible_child_name == "app") { 162 | active_window.expression.grab_focus (); 163 | } 164 | }); 165 | 166 | var back_focus_action = new SimpleAction ("back_focus", null); 167 | add_action (back_focus_action); 168 | set_accels_for_action ("app.back_focus", {"Escape"}); 169 | back_focus_action.activate.connect (() => { 170 | var active_window = get_windows ().nth_data (0) as MainWindow; 171 | if (active_window != null && active_window.app_stack.visible_child_name == "app") { 172 | active_window.active_sheet.grab_focus (); 173 | active_window.expression.text = ""; 174 | } 175 | }); 176 | } 177 | 178 | public void new_window () { 179 | int window_x, window_y, window_width, window_height; 180 | settings.get ("window-position", "(ii)", out window_x, out window_y); 181 | settings.get ("window-size", "(ii)", out window_width, out window_height); 182 | var is_maximized = settings.get_boolean ("is-maximized"); 183 | 184 | if (get_windows () != null) { 185 | window = new MainWindow (this); 186 | window.move (window_x + 30, window_y + 30); 187 | } else if (window_x != -1 || window_y != -1) { // Not a first time launch 188 | window = new MainWindow (this); 189 | window.move (window_x, window_y); 190 | } else { // First time launch 191 | window = new MainWindow (this); 192 | window.window_position = Gtk.WindowPosition.CENTER; 193 | } 194 | 195 | if (is_maximized) { 196 | window.maximize (); 197 | } 198 | 199 | window.set_default_size (window_width, window_height); 200 | window.show_all (); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Config.vala.in: -------------------------------------------------------------------------------- 1 | public const string GETTEXT_PACKAGE = @GETTEXT_PACKAGE@; 2 | public const string LOCALEDIR = @LOCALEDIR@; 3 | -------------------------------------------------------------------------------- /src/Functions/Basic.vala: -------------------------------------------------------------------------------- 1 | namespace Spreadsheet.Functions { 2 | private double number (Value num) { 3 | var res = 0.0; 4 | if (num.type () == typeof (int)) { 5 | res = (double) num.get_int (); 6 | } else if (num.type () == typeof (double)) { 7 | res = (double) num; 8 | } else if (num.type () == typeof (string)) { 9 | res = double.parse ((string) num); 10 | } 11 | return res; 12 | } 13 | 14 | public Value sum (Value[] args) { 15 | double res = number (args[0]); 16 | foreach (Value num in args[1:args.length]) { 17 | res += number (num); 18 | } 19 | return res; 20 | } 21 | 22 | public Value sub (Value[] args) { 23 | double res = number (args[0]); 24 | foreach (Value num in args[1:args.length]) { 25 | res -= number (num); 26 | } 27 | return res; 28 | } 29 | 30 | public Value mul (Value[] args) { 31 | double res = number (args[0]); 32 | foreach (Value num in args[1:args.length]) { 33 | res *= number (num); 34 | } 35 | return res; 36 | } 37 | 38 | public Value div (Value[] args) { 39 | double res = number (args[0]); 40 | foreach (Value num in args[1:args.length]) { 41 | res /= number (num); 42 | } 43 | return res; 44 | } 45 | 46 | public Value mod (Value[] args) { 47 | double res = number (args[0]); 48 | foreach (Value num in args[1:args.length]) { 49 | res %= number (num); 50 | } 51 | return res; 52 | } 53 | 54 | public Value pow (Value[] args) { 55 | return Math.pow (number (args[0]), number (args[1])); 56 | } 57 | 58 | public Value sqrt (Value[] args) { 59 | return Math.sqrt (number (args[0])); 60 | } 61 | 62 | public Value round (Value[] args) { 63 | return Math.round (number (args[0])); 64 | } 65 | 66 | public Value floor (Value[] args) { 67 | return Math.floor (number (args[0])); 68 | } 69 | 70 | public Value min (Value[] args) { 71 | double min = number (args[0]); 72 | foreach (var arg in args) { 73 | if (number (arg) < min) { 74 | min = number (arg); 75 | } 76 | } 77 | return min; 78 | } 79 | 80 | public Value max (Value[] args) { 81 | double max = number (args[0]); 82 | foreach (var arg in args) { 83 | if (number (arg) > max) { 84 | max = number (arg); 85 | } 86 | } 87 | return max; 88 | } 89 | 90 | public Value mean (Value[] args) { 91 | return ((double) sum (args)) / args.length; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Functions/Geometry.vala: -------------------------------------------------------------------------------- 1 | namespace Spreadsheet.Functions { 2 | public Value cos (Value[] args) { 3 | return Math.cos (number (args[0])); 4 | } 5 | 6 | public Value sin (Value[] args) { 7 | return Math.sin (number (args[0])); 8 | } 9 | 10 | public Value tan (Value[] args) { 11 | return Math.tan (number (args[0])); 12 | } 13 | 14 | public Value arccos (Value[] args) { 15 | return Math.acos (number (args[0])); 16 | } 17 | 18 | public Value arcsin (Value[] args) { 19 | return Math.asin (number (args[0])); 20 | } 21 | 22 | public Value arctan (Value[] args) { 23 | return Math.atan (number (args[0])); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Models/Cell.vala: -------------------------------------------------------------------------------- 1 | using Spreadsheet.Services.Formula; 2 | using Spreadsheet.Services.Parsing; 3 | 4 | public class Spreadsheet.Models.Cell : Object { 5 | 6 | public weak Page page { get; set; } 7 | public int line { get; set; } 8 | public int column { get; set; } 9 | public string display_content { get; set; default = ""; } 10 | public string formula { 11 | get { 12 | return _formula; 13 | } 14 | set { 15 | _formula = value; 16 | 17 | if (_formula == "") { 18 | display_content = _formula; 19 | return; 20 | } 21 | 22 | try { 23 | var parser = new FormulaParser (new Lexer (new FormulaGrammar ()).tokenize (value)); 24 | var expression = parser.parse (); 25 | 26 | var eval = expression.eval (page); 27 | if (eval.type () == typeof (double)) { 28 | display_content = ((double)eval).to_string (); 29 | } else if (eval.type () == typeof (string)) { 30 | display_content = (string)eval; 31 | } 32 | } catch (ParserError err) { 33 | debug ("Error: " + err.message); 34 | display_content = "Error"; 35 | } 36 | } 37 | } 38 | private string _formula = ""; 39 | public bool selected { get; set; default = false; } 40 | public FontStyle font_style { get; set; default = new FontStyle (); } 41 | public CellStyle cell_style { get; set; default = new CellStyle (); } 42 | } 43 | -------------------------------------------------------------------------------- /src/Models/CellStyle.vala: -------------------------------------------------------------------------------- 1 | public class Spreadsheet.CellStyle : Object { 2 | public Gdk.RGBA background { get; set; } 3 | public Gdk.RGBA stroke { get; set; } 4 | public double stroke_width { get; set; default = 1.0; } 5 | 6 | public CellStyle () { 7 | reset_background_color (); 8 | reset_stroke_color (); 9 | } 10 | 11 | public void reset_background_color () { 12 | background = { 1, 1, 1, 1 }; 13 | } 14 | 15 | public void reset_stroke_color () { 16 | stroke = { 0, 0, 0, 1 }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Models/FontStyle.vala: -------------------------------------------------------------------------------- 1 | public class Spreadsheet.FontStyle : Object { 2 | public Gdk.RGBA fontcolor { get; set; } 3 | public bool is_bold { get; set; } 4 | public bool is_italic { get; set; } 5 | public bool is_underline { get; set; } 6 | public bool is_strikethrough { get; set; } 7 | 8 | public FontStyle () { 9 | reset_color (); 10 | } 11 | 12 | public void reset_color () { 13 | fontcolor = { 0, 0, 0, 1 }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Models/Function.vala: -------------------------------------------------------------------------------- 1 | public class Spreadsheet.Models.Function : Object { 2 | public string name { get; set; } 3 | public ApplyFunc apply { get; set; } 4 | public string doc { get; set; } 5 | 6 | public Function (string name, owned ApplyFunc func, string doc = _("No documentation")) { 7 | Object ( 8 | name: name, 9 | apply: (owned) func, 10 | doc: doc 11 | ); 12 | } 13 | } 14 | 15 | [CCode (has_target = false)] 16 | public delegate Value Spreadsheet.Models.ApplyFunc (Value[] args); 17 | -------------------------------------------------------------------------------- /src/Models/HistoryAction.vala: -------------------------------------------------------------------------------- 1 | public class Spreadsheet.Models.StateChange : Object { 2 | public G before; 3 | public G after; 4 | 5 | public StateChange (G before, G after) { 6 | this.before = before; 7 | this.after = after; 8 | } 9 | } 10 | 11 | /** 12 | * A recorded action that can be done, undone, and done again. 13 | */ 14 | public class Spreadsheet.Models.HistoryAction : Object { 15 | public delegate StateChange DoFunc (G data, Object target); 16 | public delegate void UndoFunc (G data, Object target); 17 | 18 | public DoFunc run; 19 | public UndoFunc undo; 20 | public string description { get; set; } 21 | 22 | /** 23 | * The model or widget modified by this action 24 | */ 25 | public Object target { get; set; } 26 | 27 | public StateChange changes; 28 | 29 | public HistoryAction (string desc, Object target, owned DoFunc run, owned UndoFunc undo) { 30 | Object ( 31 | description: desc, 32 | target: target 33 | ); 34 | this.run = (owned) run; 35 | this.undo = (owned) undo; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Models/Page.vala: -------------------------------------------------------------------------------- 1 | /** 2 | * A single page of a Spreadsheet 3 | */ 4 | public class Spreadsheet.Models.Page : Object { 5 | public weak SpreadSheet document { get; set; } 6 | public string title { get; set; } 7 | public Gee.ArrayList cells { get; set; default = new Gee.ArrayList (); } 8 | public int lines { get; private set; default = 0; } 9 | public int columns { get; private set; default = 0; } 10 | 11 | public Page.empty (int cols = 100, int lines = 100) { 12 | for (int i = 0; i < cols; i++) { 13 | for (int j = 0; j < lines; j++) { 14 | var cell = new Cell () { 15 | line = j, 16 | column = i 17 | }; 18 | if (i == 0 && j == 0) { 19 | cell.selected = true; 20 | } 21 | add_cell (cell); 22 | } 23 | } 24 | } 25 | 26 | public void add_cell (Cell c) { 27 | c.page = this; 28 | cells.add (c); 29 | if (c.line + 1 > lines) { 30 | lines = c.line + 1; 31 | } 32 | if (c.column + 1 > columns) { 33 | columns = c.column + 1; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Models/Spreadsheet.vala: -------------------------------------------------------------------------------- 1 | /** 2 | * Class representing a whole Spreadsheet file. 3 | */ 4 | public class Spreadsheet.Models.SpreadSheet : Object { 5 | public string title { get; set; } 6 | public string file_path { get; set; } 7 | public Gee.ArrayList pages { get; set; default = new Gee.ArrayList (); } 8 | 9 | public void add_page (Page p) { 10 | p.document = this; 11 | pages.add (p); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Services/CSV/CSVGrammar.vala: -------------------------------------------------------------------------------- 1 | using Spreadsheet.Services.Parsing; 2 | 3 | public class Spreadsheet.Services.CSV.CSVGrammar : Grammar { 4 | public CSVGrammar () { 5 | rules["root"] = root_rules (); 6 | rules["text"] = text_rules (); 7 | } 8 | 9 | private Gee.ArrayList root_rules () { 10 | return new Gee.ArrayList.wrap ({ 11 | new Evaluator (/,/, token ("comma")), 12 | new Evaluator (/\n/, token ("new-line")), 13 | new Evaluator (re ("\""), token ("quote"), false, { "text" }), 14 | new Evaluator (/./, token ("char")) 15 | }); 16 | } 17 | 18 | private Gee.ArrayList text_rules () { 19 | return new Gee.ArrayList.wrap ({ 20 | new Evaluator (/""/, (m) => { return new Token ("char", "\""); }), 21 | new Evaluator (re ("\""), token ("quote"), true), 22 | new Evaluator (/./, token ("char")), 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Services/CSV/CSVParser.vala: -------------------------------------------------------------------------------- 1 | using Spreadsheet.Services.Parsing; 2 | using Spreadsheet.Models; 3 | using Gee; 4 | 5 | public class Spreadsheet.Services.CSV.CSVParser : Parsing.Parser { 6 | 7 | private string path { get; set; } 8 | 9 | public CSVParser.from_file (string path) { 10 | try { 11 | string content; 12 | FileUtils.get_contents (path, out content); 13 | this (new Lexer (new CSVGrammar ()).tokenize (content)); 14 | this.path = path; 15 | } catch (Error err) { 16 | critical (err.message); 17 | } 18 | } 19 | 20 | public CSVParser (ArrayList tokens) { 21 | base (tokens); 22 | } 23 | 24 | public Models.SpreadSheet parse (string page_name = "Sheet 1") throws ParserError { 25 | string basepath = Path.get_basename (path); 26 | var sheet = new Models.SpreadSheet () { 27 | title = basepath, 28 | file_path = path 29 | }; 30 | var page = new Page () { 31 | title = page_name 32 | }; 33 | parse_sheet (page); 34 | sheet.add_page (page); 35 | return sheet; 36 | } 37 | 38 | public ArrayList parse_sheet (Page page) throws ParserError { 39 | var cells = new ArrayList (); 40 | int fields_count = 0; 41 | int line = 0; 42 | int col = 0; 43 | while (true) { 44 | var cell = new Cell (); 45 | cell.line = line; 46 | cell.column = col; 47 | page.add_cell (cell); 48 | cell.formula = parse_text (); 49 | 50 | if (accept ("new-line")) { 51 | if (col != fields_count) { 52 | throw new ParserError.UNEXPECTED (@"Unexpected number of fields on line $line"); 53 | } 54 | line++; 55 | col = 0; 56 | } else if (accept ("eof")) { 57 | break; 58 | } else { 59 | expect ("comma"); 60 | col++; 61 | if (line == 0) { 62 | fields_count++; 63 | } 64 | } 65 | } 66 | return cells; 67 | } 68 | 69 | private string parse_text () throws ParserError { 70 | bool quoted = accept ("quote"); 71 | string res = ""; 72 | while (current.kind == "char") { 73 | res += current.lexeme; 74 | eat (); 75 | } 76 | if (quoted) { 77 | expect ("quote"); 78 | } 79 | return res; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Services/CSV/CSVWriter.vala: -------------------------------------------------------------------------------- 1 | using Gee; 2 | using Spreadsheet.Models; 3 | 4 | public class Spreadsheet.Services.CSV.CSVWriter : Object { 5 | public Page page { get; construct set; } 6 | 7 | public CSVWriter (Page page) { 8 | Object (page: page); 9 | } 10 | 11 | public string to_string () { 12 | ArrayList> table = new ArrayList> (); 13 | int max_records = 0; 14 | foreach (var cell in page.cells) { 15 | while (table.size - 1 < cell.line) { 16 | table.add (new ArrayList ()); 17 | } 18 | var line = table[cell.line]; 19 | while (line.size - 1 < cell.column) { 20 | line.add (""); 21 | } 22 | table[cell.line][cell.column] = cell.formula; 23 | if (cell.column > max_records) { 24 | max_records = cell.column; 25 | } 26 | } 27 | string csv = ""; 28 | foreach (var line in table) { 29 | bool first = true; 30 | int records = 0; 31 | foreach (var cell in line) { 32 | if (first) { 33 | first = false; 34 | } else { 35 | csv += ","; 36 | } 37 | csv += @"\"$cell\""; 38 | records++; 39 | } 40 | while (records < max_records) { 41 | csv += ","; 42 | } 43 | csv += "\n"; 44 | } 45 | return csv; 46 | } 47 | 48 | public void write_to_file (string path) { 49 | try { 50 | FileUtils.set_contents (path, to_string ()); 51 | } catch (Error e) { 52 | critical ("Error: " + e.message); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Services/Formula/AST/CallExpression.vala: -------------------------------------------------------------------------------- 1 | using Spreadsheet; 2 | using Spreadsheet.Models; 3 | 4 | public class Spreadsheet.Services.Formula.AST.CallExpression : Expression { 5 | 6 | public string function { get; set; } 7 | public Gee.ArrayList parameters { get; set; } 8 | 9 | public CallExpression (string func, Gee.ArrayList params) { 10 | Object (function: func, parameters: params); 11 | } 12 | 13 | public override Value eval (Page sheet) { 14 | var params = new Value[] {}; 15 | foreach (var param in parameters) { 16 | params += param.eval (sheet); 17 | } 18 | 19 | foreach (var func in App.functions) { 20 | if (func.name == function) { 21 | return func.apply (params); 22 | } 23 | } 24 | return "Error: can't find any function named %s".printf (function); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Services/Formula/AST/CellReference.vala: -------------------------------------------------------------------------------- 1 | using Spreadsheet.Models; 2 | 3 | public class Spreadsheet.Services.Formula.AST.CellReference : Expression { 4 | public string cell_name { get; set; } 5 | 6 | public override Value eval (Page sheet) { 7 | string letters = cell_name; 8 | letters.canon ("ABCDEFGHIJKLMNOPQRSTUVWXYZ", '?'); 9 | string _num = cell_name; 10 | _num.canon ("0123456789", '?'); 11 | int num = int.parse (_num.replace ("?", "")) - 1; 12 | int col = new AlphabetGenerator ().index_of (letters.replace ("?", "")); 13 | int index = num + col * (int)sheet.columns; 14 | var cell = sheet.cells[index]; 15 | if (cell.line != num || cell.column != col) { 16 | warning ("Wanted cell at %s (%d, %d), got %d %d\n", cell_name, num, col, cell.line, cell.column); 17 | } 18 | return double.parse (cell.display_content); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Services/Formula/AST/Expression.vala: -------------------------------------------------------------------------------- 1 | using Spreadsheet.Models; 2 | 3 | public abstract class Spreadsheet.Services.Formula.AST.Expression : Object { 4 | public abstract Value eval (Page sheet); 5 | } 6 | -------------------------------------------------------------------------------- /src/Services/Formula/AST/NumberExpression.vala: -------------------------------------------------------------------------------- 1 | using Spreadsheet.Models; 2 | 3 | public class Spreadsheet.Services.Formula.AST.NumberExpression : Expression { 4 | 5 | private double number { get; set; } 6 | 7 | public NumberExpression (double value) { 8 | number = value; 9 | } 10 | 11 | public override Value eval (Page sheet) { 12 | return number; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Services/Formula/AST/TextExpression.vala: -------------------------------------------------------------------------------- 1 | public class Spreadsheet.Services.Formula.AST.TextExpression : Expression { 2 | public string text { get; construct; } 3 | 4 | public TextExpression (string value) { 5 | Object ( 6 | text: value 7 | ); 8 | } 9 | 10 | public override Value eval (Spreadsheet.Models.Page sheet) { 11 | return text; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Services/Formula/FormulaGrammar.vala: -------------------------------------------------------------------------------- 1 | using Spreadsheet.Services.Parsing; 2 | 3 | public class Spreadsheet.Services.Formula.FormulaGrammar : Grammar { 4 | private string func_name_regex = ""; 5 | 6 | public FormulaGrammar () { 7 | rules["root"] = root_rules (); 8 | } 9 | 10 | private string get_func_name_regex () { 11 | if (func_name_regex == "") { 12 | string[]? func_names = null; 13 | foreach (var function in App.functions) { 14 | func_names += function.name; 15 | func_names += function.name.ascii_up (); 16 | } 17 | 18 | func_name_regex = string.joinv ("|", func_names); 19 | } 20 | 21 | return func_name_regex; 22 | } 23 | 24 | private Gee.ArrayList root_rules () { 25 | return new Gee.ArrayList.wrap ({ 26 | new Evaluator (/[ \t]/, token ("[[ignore]]")), 27 | new Evaluator (/[A-Z]+[0-9]+/, token ("cell-name")), 28 | new Evaluator (/=/, token ("equal")), 29 | new Evaluator (get_func_name_regex (), token ("identifier")), 30 | new Evaluator (/\(/, token ("left-parenthese")), // vala-lint=space-before-paren 31 | new Evaluator (/\)/, token ("right-parenthese")), 32 | new Evaluator (/,/, token ("comma")), 33 | new Evaluator (/:/, token ("colon")), 34 | new Evaluator (/\d+(\.\d+)?/, token ("number")), // vala-lint=space-before-paren 35 | new Evaluator (/\+/, token ("plus")), 36 | new Evaluator (/\*/, token ("star")), 37 | new Evaluator (/-/, token ("dash")), 38 | new Evaluator (/\//, token ("slash")), 39 | new Evaluator (/%/, token ("percent")), 40 | new Evaluator (/\^/, token ("carat")), 41 | new Evaluator (/\D+/, token ("text")) 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Services/Formula/FormulaParser.vala: -------------------------------------------------------------------------------- 1 | using Gee; 2 | using Spreadsheet.Services.Formula.AST; 3 | using Spreadsheet.Services.Parsing; 4 | 5 | public class Spreadsheet.Services.Formula.FormulaParser : Parsing.Parser { 6 | public FormulaParser (ArrayList tokens) { 7 | base (tokens); 8 | } 9 | 10 | public Expression parse () throws ParserError { 11 | return parse_block (); 12 | } 13 | 14 | private Expression parse_block () throws ParserError { 15 | bool root = !accept ("left-square-brace"); 16 | var delimiter = root ? "eof" : "right-square-brace"; 17 | Expression last; 18 | 19 | while (true) { 20 | last = parse_expression (); 21 | if (current.kind == delimiter) { 22 | break; 23 | } 24 | } 25 | 26 | expect (delimiter); 27 | return last; 28 | } 29 | 30 | private Expression parse_expression () throws ParserError { 31 | return parse_substraction (); 32 | } 33 | 34 | private Expression parse_primary_expression () throws ParserError { 35 | if (current.kind == "equal") { 36 | accept ("equal"); 37 | 38 | if (current.kind == "identifier") { 39 | return parse_call_expression (); 40 | } else if (current.kind == "number") { 41 | return parse_number (); 42 | } else if (accept ("left-parenthese")) { 43 | var res = parse_expression (); 44 | expect ("right-parenthese"); 45 | return res; 46 | } else if (current.kind == "cell-name") { 47 | return parse_cell_name (); 48 | } else { 49 | unexpected (); 50 | return new NumberExpression (0.0); 51 | } 52 | } else if (current.kind == "number") { 53 | if (next.kind == "text") { 54 | return parse_text (); 55 | } 56 | 57 | return parse_number (); 58 | } else { 59 | return parse_text (); 60 | } 61 | } 62 | 63 | private Expression parse_exponent () throws ParserError { 64 | var left = parse_primary_expression (); 65 | while (accept ("carat")) { 66 | var right = parse_primary_expression (); 67 | left = new CallExpression ("pow", new ArrayList.wrap ({ left, right })); 68 | } 69 | return left; 70 | } 71 | 72 | private Expression parse_multiplication () throws ParserError { 73 | var left = parse_exponent (); 74 | while (accept ("star")) { 75 | var right = parse_exponent (); 76 | left = new CallExpression ("mul", new ArrayList.wrap ({ left, right })); 77 | } 78 | return left; 79 | } 80 | 81 | private Expression parse_division () throws ParserError { 82 | var left = parse_multiplication (); 83 | while (accept ("slash")) { 84 | var right = parse_multiplication (); 85 | left = new CallExpression ("div", new ArrayList.wrap ({ left, right })); 86 | } 87 | return left; 88 | } 89 | 90 | private Expression parse_modulo () throws ParserError { 91 | Expression left = parse_division (); 92 | while (accept ("percent")) { 93 | var right = parse_division (); 94 | left = new CallExpression ("mod", new ArrayList.wrap ({ left, right })); 95 | } 96 | return left; 97 | } 98 | 99 | private Expression parse_substraction () throws ParserError { 100 | var left = parse_addition (); 101 | while (accept ("dash")) { 102 | var right = parse_addition (); 103 | left = new CallExpression ("sub", new ArrayList.wrap ({ left, right })); 104 | } 105 | return left; 106 | } 107 | 108 | private Expression parse_addition () throws ParserError { 109 | var left = parse_modulo (); 110 | while (accept ("plus")) { 111 | var right = parse_modulo (); 112 | left = new CallExpression ("sum", new ArrayList.wrap ({ left, right })); 113 | } 114 | return left; 115 | } 116 | 117 | private CallExpression parse_call_expression () throws ParserError { 118 | var func = current.lexeme; 119 | expect ("identifier"); 120 | expect ("left-parenthese"); 121 | var params = new ArrayList (); 122 | while (true) { 123 | params.add (parse_expression ()); 124 | 125 | if (accept ("right-parenthese")) { 126 | break; 127 | } 128 | 129 | if (!accept ("comma")) { 130 | throw new ParserError.UNEXPECTED ("Use a comma to separate parameters"); 131 | } 132 | } 133 | return new CallExpression (func, params); 134 | } 135 | 136 | private NumberExpression parse_number () throws ParserError { 137 | want ("number"); 138 | NumberExpression res; 139 | if ("." in current.lexeme) { 140 | res = new NumberExpression (double.parse (current.lexeme)); 141 | } else { 142 | res = new NumberExpression (double.parse (current.lexeme + ".0")); 143 | } 144 | eat (); 145 | return res; 146 | } 147 | 148 | private TextExpression parse_text () throws ParserError { 149 | string val = ""; 150 | 151 | while (current.kind != "eof") { 152 | val += current.lexeme; 153 | eat (); 154 | } 155 | 156 | return new TextExpression (val); 157 | } 158 | 159 | private CellReference parse_cell_name () throws ParserError { 160 | var cell = new CellReference () { cell_name = current.lexeme }; 161 | expect ("cell-name"); 162 | return cell; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Services/FuncSearchList.vala: -------------------------------------------------------------------------------- 1 | public class Spreadsheet.Services.FuncSearchList : Object { 2 | public string funcsearchlist_item { get; set; } 3 | 4 | public FuncSearchList (string name, string desctiption) { 5 | funcsearchlist_item = "%s %s".printf (name, desctiption); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Services/HistoryManager.vala: -------------------------------------------------------------------------------- 1 | using Spreadsheet.Models; 2 | 3 | public class Spreadsheet.Services.HistoryManager : Object { 4 | 5 | private const int HISTORY_LIMIT = 20; // TODO: make it configurable? 6 | 7 | public Queue undo_history = new Queue (); 8 | public Queue redo_history = new Queue (); 9 | 10 | public bool can_undo () { 11 | return !undo_history.is_empty (); 12 | } 13 | 14 | public bool can_redo () { 15 | return !redo_history.is_empty (); 16 | } 17 | 18 | public void do_action (HistoryAction act) { 19 | redo_history.clear (); 20 | 21 | undo_history.push_head (act); 22 | act.changes = act.run (null, act.target); 23 | 24 | if (undo_history.get_length () > HISTORY_LIMIT) { 25 | undo_history.pop_tail (); 26 | } 27 | } 28 | 29 | public void undo () { 30 | var act = undo_history.pop_head (); 31 | act.undo (act.changes.before, act.target); 32 | redo_history.push_head (act); 33 | 34 | if (redo_history.get_length () > HISTORY_LIMIT) { 35 | redo_history.pop_tail (); 36 | } 37 | } 38 | 39 | public void redo () { 40 | var act = redo_history.pop_head (); 41 | undo_history.push_head (act); 42 | act.run (act.changes.after, act.target); 43 | 44 | if (undo_history.get_length () > HISTORY_LIMIT) { 45 | undo_history.pop_tail (); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Services/Parsing/Evaluator.vala: -------------------------------------------------------------------------------- 1 | public delegate Spreadsheet.Services.Parsing.Token Evaluation (string match); 2 | 3 | public class Spreadsheet.Services.Parsing.Evaluator : Object { 4 | 5 | public Evaluation evaluation; 6 | 7 | public Regex pattern { get; set; } 8 | 9 | public bool pop { get; set; } 10 | 11 | public string[] push { get; set; } 12 | 13 | public Evaluator (Value _re, owned Evaluation eval, bool pop = false, string[] push = {}) { 14 | Regex re = /./; 15 | if (_re.type () == typeof (string)) { 16 | try { 17 | re = new Regex ((string) _re); 18 | } catch (Error err) { 19 | assert_not_reached (); 20 | } 21 | } else if (_re.type () == typeof (Regex)) { 22 | re = (Regex) _re; 23 | } else { 24 | error ("_re should be a Regex or a string."); 25 | } 26 | 27 | pattern = re; 28 | evaluation = (owned) eval; 29 | this.pop = pop; 30 | this.push = push; 31 | } 32 | 33 | public Token eval (string expr, out int size) { 34 | MatchInfo info; 35 | if (pattern.match (expr, RegexMatchFlags.ANCHORED, out info)) { 36 | size = info.fetch (0).length; 37 | return evaluation (info.fetch (0)); 38 | } 39 | size = 0; 40 | return new Token ("[[error]]", "oops"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Services/Parsing/Grammar.vala: -------------------------------------------------------------------------------- 1 | public abstract class Spreadsheet.Services.Parsing.Grammar : Object { 2 | public Gee.HashMap> rules { 3 | get; 4 | set; 5 | default = new Gee.HashMap> (); 6 | } 7 | 8 | protected Evaluation token (string t) { 9 | string type = t; 10 | return (m) => { 11 | return new Token (type, m); 12 | }; 13 | } 14 | 15 | protected Regex re (string pattern) { 16 | try { 17 | return new Regex (pattern); 18 | } catch (Error err) { 19 | assert_not_reached (); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Services/Parsing/Lexer.vala: -------------------------------------------------------------------------------- 1 | using Gee; 2 | 3 | public class Spreadsheet.Services.Parsing.Lexer : Object { 4 | 5 | public Grammar grammar { get; construct set; } 6 | 7 | public Lexer (Grammar g) { 8 | Object (grammar: g); 9 | } 10 | 11 | public ArrayList tokenize (string _expr) { 12 | string expr = _expr.strip (); 13 | var res = new ArrayList (); 14 | var stack = new ArrayList (); 15 | stack.add ("root"); 16 | 17 | while (expr.length > 0) { // we consume all the expression 18 | var top = stack.last (); 19 | if (grammar.rules.has_key (top)) { // we check if the context exists, but it should normally always be true. 20 | bool matched = false; 21 | foreach (var eval in grammar.rules[top]) { // we try to find a matching pattern in the context 22 | int size; 23 | var tok = eval.eval (expr, out size); 24 | if (size > 0) { // it's a match! 25 | 26 | if (tok.kind != "[[ignore]]") { 27 | res.add (tok); 28 | } 29 | 30 | if (eval.pop) { 31 | stack.remove (top); 32 | } 33 | 34 | if (eval.push != null) { 35 | stack.add_all (new ArrayList.wrap (eval.push)); 36 | } 37 | 38 | expr = expr.substring (size); 39 | matched = true; 40 | break; 41 | } 42 | } 43 | 44 | if (!matched) { 45 | debug (expr); 46 | error ("Unexpected character at %d", _expr.strip ().length - expr.length); 47 | } 48 | } else { 49 | critical ("Unknown context: %s\n", top); 50 | } 51 | } 52 | 53 | res.add (new Token ("eof", "")); 54 | return res; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Services/Parsing/Parser.vala: -------------------------------------------------------------------------------- 1 | using Gee; 2 | 3 | public errordomain Spreadsheet.Services.Parsing.ParserError { 4 | UNEXPECTED, 5 | INCOMPLETE 6 | } 7 | 8 | public abstract class Spreadsheet.Services.Parsing.Parser : Object { 9 | 10 | protected ArrayList tokens { get; set; } 11 | 12 | protected int index = 0; 13 | 14 | protected Token previous { owned get {return tokens[index - 1]; } } 15 | 16 | protected Token current { owned get { return tokens[index]; } } 17 | 18 | protected Token next { owned get { return tokens[index + 1]; } } 19 | 20 | protected Parser (ArrayList tokens) { 21 | Object (tokens: tokens); 22 | } 23 | 24 | protected void eat () { 25 | index++; 26 | } 27 | 28 | /** 29 | * Eat the current token if it is from a certain type 30 | * 31 | * @return true if the token has been eaten. 32 | */ 33 | protected bool accept (string category) { 34 | if (current.kind == category) { 35 | eat (); 36 | return true; 37 | } 38 | return false; 39 | } 40 | 41 | /** 42 | * If the current token is not of a specific type, throws an error. 43 | */ 44 | protected bool expect (string cat) throws ParserError { 45 | if (accept (cat)) { 46 | return true; 47 | } 48 | throw new ParserError.UNEXPECTED (@"Expected a '$cat', got a '$(current.kind)'"); 49 | } 50 | 51 | // Like expect, but doesn't eat the token. 52 | protected bool want (string cat) throws ParserError { 53 | if (current.kind == cat) { 54 | return true; 55 | } 56 | throw new ParserError.UNEXPECTED (@"Wanted a '$cat', got a '$(current.kind)'"); 57 | } 58 | 59 | protected void unexpected () throws ParserError { 60 | throw new ParserError.UNEXPECTED (@"Unexpected '$(current.kind)'"); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Services/Parsing/Token.vala: -------------------------------------------------------------------------------- 1 | public class Spreadsheet.Services.Parsing.Token { 2 | 3 | public string kind; 4 | public string lexeme; 5 | public Token (string? k, string l) { 6 | kind = k; 7 | lexeme = l; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/UI/MainWindow.vala: -------------------------------------------------------------------------------- 1 | using Spreadsheet.Widgets; 2 | using Spreadsheet.Models; 3 | using Spreadsheet.Services; 4 | using Spreadsheet.Services.CSV; 5 | using Spreadsheet.Services.Parsing; 6 | using Granite.Widgets; 7 | using Gtk; 8 | using Gdk; 9 | 10 | public class Spreadsheet.UI.MainWindow : ApplicationWindow { 11 | public App app { get; construct; } 12 | public HistoryManager history_manager { get; private set; default = new HistoryManager (); } 13 | private uint configure_id; 14 | 15 | public TitleBar header { get; private set; } 16 | public Widgets.ActionBar action_bar { get; private set; } 17 | 18 | public Stack app_stack { get; private set; } 19 | private Button function_list_bt; 20 | public Entry expression; 21 | private ToggleButton style_toggle; 22 | private Popover style_popup; 23 | private Gtk.ListBox list_view = new Gtk.ListBox (); 24 | private Gtk.Box welcome_box; 25 | private Gtk.Box recent_widgets_box; 26 | 27 | private Hdy.TabView tab_view = new Hdy.TabView (); 28 | 29 | public Sheet active_sheet { 30 | get { 31 | ScrolledWindow scroll = (ScrolledWindow)tab_view.selected_page.child; 32 | Viewport vp = (Viewport)scroll.get_child (); 33 | return (Sheet)vp.get_child (); 34 | } 35 | } 36 | 37 | private SpreadSheet _file; 38 | public SpreadSheet file { 39 | get { 40 | return _file; 41 | } 42 | set { 43 | _file = value; 44 | 45 | string? display_path = value.file_path; 46 | if (GLib.Environment.get_home_dir () in display_path) { 47 | display_path = display_path.replace (GLib.Environment.get_home_dir (), "~"); 48 | } 49 | 50 | header.set_titles (value.title, display_path == null ? _("Not saved yet") : display_path); 51 | 52 | while (tab_view.n_pages > 0) { 53 | tab_view.close_page (tab_view.get_nth_page (0)); 54 | } 55 | 56 | Sheet? last_sheet = null; 57 | foreach (var page in value.pages) { 58 | var scrolled = new Gtk.ScrolledWindow (null, null); 59 | var viewport = new Gtk.Viewport (null, null); 60 | viewport.set_size_request (tab_view.get_allocated_width (), tab_view.get_allocated_height ()); 61 | scrolled.add (viewport); 62 | 63 | var sheet = new Sheet (page, this); 64 | foreach (var cell in page.cells) { 65 | style_popup.foreach ((ch) => { 66 | style_popup.remove (ch); 67 | }); 68 | if (cell.selected) { 69 | style_popup.add (new StyleModal (cell.font_style, cell.cell_style)); 70 | break; 71 | } 72 | } 73 | sheet.selection_changed.connect ((cell) => { 74 | style_popup.foreach ((ch) => { 75 | style_popup.remove (ch); 76 | }); 77 | if (cell != null) { 78 | expression.text = cell.formula; 79 | function_list_bt.sensitive = true; 80 | expression.sensitive = true; 81 | style_toggle.sensitive = true; 82 | style_popup.add (new StyleModal (cell.font_style, cell.cell_style)); 83 | } else { 84 | expression.text = ""; 85 | function_list_bt.sensitive = false; 86 | expression.sensitive = false; 87 | style_toggle.sensitive = false; 88 | } 89 | }); 90 | sheet.focus_expression_entry.connect ((input) => { 91 | if (input != null) { 92 | expression.text += input; 93 | } 94 | expression.grab_focus_without_selecting (); 95 | expression.move_cursor (Gtk.MovementStep.BUFFER_ENDS, expression.text.length, false); 96 | }); 97 | 98 | sheet.selection_cleared.connect (() => { 99 | clear_formula (); 100 | }); 101 | 102 | viewport.add (sheet); 103 | last_sheet = sheet; 104 | 105 | var tabpage = tab_view.append (scrolled); 106 | tabpage.title = page.title; 107 | } 108 | last_sheet.grab_focus (); 109 | } 110 | } 111 | 112 | public MainWindow (App app) { 113 | Object ( 114 | application: app, 115 | app: app 116 | ); 117 | } 118 | 119 | construct { 120 | var cssprovider = new Gtk.CssProvider (); 121 | cssprovider.load_from_resource ("/io/github/elework/spreadsheet/Application.css"); 122 | Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), 123 | cssprovider, 124 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 125 | 126 | app_stack = new Stack (); 127 | app_stack.add_named (welcome (), "welcome"); 128 | app_stack.add_named (sheet (), "app"); 129 | 130 | header = new TitleBar (this); 131 | set_titlebar (header); 132 | 133 | add (app_stack); 134 | show_welcome (); 135 | 136 | // Follow elementary OS-wide dark preference 137 | var granite_settings = Granite.Settings.get_default (); 138 | var gtk_settings = Gtk.Settings.get_default (); 139 | 140 | gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; 141 | 142 | granite_settings.notify["prefers-color-scheme"].connect (() => { 143 | gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; 144 | }); 145 | } 146 | 147 | protected override bool configure_event (Gdk.EventConfigure event) { 148 | if (configure_id != 0) { 149 | GLib.Source.remove (configure_id); 150 | } 151 | 152 | configure_id = Timeout.add (100, () => { 153 | configure_id = 0; 154 | 155 | Spreadsheet.App.settings.set_boolean ("is-maximized", is_maximized); 156 | 157 | if (!is_maximized) { 158 | int x, y, w, h; 159 | get_position (out x, out y); 160 | get_size (out w, out h); 161 | Spreadsheet.App.settings.set ("window-position", "(ii)", x, y); 162 | Spreadsheet.App.settings.set ("window-size", "(ii)", w, h); 163 | } 164 | 165 | return false; 166 | }); 167 | 168 | return base.configure_event (event); 169 | } 170 | 171 | private Gtk.Box welcome () { 172 | var welcome = new Welcome (_("Spreadsheet"), _("Start something new, or continue what you have been working on.")); 173 | welcome.append ("document-new", _("New Sheet"), _("Create an empty sheet")); 174 | welcome.append ("document-open", _("Open File"), _("Choose a saved file")); 175 | welcome.activated.connect ((index) => { 176 | if (index == 0) { 177 | new_sheet (); 178 | } else if (index == 1) { 179 | open_sheet (); 180 | } 181 | }); 182 | 183 | welcome_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); 184 | welcome_box.get_style_context ().add_class (Gtk.STYLE_CLASS_VIEW); 185 | welcome_box.pack_start (welcome); 186 | 187 | return welcome_box; 188 | } 189 | 190 | private void update_listview () { 191 | foreach (var item in list_view.get_children ()) { 192 | item.destroy (); 193 | } 194 | 195 | var recent_files = Spreadsheet.App.settings.get_strv ("recent-files"); 196 | string[]? new_recent_files = null; 197 | 198 | foreach (var file_name in recent_files) { 199 | var file = File.new_for_path (file_name); 200 | if (file.query_exists ()) { 201 | var basename = file.get_basename (); 202 | var path = file.get_path (); 203 | string display_path = path; 204 | if (GLib.Environment.get_home_dir () in path) { 205 | display_path = path.replace (GLib.Environment.get_home_dir (), "~"); 206 | } 207 | 208 | // IconSize.DIALOG because it's 48px, just like WelcomeButton needs 209 | var spreadsheet_icon = new Gtk.Image.from_icon_name ("x-office-spreadsheet", Gtk.IconSize.DIALOG); 210 | 211 | var list_item = new Granite.Widgets.WelcomeButton (spreadsheet_icon, basename, display_path); 212 | list_item.clicked.connect (() => { 213 | try { 214 | this.file = new CSVParser.from_file (path).parse (); 215 | header.set_buttons_visibility (true); 216 | show_all (); 217 | app_stack.set_visible_child_name ("app"); 218 | add_recents (path); 219 | } catch (ParserError err) { 220 | debug ("Error: " + err.message); 221 | } 222 | }); 223 | new_recent_files += file_name; 224 | list_view.add (list_item); 225 | } else { 226 | /* In case the file doesn't exist, display a list item, but 227 | mark the file as missing? */ 228 | } 229 | } 230 | 231 | Spreadsheet.App.settings.set_strv ("recent-files", new_recent_files); 232 | } 233 | 234 | private Grid toolbar () { 235 | var toolbar = new Grid (); 236 | toolbar.border_width = 10; 237 | toolbar.column_spacing = 10; 238 | 239 | function_list_bt = new Button.with_label ("f(x)"); 240 | function_list_bt.get_style_context ().add_class ("func-list-button"); 241 | function_list_bt.tooltip_text = _("Insert functions to a selected cell"); 242 | 243 | expression = new Entry (); 244 | expression.hexpand = true; 245 | expression.tooltip_text = _("Click to insert numbers or functions to a selected cell"); 246 | 247 | var popup = new Popover (function_list_bt); 248 | popup.width_request = 320; 249 | popup.height_request = 600; 250 | popup.modal = true; 251 | popup.position = PositionType.BOTTOM; 252 | popup.border_width = 10; 253 | 254 | var function_list = new ListBox (); 255 | var functions_liststore = new GLib.ListStore (Type.OBJECT); 256 | foreach (var func in App.functions) { 257 | functions_liststore.append (new FuncSearchList (func.name, func.doc)); 258 | 259 | var row = new ListBoxRow (); 260 | row.selectable = false; 261 | row.margin_top = 3; 262 | row.margin_bottom = 3; 263 | row.add (new FunctionPresenter (func)); 264 | row.realize.connect (() => { 265 | row.get_window ().cursor = new Cursor.from_name (row.get_display (), "pointer"); 266 | }); 267 | row.button_press_event.connect ((evt) => { 268 | expression.text += ")"; 269 | expression.buffer.insert_text (expression.get_position (), (func.name + "(").data); 270 | return true; 271 | }); 272 | function_list.add (row); 273 | } 274 | 275 | var function_list_search_entry = new SearchEntry (); 276 | function_list_search_entry.margin_bottom = 6; 277 | function_list_search_entry.placeholder_text = _("Search functions"); 278 | 279 | var function_list_scrolled = new ScrolledWindow (null, null); 280 | function_list_scrolled.expand = true; 281 | function_list_scrolled.add (function_list); 282 | 283 | var function_list_grid = new Grid (); 284 | function_list_grid.orientation = Orientation.HORIZONTAL; 285 | function_list_grid.attach (function_list_search_entry, 0, 0, 1, 1); 286 | function_list_grid.attach (function_list_scrolled, 0, 1, 1, 1); 287 | 288 | popup.add (function_list_grid); 289 | 290 | function_list_bt.clicked.connect (popup.show_all); 291 | 292 | expression.activate.connect (update_formula); 293 | 294 | function_list.set_filter_func ((list_box_row) => { 295 | var item = (FuncSearchList) functions_liststore.get_item (list_box_row.get_index ()); 296 | return function_list_search_entry.text.down () in item.funcsearchlist_item.down (); 297 | }); 298 | 299 | function_list_search_entry.search_changed.connect (() => { 300 | function_list.invalidate_filter (); 301 | }); 302 | 303 | style_toggle = new ToggleButton.with_label ("Open Sans 14"); 304 | style_toggle.get_style_context ().add_class ("toggle-button"); 305 | style_toggle.tooltip_text = _("Set colors to letters in a selected cell"); 306 | bool resized = false; 307 | style_toggle.draw.connect ((cr) => { // draw the color rectangle on the right of the style button 308 | int spacing = 10; 309 | int padding = 5; 310 | int border = get_style_context ().get_border (StateFlags.NORMAL).left; 311 | int square_size = style_toggle.get_allocated_height () - (border * 2); 312 | int width = style_toggle.get_allocated_width (); 313 | 314 | if (!resized) { 315 | style_toggle.get_child ().halign = Gtk.Align.START; 316 | style_toggle.width_request += width + spacing + square_size + border; // some space for the color icon 317 | resized = true; 318 | } 319 | 320 | cr.set_source_rgb (0, 0, 0); 321 | draw_rounded_path (cr, width - (border + square_size - padding), border + padding, square_size - (padding * 2), square_size - (padding * 2), 2); 322 | cr.fill (); 323 | return false; 324 | }); 325 | 326 | style_popup = new Popover (style_toggle); 327 | style_popup.modal = true; 328 | style_popup.position = PositionType.BOTTOM; 329 | style_popup.border_width = 10; 330 | 331 | style_toggle.toggled.connect (() => { 332 | if (style_toggle.active) { 333 | style_popup.show_all (); 334 | } 335 | }); 336 | style_popup.closed.connect (() => { 337 | style_toggle.active = false; 338 | }); 339 | 340 | toolbar.attach (function_list_bt, 0, 0, 1, 1); 341 | toolbar.attach (expression, 1, 0); 342 | toolbar.add (style_toggle); 343 | return toolbar; 344 | } 345 | 346 | private Box sheet () { 347 | action_bar = new Widgets.ActionBar (); 348 | 349 | // TODO: Create new sheet on click 350 | var new_tab_button = new Gtk.Button.from_icon_name ("list-add-symbolic"); 351 | 352 | var tab_bar = new Hdy.TabBar () { 353 | view = tab_view, 354 | autohide = false, 355 | expand_tabs = false, 356 | inverted = true, 357 | start_action_widget = new_tab_button 358 | }; 359 | 360 | var layout = new Box (Orientation.VERTICAL, 0); 361 | layout.homogeneous = false; 362 | layout.pack_start (toolbar (), false); 363 | layout.pack_start (tab_bar, false); 364 | layout.pack_start (tab_view); 365 | layout.pack_end (action_bar, false); 366 | return layout; 367 | } 368 | 369 | private void new_sheet () { 370 | int id = 1; 371 | string file_name = ""; 372 | string suffix = ""; 373 | string documents = ""; 374 | File? path = null; 375 | 376 | header.set_buttons_visibility (true); 377 | 378 | do { 379 | file_name = _("Untitled Spreadsheet %i").printf (id++); 380 | suffix = ".csv"; 381 | 382 | documents = Environment.get_user_special_dir (UserDirectory.DOCUMENTS); 383 | path = File.new_for_path ("%s/%s%s".printf (documents, file_name, suffix)); 384 | } while (path.query_exists ()); 385 | 386 | var page = new Page.empty (); 387 | page.title = _("Sheet 1"); 388 | 389 | var file = new SpreadSheet (); 390 | file.title = file_name; 391 | file.file_path = path.get_path (); 392 | file.add_page (page); 393 | this.file = file; 394 | 395 | show_all (); 396 | save_sheet (); 397 | 398 | app_stack.set_visible_child_name ("app"); 399 | id++; 400 | } 401 | 402 | public void open_sheet () { 403 | var chooser = new FileChooserNative ( 404 | _("Open a file"), this, FileChooserAction.OPEN, _("_Open"), _("_Cancel") 405 | ); 406 | 407 | Gtk.FileFilter filter = new Gtk.FileFilter (); 408 | filter.add_pattern ("*.csv"); 409 | filter.set_filter_name (_("CSV files")); 410 | chooser.add_filter (filter); 411 | 412 | if (chooser.run () == ResponseType.ACCEPT) { 413 | try { 414 | file = new CSVParser.from_file (chooser.get_filename ()).parse (); 415 | } catch (ParserError err) { 416 | debug ("Error: " + err.message); 417 | } 418 | 419 | add_recents (file.file_path); 420 | header.set_buttons_visibility (true); 421 | app_stack.set_visible_child_name ("app"); 422 | show_all (); 423 | } 424 | 425 | chooser.destroy (); 426 | } 427 | 428 | // Triggered when an opened sheet is modified 429 | public void save_sheet () { 430 | new CSVWriter (active_sheet.page).write_to_file (file.file_path); 431 | add_recents (file.file_path); 432 | } 433 | 434 | public void save_as_sheet () { 435 | string path = ""; 436 | var chooser = new FileChooserNative ( 437 | _("Save your work"), this, FileChooserAction.SAVE, _("_Save"), _("_Cancel") 438 | ); 439 | 440 | Gtk.FileFilter filter = new Gtk.FileFilter (); 441 | filter.add_pattern ("*.csv"); 442 | filter.set_filter_name (_("CSV files")); 443 | chooser.add_filter (filter); 444 | chooser.do_overwrite_confirmation = true; 445 | 446 | if (chooser.run () == ResponseType.ACCEPT) { 447 | path = chooser.get_filename (); 448 | if (!path.has_suffix (".csv")) { 449 | path += ".csv"; 450 | } 451 | 452 | new CSVWriter (active_sheet.page).write_to_file (path); 453 | add_recents (path); 454 | 455 | // Open the saved file 456 | try { 457 | file = new CSVParser.from_file (path).parse (); 458 | } catch (ParserError err) { 459 | debug ("Error: " + err.message); 460 | } 461 | } 462 | 463 | chooser.destroy (); 464 | } 465 | 466 | private void add_recents (string recent_file_path) { 467 | var recents = Spreadsheet.App.settings.get_strv ("recent-files"); 468 | 469 | const int MAX_FILE_COUNT = 20; 470 | 471 | /* Create a new array, append the most recent one at the start, and 472 | then store all of the previous recent files except the most 473 | recent one. */ 474 | var new_recents = new Array (); 475 | new_recents.insert_val (0, recent_file_path); 476 | 477 | foreach (var recent in recents) { 478 | if (new_recents.length >= MAX_FILE_COUNT) { 479 | break; 480 | } 481 | 482 | if (recent != recent_file_path) { 483 | new_recents.append_val (recent); 484 | } 485 | } 486 | 487 | Spreadsheet.App.settings.set_strv ("recent-files", new_recents.data); 488 | update_listview (); 489 | } 490 | 491 | public void undo_sheet () { 492 | history_manager.undo (); 493 | header.update_header (); 494 | } 495 | 496 | public void redo_sheet () { 497 | history_manager.redo (); 498 | header.update_header (); 499 | } 500 | 501 | public void show_welcome () { 502 | header.set_buttons_visibility (false); 503 | header.set_titles (_("Spreadsheet"), null); 504 | expression.text = ""; 505 | 506 | app_stack.set_visible_child_name ("welcome"); 507 | 508 | if (Spreadsheet.App.settings.get_strv ("recent-files").length != 0) { 509 | if (recent_widgets_box == null) { 510 | welcome_box.pack_start (create_recents_view ()); 511 | } 512 | 513 | update_listview (); 514 | recent_widgets_box.show_all (); 515 | } 516 | } 517 | 518 | private Gtk.Box create_recents_view () { 519 | var title = new Gtk.Label (_("Recent files")); 520 | title.halign = Gtk.Align.CENTER; 521 | title.margin = 24; 522 | title.get_style_context ().add_class (Granite.STYLE_CLASS_H2_LABEL); 523 | 524 | var recent_files_scrolled = new Gtk.ScrolledWindow (null, null); 525 | recent_files_scrolled.hscrollbar_policy = Gtk.PolicyType.NEVER; 526 | recent_files_scrolled.halign = Gtk.Align.CENTER; 527 | recent_files_scrolled.add (list_view); 528 | 529 | var recent_files_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); 530 | recent_files_box.margin = 12; 531 | recent_files_box.pack_start (title, false, false); 532 | recent_files_box.pack_start (recent_files_scrolled); 533 | 534 | recent_widgets_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); 535 | recent_widgets_box.pack_start (new Gtk.Separator (Gtk.Orientation.VERTICAL), false); 536 | recent_widgets_box.pack_start (recent_files_box); 537 | 538 | var privacy_settings = new GLib.Settings ("org.gnome.desktop.privacy"); 539 | privacy_settings.bind ("remember-recent-files", recent_widgets_box, "visible", GLib.SettingsBindFlags.DEFAULT); 540 | 541 | return recent_widgets_box; 542 | } 543 | 544 | private void update_formula () { 545 | if (active_sheet.selected_cell != null) { 546 | history_manager.do_action (new HistoryAction ( 547 | @"Change the formula to $(expression.text)", 548 | active_sheet.selected_cell, 549 | (_text, _target) => { 550 | string text = _text == null ? expression.text : (string)_text; 551 | Cell target = (Cell)_target; 552 | 553 | string last_text = target.formula; 554 | target.formula = text; 555 | 556 | var undo_data = last_text; 557 | return new StateChange (undo_data, text); 558 | }, 559 | (_text, _target) => { 560 | string text = (string)_text; 561 | Cell target = (Cell)_target; 562 | 563 | target.formula = text; 564 | expression.text = text; 565 | } 566 | )); 567 | } 568 | header.update_header (); 569 | active_sheet.move_bottom (); 570 | active_sheet.grab_focus (); 571 | } 572 | 573 | private void clear_formula () { 574 | if (active_sheet.selected_cell != null) { 575 | history_manager.do_action (new HistoryAction ( 576 | "Clear the formula", 577 | active_sheet.selected_cell, 578 | (_text, _target) => { 579 | Cell target = (Cell)_target; 580 | string undo_data = target.formula; 581 | target.formula = ""; 582 | expression.text = ""; 583 | return new StateChange (undo_data, ""); 584 | }, 585 | (_text, _target) => { 586 | string text = (string)_text; 587 | Cell target = (Cell)_target; 588 | 589 | target.formula = text; 590 | expression.text = text; 591 | } 592 | )); 593 | } 594 | header.update_header (); 595 | active_sheet.grab_focus (); 596 | } 597 | 598 | // From http://stackoverflow.com/questions/4183546/how-can-i-draw-image-with-rounded-corners-in-cairo-gtk 599 | private void draw_rounded_path (Cairo.Context ctx, double x, double y, double width, double height, double radius) { 600 | double degrees = Math.PI / 180.0; 601 | 602 | ctx.new_sub_path (); 603 | ctx.arc (x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees); 604 | ctx.arc (x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees); 605 | ctx.arc (x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees); 606 | ctx.arc (x + radius, y + radius, radius, 180 * degrees, 270 * degrees); 607 | ctx.close_path (); 608 | } 609 | } 610 | -------------------------------------------------------------------------------- /src/UI/TitleBar.vala: -------------------------------------------------------------------------------- 1 | public class Spreadsheet.UI.TitleBar : Gtk.HeaderBar { 2 | public MainWindow window { get; construct; } 3 | 4 | private Gtk.ToolButton undo_button; 5 | private Gtk.ToolButton redo_button; 6 | 7 | public TitleBar (MainWindow window) { 8 | Object ( 9 | window: window, 10 | show_close_button: true 11 | ); 12 | } 13 | 14 | construct { 15 | var file_ico = new Gtk.Image.from_icon_name ("window-new", Gtk.IconSize.SMALL_TOOLBAR); 16 | var file_button = new Gtk.ToolButton (file_ico, null); 17 | file_button.tooltip_markup = Granite.markup_accel_tooltip ({"N"}, _("Open another window")); 18 | file_button.clicked.connect (() => { 19 | ((Spreadsheet.App) GLib.Application.get_default ()).new_window (); 20 | }); 21 | pack_start (file_button); 22 | 23 | var open_ico = new Gtk.Image.from_icon_name ("document-open", Gtk.IconSize.SMALL_TOOLBAR); 24 | var open_button = new Gtk.ToolButton (open_ico, null); 25 | open_button.tooltip_markup = Granite.markup_accel_tooltip ({"O"}, _("Open a file")); 26 | open_button.clicked.connect (() => { 27 | window.open_sheet (); 28 | }); 29 | pack_start (open_button); 30 | 31 | var save_as_ico = new Gtk.Image.from_icon_name ("document-save-as", Gtk.IconSize.SMALL_TOOLBAR); 32 | var save_as_button = new Gtk.ToolButton (save_as_ico, null); 33 | save_as_button.tooltip_markup = Granite.markup_accel_tooltip ({"S"}, _("Save this file with a different name")); 34 | save_as_button.clicked.connect (() => { 35 | window.save_as_sheet (); 36 | }); 37 | pack_start (save_as_button); 38 | 39 | var redo_ico = new Gtk.Image.from_icon_name ("edit-redo", Gtk.IconSize.SMALL_TOOLBAR); 40 | redo_button = new Gtk.ToolButton (redo_ico, null); 41 | redo_button.tooltip_markup = Granite.markup_accel_tooltip ({"Z"}, _("Redo")); 42 | redo_button.clicked.connect (() => { 43 | window.redo_sheet (); 44 | }); 45 | pack_end (redo_button); 46 | 47 | var undo_ico = new Gtk.Image.from_icon_name ("edit-undo", Gtk.IconSize.SMALL_TOOLBAR); 48 | undo_button = new Gtk.ToolButton (undo_ico, null); 49 | undo_button.tooltip_markup = Granite.markup_accel_tooltip ({"Z"}, _("Undo")); 50 | undo_button.clicked.connect (() => { 51 | window.undo_sheet (); 52 | }); 53 | pack_end (undo_button); 54 | 55 | set_buttons_visibility (false); 56 | update_header (); 57 | } 58 | 59 | public void set_buttons_visibility (bool is_visible) { 60 | foreach (var button in get_children ()) { 61 | button.visible = is_visible; 62 | button.no_show_all = !is_visible; 63 | } 64 | } 65 | 66 | public void update_header () { 67 | undo_button.sensitive = window.history_manager.can_undo (); 68 | redo_button.sensitive = window.history_manager.can_redo (); 69 | } 70 | 71 | public void set_titles (string title, string? subtitle) { 72 | this.title = title; 73 | this.subtitle = subtitle; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Widgets/ActionBar.vala: -------------------------------------------------------------------------------- 1 | public class Spreadsheet.Widgets.ActionBar : Gtk.ActionBar { 2 | public signal void zoom_level_changed (); 3 | 4 | private Gtk.Adjustment zoom_scale_adj; 5 | 6 | public int zoom_level { 7 | get { 8 | return App.settings.get_int ("zoom-level"); 9 | } 10 | set { 11 | zoom_scale_adj.value = value; 12 | App.settings.set_int ("zoom-level", value); 13 | } 14 | } 15 | 16 | public string zoom_level_text { 17 | owned get { 18 | return "%i %%".printf (zoom_level); 19 | } 20 | } 21 | 22 | construct { 23 | zoom_scale_adj = new Gtk.Adjustment (zoom_level, 10, 400, 10, 10, 0); 24 | 25 | var zoom_scale = new Gtk.Scale (Gtk.Orientation.HORIZONTAL, zoom_scale_adj); 26 | zoom_scale.tooltip_text = (_("Zoom in/out the sheet")); 27 | zoom_scale.set_size_request (100, 0); 28 | zoom_scale.draw_value = false; 29 | zoom_scale.margin = 3; 30 | zoom_scale.margin_end = 12; 31 | 32 | var zoom_level_button = new Gtk.Button.with_label (zoom_level_text); 33 | zoom_level_button.tooltip_text = (_("Reset to the default zoom level")); 34 | zoom_level_button.margin_end = 12; 35 | 36 | pack_end (zoom_level_button); 37 | pack_end (zoom_scale); 38 | 39 | zoom_scale_adj.value_changed.connect (() => { 40 | zoom_level = (int) zoom_scale_adj.value; 41 | zoom_level_button.label = zoom_level_text; 42 | zoom_level_changed (); 43 | }); 44 | 45 | zoom_level_button.clicked.connect ((event) => { 46 | zoom_level = 100; 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Widgets/FunctionPresenter.vala: -------------------------------------------------------------------------------- 1 | using Gtk; 2 | using Spreadsheet.Models; 3 | 4 | public class Spreadsheet.Widgets.FunctionPresenter : EventBox { 5 | public Function function { get; set; } 6 | 7 | public FunctionPresenter (Function func) { 8 | var box = new Box (Orientation.VERTICAL, 0); 9 | function = func; 10 | 11 | var name_label = new Label (function.name); 12 | name_label.justify = Justification.LEFT; 13 | name_label.halign = Align.START; 14 | box.pack_start (name_label); 15 | 16 | var doc_label = new Label (function.doc); 17 | doc_label.justify = Justification.FILL; 18 | doc_label.halign = Align.START; 19 | doc_label.get_style_context ().add_class (Gtk.STYLE_CLASS_DIM_LABEL); 20 | box.pack_start (doc_label); 21 | 22 | add (box); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Widgets/Sheet.vala: -------------------------------------------------------------------------------- 1 | using Gdk; 2 | using Gee; 3 | using Gtk; 4 | using Cairo; 5 | using Spreadsheet.Models; 6 | using Spreadsheet.UI; 7 | 8 | 9 | public class Spreadsheet.Widgets.Sheet : EventBox { 10 | 11 | // Cell dimensions 12 | const double DEFAULT_WIDTH = 70; 13 | const double DEFAULT_HEIGHT = 25; 14 | const double DEFAULT_PADDING = 5; 15 | const double DEFAULT_BORDER = 0.5; 16 | 17 | double width; 18 | double height; 19 | double padding; 20 | double border; 21 | double? initial_left_margin = null; 22 | 23 | public Page page { get; set; } 24 | 25 | private MainWindow window; 26 | 27 | public Cell? selected_cell { get; set; } 28 | 29 | public signal void selection_changed (Cell? new_selection); 30 | 31 | public signal void selection_cleared (); 32 | 33 | public signal void focus_expression_entry (string? input); 34 | 35 | public Sheet (Page page, MainWindow window) { 36 | this.page = page; 37 | this.window = window; 38 | foreach (var cell in page.cells) { 39 | if (selected_cell == null) { 40 | selected_cell = cell; 41 | cell.selected = true; 42 | } 43 | 44 | cell.notify["display-content"].connect (() => { 45 | queue_draw (); 46 | window.save_sheet (); 47 | }); 48 | cell.font_style.notify.connect (() => { 49 | queue_draw (); 50 | window.save_sheet (); 51 | }); 52 | cell.cell_style.notify.connect (() => { 53 | queue_draw (); 54 | window.save_sheet (); 55 | }); 56 | } 57 | can_focus = true; 58 | button_press_event.connect (on_click); 59 | 60 | update_zoom_level (); 61 | 62 | window.action_bar.zoom_level_changed.connect (() => { 63 | update_zoom_level (); 64 | }); 65 | 66 | key_press_event.connect ((key) => { 67 | // This is true if the ONLY button pressed is a modifier. If a combination 68 | // is pressed, e.g. Shift+Tab, it is not treated as a modifier, and should 69 | // instead be checked with the EventKey::state field. 70 | if (key.is_modifier != 0) { 71 | return true; 72 | } 73 | 74 | if (Gdk.ModifierType.CONTROL_MASK in key.state) { 75 | switch (key.keyval) { 76 | case Gdk.Key.plus: 77 | window.action_bar.zoom_level += 10; 78 | return true; 79 | case Gdk.Key.minus: 80 | window.action_bar.zoom_level -= 10; 81 | return true; 82 | case Gdk.Key.@0: 83 | window.action_bar.zoom_level = 100; 84 | return true; 85 | case Gdk.Key.Home: 86 | select (0, 0); 87 | return true; 88 | } 89 | } 90 | 91 | switch (key.keyval) { 92 | case Gdk.Key.Tab: 93 | move_right (); 94 | return true; 95 | case Gdk.Key.Right: 96 | move_right (); 97 | return true; 98 | case Gdk.Key.Down: 99 | case Gdk.Key.Return: 100 | move_bottom (); 101 | return true; 102 | case Gdk.Key.Up: 103 | move_top (); 104 | return true; 105 | case Gdk.Key.Left: 106 | move_left (); 107 | return true; 108 | case Gdk.Key.BackSpace: 109 | case Gdk.Key.Delete: 110 | selection_cleared (); 111 | return true; 112 | } 113 | // No special key is used, thus the intent is user input 114 | // Switch focus to the expression entry 115 | focus_expression_entry (key.str); 116 | return true; 117 | }); 118 | 119 | add_events (Gdk.EventMask.SCROLL_MASK); 120 | } 121 | 122 | protected override bool scroll_event (Gdk.EventScroll event) { 123 | if (Gdk.ModifierType.CONTROL_MASK in event.state) { 124 | switch (event.direction) { 125 | case Gdk.ScrollDirection.UP: 126 | window.action_bar.zoom_level += 10; 127 | break; 128 | case Gdk.ScrollDirection.DOWN: 129 | window.action_bar.zoom_level -= 10; 130 | break; 131 | default: 132 | break; 133 | } 134 | } 135 | 136 | return base.scroll_event (event); 137 | } 138 | 139 | private void select (int line, int col) { 140 | // Do nothing if the new selected cell are the same with the currently selected 141 | if (line == selected_cell.line && col == selected_cell.column) { 142 | return; 143 | } 144 | 145 | foreach (var cell in page.cells) { 146 | if (cell.selected) { 147 | cell.selected = false; 148 | // Unselect the cell if it was previously selected cell 149 | if (cell == selected_cell) { 150 | selected_cell = null; 151 | selection_changed (null); 152 | } 153 | } else if (cell.line == line && cell.column == col) { 154 | // Select the new cell 155 | cell.selected = true; 156 | selected_cell = cell; 157 | selection_changed (cell); 158 | } 159 | } 160 | queue_draw (); 161 | } 162 | 163 | private void move (int line_add, int col_add) { 164 | // Ignore key press to the outside of the sheet 165 | if (selected_cell.line + line_add < 0 || selected_cell.column + col_add < 0) { 166 | return; 167 | } 168 | 169 | if (selected_cell != null) { 170 | select (selected_cell.line + line_add, selected_cell.column + col_add); 171 | } else { 172 | select (0, 0); 173 | } 174 | } 175 | 176 | public void move_top () { 177 | move (-1, 0); 178 | } 179 | 180 | public void move_bottom () { 181 | move (1, 0); 182 | } 183 | 184 | public void move_right () { 185 | move (0, 1); 186 | } 187 | 188 | public void move_left () { 189 | move (0, -1); 190 | } 191 | 192 | public bool on_click (EventButton evt) { 193 | var left_margin = get_left_margin (); 194 | var col = (int)((evt.x - left_margin) / (double)width); 195 | var line = (int)((evt.y - height) / (double)height); 196 | select (line, col); 197 | grab_focus (); 198 | return false; 199 | } 200 | 201 | private double get_left_margin () { 202 | Context cr = new Context (new ImageSurface (Format.ARGB32, 0, 0)); 203 | cr.set_font_size (height - padding * 2); 204 | cr.select_font_face ("Open Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); 205 | TextExtents left_ext; 206 | cr.text_extents (page.lines.to_string (), out left_ext); 207 | return left_ext.width + border; 208 | } 209 | 210 | private double get_initial_left_margin () { 211 | if (initial_left_margin == null) { 212 | Context cr = new Context (new ImageSurface (Format.ARGB32, 0, 0)); 213 | cr.set_font_size (DEFAULT_HEIGHT - DEFAULT_PADDING * 2); 214 | cr.select_font_face ("Open Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); 215 | TextExtents left_ext; 216 | cr.text_extents (page.lines.to_string (), out left_ext); 217 | initial_left_margin = left_ext.width + DEFAULT_BORDER; 218 | } 219 | 220 | return initial_left_margin; 221 | } 222 | 223 | private void update_zoom_level () { 224 | double zoom_level = window.action_bar.zoom_level * 0.01; 225 | 226 | set_size_request ((int) ((get_initial_left_margin () + DEFAULT_WIDTH * page.columns) * zoom_level), (int) (DEFAULT_HEIGHT * page.lines * zoom_level)); 227 | width = DEFAULT_WIDTH * zoom_level; 228 | height = DEFAULT_HEIGHT * zoom_level; 229 | padding = DEFAULT_PADDING * zoom_level; 230 | border = DEFAULT_BORDER * zoom_level; 231 | 232 | queue_draw (); 233 | } 234 | 235 | public override bool draw (Context cr) { 236 | RGBA default_cell_stroke = { 0.3, 0.3, 0.3, 1 }; 237 | RGBA default_font_color = { 0, 0, 0, 1 }; 238 | 239 | var style = window.get_style_context (); 240 | 241 | RGBA normal = style.get_color (Gtk.StateFlags.NORMAL); 242 | RGBA selected = style.get_color (Gtk.StateFlags.SELECTED); 243 | 244 | cr.set_font_size (height - padding * 2); 245 | cr.select_font_face ("Open Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); 246 | 247 | double left_margin = get_left_margin (); 248 | 249 | // white background 250 | cr.set_source_rgb (1, 1, 1); 251 | cr.rectangle (left_margin, height, get_allocated_width () - left_margin, get_allocated_height () - height); 252 | cr.fill (); 253 | 254 | // draw the letters and the numbers on the side 255 | Gdk.cairo_set_source_rgba (cr, normal); 256 | cr.set_line_width (border); 257 | 258 | // numbers on the left side 259 | for (int i = 0; i < page.lines; i++) { 260 | cr.rectangle (0, height + border + i * height, left_margin, height); 261 | cr.stroke (); 262 | 263 | if (selected_cell != null && selected_cell.line == i) { 264 | cr.save (); 265 | style.render_frame (cr, 0, height + border + i * height, left_margin, height); 266 | cr.restore (); 267 | 268 | Gdk.cairo_set_source_rgba (cr, selected); 269 | } else { 270 | Gdk.cairo_set_source_rgba (cr, normal); 271 | } 272 | 273 | TextExtents extents; 274 | cr.text_extents (i.to_string (), out extents); 275 | double x = left_margin / 2 - extents.width / 2; 276 | double y = border + height * i + height / 2 + extents.height / 2; 277 | 278 | cr.move_to (x, y); 279 | if (i != 0) { 280 | cr.show_text (i.to_string ()); 281 | } 282 | } 283 | 284 | // letters on the top 285 | int i = 0; 286 | foreach (string letter in new AlphabetGenerator (page.columns)) { 287 | cr.rectangle (left_margin + border + i * width, 0, width, height); 288 | cr.stroke (); 289 | 290 | if (selected_cell != null && selected_cell.column == i) { 291 | cr.save (); 292 | style.render_frame (cr, left_margin + border + i * width, 0, width, height); 293 | cr.restore (); 294 | 295 | Gdk.cairo_set_source_rgba (cr, selected); 296 | } else { 297 | Gdk.cairo_set_source_rgba (cr, normal); 298 | } 299 | 300 | TextExtents extents; 301 | cr.text_extents (letter, out extents); 302 | double x = left_margin + border + width * i + width / 2 - extents.width / 2; 303 | double y = border + height / 2 + extents.height / 2; 304 | cr.fill (); 305 | cr.move_to (x, y); 306 | cr.show_text (letter); 307 | 308 | i++; 309 | } 310 | 311 | // draw the cells 312 | foreach (var cell in page.cells) { 313 | Gdk.RGBA bg = cell.cell_style.background; 314 | Gdk.RGBA bg_default = { 1, 1, 1, 1 }; 315 | if (bg != bg_default) { 316 | cr.save (); 317 | Gdk.cairo_set_source_rgba (cr, bg); 318 | cr.rectangle (left_margin + border + cell.column * width, height + border + cell.line * height, width, height); 319 | cr.fill (); 320 | cr.restore (); 321 | } 322 | 323 | Gdk.RGBA sr = cell.cell_style.stroke; 324 | Gdk.RGBA sr_default = { 0, 0, 0, 1 }; 325 | double sr_w = cell.cell_style.stroke_width; 326 | cr.save (); 327 | 328 | if (sr_w != 1.0) { 329 | cr.set_line_width (sr_w); 330 | } else { 331 | cr.set_line_width (1.0); 332 | } 333 | 334 | if (sr != sr_default) { 335 | Gdk.cairo_set_source_rgba (cr, sr); 336 | } else { 337 | Gdk.cairo_set_source_rgba (cr, default_cell_stroke); 338 | } 339 | 340 | if (cell.selected) { 341 | cr.set_line_width (3.0); 342 | } 343 | 344 | cr.rectangle (left_margin + border + cell.column * width, height + border + cell.line * height, width, height); 345 | cr.stroke (); 346 | cr.restore (); 347 | 348 | // display the text 349 | Gdk.RGBA color = cell.font_style.fontcolor; 350 | Gdk.RGBA color_default = { 0, 0, 0, 1 }; 351 | cr.save (); 352 | if (color != color_default) { 353 | Gdk.cairo_set_source_rgba (cr, color); 354 | } else { 355 | Gdk.cairo_set_source_rgba (cr, default_font_color); 356 | } 357 | 358 | TextExtents extents; 359 | cr.text_extents (cell.display_content, out extents); 360 | double x = left_margin + ((cell.column + 1) * width - (padding + border + extents.width)); 361 | double y = height + ((cell.line + 1) * height - (padding + border)); 362 | 363 | if (cell.font_style.is_underline) { 364 | const int UNDERLINE_PADDING = 3; 365 | cr.move_to (x, y + UNDERLINE_PADDING); 366 | cr.set_line_width (1); 367 | cr.rel_line_to (extents.width, 0); 368 | cr.stroke (); 369 | } 370 | 371 | if (cell.font_style.is_strikethrough) { 372 | cr.move_to (x, y - extents.height / 2); 373 | cr.set_line_width (1); 374 | cr.rel_line_to (extents.width, 0); 375 | cr.stroke (); 376 | } 377 | 378 | cr.move_to (x, y); 379 | var font_weight = Cairo.FontWeight.NORMAL; 380 | var font_slant = Cairo.FontSlant.NORMAL; 381 | 382 | if (cell.font_style.is_bold) { 383 | font_weight = Cairo.FontWeight.BOLD; 384 | } 385 | 386 | if (cell.font_style.is_italic) { 387 | font_slant = Cairo.FontSlant.ITALIC; 388 | } 389 | 390 | cr.select_font_face ("Open Sans", font_slant, font_weight); 391 | cr.show_text (cell.display_content); 392 | cr.restore (); 393 | } 394 | 395 | return true; 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/Widgets/StyleModal.vala: -------------------------------------------------------------------------------- 1 | public class Spreadsheet.StyleModal : Gtk.Grid { 2 | public StyleModal (FontStyle font_style, CellStyle cell_style) { 3 | var style_stack = new Gtk.Stack (); 4 | style_stack.add_titled (create_fonts_grid (font_style), "fonts-grid", _("Font")); 5 | style_stack.add_titled (create_cells_grid (cell_style), "cells-grid", _("Cell")); 6 | 7 | var style_stack_switcher = new Gtk.StackSwitcher () { 8 | homogeneous = true, 9 | halign = Gtk.Align.CENTER, 10 | stack = style_stack 11 | }; 12 | 13 | attach (style_stack_switcher, 0, 0, 1, 1); 14 | attach (style_stack, 0, 1, 1, 1); 15 | } 16 | 17 | private Gtk.Grid create_fonts_grid (FontStyle font_style) { 18 | // TODO: Add a widget that can choose a font and its size 19 | 20 | var bold_button = new Gtk.ToggleButton () { 21 | focus_on_click = false, 22 | tooltip_text = _("Bold") 23 | }; 24 | bold_button.add (new Gtk.Image.from_icon_name ("format-text-bold-symbolic", Gtk.IconSize.BUTTON)); 25 | 26 | var italic_button = new Gtk.ToggleButton () { 27 | focus_on_click = false, 28 | tooltip_text = _("Italic") 29 | }; 30 | italic_button.add (new Gtk.Image.from_icon_name ("format-text-italic-symbolic", Gtk.IconSize.BUTTON)); 31 | 32 | var underline_button = new Gtk.ToggleButton () { 33 | focus_on_click = false, 34 | tooltip_text = _("Underline") 35 | }; 36 | underline_button.add (new Gtk.Image.from_icon_name ("format-text-underline-symbolic", Gtk.IconSize.BUTTON)); 37 | 38 | var strikethrough_button = new Gtk.ToggleButton () { 39 | focus_on_click = false, 40 | tooltip_text = _("Strikethrough") 41 | }; 42 | strikethrough_button.add (new Gtk.Image.from_icon_name ("format-text-strikethrough-symbolic", Gtk.IconSize.BUTTON)); 43 | 44 | font_style.bind_property ("is_bold", bold_button, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); 45 | font_style.bind_property ("is_italic", italic_button, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); 46 | font_style.bind_property ("is_underline", underline_button, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); 47 | font_style.bind_property ("is_strikethrough", strikethrough_button, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); 48 | 49 | var style_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); 50 | style_box.get_style_context ().add_class (Gtk.STYLE_CLASS_LINKED); 51 | style_box.pack_start (bold_button); 52 | style_box.pack_start (italic_button); 53 | style_box.pack_start (underline_button); 54 | style_box.pack_start (strikethrough_button); 55 | 56 | var color_button = new Gtk.ColorButton () { 57 | halign = Gtk.Align.START, 58 | tooltip_text = _("Set font color of a selected cell") 59 | }; 60 | font_style.bind_property ("fontcolor", color_button, "rgba", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); 61 | 62 | var color_reset_button = new Gtk.Button.from_icon_name ("edit-delete-symbolic", Gtk.IconSize.BUTTON) { 63 | halign = Gtk.Align.START, 64 | tooltip_text = _("Reset font color of a selected cell to black") 65 | }; 66 | 67 | var right_grid = new Gtk.Grid (); 68 | right_grid.attach (new Granite.HeaderLabel (_("Style")), 0, 0, 1, 1); 69 | right_grid.attach (style_box, 0, 1, 1, 1); 70 | right_grid.attach (new Granite.HeaderLabel (_("Color")), 0, 2, 1, 1); 71 | right_grid.attach (color_button, 0, 3, 1, 1); 72 | right_grid.attach (color_reset_button, 1, 3, 1, 1); 73 | 74 | var fonts_grid = new Gtk.Grid () { 75 | margin_top = 6, 76 | orientation = Gtk.Orientation.VERTICAL, 77 | column_spacing = 6 78 | }; 79 | fonts_grid.attach (right_grid, 0, 0, 1, 1); 80 | 81 | // Set the sensitivity of the color_reset_button by whether it has already reset font color to the default one or not when… 82 | // 1. widgets are created 83 | Gdk.RGBA font_default_color = { 0, 0, 0, 1 }; 84 | color_reset_button.sensitive = check_color (color_button, font_default_color); 85 | // 2. user clicks the color_button and sets a new font color 86 | color_button.color_set.connect (() =>{ 87 | color_reset_button.sensitive = check_color (color_button, font_default_color); 88 | }); 89 | // 3. user clicks the color_reset_button and resets a font color 90 | color_reset_button.clicked.connect (() => { 91 | font_style.reset_color (); 92 | color_reset_button.sensitive = check_color (color_button, font_default_color); 93 | }); 94 | 95 | return fonts_grid; 96 | } 97 | 98 | private Gtk.Grid create_cells_grid (CellStyle cell_style) { 99 | var bg_button = new Gtk.ColorButton () { 100 | halign = Gtk.Align.START, 101 | tooltip_text = _("Set fill color of a selected cell") 102 | }; 103 | 104 | var bg_reset_button = new Gtk.Button.from_icon_name ("edit-delete-symbolic", Gtk.IconSize.BUTTON) { 105 | halign = Gtk.Align.START, 106 | tooltip_text = _("Remove fill color of a selected cell") 107 | }; 108 | 109 | var sr_button = new Gtk.ColorButton () { 110 | halign = Gtk.Align.START, 111 | tooltip_text = _("Set stroke color of a selected cell") 112 | }; 113 | 114 | var sr_width_spin = new Gtk.SpinButton.with_range (0.1, 3, 0.1) { 115 | halign = Gtk.Align.START, 116 | tooltip_text = _("Set the border width of a selected cell") 117 | }; 118 | 119 | var sr_reset_button = new Gtk.Button.from_icon_name ("edit-delete-symbolic", Gtk.IconSize.BUTTON) { 120 | halign = Gtk.Align.START, 121 | tooltip_text = _("Remove stroke color of a selected cell") 122 | }; 123 | 124 | cell_style.bind_property ("background", bg_button, "rgba", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); 125 | cell_style.bind_property ("stroke", sr_button, "rgba", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); 126 | cell_style.bind_property ("stroke_width", sr_width_spin, "value", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); 127 | 128 | var cells_grid = new Gtk.Grid () { 129 | margin_top = 6, 130 | column_spacing = 6 131 | }; 132 | cells_grid.attach (new Granite.HeaderLabel (_("Fill")), 0, 0, 1, 1); 133 | cells_grid.attach (bg_button, 0, 1, 1, 1); 134 | cells_grid.attach (bg_reset_button, 1, 1, 1, 1); 135 | cells_grid.attach (new Granite.HeaderLabel (_("Stroke")), 0, 2, 1, 1); 136 | cells_grid.attach (sr_button, 0, 3, 1, 1); 137 | cells_grid.attach (sr_width_spin, 1, 3, 1, 1); 138 | cells_grid.attach (sr_reset_button, 2, 3, 1, 1); 139 | 140 | // Set the sensitivities of the br_remove_button, sr_reset_button and sr_width_spin by whether they have already reset background/stroke colors to the default ones or not when… 141 | // 1. widgets are created 142 | Gdk.RGBA bg_default_color = { 1, 1, 1, 1 }; 143 | Gdk.RGBA sr_default_color = { 0, 0, 0, 1 }; 144 | bg_reset_button.sensitive = check_color (bg_button, bg_default_color); 145 | sr_reset_button.sensitive = check_color (sr_button, sr_default_color); 146 | sr_width_spin.sensitive = check_color (sr_button, sr_default_color); 147 | // 2. user clicks br_button/sr_button and sets new background/stroke colors 148 | bg_button.color_set.connect (() =>{ 149 | bg_reset_button.sensitive = check_color (bg_button, bg_default_color); 150 | }); 151 | sr_button.color_set.connect (() =>{ 152 | sr_reset_button.sensitive = check_color (sr_button, sr_default_color); 153 | sr_width_spin.sensitive = check_color (sr_button, sr_default_color); 154 | }); 155 | // 3. user clicks bg_reset_button/sr_reset_button and resets background/stroke colors 156 | bg_reset_button.clicked.connect (() => { 157 | cell_style.reset_background_color (); 158 | bg_reset_button.sensitive = check_color (bg_button, bg_default_color); 159 | }); 160 | sr_reset_button.clicked.connect (() => { 161 | cell_style.reset_stroke_color (); 162 | sr_width_spin.value = 1.0; 163 | sr_reset_button.sensitive = check_color (sr_button, sr_default_color); 164 | sr_width_spin.sensitive = check_color (sr_button, sr_default_color); 165 | }); 166 | 167 | return cells_grid; 168 | } 169 | 170 | private bool check_color (Gtk.ColorButton bt, Gdk.RGBA dc) { 171 | if (bt.rgba == dc) { 172 | return false; 173 | } else { 174 | return true; 175 | } 176 | } 177 | } 178 | --------------------------------------------------------------------------------