diff --git a/.gitignore b/.gitignore index 3a81c5f..eb711a2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ var/ *.egg-info/ .installed.cfg *.egg +deb_dist/ # PyInstaller # Usually these files are written by a python script from a template @@ -81,6 +82,7 @@ celerybeat-schedule # virtualenv venv/ ENV/ +.venv/ # Spyder project settings .spyderproject @@ -89,4 +91,7 @@ ENV/ .ropeproject # Pycharms -.idea/ \ No newline at end of file +.idea/ + +# complied resources +src/calculator/ui/resources.py \ No newline at end of file diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index e0bc060..0000000 --- a/MANIFEST +++ /dev/null @@ -1,2 +0,0 @@ -recursive-include src/calculator/ui/ *.qml -src/Makefile \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..26f6fa7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +graft src/calculator/ui +include src/Makefile +include README.md +graft debian \ No newline at end of file diff --git a/README.md b/README.md index 469513f..6e2aeb5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # IVS-VUT-BIT-2016-2017 [![Build Status](https://travis-ci.com/thejoeejoee/IVS-VUT-BIT-2016-2017.svg?token=MqEeDyeLfZw3xFmAVUzV&branch=develop)](https://travis-ci.com/thejoeejoee/IVS-VUT-BIT-2016-2017) +[![Codeship](https://img.shields.io/codeship/a2ac7ad0-fb4b-0134-7062-02a6a40c3d5e.svg)](https://app.codeship.com/projects/211472) [![codecov](https://img.shields.io/codecov/c/token/M5EwaVLlg7/github/thejoeejoee/IVS-VUT-BIT-2016-2017/develop.svg)](https://codecov.io/gh/thejoeejoee/IVS-VUT-BIT-2016-2017) [![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html) diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..1ba3654 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +calculator (0.2-1) unstable; urgency=low + + * source package automatically created by stdeb 0.8.5 + + -- Josef Kolar, Son Hai Nguyen, Martin Omacht, Robert Navratil Mon, 10 Apr 2017 14:25:02 +0200 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..8f55de6 --- /dev/null +++ b/debian/control @@ -0,0 +1,24 @@ +Source: calculator +Maintainer: Josef Kolar, Son Hai Nguyen, Martin Omacht, Robert Navratil +Section: python +Priority: optional +Build-Depends: python3-setuptools, python3-all, debhelper (>= 7.4.3) +Standards-Version: 3.9.1 +Package: python3-calculator +Architecture: all +Depends: ${misc:Depends}, ${python3:Depends}, python3-pip, python3-doxyqml, python3-doxypypy, python3-colour-runner, python3-opengl +Description: + IVS-VUT-BIT-2016-2017 + Grafická kalkulačka jako školní projekt do předmětu IVS na fakultě FIT VUT. + + Platforms + --------- + . + Ubuntu 64bit + . + Authors + ------ + . + /dej/uran/dom + - xkolar71 Josef Kolář + diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..615455f --- /dev/null +++ b/debian/postinst @@ -0,0 +1,3 @@ +pip3 install --upgrade "PyQt5==5.7.1" +pip3 install --upgrade pip +pip3 install --upgrade PyOpenGL \ No newline at end of file diff --git a/debian/py3dist-overrides b/debian/py3dist-overrides new file mode 100644 index 0000000..e69de29 diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..4807744 --- /dev/null +++ b/debian/rules @@ -0,0 +1,31 @@ +#!/usr/bin/make -f + +# This file was automatically generated by stdeb 0.8.5 at +# Mon, 10 Apr 2017 14:25:02 +0200 + +%: + dh $@ --with python3 --buildsystem=python_distutils + + +override_dh_auto_clean: + python3 setup.py clean -a + find . -name \*.pyc -exec rm {} \; + + + +override_dh_auto_build: + python3 setup.py build --force + + + +override_dh_auto_install: + python3 setup.py install --force --root=debian/python3-calculator --no-compile -O0 --install-layout=deb --prefix=/usr + + + +override_dh_python2: + dh_python2 --no-guessing-versions + + + + diff --git a/bin/qmldevengine/main.cpp b/dev/qmldevengine/main.cpp similarity index 100% rename from bin/qmldevengine/main.cpp rename to dev/qmldevengine/main.cpp diff --git a/bin/qmldevengine/qmldevengine.pro b/dev/qmldevengine/qmldevengine.pro similarity index 100% rename from bin/qmldevengine/qmldevengine.pro rename to dev/qmldevengine/qmldevengine.pro diff --git a/bin/qmldevengine/types/sides.cpp b/dev/qmldevengine/types/sides.cpp similarity index 100% rename from bin/qmldevengine/types/sides.cpp rename to dev/qmldevengine/types/sides.cpp diff --git a/bin/qmldevengine/types/sides.h b/dev/qmldevengine/types/sides.h similarity index 100% rename from bin/qmldevengine/types/sides.h rename to dev/qmldevengine/types/sides.h diff --git a/doc/complete.png b/doc/complete.png new file mode 100644 index 0000000..e09ce91 Binary files /dev/null and b/doc/complete.png differ diff --git a/doc/debugging.png b/doc/debugging.png deleted file mode 100644 index 388cbeb..0000000 Binary files a/doc/debugging.png and /dev/null differ diff --git a/doc/doc.md b/doc/doc.md index 57be7bc..931a11e 100644 --- a/doc/doc.md +++ b/doc/doc.md @@ -1,184 +1,203 @@ + # IVS-VUT-BIT-2016-2017 Documentation ## Table of contents -* [Input syntax](#input-syntax) - * [Numbers](#numbers) - * [Operators](#operators) - * [Functions](#functinos) +* [Úvod](#úvod) +* [Instalace](#instalace) +* [Odinstalace](#odinstalace) +* [Funkce](#funkce) + * [Absolutní hodnota](#absolutní-hodnota) + * [Faktorial](#faktorial) + * [Přirozený logaritmus](#přirozený-logaritmus) + * [Obecný logaritmus](#obecný-logaritmus) + * [Mocnina](#mocnina) + * [Náhodné číslo](#nahodné-číslo) + * [Obecná odmocnina](#obecná-odmocnina) + * [Odmocnina](#odmocnina) +* [Tutorial](#tutorial) + * [Komponenty](#komponenty) + * [Práce s kalkulačkou](#práce-s-kalkulačkou) -## Input syntax +## Úvod -### Numbers +Tato aplikace představuje klasickou kalkulačku se speciálními funkcemi. Jádro Barbie Calculatoru je napsáno v [Pythonu](https://www.python.org/). -#### Decimal +## Instalace -Examples: +## Odinstalace -`156` +## Funkce -`-56` +Ve všech funkcích jdou použít klasické operátory (+, -, *, /) a i jiné funkce. -`42.666` +### Absolutní hodnota -`-0.135` +Zápis: -#### Hexadecimal +`abs([číslo])` -Requires calculator to be switched to hexadecimal mode. +nebo -Valid characters: `0-9 A-F` +`|[číslo]|` -Examples: +Příklady: -`3A1F` +`|-6|` -`FFFF` +`||-12 + 4| + 5|` -`561` +`abs(-6)` -#### Octal +`abs(abs(-12 + 4) + 5)` -Requires calculator to be switched to octal mode. +### Faktorial -Valid characters: `0-7` +Zápis: -Examples: +`fact([číslo])` -`37` +nebo -`777` +`[číslo]!` -#### Binary +Příklady: -Requires calculator to be switched to binary mode. +`6!` -Valid characters: `0-1` +`6!!` -Examples: +`fact(6)` -`101101` +`fact(fact(6))` -`1111` +### Přirozený logaritmus -### Operators +Zápis: -#### Add +`ln([číslo])` -Syntax: +Příklady: -`[left-operand] + [right-operand]` +`ln(5)` -Requires two operands. +`ln(5 + 2)` -Examples: +### Obecný logaritmus -`3 + 6` +Zápis: -`-96 + 42` +`log([číslo], [základ logaritmu])` -#### Substract +Příklady: -Syntax: +`log(2, 2)` -`[left-operand] - [right-operand]` +`log(54 + 8, 15)` -Requires two operands. +### Mocnina -Examples: +Zápis: -`3 - 6` +`pow([mocněnec], [mocnitel])` -`26489 - (153 - 145)` +nebo -#### Multiply +`[mocněnec]**[mocnitel]` -Syntax: +Příklady: -`[left-operand] * [right-operand]` +`5**2` -Requires two operands. +`pow(5, 2)` -Examples: +### Náhodné číslo -`3 * 6` +Zápis: -`-96 * 42` +`rand()` -#### Divide +### Obecná odmocnina -Syntax: +Zápis: -`[left-operand] / [right-operand]` +`root([odmocněnec], [odmocnitel])` -Requires two operands. +Příklady: -Examples: +`root(3, 6)` -`3 / 6` +`root(86, 15*2)` -`-96 / 42` +### Odmocnina -#### Power of +Zápis: -Syntax: +`sqrt([odmocněnec])` -`[base] ^ [exponent]` +Příklady: -or +`sqrt(8)` -`[base] ** [exponent]` +`sqrt(96 + 42)` -or +## Tutorial -`pow([base], [exponent])` +V této kapitole bude popsána práce v Barbie Calculator, jeho funkce a užitečné vlastnosti, a dále také základní panely pro práci. -Requires two operands. +Zde na obrázku je Barbie Calculator po zapnutí -Examples: +![Prázdná kalkulačka](empty.png) -`3 ^ 6` +*Okno programu má pevně nastavený poměr stran*. -`-96 ** 42` +### Komponenty -`pow(2, 10)` +#### Převod do číselných soustav -#### Factorial +Pokud je výsledek _celočíselný_, tak bude výsledek převeden a zobrazen ve 4 číselných soustavách (desítkové, šestnáctkové, osmičkové, dvojkové). -Syntax: +![Číselné soustavy po otevření](system1.png) ![Číselné soustavy s převedeným číslem](system2.png) -`[number]!` +#### Funkce a zápisové okno -Example: +Jednou z hlavních částí je panel s funkcemi a k němu navazující okno s výrazem k výpočtu. -`6!` +![Bez výrazu - prázdné](func1.png) ![S funkcí a operací](func2.png) -`4!!` +#### Proměnné -`(21 / 3)!` +Barbie Calculator umí také používat proměnné, takže si můžete uložit výpočty do proměnných a dále je používat -### Functions +Dávejte si ale pozor na to, že proměnné jsou **case sensitive**. -#### Square root +![Panel proměnných](variable.png) -Syntax: +### Práce s kalkulačkou -`sqrt([number])` +Na následujícím obrázku je ukázané tzv. dopňování kódu -Examples: +![Doplňování kódu](complete.png) -`sqrt(8)` +Dále také rozšíření výrazu do funkce. + +Pokud výraz označíte a a kliknete na funkci, tak se celý výraz vloží do požadované funkce. + +![Výraz před použitím funkce](enfunc1.png) + +![Výraz po použití funkce](enfunc2.png) + +Jak vidíte už jsou inicializované nějaké proměnné. V jejich nastavení je možný přepis na `1` nebo `0`, a nebo také proměnnou smazat. Pokud bude proměnných příliš, můžete se k nim dostat pomocí posuvníku (případně kolečka myši). + +![Mnoho proměnných a jejich nastavení](many_vars.png) + +Další ukázkou bude kombinace mnoha funkcí s vysokým výsledkem. Při vysokých (nebo nízkých) výsledcích se výsledek vypisuje ve formátu `[cifra].[2 cifry]` se zaokrouhlením na příslušné 2. desetinné místo. + +![Posuvník u číselných soustav při vysoké hodnotě](long_result.png) -`sqrt(96 + 42)` -#### Root -Syntax: -`root([number], [nth-root]` -Examples: -`3 + 6` -`-96 + 42` diff --git a/doc/empty.png b/doc/empty.png new file mode 100644 index 0000000..224ac69 Binary files /dev/null and b/doc/empty.png differ diff --git a/doc/enfunc1.png b/doc/enfunc1.png new file mode 100644 index 0000000..5a55b82 Binary files /dev/null and b/doc/enfunc1.png differ diff --git a/doc/enfunc2.png b/doc/enfunc2.png new file mode 100644 index 0000000..ab8f80a Binary files /dev/null and b/doc/enfunc2.png differ diff --git a/doc/func1.png b/doc/func1.png new file mode 100644 index 0000000..a9fa08a Binary files /dev/null and b/doc/func1.png differ diff --git a/doc/func2.png b/doc/func2.png new file mode 100644 index 0000000..6130c77 Binary files /dev/null and b/doc/func2.png differ diff --git a/doc/long_result.png b/doc/long_result.png new file mode 100644 index 0000000..5d42479 Binary files /dev/null and b/doc/long_result.png differ diff --git a/doc/many_vars.png b/doc/many_vars.png new file mode 100644 index 0000000..51cc62b Binary files /dev/null and b/doc/many_vars.png differ diff --git a/doc/system1.png b/doc/system1.png new file mode 100644 index 0000000..ba58a4f Binary files /dev/null and b/doc/system1.png differ diff --git a/doc/system2.png b/doc/system2.png new file mode 100644 index 0000000..c67e957 Binary files /dev/null and b/doc/system2.png differ diff --git a/doc/variable.png b/doc/variable.png new file mode 100644 index 0000000..e3e81a7 Binary files /dev/null and b/doc/variable.png differ diff --git a/mockup/hidden-features.svg b/mockup/hidden-features.svg new file mode 100644 index 0000000..3537542 --- /dev/null +++ b/mockup/hidden-features.svg @@ -0,0 +1,691 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Error + + Naskytla se nějaká chyba. + + + Ok + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Graph + + + + + + + + diff --git a/mockup/shown-features-countdown.svg b/mockup/shown-features-countdown.svg new file mode 100644 index 0000000..9a9692d --- /dev/null +++ b/mockup/shown-features-countdown.svg @@ -0,0 +1,698 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Error + + Naskytla se nějaká chyba. + + + Ok + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mockup/shown-features.svg b/mockup/shown-features.svg new file mode 100644 index 0000000..8b9b9ce --- /dev/null +++ b/mockup/shown-features.svg @@ -0,0 +1,806 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Error + + Naskytla se nějaká chyba. + + + Ok + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plan/ui-design.svg b/plan/ui-design.svg index 5b8b21a..93ca9a6 100644 --- a/plan/ui-design.svg +++ b/plan/ui-design.svg @@ -4,8 +4,8 @@ - - Zadejte vstup... + + Zadejte vstup... @@ -26,85 +26,515 @@ - Barbie Calculator + + + + + + + + + + + + + + + + + + - 645,74540 + + + + + + + + + + + - pow + + + + + - root + + + + + + - fact + + + + + + - rand + + + + + + - log + + + + + - e + + + - pi - - pow - - root - - fact - - rand - - log - - - e - - - pi + + + + - BIN 1010 1010101... - DEC 103 - OCT 403 - HEX 4FA - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - Ans + + + + + - 3*(343)+44 - 54,4 + + + + + + + + + + + + + + + + + + - DF - 3*(343)+44 - 54,4 + + + + + + + + + + + + + + + + + + + + + + - XY - 3*(343)+44 - 5454,4 + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - FOO - 3*(343)+44 - 54,4 + + + + + + + + + + + + + + + + + + + + + + + @@ -131,16 +561,127 @@ - =1 - =0 + + + + + + + + + + + + + Error + Naskytla se nějaká chyba. + + + Ok + + + + + + + + + + + + + + + + + + + + + + + - 3*(343)+44 + + + + + + + + + + + + diff --git a/requirements.txt b/requirements.txt index 671d5c4..33d822b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,10 @@ -colour-runner -doxypypy +# needs to run +PyOpenGL PyQt5==5.7.1 -termcolor -doxyqml -# on Debian + Nvidia you need to install also package PyOpenGL \ No newline at end of file +# dev deps +colour_runner +termcolor +stdeb +doxypy +doxyqml \ No newline at end of file diff --git a/setup.py b/setup.py index 6ac18db..efd7dae 100644 --- a/setup.py +++ b/setup.py @@ -1,37 +1,58 @@ # coding=utf-8 -from distutils.core import setup +from distutils import core +from os.path import abspath, dirname, join from setuptools import find_packages +base_path = abspath(dirname(__file__)) -def install_requires(): - with open('requirements.txt') as f: - return f.readlines() - - -setup( - name='MathCalculator', - version='0.1', - license='GNU GENERAL PUBLIC LICENSE Version 3', - long_description=open('README.md').read(), - url='https://github.com/thejoeejoee/IVS-VUT-BIT-2016-2017', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Environment :: X11 Applications :: Qt', - 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3 :: Only', - 'Topic :: Scientific/Engineering :: Mathematics', - 'Topic :: Utilities' - ], - author='Josef Kolar, Son Hai Nguyen, Martin Omacht, Robert Navratil', - author_email='xkolar71@stud.fit.vutbr.cz, xnguye16@stud.fit.vutbr.cz,' - 'xomach00@stud.fit.vutbr.cz, xnavra61@stud.fit.vutbr.cz', - keywords='calculator expression mathematics', - packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), - install_requires=install_requires(), - package_data={'': ['Makefile']}, -) + +def setup(): + core.setup( + name='calculator', + version='0.3', + license='GNU GENERAL PUBLIC LICENSE Version 3', + long_description=open(join(base_path, 'README.md')).read(), + url='https://github.com/thejoeejoee/IVS-VUT-BIT-2016-2017', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: X11 Applications :: Qt', + 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Scientific/Engineering :: Mathematics', + 'Topic :: Utilities' + ], + author='Josef Kolar, Son Hai Nguyen, Martin Omacht, Robert Navratil', + author_email='xkolar71@stud.fit.vutbr.cz, xnguye16@stud.fit.vutbr.cz,' + 'xomach00@stud.fit.vutbr.cz, xnavra61@stud.fit.vutbr.cz', + keywords='calculator expression mathematics', + packages=find_packages('src', exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), + install_requires=[ + 'PyOpenGL', + 'PyQt5==5.7.1' + ], + requires=[ + 'colour_runner', + 'termcolor', + 'stdeb', + 'doxypy', + 'doxyqml', + ], + package_dir={'': 'src/'}, + entry_points={ + 'console_scripts': [ + 'calculator-console=calculator.console:main', + 'calculator-app=calculator.main:main', + ] + }, + include_package_data=True, + test_suite='tests', + ) + + +if __name__ == '__main__': + setup() diff --git a/src/Makefile b/src/Makefile index bf12fd4..385e379 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,26 +1,40 @@ - -VENV=../.venv REQUIREMENTS=../requirements.txt -VENV_ACTIVATE=$(VENV)/bin/activate -PYTHON="$(VENV)/bin/python" -PIP="$(VENV)/bin/pip" -PYRCC5="$(VENV)/bin/pyrcc5" -install: install-python venv +PYTHON=python3 +PIP=pip3 +PYRCC5=pyrcc5 +DIR:=$(CURDIR) + +all: run + +.PHONY: all install install-python install-calculator test run clean-pyc compile-resources clean + +install: install-python install-calculator install-python: - apt-get install python3.5 python3.5-venv python3-pip + @if ! dpkg -s python3.5 > /dev/null && dpkg -s python3-pip > /dev/null; then\ + apt-get update && apt-get -y install python3-all python3.5 python3-pip python3-venv;\ + fi + pip3 install --upgrade pip > /dev/null -compile-qt-rcc: venv - $(PYRCC5) -o calculator/ui/resources.py calculator/ui/qml.qrc +install-calculator: + cd ..; $(PYTHON) $(DIR)/../setup.py install -test: venv - $(PYTHON) test.py || true +test: + $(PYTHON) $(DIR)/test.py || true -run: venv compile-qt-rcc - $(PYTHON) main.py +run: + $(PYTHON) $(DIR)/calculator/main.py + +clean: + rm -rf ../dist/ ../deb_dist/ ./calculator.egg-info/ + +pack-deb: clean clean-pyc compile-resources + cd ..; $(PYTHON) setup.py --command-packages=stdeb.command bdist_deb + +clean-pyc: + find . -name "*.pyc" -delete + +compile-resources: + $(PYRCC5) -o calculator/ui/resources.py calculator/ui/qml.qrc -venv: $(REQUIREMENTS) - test -d $(VENV) || pyvenv $(VENV) - $(PIP) install -Ur ../requirements.txt - touch $(VENV_ACTIVATE) diff --git a/src/calculator.pro b/src/calculator.pro index 72dc782..6b7225f 100644 --- a/src/calculator.pro +++ b/src/calculator.pro @@ -1,6 +1,5 @@ -SOURCES = $$files(*.py, true) $$files(*.qml, true) - -TRANSLATIONS = calculator/translations/cs.ts +SOURCES = $$files(*.py, true) $$files(*.qml, true) $$files(*.js, true) +TRANSLATIONS = calculator/ui/translations/cs.ts CODECFORTR = UTF-8 diff --git a/src/calculator/__init__.py b/src/calculator/__init__.py index 9bad579..2f06d87 100644 --- a/src/calculator/__init__.py +++ b/src/calculator/__init__.py @@ -1 +1,4 @@ # coding=utf-8 + +# noinspection PyProtectedMember +from calculator._typing import * diff --git a/src/calculator/typing.py b/src/calculator/_typing.py similarity index 60% rename from src/calculator/typing.py rename to src/calculator/_typing.py index caa9a4c..e6e777c 100644 --- a/src/calculator/typing.py +++ b/src/calculator/_typing.py @@ -1,7 +1,7 @@ # coding=utf-8 -from typing import Union, Tuple, Callable, Set +from typing import Union, Tuple, Callable, Set, NewType -NumericValue = Union[int, float] # possible types of result of mathematical expression +NumericValue = NewType('NumericValue', Union[int, float]) # possible types of result of mathematical expression Variable = Tuple[ NumericValue, # actual value diff --git a/src/calculator/console.py b/src/calculator/console.py new file mode 100644 index 0000000..c5a7ef5 --- /dev/null +++ b/src/calculator/console.py @@ -0,0 +1,27 @@ +# coding=utf-8 +from pprint import pformat + +from calculator.core.calculator import Calculator + +calculator = Calculator() + +try: + import readline +except ImportError: + pass + + +def main(): + while True: + user_input = input('>>> ').strip() + if not user_input: + continue + try: + result, variables = calculator.process(user_input) + print(" === {}\n === {}\n".format(result, pformat(dict(variables)))) + except Exception as e: + print(e, repr(e)) + + +if __name__ == '__main__': + main() diff --git a/src/calculator/core/calculator/calculator.py b/src/calculator/core/calculator/calculator.py index f950ba1..56224f4 100644 --- a/src/calculator/core/calculator/calculator.py +++ b/src/calculator/core/calculator/calculator.py @@ -3,8 +3,8 @@ from typing import Dict, Tuple, Set, Optional from calculator.core.solver.solver import Solver -from calculator.exceptions import VariableError, VariableRemoveRestrictError -from calculator.typing import NumericValue, Variable +from calculator.exceptions import VariableError, VariableRemoveRestrictError, VariableNameError +from calculator import NumericValue, Variable from calculator.utils import OrderedDefaultDict @@ -15,6 +15,7 @@ class Calculator(object): ANSWER_VARIABLE_NAME = 'Ans' DEFAULT_VARIABLE_TYPE = int + MAX_VARIABLE_NAME_LEN = 10 def __init__(self): super().__init__() @@ -46,11 +47,15 @@ def process(self, expression: str) -> Tuple[Optional[NumericValue], Dict[str, Va if len(root_node.targets) != 1 or not isinstance(root_node.targets[0], Name): raise SyntaxError('Assign to multiple variables or to indexed variable is not supported.') + variable_name = root_node.targets[0].id + if len(variable_name) > self.MAX_VARIABLE_NAME_LEN: + raise VariableNameError('Variable name "{var}" is too long (max {max} characters.)' + .format(var=variable_name, max=self.MAX_VARIABLE_NAME_LEN)) + value = self._solver.compute(root_node.value, self.variables) used_variables = self._solver.get_used_variables() # test recursive assign - variable_name = root_node.targets[0].id if self._has_circular_dependence(variable_name, used_variables): raise VariableError( "Assignment to a variable '{variable_name}' would create a circular dependency.".format( @@ -58,7 +63,9 @@ def process(self, expression: str) -> Tuple[Optional[NumericValue], Dict[str, Va )) # update by new created variables from Solver - self._variables.update(self._solver.variables) + variables = self._solver.variables + self._check_variable_names(variables) + self._variables.update(variables) # create new var self._variables[variable_name] = value, expression, used_variables # and refresh all depending @@ -69,7 +76,10 @@ def process(self, expression: str) -> Tuple[Optional[NumericValue], Dict[str, Va # simple expression result = self._solver.compute(expression, self.variables) # update new vars used by solver resolved from expression - self._variables.update(self._solver.variables) + variables = self._solver.variables + self._check_variable_names(variables) + self._variables.update(variables) + self._variables[self.ANSWER_VARIABLE_NAME] = result, expression, self._solver.get_used_variables() return result, self._variables.copy() @@ -150,3 +160,15 @@ def _get_depending_variables(self, variable: str) -> Set[str]: :return: set of depending vars """ return {name for name, definition in self.variables.items() if variable in definition[2]} + + def _check_variable_names(self, variables: Dict[str, Variable]) -> None: + """ + Check if all variable names are valid, if not raise VariableNameError + + :param variables: variables to check + :return: None + """ + for var in variables: + if len(var) > self.MAX_VARIABLE_NAME_LEN: + raise VariableNameError('Variable name "{var}" is too long (max {max} characters.)' + .format(var=var, max=self.MAX_VARIABLE_NAME_LEN)) diff --git a/src/calculator/core/math/math.py b/src/calculator/core/math/math.py index 8b0a818..371ab19 100644 --- a/src/calculator/core/math/math.py +++ b/src/calculator/core/math/math.py @@ -3,9 +3,9 @@ import operator import random +from calculator._typing import BinaryNumericFunction +from calculator._typing import NumericValue from calculator.exceptions import MathError -from calculator.typing import BinaryNumericFunction -from calculator.typing import NumericValue class Math(object): @@ -16,8 +16,14 @@ class Math(object): add = operator.add # type: BinaryNumericFunction subtract = operator.sub # type: BinaryNumericFunction multiple = operator.mul # type: BinaryNumericFunction - abs = math.fabs # type: BinaryNumericFunction - pow = math.pow # type: BinaryNumericFunction + + @staticmethod + def abs(x: NumericValue) -> NumericValue: + return math.fabs(x) + + @staticmethod + def pow(x: NumericValue, y: NumericValue) -> NumericValue: + return math.pow(x, y) @staticmethod def divide(a: NumericValue, b: NumericValue) -> NumericValue: @@ -41,15 +47,21 @@ def modulo(a: NumericValue, b: NumericValue) -> NumericValue: raise MathError from e @staticmethod - def fact(n: int) -> int: + def fact(n: NumericValue) -> NumericValue: + if abs(int(n) - n) > 0: + raise MathError('Invalid parameter for factorial.') + try: - return math.factorial(n) + return math.gamma(n + 1) except ValueError as e: raise MathError from e - @classmethod - def log(cls, x: NumericValue, base: NumericValue = 10) -> NumericValue: - return cls.ln(x, base) + @staticmethod + def log(x: NumericValue, base: NumericValue = 10) -> NumericValue: + try: + return math.log(x, base) + except ValueError as e: + raise MathError from e @staticmethod def root(x: NumericValue, y: NumericValue = 2) -> NumericValue: @@ -69,10 +81,6 @@ def rand() -> NumericValue: """ return random.random() - @staticmethod - def ln(x: NumericValue, base: NumericValue = math.e) -> NumericValue: - # TODO choosing more accurate functions - try: - return math.log(x, base) - except ValueError as e: - raise MathError from e + @classmethod + def ln(cls, x: NumericValue) -> NumericValue: + return cls.log(x, math.e) diff --git a/src/calculator/core/parser/parser.py b/src/calculator/core/parser/parser.py index 580eb79..b2caf17 100644 --- a/src/calculator/core/parser/parser.py +++ b/src/calculator/core/parser/parser.py @@ -4,7 +4,7 @@ from calculator.core.parser.preprocessor import AbsoluteValuePreprocessor from calculator.core.parser.preprocessor import FactorialPreprocessor -from calculator.core.parser.transform import ComplexRestrictTransform +from calculator.core.parser.transform import ComplexRestrictTransform, AugAssignRestrictTransform from calculator.exceptions import ParserSyntaxError, SyntaxRestrictError @@ -17,9 +17,9 @@ class Parser(object): AbsoluteValuePreprocessor, FactorialPreprocessor, ) - # TODO solve problem with hexadecimals literals like '4A' DEFAULT_TRANSFORMS = ( ComplexRestrictTransform, + AugAssignRestrictTransform ) _transforms = () diff --git a/src/calculator/core/parser/preprocessor/caret_power.py b/src/calculator/core/parser/preprocessor/caret_power.py new file mode 100644 index 0000000..a1409a9 --- /dev/null +++ b/src/calculator/core/parser/preprocessor/caret_power.py @@ -0,0 +1,12 @@ +# coding=utf-8 + + +class CaretPowerPreprocessor(object): + """ + Preprocessor, which converts all carets as ^ (as xor) to ** (as power). + """ + CARET_SIGN = '^' + POWER_SIGN = '**' + + def __call__(self, expression: str) -> str: + return expression.replace(self.CARET_SIGN, self.POWER_SIGN) diff --git a/src/calculator/core/parser/preprocessor/factorial.py b/src/calculator/core/parser/preprocessor/factorial.py index b4bec5d..fea4e8d 100644 --- a/src/calculator/core/parser/preprocessor/factorial.py +++ b/src/calculator/core/parser/preprocessor/factorial.py @@ -18,11 +18,11 @@ class FactorialPreprocessor(object): # position is reverted, because factorial is matched from !, which is at the end of wanted expression _MATH_EXPRESSION_REGEX = re.compile( r''' - [\dA-F.]+ # number or variable name + [\wA-F.]+ # number or variable name | - \)[\d\w+-/* ]+\(\w* # or function call with any arguments + \)[\w+-/* ]+\(\w* # or function call with any arguments | - \)[()\d\w+-/* ]+\( # or any expression + \)[()\w+-/* ]+\( # or any expression ''', re.VERBOSE ) diff --git a/src/calculator/core/parser/transform/__init__.py b/src/calculator/core/parser/transform/__init__.py index d5878ec..ee76e74 100644 --- a/src/calculator/core/parser/transform/__init__.py +++ b/src/calculator/core/parser/transform/__init__.py @@ -1,5 +1,6 @@ # coding=utf-8 +from .aug_assign_restrict import AugAssignRestrictTransform from .complex_restrict import ComplexRestrictTransform -__all__ = ('ComplexRestrictTransform', ) +__all__ = ('ComplexRestrictTransform', 'AugAssignRestrictTransform') diff --git a/src/calculator/core/parser/transform/aug_assign_restrict.py b/src/calculator/core/parser/transform/aug_assign_restrict.py new file mode 100644 index 0000000..24cc592 --- /dev/null +++ b/src/calculator/core/parser/transform/aug_assign_restrict.py @@ -0,0 +1,16 @@ +# coding=utf-8 +from ast import AugAssign, NodeTransformer + +from calculator.exceptions import SyntaxRestrictError + + +class AugAssignRestrictTransform(NodeTransformer): + """ + Restrict for aug assignments like a += 5. + """ + + def visit_AugAssign(self, assign: AugAssign) -> None: + """ + :param assign: AugAssign node of found assign in processed AST + """ + raise SyntaxRestrictError('Aug assign is not actually supported.') diff --git a/src/calculator/core/solver/solver.py b/src/calculator/core/solver/solver.py index 0864ce9..6db679c 100644 --- a/src/calculator/core/solver/solver.py +++ b/src/calculator/core/solver/solver.py @@ -1,14 +1,14 @@ # coding=utf-8 from ast import BinOp, Add, Num, Sub, Div, Mult, Call, AST, UnaryOp, USub, Name, Pow, FloorDiv, Mod +from inspect import Signature +from operator import attrgetter from typing import Dict, Union, Type, Set, Optional +from calculator import BinaryNumericFunction, NumericFunction, NumericValue, Variable from calculator.core.math import Math from calculator.core.parser import Parser +from calculator.exceptions import InvalidFunctionCallError from calculator.settings import BuiltinFunction -from calculator.typing import BinaryNumericFunction -from calculator.typing import NumericFunction -from calculator.typing import NumericValue -from calculator.typing import Variable from calculator.utils import method_single_dispatch @@ -112,12 +112,35 @@ def _(self, call: Call) -> NumericValue: :return: result of the called function """ # TODO I am not sure, if call.func is always Name node with .id attribute - function = self.builtin_functions.get(call.func.id) + function_name = call.func.id + function = self.builtin_functions.get(function_name) if not callable(function): - raise NotImplementedError(call.func.id) - - return function(*map(self._resolve, call.args)) + raise NameError(function_name) + + signature = Signature.from_callable(function) + + args = tuple(map(self._resolve, call.args)) + kwargs = dict(zip( + map(attrgetter('arg'), call.keywords), + map( + self._resolve, + map( + attrgetter('value'), + call.keywords + ) + ) + )) + + try: + signature.bind(*args, **kwargs) + except TypeError as e: + raise InvalidFunctionCallError( + function_name, + 'Given parameters does not correspond to function signature.' + ) from e + + return function(*args, **kwargs) @_resolve.register(Num) def _(self, num: Num) -> NumericValue: diff --git a/src/calculator/exceptions.py b/src/calculator/exceptions.py index 39f2e43..8b5153a 100644 --- a/src/calculator/exceptions.py +++ b/src/calculator/exceptions.py @@ -25,11 +25,33 @@ class VariableError(Exception): """ +class VariableNameError(VariableError): + """ + Raised when variable has an invalid name. + """ + + +class UnsupportedBaseError(Exception): + """ + Raised when trying convert number into unsupported base + """ + + class VariableRemoveRestrictError(VariableError): """ Raised from Calculator, if there are any depending variables to variable to remove. """ def __init__(self, dependencies, *args, **kwargs): - self._dependencies = dependencies + self.dependencies = dependencies + super().__init__(*args, **kwargs) + + +class InvalidFunctionCallError(TypeError): + """ + Raised to signalize of call function with incorrect count of parameters. + """ + + def __init__(self, function_name, *args, **kwargs): + self.function_name = function_name super().__init__(*args, **kwargs) diff --git a/src/calculator/main.py b/src/calculator/main.py new file mode 100755 index 0000000..80779cf --- /dev/null +++ b/src/calculator/main.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# coding=utf-8 + +import sys +from fileinput import input +from glob import iglob +from itertools import chain +from os import W_OK, access +from os.path import exists, getmtime, abspath, dirname, join + +# definitive solution for calculator imports - due running from non standard working directories +base_path = abspath(dirname(__file__)) +sys.path.insert(0, join(base_path, '..')) + +QRC_FILE = abspath(join(base_path, './ui/qml.qrc')) +RESOURCES_FILE = abspath(join(base_path, './ui/resources.py')) + + +def main(): + if len(sys.argv) > 1 and sys.argv[1] in {'-sd', '--standard-deviation'}: + from calculator.standard_deviation import main as sd_main + sys.exit(sd_main( + # file from first parameter if given else std input + input(files=sys.argv[2] if len(sys.argv) > 2 else '-') + )) + + if not update_qrc(): + print('Application cannot be started due problems with resources file.') + sys.exit(1) + + # local import due to resources.py + from calculator.ui.app import CalculatorApp + app = CalculatorApp(sys.argv) + + sys.exit(app.run()) + + +def update_qrc(): + """ + Conditionally updates the qrc file from QML and dependent resources. + :return: True, if file was successfully updated or update is not required, else None + """ + file_types = 'qml qrc js ttf otf svg qm png'.split() + + last_modification = max( + map( + getmtime, + chain( + *( + file_ for file_ in ( + iglob(join( + abspath(dirname(__file__)), + '{base_path}/**/*.{file_type}'.format( + file_type=file_type, + base_path=base_path) + ), recursive=True + ) for file_type in file_types) + ) + ) + ) + ) + + if exists(RESOURCES_FILE) and (last_modification - getmtime(RESOURCES_FILE)) < 2: + return True + + if exists(RESOURCES_FILE) and not access(RESOURCES_FILE, W_OK): + print('Resources file {} is outdated.'.format( + RESOURCES_FILE + ), file=sys.stderr) + return True + + print('Change in UI files detected, recompiling resources.py...', file=sys.stderr) + if not access(RESOURCES_FILE, W_OK): + print('Resources file {} is not exist and is not writable, please call with write permissions.'.format( + RESOURCES_FILE + ), file=sys.stderr) + return False + + from PyQt5.pyrcc_main import processResourceFile + if processResourceFile([QRC_FILE], RESOURCES_FILE, False): + print('Resources.py successfully recompiled.', file=sys.stderr) + return True + + print('Problem with compiling resources.py.', file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/src/calculator/settings.py b/src/calculator/settings.py index 6160bf7..1dec5eb 100644 --- a/src/calculator/settings.py +++ b/src/calculator/settings.py @@ -5,6 +5,8 @@ from PyQt5.QtQml import QQmlEngine, QJSEngine ICON_SIZES = (16, 24, 32, 48, 256) +SUPPORTED_BASES = (10, 2, 8, 16) + class BuiltinFunction(object): ABS = 'abs' @@ -17,7 +19,9 @@ class BuiltinFunction(object): RAND = 'rand' -EXPRESSION_SPLITTERS = ("+", "-", "(", "*", "/") +EXPRESSION_SPLITTERS = set("+-(*/,%") + + class Expression(QObject): class ExpressionTypes(IntEnum): Function = 0 @@ -29,6 +33,7 @@ class ExpressionTypes(IntEnum): def singletonProvider(engine: QQmlEngine, script_engine: QJSEngine) -> QObject: return Expression() + class Expansion(QObject): class ExpansionType(IntEnum): Normal = 0 @@ -57,14 +62,13 @@ def singletonProvider(engine: QQmlEngine, script_engine: QJSEngine) -> QObject: (("(n)(y)(a)(n)",), "#ED1869 #F2BC1F #39BFC1 #672980".split()), ) - EXPRESSION_EXPANSIONS = ( - (BuiltinFunction.ABS, 'abs(', Expansion.ExpansionType.BracketsPack), - (BuiltinFunction.FACT, 'fact(', Expansion.ExpansionType.BracketsPack), - (BuiltinFunction.LOG, 'log(', Expansion.ExpansionType.BracketsPack), - (BuiltinFunction.LN, 'ln(', Expansion.ExpansionType.BracketsPack), - (BuiltinFunction.ROOT, 'root(', Expansion.ExpansionType.BracketsPack), - (BuiltinFunction.POW, 'pow(', Expansion.ExpansionType.BracketsPack), - (BuiltinFunction.SQRT, 'sqrt(', Expansion.ExpansionType.BracketsPack), - (BuiltinFunction.RAND, 'rand(', Expansion.ExpansionType.BracketsPack) + (BuiltinFunction.ABS, 'abs', Expansion.ExpansionType.BracketsPack), + (BuiltinFunction.FACT, 'fact', Expansion.ExpansionType.BracketsPack), + (BuiltinFunction.LOG, 'log', Expansion.ExpansionType.BracketsPack), + (BuiltinFunction.LN, 'ln', Expansion.ExpansionType.BracketsPack), + (BuiltinFunction.ROOT, 'root', Expansion.ExpansionType.BracketsPack), + (BuiltinFunction.POW, 'pow', Expansion.ExpansionType.BracketsPack), + (BuiltinFunction.SQRT, 'sqrt', Expansion.ExpansionType.BracketsPack), + (BuiltinFunction.RAND, 'rand', Expansion.ExpansionType.BracketsPack) ) diff --git a/src/standard_deviation.py b/src/calculator/standard_deviation.py similarity index 97% rename from src/standard_deviation.py rename to src/calculator/standard_deviation.py index fc1276c..9601736 100644 --- a/src/standard_deviation.py +++ b/src/calculator/standard_deviation.py @@ -4,8 +4,8 @@ from typing import List from typing import Sequence +from calculator import NumericValue from calculator.core.math import Math -from calculator.typing import NumericValue def mean(values: Sequence[NumericValue]) -> float: diff --git a/src/calculator/translations/cs.qm b/src/calculator/translations/cs.qm deleted file mode 100644 index 060bd10..0000000 Binary files a/src/calculator/translations/cs.qm and /dev/null differ diff --git a/src/calculator/translations/cs.ts b/src/calculator/translations/cs.ts deleted file mode 100644 index 44fe7aa..0000000 --- a/src/calculator/translations/cs.ts +++ /dev/null @@ -1,64 +0,0 @@ - - - - - Adapter - - - Math error occured. - Matematická chyba. - - - - Error in defining variable. - Chyba při definování proměnné. - - - - Result is too big. - Výsledek je příliš velký. - - - - Expression contains syntax error. - Výraz obsahuje syntaktickou chybu. - - - - Error - - - Error - Chyba - - - - Ok - Ok - - - - ExpressionInput - - - Enter expression... - Zadejte výraz... - - - - Game - - - Easter egg - Easter egg - - - - main - - - Barbie Calculator - Barbie Calculator - - - diff --git a/src/calculator/ui/adapter.py b/src/calculator/ui/adapter.py index c92df19..07b42d4 100644 --- a/src/calculator/ui/adapter.py +++ b/src/calculator/ui/adapter.py @@ -1,18 +1,16 @@ # coding=utf-8 -import re - from typing import Dict, Tuple, Set from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QVariant from PyQt5.QtQml import QJSEngine, QQmlEngine +from calculator import Variable, NumericValue from calculator.core.calculator import Calculator -from calculator.exceptions import MathError, VariableError -from calculator.typing import Variable, NumericValue -from calculator.utils.number_formatter import NumberFormatter +from calculator.exceptions import MathError, VariableError, UnsupportedBaseError, InvalidFunctionCallError, \ + VariableRemoveRestrictError +from calculator.settings import (BUILTIN_FUNCTIONS, HIGHLIGHT_RULES, EXPRESSION_SPLITTERS, Expression) +from calculator.utils.formatter import Formatter from calculator.utils.translate import translate -from calculator.settings import (BUILTIN_FUNCTIONS, EXPRESSION_EXPANSIONS, HIGHLIGHT_RULES, EXPRESSION_SPLITTERS, - Expression) class UIAdapter(QObject): @@ -23,24 +21,37 @@ class UIAdapter(QObject): identifiersTypesChanged = pyqtSignal(QVariant) processed = pyqtSignal(QVariant) error = pyqtSignal(str) + _variables = dict() # type: Dict[str, Variable] - _formatter = NumberFormatter + _formatter = Formatter func_identifiers_types = [{"identifier": func, "type": Expression.ExpressionTypes.Function} for func in BUILTIN_FUNCTIONS] + @pyqtSlot(float, int, result=str) + def convertToBase(self, value: str, base: int) -> str: + try: + return self._formatter.format_number_in_base(value, base) + except UnsupportedBaseError as e: + return translate("Adapter", "Unsupported base.") + except ValueError as e: + return "-" + @pyqtSlot(str) def process(self, expression: str) -> None: try: result, variables = self._calculator.process(expression) created_variables, modified_variables = self._commit_new_variables_state(variables=variables) + if Calculator.ANSWER_VARIABLE_NAME in modified_variables: + result, *_ = variables.get(Calculator.ANSWER_VARIABLE_NAME) self.identifiersTypesChanged.emit(self.identifiersTypes) self.processed.emit(QVariant({ - "result": None if result is None else self._formatter.format(result, 16), + "result": None if result is None else self._formatter.format_number(result, characters_limit=12), + "unformattedResult": str(result), "variables": { key: dict( - value=self._formatter.format(value), + value=self._formatter.format_number(value, characters_limit=8), expression=self._format_source_expression( variable=key, source_expression=expression @@ -55,14 +66,22 @@ def process(self, expression: str) -> None: } })) - except SyntaxError as e: + except SyntaxError: self.error.emit(translate("Adapter", "Expression contains syntax error.")) - except MathError as e: - self.error.emit(translate("Adapter", "Math error occured.")) - except VariableError as e: + except MathError: + self.error.emit(translate("Adapter", "Math error occurred.")) + except VariableError: self.error.emit(translate("Adapter", "Error in defining variable.")) except OverflowError: self.error.emit(translate("Adapter", "Result is too big.")) + except InvalidFunctionCallError as e: + self.error.emit(translate("Adapter", "Given parameters does not match to function {}.").format( + e.function_name + )) + except NameError: + self.error.emit(translate("Adapter", "Function is not defined.")) + except NotImplementedError: + self.error.emit(translate("Adapter", "This construct is not supported.")) @pyqtSlot(str, int) def setVariableValue(self, variable: str, value: NumericValue): @@ -78,7 +97,7 @@ def setVariableValue(self, variable: str, value: NumericValue): "result": None, "variables": { key: dict( - value=self._formatter.format(value), + value=self._formatter.format_number(value), expression=self._format_source_expression( variable=key, source_expression=expression @@ -93,11 +112,17 @@ def setVariableValue(self, variable: str, value: NumericValue): } })) - @pyqtSlot(str) - def removeVariable(self, variable_identifier: str) -> None: - print(variable_identifier) - self._calculator.remove_variable(variable_identifier) + @pyqtSlot(str, result=bool) + def removeVariable(self, variable_identifier: str) -> bool: + try: + self._calculator.remove_variable(variable_identifier) + except VariableRemoveRestrictError as e: + # TODO: deps! + self.error.emit(translate("Adapter", "#TODO")) + return False + self._variables = self._calculator.variables.copy() + return True @pyqtProperty(QVariant) def highlightRules(self) -> QVariant: @@ -109,11 +134,6 @@ def highlightRules(self) -> QVariant: def builtinFunctions(self) -> QVariant: return QVariant(list(BUILTIN_FUNCTIONS)) - @pyqtProperty(QVariant) - def expressionsExpansion(self) -> QVariant: - return QVariant({expression: dict(expansion=expansion, expansionType=expansion_type) - for expression, expansion, expansion_type in EXPRESSION_EXPANSIONS}) - @staticmethod def singletonProvider(engine: QQmlEngine, script_engine: QJSEngine) -> QObject: adapter = UIAdapter() @@ -144,16 +164,10 @@ def _commit_new_variables_state(self, variables: Dict[str, Variable]) -> Tuple[S def expressionSplitters(self) -> QVariant: return QVariant(list(EXPRESSION_SPLITTERS)) - @pyqtProperty(str) - def expressionSplittersRegExp(self) -> str: - result = re.escape("".join(EXPRESSION_SPLITTERS)) - return "".join(("[", result ,"]")) - @pyqtProperty(QVariant, notify=identifiersTypesChanged) def variables(self) -> QVariant: return QVariant(list(self._calculator.variables.keys())) - #TODO notify @pyqtProperty(QVariant, notify=identifiersTypesChanged) def identifiersTypes(self): return [{"identifier": var_identifier, "type": Expression.ExpressionTypes.Variable} @@ -168,4 +182,6 @@ def _format_source_expression(variable: str, source_expression: str) -> str: :param source_expression: source expression for variable :return: striped source expression """ - return source_expression.lstrip(variable).lstrip() + return source_expression.replace(variable, '', 1).strip() \ + if '=' in source_expression else \ + source_expression.strip() diff --git a/src/calculator/ui/app/calculator_app.py b/src/calculator/ui/app/calculator_app.py index 2b0327e..a28d30b 100644 --- a/src/calculator/ui/app/calculator_app.py +++ b/src/calculator/ui/app/calculator_app.py @@ -2,24 +2,30 @@ import platform from typing import List -from PyQt5.QtCore import QSize +from termcolor import colored + +from PyQt5.QtCore import (QSize, QtFatalMsg, QtCriticalMsg, QtWarningMsg, QtInfoMsg, + qInstallMessageHandler, QtDebugMsg) from PyQt5.QtCore import QTranslator from PyQt5.QtCore import QUrl, QLocale from PyQt5.QtGui import QIcon -from PyQt5.QtGui import QPixmap -from PyQt5.QtQml import QQmlApplicationEngine, qmlRegisterSingletonType, qmlRegisterType +from PyQt5.QtQml import QQmlApplicationEngine, qmlRegisterSingletonType from PyQt5.QtWidgets import QApplication from calculator.ui.adapter import UIAdapter from calculator.ui.types.core import Sides -from calculator.ui.types.syntaxhighlight import ExpSyntaxHighlighter +from calculator.ui.types.expression import ExpSyntaxHighlighter, ExpAnalyzer from calculator.ui.types.qmlwrapper.utils import TypeRegister +from calculator.ui.types.window import AppWindow from calculator.settings import Expansion, ICON_SIZES, Expression if platform.system() == "Linux": # Needed for platform.linux_distribution, which is not available on Windows and OSX # For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 platform_identifier = platform.platform() - if 'Ubuntu' in platform_identifier or 'Debian' in platform_identifier: # Just in case it also happens on Debian, so it can be added + # Just in case it also happens on Debian, so it can be added + if 'Ubuntu' in platform_identifier or 'Debian' in platform_identifier: + # noinspection PyUnresolvedReferences + from OpenGL import GLU # noinspection PyUnresolvedReferences from OpenGL import GL @@ -27,21 +33,44 @@ import calculator.ui.resources +def qt_message_handler(mode, context, message): + modes = { + QtInfoMsg: "Info", + QtWarningMsg: "Warning", + QtCriticalMsg: "Critical", + QtFatalMsg: "Fatal", + QtDebugMsg: "Debug" + } + + modes_colors = { + QtInfoMsg: "blue", + QtWarningMsg: "yellow", + QtCriticalMsg: "red", + QtFatalMsg: "red", + QtDebugMsg: "green" + } + + mode = colored(modes[mode], modes_colors[mode]) + + if context.file is None: + print('{mode}: {msg}'.format(mode=mode, msg=message)) + else: + print('{mode}: {msg}\t\t\t\tline: {line}, function: {func}, file: {file}'.format( + mode=mode, line=context.line, func=context.function, file=context.file, msg=message)) + +qInstallMessageHandler(qt_message_handler) + class CalculatorApp(QApplication): def __init__(self, argv: List[str]): super().__init__(argv) self._translator = QTranslator() - self._translator.load("".join((":/translations/", QLocale().system().name(), ".qsm"))) + self._translator.load("".join((":/translations/", QLocale().system().name(), ".qm"))) self.installTranslator(self._translator) - - print(QPixmap(":/assets/images/icon.png").width()) icon = QIcon() for size in ICON_SIZES: - print(":/assets/icons/{}x{}.png".format(size, size)) icon.addFile(":/assets/icons/{}x{}.png".format(size, size), QSize(size, size)) - #icon.addPixmap(QPixmap(":/assets/icons/{}x{}.png".format(size, size))) self.setWindowIcon(icon) @@ -53,6 +82,8 @@ def registerTypes(): qmlRegisterSingletonType(Expression, "Expression", 1, 0 ,"Expression", Expression.singletonProvider) qmlRegisterSingletonType(UIAdapter, "Calculator", 1, 0, "Calculator", UIAdapter.singletonProvider) TypeRegister.register_type(ExpSyntaxHighlighter) + TypeRegister.register_type(AppWindow) + TypeRegister.register_type(ExpAnalyzer) def run(self) -> int: CalculatorApp.registerTypes() diff --git a/src/calculator/ui/assets/contents/help.js b/src/calculator/ui/assets/contents/help.js new file mode 100644 index 0000000..1bad936 --- /dev/null +++ b/src/calculator/ui/assets/contents/help.js @@ -0,0 +1,25 @@ +var content = [ + { + "title": qsTr("Barbie calculator"), + "content": qsTr("Manual on how to use Barbie Calculator.") + }, + { + "subtitle": qsTr("Getting started"), + "content": qsTr("First we need to enter expression, which we want to calculate into text input. Then we press the button with equal sign or press Enter/Return key. Congratulations you have done your very first calculation using Barbie calculator.") + }, + { + "title": qsTr("Advance") + }, + { + "subtitle": qsTr("Using completer"), + "content": qsTr("To show completer press Ctrl+Space. Completer will be shown automatically every time after entering operator. To choose item you want to complete click on the item or use arrows to navigate to item and then pres key Enter/Return.") + }, + { + "subtitle": qsTr("Variables intro"), + "content": qsTr("Barbie calculator also provide support of custom variables. To create variable 'a' simply enter assignment of 'a' as 'a=' and then you add expression. If variable depends on another variable then it's value will be updated every time any of the dependent variables change.") + }, + { + "subtitle": qsTr("Variables manipulation"), + "content": qsTr("To enter into input expression from which variable was calculated click on the expression of variable in variable panel. If you click on variable in panel it will enter it's identifier and after right-click it's value. To delete or set value hover menu header(three dots) to show menu where you can choose desired action.") + } +] diff --git a/src/calculator/ui/assets/images/arrow_left_dark.svg b/src/calculator/ui/assets/images/arrow_left_dark.svg new file mode 100644 index 0000000..47f2b42 --- /dev/null +++ b/src/calculator/ui/assets/images/arrow_left_dark.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/calculator/ui/assets/images/arrow_left_light.svg b/src/calculator/ui/assets/images/arrow_left_light.svg new file mode 100644 index 0000000..6799674 --- /dev/null +++ b/src/calculator/ui/assets/images/arrow_left_light.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/calculator/ui/assets/images/logo.svg b/src/calculator/ui/assets/images/logo.svg new file mode 100644 index 0000000..0136e8f --- /dev/null +++ b/src/calculator/ui/assets/images/logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/calculator/ui/assets/styles/UIStyles.qml b/src/calculator/ui/assets/styles/UIStyles.qml index fd37e63..e4a7370 100644 --- a/src/calculator/ui/assets/styles/UIStyles.qml +++ b/src/calculator/ui/assets/styles/UIStyles.qml @@ -1,6 +1,7 @@ pragma Singleton import QtQuick 2.0 import QtQuick.Controls.Styles 1.4 + import Expression 1.0 QtObject { @@ -9,7 +10,7 @@ QtObject { property QtObject functionPanel: QtObject { property color backgroundColor: "#2A2A2A" property color textColor: "white" - property color hoverTextColor: "#ED1D3D" + property color hoverColor: "white" } property QtObject expressionInput: QtObject { @@ -62,9 +63,6 @@ QtObject { property QtObject variablesPanel: QtObject { property color backgroundColor: "#2A2A2A" - property color textColor: "white" - property color identifierColor: "#ED1946" - property color expressionHoverColor: "#3D3D3D" property color scrollBarColor: "#B7B7B7" property font font: Qt.font({ family: "Roboto Light" @@ -75,11 +73,20 @@ QtObject { property color backgroundColor: "#C1C0C0" property color textColor: "white" property color identifierColor: "black" - property color expressionHoverColor: "#AAAAAA" + property color expressionHoverColor: "gray" + property color scrollbarColor: "black" + property string prompterTheme: "dark" + property color hoverColor: "black" property font font: styles.variablesPanel.font } property QtObject variableItem: QtObject { + property color scrollbarColor: "#ED1D3D" + property string prompterTheme: "light" + property color hoverColor: "gray" + property color textColor: "white" + property color identifierColor: "#ED1946" + property color expressionHoverColor: "#3D3D3D" property QtObject dots: QtObject { property color color: "#3D3D3D" } @@ -105,6 +112,7 @@ QtObject { property QtObject calculateButton: QtObject { property color backgroundColor: "#ED1946" + property color hoverColor: "black" } property QtObject errorDialog: QtObject { @@ -114,8 +122,15 @@ QtObject { property font font: Qt.font({family: "Roboto Light"}) } + property QtObject infoDialog: QtObject { + property color maskColor: "black" + property color color: "#FFCE00" + property color textColor: "white" + property font font: Qt.font({family: "Roboto Light"}) + } + property QtObject completer: QtObject { - property color color: "#2A2A2A" + property color color: "black" property color hoverColor: "#ED1946" property color textColor: "#C1C0C0" property color scrollBarColor: "#9F9F9F" @@ -127,4 +142,57 @@ QtObject { typeColors[Expression.Variable] = "#C1C0C0" } } + + property QtObject countDown: QtObject { + property var textColors: {0: "#ED1869", 1: "#F2BC1F", 2: "#39BFC1", 3: "#672980"} + property font font: Qt.font({family: "Roboto Light"}) + } + + property QtObject resultSystemDisplay: QtObject { + property color color: "#F2F2F2" + property color baseTextColor: "#ED1D3D" + property color valueTextColor: "#3D3D3D" + property color scrollbarColor: "#ED1D3D" + property string prompterTheme: "dark" + property font font: Qt.font({family: "Roboto Light"}) + } + + property QtObject functionSignatureDisplay: QtObject { + property color color: "black" + property color textColor: "white" + property font font: Qt.font({family: "Roboto Light"}) + } + + property QtObject appMenuBar: QtObject { + property color color: "#2C2C2C" + property font font: Qt.font({family: "Roboto Light"}) + + property QtObject title: QtObject { + property color color: "white" + property color activeColor: "#ED1946" + } + + property QtObject item: QtObject { + property color backgroundColor: "white" + property color borderColor: "lightGray" + property color hoverColor: "lightGray" + property color textColor: "#3D3D3D" + property color hoverTextColor: "white" + } + } + + property QtObject aboutWindow: QtObject { + property color color: "black" + property color textColor: "lightGray" + property color titleColor: "#ED1946" + property font font: Qt.font({family: "AbeeZee"}) + } + + property QtObject helpWindow: QtObject { + property color textColor: "gray" + property color titleColor: "#ED1946" + property color subtitleColor: "#2C2C2C" + property color scrollBarColor: "#2C2C2C" + property font font: Qt.font({family: "AbeeZee"}) + } } diff --git a/src/calculator/ui/easteregg/Game.qml b/src/calculator/ui/easteregg/Game.qml index a4c4412..42b2509 100644 --- a/src/calculator/ui/easteregg/Game.qml +++ b/src/calculator/ui/easteregg/Game.qml @@ -7,16 +7,29 @@ import "entities" as Entities import "visualization" as Visualization import "logic/collision" as Collision +/** + Standalone window with game(pong) + */ ApplicationWindow { id: gameWindow + /** + Used as function to start game + */ signal run() + /** + Emits when some of players loose + @param msg Message to player + */ signal gameOver(string msg) - property bool running: false + /// Score of player property int playerScore: 0 + /// Score of other player property int enemyScore: 0 - property int maxScore: 3 + /// Max score which player need to archieve to win + readonly property int maxScore: 3 + /// Size of player rebound area property size playerSize: Qt.size(10, 100) width: 1500 @@ -120,8 +133,12 @@ ApplicationWindow { Component.onCompleted: collisionSystem.registerPair(nyan, ai) } + /** + Resets component to begin game + */ function startGame() { nyan.vector = Qt.point(Math.cos(Math.PI / 3), -Math.sin(Math.PI / 3)) + nyan.rotateNyanCat() nyan.x = gameWindow.width / 2 - nyan.width / 2 nyan.y = gameWindow.height / 2 - nyan.height / 2 gameWindow.playerScore = 0 @@ -130,11 +147,19 @@ ApplicationWindow { frameTimer.running = true } + /** + Sets component to end game + */ function endGame() { frameTimer.running = false gameWindow.visible = false } + /** + React to collision of ball to increment score + @param obj Ball reference + @param side Side of collision to determinate who has point + */ function handleCollisionWithWall(obj, side) { if(!obj.isVoid) return @@ -145,8 +170,8 @@ ApplicationWindow { gameWindow.enemyScore += 1 if(gameWindow.enemyScore == gameWindow.maxScore) - gameWindow.gameOver("YouAreLooser") + gameWindow.gameOver(qsTr("You are looser.")) if(gameWindow.playerScore == gameWindow.maxScore) - gameWindow.gameOver("AIIsNoob") + gameWindow.gameOver(qsTr("AI is noob.")) } } diff --git a/src/calculator/ui/easteregg/animations/ParticleGenerator.qml b/src/calculator/ui/easteregg/animations/ParticleGenerator.qml index 8c542ae..5ab9ac4 100644 --- a/src/calculator/ui/easteregg/animations/ParticleGenerator.qml +++ b/src/calculator/ui/easteregg/animations/ParticleGenerator.qml @@ -1,14 +1,22 @@ import QtQuick 2.0 import QtQuick.Particles 2.0 +/** + Component which generates particles + */ Item { id: component + /// Expose emitter component property alias emitter: emitter + /// String name to identify group of particles which are the same property string groupName + /// Parent of particles, determinates particle coord system and visibility property Item particleParent + /// Stores reference to global particle system property Item particleSystem + /// Property which store component of particle, which will be delegated property Component particle: Rectangle { width: component.width height: width diff --git a/src/calculator/ui/easteregg/animations/RainbowTail.qml b/src/calculator/ui/easteregg/animations/RainbowTail.qml index a888cfe..2b0f95a 100644 --- a/src/calculator/ui/easteregg/animations/RainbowTail.qml +++ b/src/calculator/ui/easteregg/animations/RainbowTail.qml @@ -1,12 +1,19 @@ import QtQuick 2.0 import QtQuick.Particles 2.0 +/** + Component which generates rainbow like particles + */ Item { id: rainbowTail + /// Angle of particles, determinates angle of their trajectory property real generationAngle: 0 + /// Size of particle in pixels property size particleSize: Qt.size(10, 10) + /// Reference to root item, so particle could not be affected by any relatice coord system property Item rootItem + /// Reference to item to which particle emitters attached property Item containerItem clip: false diff --git a/src/calculator/ui/easteregg/entities/Player.qml b/src/calculator/ui/easteregg/entities/Player.qml index b38b858..837fc7a 100644 --- a/src/calculator/ui/easteregg/entities/Player.qml +++ b/src/calculator/ui/easteregg/entities/Player.qml @@ -2,14 +2,22 @@ import QtQuick 2.0 import "../logic/extendedmath.js" as EMath import "../logic/collision" as Collision +/** + This entitiy is used as player(rebound area) + */ Collision.BoxCollider { id: component + /// Position to which player will move property real wantedPosition: 0 + /// Step of movement in pixels per frame property real step color: "orange" + /** + Set Player new position, need to be called every frame + */ function frameMove() { var posDifference = wantedPosition - component.y - component.height / 2 diff --git a/src/calculator/ui/easteregg/entities/Wall.qml b/src/calculator/ui/easteregg/entities/Wall.qml index e68719d..aa175c3 100644 --- a/src/calculator/ui/easteregg/entities/Wall.qml +++ b/src/calculator/ui/easteregg/entities/Wall.qml @@ -3,6 +3,9 @@ import QtQuick.Controls 2.0 import Sides 1.0 import "../logic/collision" as Collision +/** + This entity works as rebound area, but is static + */ Collision.BoxCollider { id: component diff --git a/src/calculator/ui/easteregg/entities/WallSystem.qml b/src/calculator/ui/easteregg/entities/WallSystem.qml index 64917fe..8fc8622 100644 --- a/src/calculator/ui/easteregg/entities/WallSystem.qml +++ b/src/calculator/ui/easteregg/entities/WallSystem.qml @@ -2,13 +2,23 @@ import QtQuick 2.0 import Sides 1.0 import "../logic/collision" +/** + System of walls to border all playground edges + */ Item { id: component + /** + Emits collision with some wall in system + @param side Side of collision related to wall + */ signal voidCollided(int side) + /// Reference to global CollisionSystem to determinate collision property CollisionSystem collisionSystem + /// Width of walls in pixels property real wallWidth: 10 + /// Color of walls property color wallColor Wall { diff --git a/src/calculator/ui/easteregg/logic/collision/BoxCollider.qml b/src/calculator/ui/easteregg/logic/collision/BoxCollider.qml index 0aebc45..f8adf7b 100644 --- a/src/calculator/ui/easteregg/logic/collision/BoxCollider.qml +++ b/src/calculator/ui/easteregg/logic/collision/BoxCollider.qml @@ -1,9 +1,18 @@ import QtQuick 2.0 +/** + Base type for Entity + */ Rectangle { id: component + /** + Emits after collision with other BoxCollider + @param obj Object with which current item collided + @param side Side of collision relative to current item + */ signal collided(var obj, int side) + /// Defines whether is BoxCollider as void border property bool isVoid: false } diff --git a/src/calculator/ui/easteregg/logic/collision/CollisionSystem.qml b/src/calculator/ui/easteregg/logic/collision/CollisionSystem.qml index 9482cb3..ce2996f 100644 --- a/src/calculator/ui/easteregg/logic/collision/CollisionSystem.qml +++ b/src/calculator/ui/easteregg/logic/collision/CollisionSystem.qml @@ -2,9 +2,18 @@ import QtQuick 2.0 import Sides 1.0 import "../extendedmath.js" as EMath +/** + System to determinate collision of registered objects + */ QtObject { id: system + /** + Checks whether collision between two objects happened + @param bc1 First object + @param bc2 Second object + @return Side of collision referenced to first object, if collision did not happened return -1 + */ function checkCollision(bc1, bc2) { var r1 = bc1.mapToItem(null, 0, 0, bc1.width, bc1.height) var r2 = bc2.mapToItem(null, 0, 0, bc2.width, bc2.height) @@ -34,6 +43,11 @@ QtObject { return Sides.Right } + /** + Checks for collisions and if collision occure then emits signals of objects + @param bc1 First object + @param bc2 Second object + */ function watchCollision(bc1, bc2) { var result = system.checkCollision(bc1, bc2) @@ -43,6 +57,11 @@ QtObject { } } + /** + Register objects to system + @param bc1 First object + @param bc2 Second object + */ function registerPair(bc1, bc2) { bc1.xChanged.connect((function() {system.watchCollision(bc1, bc2)})) bc1.yChanged.connect((function() {system.watchCollision(bc1, bc2)})) diff --git a/src/calculator/ui/easteregg/logic/extendedmath.js b/src/calculator/ui/easteregg/logic/extendedmath.js index 119adce..953d893 100644 --- a/src/calculator/ui/easteregg/logic/extendedmath.js +++ b/src/calculator/ui/easteregg/logic/extendedmath.js @@ -1,17 +1,37 @@ .pragma library +/** + Return sign of number as positive or negative 1, if number is zero then it returns 0 + @param number Number which sign will be returned + @return Return -1 or 1 in case if number is not zero else returns 0 + */ function sgn(number) { if(number === 0) return 0 return Math.abs(number) / number } +/** + Checks whether is in interval + @param number Number which will be tested + @param start Bottom border of interval + @param end Top border of interval + @return True if number is in interval else returns false + */ function numberInInterval(number, start, end) { if(number >= start && number <= end) return true return false } +/** + Checks whether two intervals intersects + @param start1 Bottom border of first interval + @param end1 Top border of first interval + @param start2 Bottom border of second interval + @param end2 Top border of second interval + @return True if intervals intersects else returns false + */ function intervalHasIntersection(start1, end1, start2, end2) { if(numberInInterval(start1, start2, end2) || numberInInterval(end1, start2, end2)) return true diff --git a/src/calculator/ui/easteregg/visualization/Score.qml b/src/calculator/ui/easteregg/visualization/Score.qml index 48ebdf3..8d7f2dc 100644 --- a/src/calculator/ui/easteregg/visualization/Score.qml +++ b/src/calculator/ui/easteregg/visualization/Score.qml @@ -1,10 +1,16 @@ import QtQuick 2.0 +/** + Display score in format X:Y + */ Item { id: component + /// Score of player property int playerScore: 0 + /// Score of other player property int enemyScore: 0 + /// Color of score text property alias textColor: text.color Text { diff --git a/src/calculator/ui/qml.qrc b/src/calculator/ui/qml.qrc index aaec22e..ce08015 100644 --- a/src/calculator/ui/qml.qrc +++ b/src/calculator/ui/qml.qrc @@ -37,9 +37,9 @@ qml/controls/CalculateButton.qml assets/images/equal.svg qml/visualization/AnimatedText.qml - qml/visualization/Error.qml + qml/visualization/PopUp.qml qml/controls/DefaultButton.qml - ../translations/cs.qm + translations/cs.qm assets/icons/16x16.png assets/icons/24x24.png assets/icons/32x32.png @@ -47,5 +47,17 @@ assets/icons/256x256.png qml/controls/DropDown.qml qml/controls/Completer.qml + qml/visualization/ResultSystemDisplay.qml + qml/visualization/CountDown.qml + qml/visualization/FunctionSignatureDisplay.qml + qml/visualization/FlickableAnimatedText.qml + qml/menu/AppMenuBar.qml + qml/windows/About.qml + assets/images/logo.svg + qml/controls/FilledClickable.qml + assets/images/arrow_left_dark.svg + assets/images/arrow_left_light.svg + qml/windows/Help.qml + assets/contents/help.js diff --git a/src/calculator/ui/qml/containers/FunctionsPanel.qml b/src/calculator/ui/qml/containers/FunctionsPanel.qml index 8fbaa67..5d9f85e 100644 --- a/src/calculator/ui/qml/containers/FunctionsPanel.qml +++ b/src/calculator/ui/qml/containers/FunctionsPanel.qml @@ -1,16 +1,25 @@ import QtQuick 2.0 import "../controls" as Control +/** + Panel of function buttons + */ Item { id: component + /// Emits after clicking on function button, so function identifier could be expanded signal expandRequest(string func) + /// List of functions to be delegated property var items + /// Number of columns in which will be button displayed property alias columns: grid.columns + /// Background color of buttons property color backgroundColor + /// Text color of labels on buttons property color textColor - property color hoverTextColor + /// Background color of mask when button is hovered + property color hoverColor Grid { id: grid @@ -23,7 +32,7 @@ Item { buttonText: modelData textColor: component.textColor color: component.backgroundColor - hoverTextColor: component.hoverTextColor + hoverColor: component.hoverColor width: component.width / grid.columns height: component.height / grid.rows diff --git a/src/calculator/ui/qml/containers/Stack.qml b/src/calculator/ui/qml/containers/Stack.qml index 6ffff8b..2010e3e 100644 --- a/src/calculator/ui/qml/containers/Stack.qml +++ b/src/calculator/ui/qml/containers/Stack.qml @@ -1,5 +1,8 @@ import QtQuick 2.0 +/** + Container item with addition and move animation + */ Column { id: component diff --git a/src/calculator/ui/qml/containers/VariablesPanel.qml b/src/calculator/ui/qml/containers/VariablesPanel.qml index a28e043..557b318 100644 --- a/src/calculator/ui/qml/containers/VariablesPanel.qml +++ b/src/calculator/ui/qml/containers/VariablesPanel.qml @@ -2,33 +2,81 @@ import QtQuick 2.0 import "../managers" import "../visualization" +/** + Interactive panel which display all variables with their attributes + */ Item { id: component + /** + Emit after clicking on set button of variable + @param identifier Identifier of variable + @param value Value requested to be set to value + */ signal setVariableRequest(string identifier, real value) + /** + Emits after clicking on delete button of variable + @param identifier Identifier of variable + */ signal deleteVariableRequest(string identifier) + /** + Emits after clicking on certain part, it requests to some string to be expanded into user input + @param data String which is requested to expansion + */ signal expandRequest(string data) + /** + Emits after clicking on certain part, it requests to some string to overwrite user input + @param data String which is requested to overwrite + */ signal overwriteRequest(string data) + /// Height if single variable item property real itemHeight: 0 + /// Text color of expression and value of variable property color textColor + /// Text color of identifier and value of variable property color identifierTextColor + /// Background color of single variable panel property color color: backgroundColor + /// Background of panel property alias backgroundColor: background.color + /// Color of scrollbar property alias scrollBarColor: scrollBar.color + /// Background color of expression while hovering property color expressionHoverColor + /// Background color of Ans variable property alias ansColor: ansItem.color + /// Text color of identifier and value of Ans variable property alias ansTextColor: ansItem.textColor + /// Text color of identifier and value of Ans variable property alias ansIdentifierTextColor: ansItem.identifierTextColor + /// Background color of Ans variable expression while hovering property alias ansExpressionHoverColor: ansItem.expressionHoverColor - + /// Color of value scrollbar Ans variable + property alias ansScrollbarColor: ansItem.scrollbarColor + /// Prompter of value flickable theme in Ans + property alias ansPrompterTheme: ansItem.prompterTheme + /// Hover color of ans + property alias ansHoverColor: ansItem.hoverColor + + /// Background color of dots(area to slide variable options) property color dotsBackgroundColor + /// Background color of variable remove button property color removeButtonColor + /// Background color of variable setters property color settersColor + /// Background color of variable setters when hovering property color settersHoveredColor + /// Text color of variable setters property color settersTextColor - + /// Color of value scrollbar + property color itemScrollbarColor + /// Prompter of value flickable theme of item + property string prompterTheme + /// Hover color of item + property color hoverColor + /// Font of panel property font font clip: true @@ -55,6 +103,9 @@ Item { object.settersColor = Qt.binding(function() { return component.settersColor }) object.settersHoveredColor = Qt.binding(function() { return component.settersHoveredColor }) object.settersTextColor = Qt.binding(function() { return component.settersTextColor }) + object.scrollbarColor = Qt.binding(function() { return component.itemScrollbarColor }) + object.prompterTheme = Qt.binding(function() { return component.prompterTheme }) + object.hoverColor = Qt.binding(function() { return component.hoverColor }) object.width = Qt.binding(function() { return component.width }) object.height = Qt.binding(function() { return component.itemHeight }) @@ -77,6 +128,7 @@ Item { variableValue: "0" height: component.itemHeight + z: 2 font.family: component.font.family @@ -113,15 +165,18 @@ Item { opacity: (flick.visibleArea.heightRatio === 1) ?0 :1 - y: flick.visibleArea.yPosition * component.height + y: flick.visibleArea.yPosition * flick.height + flick.y width: 3 - height: component.height * flick.visibleArea.heightRatio + height: flick.height * flick.visibleArea.heightRatio Behavior on opacity { NumberAnimation { duration: 400 } } } + /** + Create once component to be able later instance that component + */ function _initComponent() { var itemComponent = Qt.createComponent("qrc:/qml/item/VariableItem.qml") manager.itemComponent = itemComponent @@ -132,6 +187,12 @@ Item { }); } + /** + Use create or modify variable according to if variable exists + @param identifier Identifier of variable + @param expression Expression of variable + @param value Value of variable + */ function handleVariableAction(identifier, expression, value) { if(manager.findVariable(identifier) === null) manager.addVariable(identifier, expression, value) @@ -139,12 +200,32 @@ Item { manager.setVariable(identifier, expression, value) } + /** + Create new variable in panel + @param identifier Identifier of variable + @param expression Expression of variable + @param value Value of variable + */ function createVariable(identifier, expression, value) { manager.addVariable(identifier, expression, value) } + /** + Modifies variable(set expression or value) + @param identifier Identifier of variable + @param expression New expression of variable + @param value New value of variable + */ function modifyVariable(identifier, expression, value) { manager.setVariable(identifier, expression, value) } + + /** + Delete variable item + @param variableIdentifier Identifier of variable + */ + function deleteVariable(variableIdentifier) { + manager.deleteVariable(variableIdentifier) + } } diff --git a/src/calculator/ui/qml/controls/CalculateButton.qml b/src/calculator/ui/qml/controls/CalculateButton.qml index e4192fd..b1722a3 100644 --- a/src/calculator/ui/qml/controls/CalculateButton.qml +++ b/src/calculator/ui/qml/controls/CalculateButton.qml @@ -1,12 +1,13 @@ import QtQuick 2.0 -Clickable { - property alias color: background.color +/** + Button to confirm calculation + */ +FilledClickable { + id: component - Rectangle { - id: background - anchors.fill: parent - } + hoverMaskEnabled: true + hoverEnabled: true Image { source: "qrc:/assets/images/equal.svg" diff --git a/src/calculator/ui/qml/controls/Clickable.qml b/src/calculator/ui/qml/controls/Clickable.qml index 02f7cfa..336a1d6 100644 --- a/src/calculator/ui/qml/controls/Clickable.qml +++ b/src/calculator/ui/qml/controls/Clickable.qml @@ -1,22 +1,68 @@ import QtQuick 2.0 +/** + Base component for all mouse interactive components + */ Item { id: component - signal clicked(point pos) + /** + Emits when clicked on component + @param mouse Contains event data + */ + signal clicked(var mouse) + /** + Emits when mouse entered area of component + */ signal entered() + /** + Emits when mouse leaved area of component + */ signal exited() + /** + Emits after mouse is pressed + @param mouse Contains event data + */ + signal pressed(var mouse) + /** + Emits after mouse is released + @param mouse Contains event data + */ + signal released(var mouse) + /// If set to true hover is enabled else disable hover property alias hoverEnabled: mouseArea.hoverEnabled + /// Holds whether component is hovered readonly property alias hovered: mouseArea.containsMouse + /// Sets to manual signal emitting + property bool manual: false + /// Expose MouseArea + readonly property alias mouseArea: mouseArea MouseArea { id: mouseArea anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: { + if(!component.manual) + component.pressed(mouse) + } + + onReleased: { + if(!component.manual) + component.released(mouse) + } + + onClicked: { + if(!component.manual) + component.clicked(mouse) + } - onClicked: component.clicked(Qt.point(mouse.x, mouse.y)) onContainsMouseChanged: { + if(component.manual) + return if(mouseArea.containsMouse) component.entered() else diff --git a/src/calculator/ui/qml/controls/Completer.qml b/src/calculator/ui/qml/controls/Completer.qml index 26d5152..4c0a97b 100644 --- a/src/calculator/ui/qml/controls/Completer.qml +++ b/src/calculator/ui/qml/controls/Completer.qml @@ -2,14 +2,23 @@ import QtQuick 2.0 import QtQuick.Controls 2.0 import StyleSettings 1.0 +/** + Component which offer suggestion to text inputs + */ DropDown { id: component + /// Background color of completer property color color + /// List of suggestions property var constantModel + /// Reference to targeted text input property var target + /// Holds current word, so only matching words could be suggested property string currentText + /// Background color of suggestion item when hovered property color hoverColor + /// Text color of suggestions property color textColor scrollbarWidth: 3 @@ -23,20 +32,10 @@ DropDown { onCurrentTextChanged: { var newModel = [] - if(currentText.search(/^[ ]*$/) != -1) { - component.model = constantModel - return - } - for(var key in component.constantModel) { var value = component.constantModel[key] - var currentTextWithoutSpace = currentText.replace(new RegExp("[ ]*(?=\\S+)", 'g'), "") - - // contains ")" which need to be escaped - if(currentTextWithoutSpace.search(/\)/) !== -1) - currentTextWithoutSpace = currentText.replace(/\)/g, "\\)") - if(value["identifier"].search("^" + currentTextWithoutSpace) !== -1) + if(value["identifier"].search("^" + currentText) !== -1) newModel.push(value) } @@ -84,6 +83,10 @@ DropDown { component.target.Keys.downPressed.connect(component.moveDown) } + /** + Manage completer actions based on key pressed. Cannot steal key event from text input, so connect it to target event + @param event Passed key event + */ function handleOtherKeys(event) { if(event.key == Qt.Key_Space && (event.modifiers & Qt.ControlModifier)) { component.show() diff --git a/src/calculator/ui/qml/controls/DefaultButton.qml b/src/calculator/ui/qml/controls/DefaultButton.qml index d146fab..5bd6ca8 100644 --- a/src/calculator/ui/qml/controls/DefaultButton.qml +++ b/src/calculator/ui/qml/controls/DefaultButton.qml @@ -1,11 +1,18 @@ import QtQuick 2.0 +/** + Regular styled button + */ Clickable { id: component + /// Color of button property color color + /// Color of background which is behind button property color backgroundColor + /// Label of button property alias text: buttonText.text + /// Font of button property font font hoverEnabled: true diff --git a/src/calculator/ui/qml/controls/DropDown.qml b/src/calculator/ui/qml/controls/DropDown.qml index 521b56e..94257cc 100644 --- a/src/calculator/ui/qml/controls/DropDown.qml +++ b/src/calculator/ui/qml/controls/DropDown.qml @@ -3,29 +3,66 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQuick.Window 2.0 +/** + Base dropdown menu, to select one item + */ Item { id: component + /** + Used as function to hide dropdown + */ signal hide() + /** + Used as function to show dropdown + */ signal show() + /** + Used as function to move upward item selection of item + */ signal moveUp() + /** + Used as function to move downward item selection of item + */ signal moveDown() + /** + Used as function to choose selected item + */ signal chooseCurrent() + /** + Emit when item was chosen + */ signal itemChoosed() + /** + Signal to start show animation + */ signal showAnimation() + /** + Signal to start hide animation + */ signal hideAnimation() + /// Holds chosen item property var currentItem: model[0] + /// Holds seleted item index property int currentItemIndex: 0 + /// Number of visible items without scroll property int visibleItemCount: 4 + /// Color of scrollbar property alias scrollBarColor: scrollbar.color + /// Width of scrollbar property alias scrollbarWidth: scrollbar.width + /// Holds whether component is shown readonly property bool dropMenuVisible: dropMenu.visible + /// Height of single selection item property int itemHeight + /// List of labels to be generation into items in dropdown property var model + /// Component which will be loaded to represent background property Component dropDownMenuBackground + /// Component which will be loaded to represent single dropdown item property Component menuItem clip: true @@ -98,6 +135,13 @@ Item { ScriptAction { script: { component.visible = false }} } + MouseArea { + parent: root + enabled: component.visible + anchors.fill: parent + onClicked: component.hide() + } + Rectangle { id: scrollbar @@ -148,12 +192,17 @@ Item { height: dropMenu.height / component.model.length MouseArea { + signal checkMousePos + anchors.fill: parent hoverEnabled: true - onEntered: component.currentItemIndex = index - onClicked: { - component.hide() - component.currentItem = modelData + onClicked: component.chooseCurrent() + onMouseXChanged: checkMousePos() + onMouseYChanged: checkMousePos() + onContainsMouseChanged: checkMousePos() + onCheckMousePos: { + if(containsMouse && flick.y == 0) + component.currentItemIndex = index } } } diff --git a/src/calculator/ui/qml/controls/ExpressionInput.qml b/src/calculator/ui/qml/controls/ExpressionInput.qml index a4d85f2..f416ba7 100644 --- a/src/calculator/ui/qml/controls/ExpressionInput.qml +++ b/src/calculator/ui/qml/controls/ExpressionInput.qml @@ -2,10 +2,15 @@ import QtQuick 2.0 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 +/** + Specialized input for calculation expression + */ TextArea { id: expInput + /// Emits after expression was confirmed with enter key signal confirmed() + /// Text color of placeholder property alias placeholderTextColor: placeholderText.color focus: true diff --git a/src/calculator/ui/qml/controls/FilledClickable.qml b/src/calculator/ui/qml/controls/FilledClickable.qml new file mode 100644 index 0000000..36098c1 --- /dev/null +++ b/src/calculator/ui/qml/controls/FilledClickable.qml @@ -0,0 +1,39 @@ +import QtQuick 2.0 + +/** + Base component for all buttons with background + */ +Clickable { + id: component + + /// Background color + property alias color: background.color + /// Background color of mask when button is hovered + property alias hoverColor: mask.color + /// If true mask is applied when hovered + property alias hoverMaskEnabled: mask.visible + + Rectangle { + id: background + anchors.fill: parent + } + + Rectangle { + id: mask + + opacity: 0 + visible: false + + anchors.fill: parent + } + + hoverEnabled: true + onPressed: mask.opacity = 0.2 + onReleased: { + if(component.hovered) + mask.opacity = 0.1 + } + + onEntered: mask.opacity = 0.1 + onExited: mask.opacity = 0 +} diff --git a/src/calculator/ui/qml/controls/TextButton.qml b/src/calculator/ui/qml/controls/TextButton.qml index 19ed67b..7f520c4 100644 --- a/src/calculator/ui/qml/controls/TextButton.qml +++ b/src/calculator/ui/qml/controls/TextButton.qml @@ -1,22 +1,19 @@ import QtQuick 2.0 import "../controls" as Controls -Controls.Clickable { +/** + Button with label and simple animation + */ +FilledClickable { id: component + /// Label of button property alias buttonText: innerText.text; - property color color: "black" - property color textColor: "black" - property color hoverTextColor: "white" + /// Text color + property color textColor + hoverMaskEnabled: true hoverEnabled: true - onEntered: innerText.color = hoverTextColor - onExited: innerText.color = textColor - - Rectangle { - color: component.color - anchors.fill: parent - } Text { id: innerText @@ -27,10 +24,5 @@ Controls.Clickable { font.pixelSize: component.height * 0.55 anchors.centerIn: parent - - Behavior on color { - ColorAnimation { duration: 300 } - } } - } diff --git a/src/calculator/ui/qml/controls/VariableSetButton.qml b/src/calculator/ui/qml/controls/VariableSetButton.qml index 57faa02..e86252e 100644 --- a/src/calculator/ui/qml/controls/VariableSetButton.qml +++ b/src/calculator/ui/qml/controls/VariableSetButton.qml @@ -1,13 +1,21 @@ import QtQuick 2.0 import "../controls" as Controls +/** + Specialized button to set value to variable + */ Controls.Clickable { id: component + /// Value which will be set to variable property int value: 0 + /// Background color property color color + /// Background color when hovered property color hoverColor + /// Text color property color textColor + /// Font of text property font font hoverEnabled: true diff --git a/src/calculator/ui/qml/item/VariableItem.qml b/src/calculator/ui/qml/item/VariableItem.qml index 9969c75..a7337b8 100644 --- a/src/calculator/ui/qml/item/VariableItem.qml +++ b/src/calculator/ui/qml/item/VariableItem.qml @@ -3,29 +3,71 @@ import QtQuick 2.0 import "../visualization" as Visualization import "../menu" as Menu +/** + Single item which represents variable + */ Item { id: component + /** + Emits to request variable set value + @param idendifier Identifier of variable + @param value Reqested new value of variable + */ signal valueSetRequest(string identifier, int value) + /** + Emits to request variable deletion + @param idendifier Identifier of variable + */ signal deleteRequest(string identifier) + /** + Emits request to expand text by variable data + @param data String requested to be expanded + */ signal expandRequest(string data) + /** + Emits request to overwrite text by variable data + @param data String requested to be expanded + */ signal overwriteRequest(string data) + /** + Used as function to delete item + */ + signal deleteItem() + /// Background color property alias color: content.color + /// Text color property alias textColor: content.textColor + /// Text color of identifier property alias identifierTextColor: content.identifierTextColor + /// Variable identifier property alias variableIdentifier: content.variableIdentifier + /// Variable expression property alias variableExpression: content.variableExpression + /// Background color of expression when hovered property alias expressionHoverColor: content.expressionHoverColor + /// Variable value property alias variableValue: content.variableValue + /// Used font property alias font: content.font + /// Color of value scrollbar + property alias scrollbarColor: content.scrollbarColor + /// Prompter of value flickable theme + property alias prompterTheme: content.prompterTheme + /// Background color of dots(area to slide variable options) property alias dotsBackgroundColor: optionsMenu.dotsBackgroundColor + /// Background color of variable remove button property alias removeButtonColor: optionsMenu.removeButtonColor + /// Background color of variable setters property alias settersColor: optionsMenu.settersColor + /// Background color of variable setters when hovering property alias settersHoveredColor: optionsMenu.settersHoveredColor + /// Text color of variable setters property alias settersTextColor: optionsMenu.settersTextColor - + /// Hover color of item + property alias hoverColor: content.hoverColor clip: true @@ -52,9 +94,11 @@ Item { anchors.leftMargin: -optionsMenu.menuWidth onValueSetRequest: component.valueSetRequest(component.variableIdentifier, value) - onDeleteRequest: SequentialAnimation { - NumberAnimation { target: component; property: "opacity"; from: 1; to: 0; duration: 200 } - ScriptAction { script: component.deleteRequest(component.variableIdentifier) } - } + onDeleteRequest: component.deleteRequest(component.variableIdentifier) + } + + onDeleteItem: SequentialAnimation { + NumberAnimation { target: component; property: "opacity"; from: 1; to: 0; duration: 200 } + ScriptAction { script: component.destroy() } } } diff --git a/src/calculator/ui/qml/loaders/FontsLoader.qml b/src/calculator/ui/qml/loaders/FontsLoader.qml index 0795931..44ccb53 100644 --- a/src/calculator/ui/qml/loaders/FontsLoader.qml +++ b/src/calculator/ui/qml/loaders/FontsLoader.qml @@ -1,5 +1,8 @@ import QtQuick 2.0 +/** + Component which loads all fonts + */ Item { // loading font FontLoader { diff --git a/src/calculator/ui/qml/main.qml b/src/calculator/ui/qml/main.qml index f01b61e..89dd841 100644 --- a/src/calculator/ui/qml/main.qml +++ b/src/calculator/ui/qml/main.qml @@ -1,34 +1,78 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 -// TODO allow + import ExpSyntaxHighlighter 1.0 +import ExpAnalyzer 1.0 import Sides 1.0 import Calculator 1.0 import Expansion 1.0 import Expression 1.0 import StyleSettings 1.0 +import AppWindow 1.0 import "controls" as Control +import "menu" import "../easteregg" import "loaders" as Loaders +import "windows" as Windows import "containers" import "visualization" -ApplicationWindow { +AppWindow { id: mainWindow width: 1101 - minimumHeight: width * (522 / 1101) - maximumHeight: minimumHeight + ratio: 522/1101. + height: 522 - title: qsTr("Barbie Calculator") + title: qsTr("Barbie calculator") visible: true + Item { + id: root + anchors.fill: parent + } + + AppMenuBar { + color: StyleSettings.appMenuBar.color + font: StyleSettings.appMenuBar.font + + title: qsTr("Help") + titleColor: StyleSettings.appMenuBar.title.color + titleActiveColor: StyleSettings.appMenuBar.title.activeColor + + items: [qsTr("Help"), qsTr("About")] + + itemBackgroundColor: StyleSettings.appMenuBar.item.backgroundColor + itemBorderColor: StyleSettings.appMenuBar.item.borderColor + itemHoverColor: StyleSettings.appMenuBar.item.hoverColor + itemTextColor: StyleSettings.appMenuBar.item.textColor + itemHoverTextColor: StyleSettings.appMenuBar.item.hoverTextColor + + z: 3 + width: 200 + height: 24 + menuWidth: 100 + menuItemHeight: 19 + + onItemChoosed: { + if(item == qsTr("About")) + aboutWindow.show() + if(item == qsTr("Help")) + helpWindow.show() + } + } + Game { id: game - onGameOver: console.log(msg) + onGameOver: info.show(msg) + } + + ExpAnalyzer { + id: exa + target: expInput } ExpSyntaxHighlighter { @@ -52,21 +96,27 @@ ApplicationWindow { id: variablePanel backgroundColor: StyleSettings.variablesPanel.backgroundColor - textColor: StyleSettings.variablesPanel.textColor - identifierTextColor: StyleSettings.variablesPanel.identifierColor scrollBarColor: StyleSettings.variablesPanel.scrollBarColor - expressionHoverColor: StyleSettings.variablesPanel.expressionHoverColor ansTextColor: StyleSettings.ans.textColor ansIdentifierTextColor: StyleSettings.ans.identifierColor ansColor: StyleSettings.ans.backgroundColor ansExpressionHoverColor: StyleSettings.ans.expressionHoverColor + ansScrollbarColor: StyleSettings.ans.scrollbarColor + ansPrompterTheme: StyleSettings.ans.prompterTheme + ansHoverColor: StyleSettings.ans.hoverColor dotsBackgroundColor: StyleSettings.variableItem.dots.color removeButtonColor: StyleSettings.variableItem.removeButton.color settersColor: StyleSettings.variableItem.setters.color settersHoveredColor: StyleSettings.variableItem.setters.hoverColor settersTextColor: StyleSettings.variableItem.setters.textColor + itemScrollbarColor: StyleSettings.variableItem.scrollbarColor + textColor: StyleSettings.variableItem.textColor + identifierTextColor: StyleSettings.variableItem.identifierColor + expressionHoverColor: StyleSettings.variableItem.expressionHoverColor + prompterTheme: StyleSettings.variableItem.prompterTheme + hoverColor: StyleSettings.variableItem.hoverColor font.family: StyleSettings.variablesPanel.font.family @@ -77,9 +127,12 @@ ApplicationWindow { anchors.top: parent.top anchors.right: parent.right - onDeleteVariableRequest: Calculator.removeVariable(identifier) + onDeleteVariableRequest: { + if(Calculator.removeVariable(identifier)) + variablePanel.deleteVariable(identifier) + } onSetVariableRequest: Calculator.setVariableValue(identifier, value) - onExpandRequest: expandExpression(data) + onExpandRequest: forceExpandExpression(data) onOverwriteRequest: overwriteExpression(data) } @@ -90,7 +143,7 @@ ApplicationWindow { columns: 1 backgroundColor: StyleSettings.functionPanel.backgroundColor textColor: StyleSettings.functionPanel.textColor - hoverTextColor: StyleSettings.functionPanel.hoverTextColor + hoverColor: StyleSettings.functionPanel.hoverColor height: parent.height * 0.45 width: parent.width / 15.7 @@ -98,7 +151,7 @@ ApplicationWindow { anchors.left: parent.left anchors.bottom: parent.bottom - onExpandRequest: expandExpression(func) + onExpandRequest: expandExpressionAroundWord(func) } FontMetrics { @@ -126,27 +179,40 @@ ApplicationWindow { Calculator.process(expInput.text) } onTextChanged: { - if(text.search("nyan") != -1) { - expInput.text = "" - game.run() - } + if(text.search("nyan") != -1) + countDown.start(3) completeText() + showFunctionSignature() } onSelectedTextChanged: { if(selectedText.length) { completer.model = completer.constantModel completer.currentText = "" } - else + else { completeText() + showFunctionSignature() + } + } + onCursorPositionChanged: { + completeText() + showFunctionSignature() + } + + Keys.onPressed: { + if(expInput.selectedText != "" && event.text == "(") { + expInput.insert(expInput.selectionEnd, ")") + expInput.cursorPosition = expInput.selectionStart + expInput.deselect() + } } - onCursorPositionChanged: completeText() } ResultDisplay { id: resultDisplay + result: "0" color: StyleSettings.resultDisplay.backgroundColor textColor: StyleSettings.resultDisplay.textColor font.family: StyleSettings.resultDisplay.font.family @@ -154,7 +220,26 @@ ApplicationWindow { anchors.top: parent.top anchors.left: parent.left anchors.right: variablePanel.left + anchors.bottom: resultSystemDisplay.top + } + + ResultSystemDisplay { + id: resultSystemDisplay + + value: 0 + basesList: ["BIN", "OCT", "DEC", "HEX"] + bases: { "DEC": 10, "BIN": 2, "HEX": 16, "OCT": 8 } + height: parent.height / 5.1 + scrollbarColor: StyleSettings.resultSystemDisplay.scrollbarColor + baseTextColor: StyleSettings.resultSystemDisplay.baseTextColor + valueTextColor: StyleSettings.resultSystemDisplay.valueTextColor + prompterTheme: StyleSettings.resultSystemDisplay.prompterTheme + font: StyleSettings.resultSystemDisplay.font + anchors.bottom: functionPanel.top + anchors.left: parent.left + anchors.right: variablePanel.left + anchors.rightMargin: height / 20 } Control.CalculateButton { @@ -163,6 +248,7 @@ ApplicationWindow { width: parent.width / 14 color: StyleSettings.calculateButton.backgroundColor + hoverColor: StyleSettings.calculateButton.hoverColor anchors.top: expInput.top anchors.bottom: parent.bottom @@ -171,30 +257,76 @@ ApplicationWindow { onClicked: Calculator.process(expInput.text) } - Error { + PopUp { id: error + title: qsTr("Error") maskColor: StyleSettings.errorDialog.maskColor dialogColor: StyleSettings.errorDialog.color textColor: StyleSettings.errorDialog.textColor font: StyleSettings.errorDialog.font + z: 2 + + anchors.fill: parent + + onHidden: expInput.focus = true + } + + PopUp { + id: info + + title: qsTr("Info") + maskColor: StyleSettings.infoDialog.maskColor + dialogColor: StyleSettings.infoDialog.color + textColor: StyleSettings.infoDialog.textColor + font: StyleSettings.infoDialog.font + z: 2 anchors.fill: parent onHidden: expInput.focus = true } + CountDown { + id: countDown + + anchors.fill: parent + font.family: StyleSettings.countDown.font + color: StyleSettings.countDown.textColors[count + 1] + + + onTriggered: { + expInput.focus = true + expInput.text = "" + game.run() + } + } + Component.onCompleted: { Calculator.processed.connect(handleResult) Calculator.error.connect(error.show) } + FunctionSignatureDisplay { + id: functionSignature + + color: StyleSettings.functionSignatureDisplay.color + textColor: StyleSettings.functionSignatureDisplay.textColor + font: StyleSettings.functionSignatureDisplay.font + constantOpacity: 0.8 + + x: completer.calcTextInfoPos(width) + y: expInput.cursorRectangle.y - height + expInput.y + height: completer.itemHeight * 1.2 + } + Control.Completer { id: completer target: expInput constantModel: Calculator.identifiersTypes + opacity: 0.8 color: StyleSettings.completer.color hoverColor: StyleSettings.completer.hoverColor textColor: StyleSettings.completer.textColor @@ -202,90 +334,146 @@ ApplicationWindow { width: parent.width * 0.18 itemHeight: width / 8 - x: calcPos() + x: calcTextInfoPos(width) y: expInput.cursorRectangle.y + expInput.cursorRectangle.height + expInput.y onItemChoosed: { - var end = expInput.cursorPosition - var start = end - currentText.length - - expInput.remove(start, end) expandExpression(currentItem["identifier"]) } - function calcPos() { - if(expInput.cursorRectangle.x + completer.width + fmExpInput.advanceWidth(" ") < expInput.width) + function calcTextInfoPos(infoWidth) { + if(expInput.cursorRectangle.x + infoWidth + fmExpInput.advanceWidth(" ") < expInput.width) return expInput.cursorRectangle.x + expInput.x else - return expInput.x + expInput.width - completer.width - expInput.textMargin + return expInput.x + expInput.width - infoWidth - expInput.textMargin } + } - function currentWord() { - var result = "" - var startIndex = expInput.cursorPosition - 1 - var regExp = new RegExp(Calculator.expressionSplittersRegExp) + Windows.About { + id: aboutWindow - if(expInput.cursorPosition == 0) - return + revision: "3eecc99" + color: StyleSettings.aboutWindow.color + textColor: StyleSettings.aboutWindow.textColor + titleColor: StyleSettings.aboutWindow.titleColor + font: StyleSettings.aboutWindow.font - while(expInput.text[startIndex]) { - if(expInput.text[startIndex].match(regExp)) - break; - --startIndex - } - startIndex++; + z: 3 + width: mainWindow.width + height: mainWindow.height - while(expInput.text[startIndex]) { - if(expInput.text[startIndex].match(regExp)) - break - result += expInput.text[startIndex] - startIndex++ - } + onHidden: expInput.focus = true + } - return result + Windows.Help { + id: helpWindow + + textColor: StyleSettings.helpWindow.textColor + titleColor: StyleSettings.helpWindow.titleColor + subtitleColor: StyleSettings.helpWindow.subtitleColor + scrollBarColor: StyleSettings.helpWindow.scrollBarColor + textFont.family: StyleSettings.helpWindow.font.family + + width: 400 + height: 600 } + /** + Show suggestion box with filtered suggestions + */ function completeText() { var lastChar = expInput.text.slice(-1) + var currentWord = exa.currentWord() + + if(expInput.cursorPosition) + lastChar = expInput.text[expInput.cursorPosition - 1] + if(Calculator.expressionSplitters.indexOf(lastChar) != -1) completer.show() - if(typeof currentWord() != "undefined") - completer.currentText = currentWord() - else - completer.currentText = "" - + completer.currentText = currentWord completer.currentTextChanged(completer.currentText) } + function showFunctionSignature() { + var funcSignature = exa.currentFunctionSignature() + + functionSignature.show(funcSignature) + } + + /** + Overwrite current expression by new expression + @param newExpression New expression + */ function overwriteExpression(newExpression) { expInput.text = newExpression } - function expandExpression(expansionKey) { - var expansionData = Calculator.expressionsExpansion[expansionKey] - var selectedStart = expInput.selectionStart - var selectedText = expInput.selectedText - var selectedEnd = expInput.selectionEnd - var expansion, expansionType + /** + Expand expression into current expresion without any optimatizations + @param expression Inserting expansion + */ + function forceExpandExpression(expansion) { + expInput.insert(expInput.selectionStart, expansion) + } - // if not found, then it is not in Settings, so use normal expansion - expansion = (typeof expansionData === "undefined") ?expansionKey :expansionData["expansion"] - expansionType = (typeof expansionData === "undefined") ?Expansion.Normal :expansionData["expansionType"] + /** + Expand expression into current expresion around currend word if there isn't and text selected + @param expression Key of builtin expression or dynamic expression + */ + function expandExpressionAroundWord(expansion) { + expansion = exa.expandExpression(expansion, true) + var borders = exa.currentWordBorders() - expInput.remove(selectedStart, selectedEnd) + if(expInput.selectedText == "") + expInput.remove(borders["start"], borders["end"]) + else + expInput.remove(expInput.selectionStart, expInput.selectionEnd) - if(expansionType == Expansion.BracketsPack) - expInput.insert(selectedStart, expansion + selectedText + ")") + expInput.insert(expInput.selectionStart, expansion) + } + + /** + Expand expression into current expresion + @param expression Key of builtin expression or dynamic expression + */ + function expandExpression(expansion) { + var moveCursorInsideFunction = false + var expansionType = exa.expansionType(expansion) + expansion = exa.expandExpression(expansion, false) + + if(expInput.selectedText == "") { + var borders = exa.currentWordBorders() + expInput.remove(borders["start"], borders["end"]) + moveCursorInsideFunction = true + } else - expInput.insert(selectedStart, expansion) + expInput.remove(expInput.selectionStart, expInput.selectionEnd) + + expInput.insert(expInput.selectionStart, expansion) + + // if only expand empty function put cursor inside its body + // need to move to get know function signature + if(moveCursorInsideFunction && expansionType == Expansion.BracketsPack) + expInput.cursorPosition--; + + // but if function does not take any paramaters put cursor on the end + // if contains "()" in signature => it's function that does not take any parameter + if(exa.currentFunctionSignature().indexOf("()") != -1) + expInput.cursorPosition++; } + /** + After calculation handles variable and display synchronization + @param data Data with result and variables + */ function handleResult(data) { - if(typeof data["result"] !== "undefined") + if(typeof data["result"] !== "undefined") { resultDisplay.result = data["result"] + resultSystemDisplay.value = data["unformattedResult"] + } else expInput.text = "" diff --git a/src/calculator/ui/qml/managers/VariablesManager.qml b/src/calculator/ui/qml/managers/VariablesManager.qml index ecfb9bf..08a1b27 100644 --- a/src/calculator/ui/qml/managers/VariablesManager.qml +++ b/src/calculator/ui/qml/managers/VariablesManager.qml @@ -1,20 +1,49 @@ import QtQuick 2.0 +/** + Manager of dynamic manipulation with Variable items + */ QtObject { id: manager + /** + Emit after creation of new object + @param object Reference to new object + */ signal newItem(var object) + /** + Emit after deletion of new object + @param identifier Identifier of deleted variable + */ signal deleteItem(string identifier) + /** + Emit after variable set, so other system could synchronize + @param identifer Identifier of variable + @param value New value of variable + */ signal setItem(string identifier, real value) + /// Component from which objects will be created property var itemComponent + /// Parent of new objects property Item componentsParent + /// List of existing objects property var _variableItems: [] + /** + Register item in system + @param variableItem Item to be registered + */ function registerVariable(variableItem) { manager._variableItems.push(variableItem) } + /** + Create and register new variable item according to data + @param identifier Identifier of variable + @param expression Expression of variable + @param value Value of variable + */ function addVariable(identifier, expression, value) { var object = manager.itemComponent.createObject(manager.componentsParent, { "variableIdentifier": identifier, @@ -28,6 +57,12 @@ QtObject { manager.newItem(object) } + /** + Sets value or expression to variable item + @param identifier Identifier of variable + @param expression New expression of variable + @param value New value of variable + */ function setVariable(identifier, expression, value) { var variable = manager.findVariable(identifier) if(variable == null) { @@ -39,6 +74,24 @@ QtObject { variable.variableValue = value } + /** + Unregister variable item from manager + @param variableIdentifier Identifier of variable + */ + function _unregisterVariable(variableIdentifier) { + for(var key in manager._variableItems) { + + if(manager._variableItems[key].variableIdentifier == variableIdentifier){ + manager._variableItems.splice(key, 1); + } + } + } + + /** + Finds variable item in system + @param variableIdentifier Identifier of variable + @return Item if is found else return null + */ function findVariable(variableIndetifier) { var object @@ -53,13 +106,32 @@ QtObject { return null } + /** + Delete variable item + @param variableIdentifier Identifier of variable + */ + function deleteVariable(variableIdentifier) { + var object = findVariable(variableIdentifier) + _unregisterVariable(variableIdentifier) + object.deleteItem() + } + + /** + Request deletion of variable item + @param variableIdentifier Identifier of variable + */ function handleDeleteRequest(variableIndetifier) { var object = manager.findVariable(variableIndetifier) - object.destroy() manager.deleteItem(variableIndetifier) } + /** + Sets value or expression to variable item and emit change + @param identifier Identifier of variable + @param expression New expression of variable + @param value New value of variable + */ function handleSetRequest(variableIndetifier, value) { var object = manager.findVariable(variableIndetifier) diff --git a/src/calculator/ui/qml/menu/AppMenuBar.qml b/src/calculator/ui/qml/menu/AppMenuBar.qml new file mode 100644 index 0000000..bc5ae04 --- /dev/null +++ b/src/calculator/ui/qml/menu/AppMenuBar.qml @@ -0,0 +1,131 @@ +import QtQuick 2.0 +import "../controls" + +/** + Component which has same functionality as MenuBar, but it does not affect window + container size. + */ +Canvas { + id: component + + /** + Emits after clicking on item in menu + @param item Label of item + */ + signal itemChoosed(string item) + + /// Background color of bar + property color color + /// Text margins of title + property int titleMargins: 10 + /// Title text + property alias title: label.text + /// Text color of title + property alias titleColor: label.color + /// Text color of title, when it's active + property color titleActiveColor + /// List of items in menu + property alias items: menu.model + /// Width of menu + property alias menuWidth: menu.width + /// Height of single item in menu + property alias menuItemHeight: menu.itemHeight + /// Background color of item in menu + property color itemBackgroundColor + /// Border color of item in menu + property color itemBorderColor + /// Hover color of item in menu + property color itemHoverColor + /// Text color of item in menu + property color itemTextColor + /// Hover text color of item in menu + property color itemHoverTextColor + /// Used font + property font font + + onColorChanged: component.requestPaint() + + onPaint: { + var ctx = getContext("2d") + ctx.clearRect(0, 0, component.width, component.height) + + ctx.fillStyle = component.color + + // draw shape + //|------------/ + //|-----------/ + + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(component.width, 0) + ctx.lineTo(component.width - component.height, component.height) + ctx.lineTo(component.width, component.height) + ctx.lineTo(0, component.height) + ctx.lineTo(0, 0) + + ctx.fill() + } + + Rectangle { + id: title + + color: (menu.visible) ?component.titleActiveColor :"transparent" + width: label.width + 2 * component.titleMargins + height: component.height + + Behavior on color { + ColorAnimation { duration: 200 } + } + + Text { + id: label + + font.pixelSize: component.height * 0.8 + font.family: component.font.family + + anchors.centerIn: parent + } + } + + Item { + y: component.height + + DropDown { + id: menu + + onItemChoosed: component.itemChoosed(menu.currentItem) + + dropDownMenuBackground: Rectangle { + color: component.itemBackgroundColor + border.color: component.itemBorderColor + anchors.fill: parent + } + + menuItem: Item { + Rectangle { // hover overlay + opacity: 0.8 + color: (hovered) ?component.itemHoverColor :"transparent" + + anchors.fill: parent + } + + Text { + color: (hovered) ?component.itemHoverTextColor :component.itemTextColor + text: itemData + + font.family: component.font.family + font.pixelSize: parent.height * 0.8 + + anchors.leftMargin: menu.itemHeight / 2 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: menu.show() + } +} diff --git a/src/calculator/ui/qml/menu/VariableOptions.qml b/src/calculator/ui/qml/menu/VariableOptions.qml index 3a61483..144f7df 100644 --- a/src/calculator/ui/qml/menu/VariableOptions.qml +++ b/src/calculator/ui/qml/menu/VariableOptions.qml @@ -1,19 +1,37 @@ import QtQuick 2.0 import "../controls" as Controls +/** + Menu with variable actions + */ Item { id: component + /** + Emits after clicking on setter + @param value Value requested to be set + */ signal valueSetRequest(int value) + /** + Emits after clicking on delete button + */ signal deleteRequest() + /// Holds current width of menu(dynamically changes) readonly property alias menuWidth: dots.width + /// Sets true if menu is running show animation readonly property alias animationRunning: xAnimation.running + /// Background color of dots(area to slide up menu) property alias dotsBackgroundColor: dotsBackground.color + /// Background color of remove button property alias removeButtonColor: removeButtonBackground.color + /// Background color of setters property color settersColor + /// Background color of setters when hovered property color settersHoveredColor + /// Text color of setters property color settersTextColor + /// Font of component property font font state: "hidden" diff --git a/src/calculator/ui/qml/visualization/AnimatedText.qml b/src/calculator/ui/qml/visualization/AnimatedText.qml index ce09448..432e46c 100644 --- a/src/calculator/ui/qml/visualization/AnimatedText.qml +++ b/src/calculator/ui/qml/visualization/AnimatedText.qml @@ -1,10 +1,16 @@ import QtQuick 2.7 +/** + Animate change of text + */ Item { id: component + /// Text to be displayed property string text: "" + /// Font of text property font font: Qt.font() + /// Text color property color color: "black" width: baseText.width @@ -20,11 +26,6 @@ Item { }} } - FontMetrics { - id: fontMetrics - font: component.font - } - Text { id: baseText diff --git a/src/calculator/ui/qml/visualization/CountDown.qml b/src/calculator/ui/qml/visualization/CountDown.qml new file mode 100644 index 0000000..2c9d199 --- /dev/null +++ b/src/calculator/ui/qml/visualization/CountDown.qml @@ -0,0 +1,75 @@ +import QtQuick 2.0 + +/** + Simple countdown display only number of remaining time + */ +Item { + id: component + + /** + Start countdown and display component + @param seconds Count of seconds to be counted down + */ + signal start(int seconds) + /** + Emits after time runs out + */ + signal triggered() + + /// Holds current remaining time in seconds + readonly property alias count: timer.count + /// Text color of numbers + property alias color: text.color + /// Font of text + property font font + + focus: visible + visible: opacity + opacity: 0 + + onStart: { + component.opacity = 1 + timer.count = seconds + timer.start() + } + + Behavior on opacity { + NumberAnimation { duration: 200 } + } + + MouseArea { + anchors.fill: parent + } + + Text { + id: text + + font.pixelSize: parent.height / 1.2 + font.family: component.font.family + + anchors.centerIn: parent + } + + Timer { + id: timer + + property int count + + interval: 1000 + running: false + repeat: true + triggeredOnStart: true + + onTriggered: { + if(!timer.count) { + timer.stop() + component.opacity = 0 + component.triggered() + } + + else + text.text = timer.count.toString() + timer.count-- + } + } +} diff --git a/src/calculator/ui/qml/visualization/FlickableAnimatedText.qml b/src/calculator/ui/qml/visualization/FlickableAnimatedText.qml new file mode 100644 index 0000000..44ea812 --- /dev/null +++ b/src/calculator/ui/qml/visualization/FlickableAnimatedText.qml @@ -0,0 +1,83 @@ +import QtQuick 2.7 + +/** + Text with ability of flicking. + */ + +Item { + id: component + + /// Displayed text + property alias text: text.text + /// Text Color + property alias color: text.color + /// Used font + property alias font: text.font + /// Expose flickable component + readonly property alias flick: flick + /// Color type of theme ["light", "dark"] + property string theme: "dark" + + Flickable { + id: flick + + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.HorizontalFlick + clip: true + + contentHeight: text.height + contentWidth: text.width + + anchors.fill: parent + + AnimatedText { + id: text + } + } + + Rectangle { + id: prompter + + color: (component.theme == "dark") ?"black" :"white" + opacity: (!flick.contentX && flick.visibleArea.widthRatio != 1) ?0.8 :0 + visible: opacity + clip: true + + width: height * 2.5 + height: fontMetrics.height * 0.8 + radius: 3 + + anchors.verticalCenter: flick.verticalCenter + anchors.right: flick.right + + Behavior on opacity { + NumberAnimation { duration: 200 } + } + + FontMetrics { + id: fontMetrics + font: component.font + } + + Row { + height: parent.height + width: height * 0.6 * 4 * (2/5.) + anchors.centerIn: parent + + Repeater { + model: 3 + + Image { + source: (component.theme == "dark") ?"qrc:/assets/images/arrow_left_light.svg" + :"qrc:/assets/images/arrow_left_dark.svg" + fillMode: Image.PreserveAspectFit + + height: parent.height * 0.6 + sourceSize.height: height + + anchors.verticalCenter: parent.verticalCenter + } + } + } + } +} diff --git a/src/calculator/ui/qml/visualization/FunctionSignatureDisplay.qml b/src/calculator/ui/qml/visualization/FunctionSignatureDisplay.qml new file mode 100644 index 0000000..45c42a9 --- /dev/null +++ b/src/calculator/ui/qml/visualization/FunctionSignatureDisplay.qml @@ -0,0 +1,53 @@ +import QtQuick 2.0 + +/** + Component to display function signature + */ +Rectangle { + id: component + + /** + Used as function to display component + @param signature Signature to display + */ + signal show(string signature) + /** + Used as function to hide component + */ + signal hide() + + /// Contains function signature in string + property alias text: text.text + /// Text color display + property alias textColor: text.color + property real constantOpacity + /// Used font + property font font + + opacity: constantOpacity + width: text.width + height * 0.2 + + onShow: { + text.text = signature + + if(signature == "") + component.hide() + else + component.opacity = component.constantOpacity + } + + onHide: component.opacity = 0 + + Behavior on opacity { + NumberAnimation { duration: 250 } + } + + Text { + id: text + + font.family: component.font.family + font.pixelSize: parent.height * 0.7 + + anchors.centerIn: parent + } +} diff --git a/src/calculator/ui/qml/visualization/Error.qml b/src/calculator/ui/qml/visualization/PopUp.qml similarity index 70% rename from src/calculator/ui/qml/visualization/Error.qml rename to src/calculator/ui/qml/visualization/PopUp.qml index abdcf96..1008ef5 100644 --- a/src/calculator/ui/qml/visualization/Error.qml +++ b/src/calculator/ui/qml/visualization/PopUp.qml @@ -1,19 +1,45 @@ import QtQuick 2.0 import "../controls" as Controls +/** + Base for modal PopUp dialogs + */ Item { id: component + /** + Used as function to hide component + */ signal hide() + /** + Emits after component is hidden + */ signal hidden() - signal show(string error) - signal hideAnimation() + /** + Used as function to show component + @param msg Message to be displayed + */ + signal show(string msg) + /** + Signal to start show animation + */ signal showAnimation() + /** + Signal to start hide animation + */ + signal hideAnimation() + /// Background color of modal property alias maskColor: mask.color + /// Background color of dialog property alias dialogColor: dialog.color - property alias textColor: errorText.color - property alias errorMessage: errorMsg.text + /// Text color + property alias textColor: title.color + /// Text to be displayed in dialog + property alias message: message.text + /// Title of dialog + property alias title: title.text + /// Used font property font font visible: false @@ -22,7 +48,7 @@ Item { Keys.onPressed: component.hide() onShow: { - component.errorMessage = error + component.message = msg component.showAnimation() } @@ -58,9 +84,7 @@ Item { anchors.verticalCenter: parent.verticalCenter Text { - id: errorText - - text: qsTr("Error") + id: title font.pixelSize: parent.height / 3 font.family: component.font.family @@ -72,21 +96,21 @@ Item { } Text { - id: errorMsg + id: message - color: errorText.color + color: title.color font.pixelSize: parent.height / 7 font.family: component.font.family - anchors.left: errorText.left - anchors.top: errorText.bottom + anchors.left: title.left + anchors.top: title.bottom anchors.topMargin: parent.height / 25 } Controls.DefaultButton { text: qsTr("Ok") - color: errorText.color + color: title.color backgroundColor: component.dialogColor font.family: component.font.family diff --git a/src/calculator/ui/qml/visualization/ResultDisplay.qml b/src/calculator/ui/qml/visualization/ResultDisplay.qml index c6d1b26..657da14 100644 --- a/src/calculator/ui/qml/visualization/ResultDisplay.qml +++ b/src/calculator/ui/qml/visualization/ResultDisplay.qml @@ -1,21 +1,26 @@ import QtQuick 2.0 +/** + Specialized component to show calculation result + */ Rectangle { id: component + /// Result to be displayed property alias result: text.text + /// Text color property alias textColor: text.color + /// Used font property font font AnimatedText { id: text font.family: component.font.family - font.pixelSize: parent.height / 3.8 + font.pixelSize: parent.height / 1.9 anchors.right: parent.right - anchors.rightMargin: font.pixelSize / 1.8 - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: -parent.height / 20 + anchors.rightMargin: font.pixelSize / 3.4 + anchors.bottom: parent.bottom } } diff --git a/src/calculator/ui/qml/visualization/ResultSystemDisplay.qml b/src/calculator/ui/qml/visualization/ResultSystemDisplay.qml new file mode 100644 index 0000000..fb38fc9 --- /dev/null +++ b/src/calculator/ui/qml/visualization/ResultSystemDisplay.qml @@ -0,0 +1,97 @@ +import QtQuick 2.7 +import Calculator 1.0 + +/** + Display result of calculation in different bases + */ +Rectangle { + id: component + + /// Ordered list of bases to be displayed + property var basesList + /// Dict of bases with number base to be displayed + property var bases + /// Value which will be converted to different bases + property real value: 0 + /// Used font + property font font + /// Text color of base name + property color baseTextColor + /// Text color of converted value + property color valueTextColor + /// Color of value scrollbar + property color scrollbarColor + /// Margin of text + readonly property int margin: 10 + /// Prompter of value flickable theme + property string prompterTheme + + Column { + id: container + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.topMargin: component.margin + anchors.bottomMargin: component.margin + anchors.leftMargin: component.margin * 2 + + Repeater { + model: component.basesList + Item { + width: container.width + height: container.height / component.basesList.length + + Text { + id: baseText + + text: modelData + color: component.baseTextColor + + font.pixelSize: parent.height * 0.9 + font.family: component.font.family + + anchors.top: parent.top + anchors.left: parent.left + } + + FontMetrics { + id: fm + font: baseText.font + } + + Rectangle { + color: component.scrollbarColor + opacity: (valueText.flick.visibleArea.widthRatio != 1 && valueText.flick.moving) + + x: valueText.width * valueText.flick.visibleArea.xPosition + valueText.x + width: valueText.width * valueText.flick.visibleArea.widthRatio + height: 2 + + anchors.top: valueText.bottom + anchors.topMargin: height + + Behavior on opacity { + NumberAnimation { duration: 200 } + } + } + + FlickableAnimatedText { + id: valueText + + text: Calculator.convertToBase(component.value, component.bases[modelData]) + color: component.valueTextColor + font: baseText.font + theme: component.prompterTheme + + width: parent.width - anchors.leftMargin + height: parent.height + + anchors.left: parent.left + anchors.leftMargin: font.pixelSize * 3 // some constant to measure font width + } + } + } + } +} diff --git a/src/calculator/ui/qml/visualization/VariableDisplay.qml b/src/calculator/ui/qml/visualization/VariableDisplay.qml index a611afc..7e53349 100644 --- a/src/calculator/ui/qml/visualization/VariableDisplay.qml +++ b/src/calculator/ui/qml/visualization/VariableDisplay.qml @@ -1,18 +1,51 @@ import QtQuick 2.0 +import "../controls" as Controls -Rectangle { +Controls.FilledClickable { id: component + /** + Emits request to expand text by variable data + @param data String requested to be expanded + */ signal expandRequest(string data) + /** + Emits request to overwrite text by variable data + @param data String requested to be expanded + */ signal overwriteRequest(string data) + /// Text color property color textColor + /// Text color of identifier property color identifierTextColor + /// Background color of expression when hovered property color expressionHoverColor + /// Variable identifier property string variableIdentifier: "" + /// Variable expression property string variableExpression: "" + /// Variable value property string variableValue: "0" + /// Used font property font font + /// Color of value scrollbar + property color scrollbarColor + /// Prompter of value flickable theme + property string prompterTheme + + hoverEnabled: true + hoverMaskEnabled: true + manual: true + + mouseArea.onClicked: { + if(mouse.button == Qt.LeftButton) + component.expandRequest(component.variableIdentifier) + else + createExpansionRequest() + } + + mouseArea.onContainsMouseChanged: component.handleHoverEvent() QtObject { id: internal @@ -20,17 +53,6 @@ Rectangle { property real sideMargin: height / 6.5 } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: { - if(mouse.button == Qt.LeftButton) - component.expandRequest(component.variableIdentifier) - else - component.overwriteRequest(component.variableIdentifier + ' ' + component.variableExpression) - } - } - Rectangle { id: expressionBackground @@ -45,6 +67,19 @@ Rectangle { } } + // value display + AnimatedText { + text: component.variableValue + color: component.textColor + + font.family: component.font.family + font.pixelSize: parent.height * 0.55 + + anchors.right: parent.right + anchors.rightMargin: internal.sideMargin + anchors.bottom: leftSide.bottom + } + // display expression and variable name Item { id: leftSide @@ -67,25 +102,56 @@ Rectangle { anchors.left: parent.left } - AnimatedText { + Rectangle { + color: component.scrollbarColor + opacity: (expression.flick.visibleArea.widthRatio != 1 && expression.flick.moving) + + x: expression.width * expression.flick.visibleArea.xPosition + expression.x + width: expression.width * expression.flick.visibleArea.widthRatio + height: 2 + z: 2 + + anchors.top: expression.bottom + + Behavior on opacity { + NumberAnimation { duration: 200 } + } + } + + FlickableAnimatedText { id: expression antialiasing: true text: component.variableExpression color: component.textColor + theme: component.prompterTheme font.family: component.font.family font.pixelSize: parent.height * 0.23 + width: component.width - internal.sideMargin * 2 + height: flick.contentHeight + + anchors.left: parent.left anchors.top: parent.top MouseArea { - anchors.fill: parent + id: expressionMouseArea + + parent: expression.flick hoverEnabled: true + propagateComposedEvents: true + + anchors.fill: parent + + onClicked: { + createExpansionRequest() + mouse.accepted = false + } - onClicked: component.overwriteRequest(component.variableIdentifier + ' ' + component.variableExpression) onContainsMouseChanged: { + component.handleHoverEvent() if(containsMouse) expressionBackground.color = component.expressionHoverColor else @@ -95,16 +161,18 @@ Rectangle { } } - // value display - AnimatedText { - text: component.variableValue - color: component.textColor - - font.family: component.font.family - font.pixelSize: parent.height * 0.55 + function handleHoverEvent() { + if(component.mouseArea.containsMouse || expressionMouseArea.containsMouse) + component.entered() + else if((!expressionMouseArea.containsMouse) && (!component.mouseArea.containsMouse)){ + component.exited() + } + } - anchors.right: parent.right - anchors.rightMargin: internal.sideMargin - anchors.bottom: leftSide.bottom + function createExpansionRequest() { + if(component.variableExpression.indexOf("=") != -1) + component.overwriteRequest(component.variableIdentifier + ' ' + component.variableExpression) + else + component.expandRequest(component.variableExpression) } } diff --git a/src/calculator/ui/qml/windows/About.qml b/src/calculator/ui/qml/windows/About.qml new file mode 100644 index 0000000..f7a1f87 --- /dev/null +++ b/src/calculator/ui/qml/windows/About.qml @@ -0,0 +1,115 @@ +import QtQuick 2.5 +import QtQuick.Window 2.0 +import QtQuick.Controls 1.4 + +/** + Component which displays info about Calculator app + */ +Item { + id: component + + /** + Used as function to display component + */ + signal show + /** + Used as function to hide component + */ + signal hide + /** + Emit after component is not visible anymore + */ + signal hidden + + /// Padding of text in component + property int padding: 20 + /// Revision from which is app released + property string revision + /// Background color of component + property color color: "black" + /// Text color + property color textColor: "lightGray" + /// Text color of title + property alias titleColor: title.color + /// Used font + property font font + + visible: opacity + focus: visible + opacity: 0 + + onShow: NumberAnimation { target: component; property: "opacity"; to: 1; duration: 200 } + onHide: SequentialAnimation { + NumberAnimation { target: component; property: "opacity"; to: 0; duration: 200 } + ScriptAction { script: { component.hidden() } } + } + + Keys.onEscapePressed: component.hide() + + MouseArea { + anchors.fill: parent + onClicked: component.hide() + } + + Rectangle { + color: component.color + opacity: 0.8 + anchors.fill: parent + } + + Rectangle { + color: component.color + + width: 360 + height: 260 + + anchors.centerIn: parent + + + Image { + source: "qrc:/assets/images/logo.svg" + fillMode: Image.PreserveAspectFit + opacity: 0.2 + + width: parent.width * 0.7 + height: parent.height * 0.7 + + sourceSize.width: width + sourceSize.height: height + + anchors.centerIn: parent + } + + Text { + id: title + + text: qsTr("Barbie calculator") + " " + "0.1" + + font.pixelSize: 25 + font.family: component.font.family + + anchors.top: parent.top + anchors.left: parent.left + anchors.topMargin: component.padding + anchors.leftMargin: component.padding + } + + Text { + color: component.textColor + font.pixelSize: 15 + font.family: component.font.family + + anchors.left: title.left + anchors.top: title.bottom + + text: qsTr("From revision") + " - " + component.revision + "\n\n" + + qsTr("Created by:") + "\n" + + " Josef Kolář\n" + + " Son Hai Nguyen\n" + + " Martin Omacht\n" + + " Robert Navrátil\n\n" + + "Copyright 2016-2017 Syan Entertainment.\n" + + qsTr("All rights reserved.") + } + } +} diff --git a/src/calculator/ui/qml/windows/Help.qml b/src/calculator/ui/qml/windows/Help.qml new file mode 100644 index 0000000..2214fe6 --- /dev/null +++ b/src/calculator/ui/qml/windows/Help.qml @@ -0,0 +1,99 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.0 +import "../../assets/contents/help.js" as HelpContent + +/** + Component to display help content. + */ +ApplicationWindow { + id: window + + /// Color of content text + property alias textColor: text.color + /// Text color of title + property color titleColor + /// Text color of subtitle + property color subtitleColor + /// Used font + property font textFont + /// Color of scrollbar + property alias scrollBarColor: scrollBar.color + + title: qsTr("Help") + visible: false + + Flickable { + id: flick + + clip: true + boundsBehavior: Flickable.StopAtBounds + + contentWidth: width + contentHeight: text.height + + anchors.fill: parent + + Text { + id: text + + readonly property string style: '' + .arg(titleColor) + .arg(subtitleColor) + .arg(15) // body margin + .arg(10) // text margins + .arg(15) // text font size + .arg(20) // title font size + + width: parent.width - 7 + textFormat: Text.RichText + wrapMode: TextEdit.WordWrap + text: '' + font.family: window.font.family + + Component.onCompleted: { + var content = "" + + for(var key in HelpContent.content) { + var section = HelpContent.content[key] + + if(typeof section["title"] != "undefined") + content += '

%1

'.arg(section["title"]) + if(typeof section["subtitle"] != "undefined") + content += '

%1

'.arg(section["subtitle"]) + if(typeof section["content"] != "undefined") + content += "

%1

".arg(section["content"]) + } + text.text += "%1%2".arg(text.style).arg(content) + } + } + } + + Rectangle { + id: scrollBar + + opacity: (flick.visibleArea.heightRatio === 1) ?0 :1 + + y: flick.visibleArea.yPosition * flick.height + width: 3 + height: flick.visibleArea.heightRatio * flick.height + + anchors.right: parent.right + + Behavior on opacity { + NumberAnimation { duration: 400 } + } + } +} diff --git a/src/calculator/ui/translations/cs.qm b/src/calculator/ui/translations/cs.qm new file mode 100644 index 0000000..3f29a29 Binary files /dev/null and b/src/calculator/ui/translations/cs.qm differ diff --git a/src/calculator/ui/translations/cs.ts b/src/calculator/ui/translations/cs.ts new file mode 100644 index 0000000..2a78fdf --- /dev/null +++ b/src/calculator/ui/translations/cs.ts @@ -0,0 +1,228 @@ + + + + + About + + + Barbie calculator + Barbie calculator + + + + From revision + Z revize + + + + Created by: + Vytvořeno: + + + + All rights reserved. + Všechna práva vyhrazena. + + + + Adapter + + + Unsupported base. + Nepodporovaná číselná soustava. + + + Math error occured. + Matematická chyba. + + + + Math error occurred. + Matematická chyba. + + + + Error in defining variable. + Chyba při definování proměnné. + + + + Result is too big. + Výsledek je příliš velký. + + + + Given parameters does not match to function {}. + Předané parametry neodpovídají funkci {}. + + + + This construct is not supported. + Tato konstrukce není podporovaná. + + + + #TODO + + + + Parameters count does not match function. + Počet parametrů neodpovídá funkci. + + + + Function is not defined. + Funkce není definována. + + + + Expression contains syntax error. + Výraz obsahuje syntaktickou chybu. + + + + Error + + Error + Chyba + + + Ok + Ok + + + + ExpressionInput + + + Enter expression... + Zadejte výraz... + + + + Game + + + Easter egg + Easter egg + + + + You are looser. + Jste looser. + + + + AI is noob. + AI je noob. + + + + Help + + + Help + Nápověda + + + + PopUp + + + Ok + Ok + + + + help + + Barbie Calculator + Barbie Calculator + + + + Manual on how to use Barbie Calculator. + Manuál jak používat Barbie Calculator. + + + + Getting started + Začínáme + + + + Barbie calculator + Barbie calculator + + + + First we need to enter expression, which we want to calculate into text input. Then we press the button with equal sign or press Enter/Return key. Congratulations you have done your very first calculation using Barbie calculator. + Nejdříve musíme zadat výraz, který chceme vypočítat, do textového vstupu. Následně stiskneme tlačítko se symbolem rovnítka nebo zmáčkneme klávesu Enter. Gratulujeme provedli jste svůj první výpočet pomocí Barbie calculatoru. + + + + Advance + Pokročilé + + + + Using completer + Použití našeptávače + + + + To show completer press Ctrl+Space. Completer will be shown automatically every time after entering operator. To choose item you want to complete click on the item or use arrows to navigate to item and then pres key Enter/Return. + K zobrazení našeptávače stiskněte Ctrl+Mezerník. Našeptávač se zobrazí automaticky pokaždém zadání operátoru. Můžeme vybrat položku kliknutím nebo posunout se pomocí šipek a poté stisknout klávesu Enter. + + + + Variables intro + Začínáme s proměnnými + + + + Barbie calculator also provide support of custom variables. To create variable 'a' simply enter assignment of 'a' as 'a=' and then you add expression. If variable depends on another variable then it's value will be updated every time any of the dependent variables change. + Barbie calculator také podporuje tvorbu vlastních proměnných. Abychom vytvořili proměnnou 'a', jednoduše zadáme přiřazení 'a' tedy 'a=' a poté doplníme výraz. Pokud proměnná závisí na jiných proměnných, pak bude její hodnota přepočítá, ppokaždé když hodnota proměnných, na kterých je závislá, změní. + + + + Variables manipulation + Manipulace s proměnnými + + + + To enter into input expression from which variable was calculated click on the expression of variable in variable panel. If you click on variable in panel it will enter it's identifier and after right-click it's value. To delete or set value hover menu header(three dots) to show menu where you can choose desired action. + K vložení výrazu, ze kterého byla proměnná spočítána, musíme kliknout na výraz proměnné v panelu proměnných. Pokud klikneme na proměnnou v panelu, tak vložíme její identifikátor a pravým kliknutím její hodnotu. Abychom nastavili nebo vymazali proměnnou, najedeme na hlavičku menu(tři tečky), tím zobrazíme menu, kde vybereme danou akci. + + + + main + + Barbie Calculator + Barbie Calculator + + + + + + Help + Nápověda + + + + + About + O programu + + + + Error + Chyba + + + + Info + Info + + + diff --git a/src/calculator/ui/types/__init__.py b/src/calculator/ui/types/__init__.py new file mode 100644 index 0000000..9bad579 --- /dev/null +++ b/src/calculator/ui/types/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/src/calculator/ui/types/syntaxhighlight/__init__.py b/src/calculator/ui/types/expression/__init__.py similarity index 52% rename from src/calculator/ui/types/syntaxhighlight/__init__.py rename to src/calculator/ui/types/expression/__init__.py index 4194e72..fe927dc 100644 --- a/src/calculator/ui/types/syntaxhighlight/__init__.py +++ b/src/calculator/ui/types/expression/__init__.py @@ -1,5 +1,6 @@ # coding=utf-8 from .syntax_highlighter import SyntaxHighlighter, HighlightRule from .exp_syntax_highlighter import ExpSyntaxHighlighter +from .exp_analyzer import ExpAnalyzer -__all_ = ("SyntaxHighlighter", "HighlightRule", "ExpSyntaxHighlighter") \ No newline at end of file +__all__ = ("SyntaxHighlighter", "HighlightRule", "ExpSyntaxHighlighter", "ExpAnalyzer") \ No newline at end of file diff --git a/src/calculator/ui/types/expression/exp_analyzer.py b/src/calculator/ui/types/expression/exp_analyzer.py new file mode 100644 index 0000000..d0668db --- /dev/null +++ b/src/calculator/ui/types/expression/exp_analyzer.py @@ -0,0 +1,246 @@ +# coding=utf-8 +import re +import inspect + +from typing import Optional, Tuple, List, Dict + +from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot, QVariant +from PyQt5.QtQuick import QQuickItem + +from calculator.core.solver import Solver +from calculator.settings import EXPRESSION_SPLITTERS, EXPRESSION_EXPANSIONS, Expansion, BUILTIN_FUNCTIONS +from calculator.utils.formatter import Formatter + + +class ExpAnalyzer(QObject): + targetChanged = pyqtSignal(QQuickItem) + + def __init__(self, parent: Optional[QObject] = None) -> None: + super().__init__(parent) + + self._target = None + + def _get_content(self): + return self._target.property("text") + + def _get_cursor(self): + return self._target.property("cursorPosition") + + def _get_selection(self): + return (self._target.property("selectionStart"), + self._target.property("selectionEnd"), + self._target.property("selectedText")) + + @pyqtSlot(result=str) + def currentFunctionSignature(self) -> str: + """ + :return: Return signature of current function if cursor is not in body of any function then + it returns empty string + """ + func_identifier = self.currentFunction() + + if func_identifier in BUILTIN_FUNCTIONS: + func = Solver.builtin_functions[func_identifier] + + try: + return Formatter.format_function_args_spec(func_identifier, inspect.signature(func)) + except ValueError: + print("No signature") + return "" + + @pyqtSlot(result=str) + def currentFunction(self) -> str: + """ + :return: Identifier of function in which is cursor is located if cursor is not in body of + funnction then it return empty string + """ + + cursor = self._get_cursor() + content = self._get_content() + functions = [] + mutable_content = list(content) + + for func in self._functions_list(content): + start, end = self._function_body_borders(func, "".join(mutable_content)) + functions.append((func, start, end)) + + for i in range(start - len(func) -1, start): + mutable_content[i] = "_" + + current_functions = [] # functions can be nested, so then display the most nested function + + for func, start, end in functions: + if start <= cursor <= end + 1: + current_functions.append((func, end - start + 1)) + + if current_functions: + return min(current_functions, key=lambda t: t[1])[0] + return "" + + def _function_body_borders(self, func: str, expr: str) -> Tuple[int, int]: + """ + :param func: Function identifier + :param expr: Expression where it will be finding + :return: Start and end index of body of function + """ + brackets_count = {"(": 1, ")": 0} # 1 for function + match = re.search('{} *\('.format(re.escape(func)), expr) + + for i in range(match.end(), len(expr)): + current_char = expr[i] + + if current_char in "()": + brackets_count[current_char] += 1 + + if brackets_count["("] == brackets_count[")"]: + return match.end(), i - 1 + return match.end(), len(expr) + + def _functions_list(self, expr: str) -> List[str]: + """ + Return list of all functions identifiers including desirable duplication + :param expr: Input string expression + :return: List of functions identifiers + """ + result = [] + + for splitter in EXPRESSION_SPLITTERS - {"("}: + expr = expr.replace(splitter, ".") + splitted_expressions = [ s.strip() for s in expr.split(".")] + + for e in splitted_expressions: + result.extend([match.group() for match in re.finditer('(?!(\(|\)| |\.)).+?(?=\()', e)]) + + return result + + @pyqtSlot(str, result=int) + def expansionType(self, expansion: str) -> int: + """ + Return type of expansion. + :param expansion: String to expand + :return: Type of expansion + """ + + try: + return {func: expansion_type + for func, _, expansion_type in EXPRESSION_EXPANSIONS}[expansion] + except KeyError: + return Expansion.ExpansionType.Normal + + @pyqtSlot(str, result=str) + def getExpansion(self, expansion: str) -> str: + """ + Return Whole expansion if expansion is in settings else return inputted expansion. + :param expansion: Expansion key + :return: Whole expansion + """ + + for func, func_expansion, _ in EXPRESSION_EXPANSIONS: + if func == expansion: + return func_expansion + return expansion + + + @pyqtSlot(str, bool, result=str) + def expandExpression(self, expansion: str, expand_around_current_word: bool) -> str: + """ + :param expansion: String to expand + :param expand_around_current_word: If set to True, then exand around current word + :return: Expanded expansion + """ + + _, _, selected_text = self._get_selection() + if not selected_text and expand_around_current_word: + selected_text = self.currentWord().replace("\\", "") + + expansion_type = self.expansionType(expansion) + expansion = self.getExpansion(expansion) + + if expansion_type != Expansion.ExpansionType.BracketsPack: + return expansion + + borders = self._currentWordBorders() + content = self._get_content() + + if borders["end"] == -1 and not selected_text: + return "{}()".format(expansion) + + for i in range(borders["end"], len(content)): + current_char = content[i] + + if current_char == "(": + return expansion + if not current_char.isspace(): + break + + if selected_text: + return "{}({})".format(expansion, selected_text) + else: + return "{}()".format(expansion) + + @pyqtSlot(result=QVariant) + def currentWordBorders(self) -> QVariant: + return QVariant(self._currentWordBorders()) + + def _currentWordBorders(self) -> Dict[str, int]: + """ + :return: Current word start and end + """ + + content = self._get_content() + cursor = self._get_cursor() + word_start = 0 + word_end = len(content) + + if self._get_cursor() == 0: + return {"start": -1, "end": -1} + + splitter_positions = list() + for splitter in EXPRESSION_SPLITTERS: + splitter_positions.extend([i for i, letter in enumerate(content) if letter == splitter]) + + left_splitters_pos = list(filter(lambda x: x <= cursor, splitter_positions)) + right_splitters_pos = list(filter(lambda x: x > cursor, splitter_positions)) + + if left_splitters_pos: + word_start = min(left_splitters_pos, key=lambda x: abs(x - cursor)) + 1 + if right_splitters_pos: + word_end = min(right_splitters_pos, key=lambda x: abs(x - cursor)) + + word = content[word_start:word_end:].strip() + + if word: + if self._get_cursor() != word_end: + brackets_behind_cursor = re.search( + '\)*', + self._get_content()[self._get_cursor()::] + ).group() + + word_end -= len(brackets_behind_cursor) + + return {"start": word_start, "end": word_end} + + @pyqtSlot(result=str) + def currentWord(self) -> str: + """ + According to cursor in text it determines current word which is edited + :return: Current word + """ + content = self._get_content() + borders = self._currentWordBorders() + + if borders["start"] != -1 and borders["end"] != -1: + word = content[borders["start"]:borders["end"]:].strip() + + return re.escape(word) + return "" + + @pyqtProperty(QQuickItem) + def target(self) -> QQuickItem: + return self._target + + @target.setter + def target(self, v: QQuickItem) -> None: + if self._target != v: + self._target = v + self.targetChanged.emit(v) \ No newline at end of file diff --git a/src/calculator/ui/types/syntaxhighlight/exp_syntax_highlighter.py b/src/calculator/ui/types/expression/exp_syntax_highlighter.py similarity index 96% rename from src/calculator/ui/types/syntaxhighlight/exp_syntax_highlighter.py rename to src/calculator/ui/types/expression/exp_syntax_highlighter.py index b49e9ea..68ef37d 100644 --- a/src/calculator/ui/types/syntaxhighlight/exp_syntax_highlighter.py +++ b/src/calculator/ui/types/expression/exp_syntax_highlighter.py @@ -7,7 +7,7 @@ from PyQt5.QtQml import QJSValue from PyQt5.QtQuick import QQuickItem -from calculator.ui.types.syntaxhighlight import SyntaxHighlighter, HighlightRule +from calculator.ui.types.expression import SyntaxHighlighter, HighlightRule class ExpSyntaxHighlighter(QObject): diff --git a/src/calculator/ui/types/syntaxhighlight/syntax_highlighter.py b/src/calculator/ui/types/expression/syntax_highlighter.py similarity index 100% rename from src/calculator/ui/types/syntaxhighlight/syntax_highlighter.py rename to src/calculator/ui/types/expression/syntax_highlighter.py diff --git a/src/calculator/ui/types/window/__init__.py b/src/calculator/ui/types/window/__init__.py new file mode 100644 index 0000000..f77ed5e --- /dev/null +++ b/src/calculator/ui/types/window/__init__.py @@ -0,0 +1,4 @@ +# coding=utf-8 +from .app_window import AppWindow + +__all__= ("AppWindow") \ No newline at end of file diff --git a/src/calculator/ui/types/window/app_window.py b/src/calculator/ui/types/window/app_window.py new file mode 100644 index 0000000..478f584 --- /dev/null +++ b/src/calculator/ui/types/window/app_window.py @@ -0,0 +1,39 @@ +# coding=utf-8 +from PyQt5.QtCore import QSize, QCoreApplication +from PyQt5.QtCore import pyqtProperty +from PyQt5.QtCore import qDebug +from PyQt5.QtGui import QResizeEvent +from PyQt5.QtGui import QWindow +from PyQt5.QtQuick import QQuickWindow + + +class AppWindow(QQuickWindow): + def __init__(self, parent: QWindow = None) -> None: + super().__init__(parent) + + self.__ratio = 1 + + def resizeEvent(self, e: QResizeEvent) -> None: + new_width = e.size().width() + new_height = new_width * self.__ratio + + self.setMinimumHeight(new_height) + self.setMaximumHeight(new_height) + self.contentItem().setProperty("width", new_width) + self.contentItem().setProperty("height", new_height) + + @pyqtProperty(str) + def title(self) -> str: + return self.title() + + @title.setter + def title(self, v: str) -> None: + self.setTitle(v) + + @pyqtProperty(float) + def ratio(self) -> float: + return self.__ratio + + @ratio.setter + def ratio(self, v: float) -> None: + self.__ratio = v \ No newline at end of file diff --git a/src/calculator/utils/formatter.py b/src/calculator/utils/formatter.py new file mode 100644 index 0000000..ceb4136 --- /dev/null +++ b/src/calculator/utils/formatter.py @@ -0,0 +1,94 @@ +# coding=utf-8 +import re +from decimal import Decimal +from inspect import Signature, Parameter + +from calculator import NumericValue +from calculator.exceptions import UnsupportedBaseError +from calculator.settings import SUPPORTED_BASES + + +class Formatter(object): + EXP_FORMAT = '{value}×10{exp}' + BASE_CONVERTERS = { + 2: bin, + 8: oct, + 16: hex, + 10: str + } + CLASS_TO_TYPE_REGEX = re.compile(r"(?<=').+(?=')", re.VERBOSE) + + _EXP_DIVIDER = 'e' + + @classmethod + def format_number(cls, value: NumericValue, characters_limit: int) -> str: + stringed = str(value) + + if not (cls._EXP_DIVIDER in stringed or len(stringed) > characters_limit): + return stringed[:characters_limit] + + value, exp = ('{:.%de}' % (max((characters_limit - 4, 2)))).format( + Decimal.from_float(value), + ).split(cls._EXP_DIVIDER) # type: str, str + return cls.EXP_FORMAT.format( + value=value, + exp=exp.lstrip('+') + ) + + @classmethod + def format_number_in_base(cls, value: str, base: int) -> str: + """ + Formats the given number to string in given base. + :param value: value, to be formatted + :param base: base of formatted value + :return: formatted value in wanted base + """ + value = float(value) + + if abs(value - int(value)): + raise ValueError('Only integer values can be formatted into other bases.') + if base not in SUPPORTED_BASES: + raise UnsupportedBaseError("Base {} is not supported.".format(base)) + + return cls.BASE_CONVERTERS.get(base, str)(int(value)) + + @classmethod + def format_function_args_spec(cls, func_identifier: str, func_signature: Signature) -> str: + """ + From function name and signature formats informal string to represent parameter types of given function. + :param func_identifier: function name + :param func_signature: Signature object + :return: formatted string + """ + raw_args = [ + (arg_identifier, + func_signature.parameters[arg_identifier].default, + func_signature.parameters[arg_identifier].annotation, + ) + for arg_identifier in func_signature.parameters.keys() + ] + + formatted_args = list() + for identifier, default, annotation in raw_args: + annotation_repr = repr(annotation) + + formatted_args.append(( + identifier, + default, + cls.CLASS_TO_TYPE_REGEX.search(annotation_repr).group(0) + if " str: - stringed = str(value) - - if cls._EXP_DIVIDER not in stringed and not (len(stringed) > characters_limit): - return stringed[:characters_limit] - - value, exp = '{:.2e}'.format(Decimal.from_float(value)).split(cls._EXP_DIVIDER) - return cls.EXP_FORMAT.format( - value=value, - exp=exp.lstrip('+') - ) diff --git a/src/console.py b/src/console.py deleted file mode 100644 index dc1b271..0000000 --- a/src/console.py +++ /dev/null @@ -1,21 +0,0 @@ -# coding=utf-8 -from pprint import pformat - -from calculator.core.calculator import Calculator - -calculator = Calculator() - -try: - import readline -except ImportError: - pass - -while True: - user_input = input('>>> ').strip() - if not user_input: - continue - try: - result, variables = calculator.process(user_input) - print(" === {}\n === {}\n".format(result, pformat(dict(variables)))) - except Exception as e: - print(e, repr(e)) diff --git a/src/main.py b/src/main.py deleted file mode 100755 index 50fb298..0000000 --- a/src/main.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -# coding=utf-8 - -import sys -from fileinput import input - -from standard_deviation import main as sd_main - -sys._excepthook = sys.excepthook -def exception_hook(exctype, value, traceback): - sys._excepthook(exctype, value, traceback) -sys.excepthook = exception_hook - -if __name__ == "__main__": - if len(sys.argv) > 1 and (sys.argv[1] == '-sd' or sys.argv[1] == '--standard-deviation'): - exit(sd_main(input(files=sys.argv[2] if len(sys.argv) > 2 else '-'))) - - from calculator.ui.app import CalculatorApp - app = CalculatorApp(sys.argv) - - sys.exit(app.run()) diff --git a/src/tests/calculator/core/calculator/calculator.py b/src/tests/calculator/core/calculator/calculator.py index 380d3a8..4a41053 100644 --- a/src/tests/calculator/core/calculator/calculator.py +++ b/src/tests/calculator/core/calculator/calculator.py @@ -2,7 +2,7 @@ from unittest.case import TestCase from calculator.core.calculator import Calculator -from calculator.exceptions import VariableError, VariableRemoveRestrictError +from calculator.exceptions import VariableError, VariableRemoveRestrictError, VariableNameError class CalculatorTest(TestCase): @@ -207,3 +207,73 @@ def test_variable_process(self): variables, 'Variables after variable process.' ) + + def test_too_long_variable_name(self): + var_name = 'a' * (self.calculator.MAX_VARIABLE_NAME_LEN + 1) + msg = 'Error when creating variable with name longer than {}'.format(self.calculator.MAX_VARIABLE_NAME_LEN) + with self.assertRaises(VariableNameError, msg=msg): + self.calculator.process("{} = 42".format(var_name)) + + self.assertDictEqual( + self.calculator.variables, + { + Calculator.ANSWER_VARIABLE_NAME: self._default_variable_definition + }, + 'No new variables should be created.' + ) + + def test_too_long_variable_name_in_expr(self): + var_name = 'a' * (self.calculator.MAX_VARIABLE_NAME_LEN + 1) + msg = 'Error when creating variable with name longer ' \ + 'than {} in expression with default value.'.format(self.calculator.MAX_VARIABLE_NAME_LEN) + with self.assertRaises(VariableNameError, msg=msg): + self.calculator.process("5 + {}".format(var_name)) + + self.assertDictEqual( + self.calculator.variables, + { + Calculator.ANSWER_VARIABLE_NAME: self._default_variable_definition + }, + 'No new variables should be created.' + ) + + def test_assign_with_long_var_in_expr(self): + var_name = 'a' * (self.calculator.MAX_VARIABLE_NAME_LEN + 1) + msg = 'Error when creating variable with name longer ' \ + 'than {} in assign expression.'.format(self.calculator.MAX_VARIABLE_NAME_LEN) + with self.assertRaises(VariableNameError, msg=msg): + self.calculator.process("a = 5 + {}".format(var_name)) + + self.assertDictEqual( + self.calculator.variables, + { + Calculator.ANSWER_VARIABLE_NAME: self._default_variable_definition + }, + 'No new variables should be created.' + ) + + def test_variable_with_max_name_len(self): + var_name = 'a' * self.calculator.MAX_VARIABLE_NAME_LEN + result, variables = self.calculator.process("{} = 42".format(var_name)) + + self.assertIsNone(result, 'None is result of assign.') + self.assertDictEqual( + variables, + { + self.calculator.ANSWER_VARIABLE_NAME: self._default_variable_definition, + var_name: (42, "{} = 42".format(var_name), set()) + }, + 'New variable {} should be created.'.format(var_name) + ) + def test_variable_with_max_name_in_expr(self): + var_name = 'a' * self.calculator.MAX_VARIABLE_NAME_LEN + result, variables = self.calculator.process("5 + {}".format(var_name)) + + self.assertEqual(result, 5, 'Result should be calculated.') + self.assertDictEqual( + variables, + { + self.calculator.ANSWER_VARIABLE_NAME: (5, '5 + {}'.format(var_name), {var_name}), + var_name: (0, '0', set()) + } + ) diff --git a/src/tests/calculator/core/math/math.py b/src/tests/calculator/core/math/math.py index 9e85ec6..5a9b2a5 100644 --- a/src/tests/calculator/core/math/math.py +++ b/src/tests/calculator/core/math/math.py @@ -87,9 +87,20 @@ def test_fact(self): 6, 'Factorial of 3' ) + self.assertEqual( + self.math.fact(170), + 7.257415615307998e+306, + 'Maximum defined factorial' + ) with self.assertRaises(MathError, msg='Invalid factorial of -5.'): self.math.fact(-5) + with self.assertRaises(MathError, msg='Invalid factorial of 0.25.'): + self.math.fact(0.25) + + with self.assertRaises(MathError, msg='Invalid factorial of 5.25.'): + self.math.fact(5.25) + def test_root(self): self.assertEqual( self.math.root(16, 4), diff --git a/src/tests/calculator/core/parser/preprocessor/caret_power.py b/src/tests/calculator/core/parser/preprocessor/caret_power.py new file mode 100644 index 0000000..f603086 --- /dev/null +++ b/src/tests/calculator/core/parser/preprocessor/caret_power.py @@ -0,0 +1,27 @@ +# coding=utf-8 +from unittest import TestCase + +from calculator.core.parser.preprocessor.caret_power import CaretPowerPreprocessor + + +class CaretPowerPreprocessorTest(TestCase): + def setUp(self): + self.preprocessor = CaretPowerPreprocessor() + + def test_simple(self): + self.assertEqual( + self.preprocessor('2 ^ 4'), + '2 ** 4' + ) + + def test_advanced(self): + self.assertEqual( + self.preprocessor('(2 ^ 5) ^ 6'), + '(2 ** 5) ** 6' + ) + + def test_power_unchanged(self): + self.assertEqual( + self.preprocessor('2 ** 9'), + '2 ** 9' + ) diff --git a/src/tests/calculator/core/parser/preprocessor/factorial.py b/src/tests/calculator/core/parser/preprocessor/factorial.py index 04971e5..1e6636f 100644 --- a/src/tests/calculator/core/parser/preprocessor/factorial.py +++ b/src/tests/calculator/core/parser/preprocessor/factorial.py @@ -66,6 +66,20 @@ def test_factorized_function(self): 'Test for factorized of result from function call.' ) + def test_variable_factorial(self): + self.assertEqual( + self._format_factorial('foobar'), + self.preprocessor('foobar!'), + 'Test for factorized foo variable call.' + ) + + def test_advanced_variable_factorial(self): + self.assertEqual( + self._format_factorial('foobar56 + bar5'), + self.preprocessor('(foobar56 + bar5)!'), + 'Test for factorized foo variable call.' + ) + @staticmethod def _format_factorial(expr: str = '', pre: str = '', post: str = '') -> str: return '{}{}({}){}'.format( diff --git a/src/tests/calculator/core/parser/transform/aug_assign_restrict.py b/src/tests/calculator/core/parser/transform/aug_assign_restrict.py new file mode 100644 index 0000000..cd03ea1 --- /dev/null +++ b/src/tests/calculator/core/parser/transform/aug_assign_restrict.py @@ -0,0 +1,26 @@ +# coding=utf-8 + +from ast import Num, AugAssign, Name, Add, Assign +from unittest import TestCase + +from calculator.core.parser.transform.aug_assign_restrict import AugAssignRestrictTransform +from calculator.exceptions import SyntaxRestrictError + + +class AugAssignRestrictTransformTest(TestCase): + def setUp(self): + self.transform = AugAssignRestrictTransform() + + def test_restrict_aug_assign(self): + with self.assertRaises(SyntaxRestrictError): + self.transform.visit( + AugAssign(target=Name(), op=Add(), value=Num()) + ) + + def test_no_restrict_assign(self): + try: + self.transform.visit( + Assign(targets=(Name(),), value=Num()) + ) + except Exception as e: + self.fail(str(e)) diff --git a/src/tests/calculator/core/solver/solver_resolve.py b/src/tests/calculator/core/solver/solver_resolve.py index bacc0b9..afe9b46 100644 --- a/src/tests/calculator/core/solver/solver_resolve.py +++ b/src/tests/calculator/core/solver/solver_resolve.py @@ -1,10 +1,11 @@ # coding=utf-8 -from ast import Num, BinOp, AST, UnaryOp, USub, UAdd, Name, Call +from ast import Num, BinOp, AST, UnaryOp, USub, UAdd, Name, Call, keyword from operator import attrgetter from random import randint from unittest import TestCase from calculator.core.solver import Solver +from calculator.exceptions import InvalidFunctionCallError class SolverResolveTest(TestCase): @@ -93,7 +94,8 @@ def function(*args): func=Name( id=function.__name__, ), - args=args_to_call + args=args_to_call, + keywords=list() ) ), 'Resolve Call should return same value as mocked function.' @@ -105,6 +107,69 @@ def function(*args): ) self.assertTrue(called, 'Function should to be called.') + def test_invalid_call_node(self): + called = False + + def function(a, b=42): + nonlocal called + called = True + return a + + self.solver.builtin_functions = { + function.__name__: function + } + with self.assertRaises(InvalidFunctionCallError, msg='Call without required parameter.'): + self.solver._resolve( + Call( + func=Name( + id=function.__name__, + ), + args=tuple(), + keywords=list() + ) + ) + self.assertFalse(called, 'Function should not to be called after invalid try.') + + with self.assertRaises(InvalidFunctionCallError, msg='Call with too much params.'): + self.solver._resolve( + Call( + func=Name( + id=function.__name__, + ), + args=(Num(n=4), Num(n=9), Num(n=5)), + keywords=list() + ) + ) + self.assertFalse(called, 'Function should not to be called after invalid try.') + + with self.assertRaises(InvalidFunctionCallError, msg='Call with keyword param.'): + self.solver._resolve( + Call( + func=Name( + id=function.__name__, + ), + args=(Num(n=9),), + keywords=[keyword(arg='e', value=Num(n=9)), ] + ) + ) + self.assertFalse(called, 'Function should not to be called after invalid try.') + + args_to_call = Num(n=3), + self.assertEqual( + 3, + self.solver._resolve( + Call( + func=Name( + id=function.__name__, + ), + args=args_to_call, + keywords=[] + ) + ), + 'Resolve Call should return same value as mocked function.' + ) + self.assertTrue(called, 'Function should to be called after valid call try.') + def test_resolve_unknown_node(self): class Unknown(AST): pass diff --git a/src/tests/calculator/core/standard_deviation.py b/src/tests/calculator/core/standard_deviation.py index 325cab4..1092057 100644 --- a/src/tests/calculator/core/standard_deviation.py +++ b/src/tests/calculator/core/standard_deviation.py @@ -3,7 +3,7 @@ from statistics import mean as stats_mean, stdev from unittest.case import TestCase -from standard_deviation import mean as my_mean, standard_deviation +from calculator.standard_deviation import mean as my_mean, standard_deviation class StandardDeviationTest(TestCase): diff --git a/src/tests/calculator/utils/number_formatter.py b/src/tests/calculator/utils/number_formatter.py index 317a4a0..6d3e8c6 100644 --- a/src/tests/calculator/utils/number_formatter.py +++ b/src/tests/calculator/utils/number_formatter.py @@ -1,12 +1,12 @@ # coding=utf-8 from unittest.case import TestCase -from calculator.utils.number_formatter import NumberFormatter +from calculator.utils.formatter import Formatter class TestNumberFormatter(TestCase): def setUp(self): - self.formatter = NumberFormatter() + self.formatter = Formatter() def test_simple(self): self.skipTest('Formatted number in HTML cannot be actually tested.') @@ -20,7 +20,7 @@ def test_simple(self): ) for value, formatted in cases: self.assertEqual( - self.formatter.format(value=value), + self.formatter.format_number(value=value), formatted, 'Formatted value.' ) diff --git a/stdeb.cfg b/stdeb.cfg new file mode 100644 index 0000000..5c333a7 --- /dev/null +++ b/stdeb.cfg @@ -0,0 +1,3 @@ +[DEFAULT] +XS-Python-Version: 3.5 +# comment \ No newline at end of file