diff --git a/.flake8 b/.flake8 index f2b41384..fe0d7612 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,6 @@ +[flake8] max-line-length=100 -application_import_names=projectt +application_import_names=src ignore=P102,B311,W503,E226,S311,W504,F821 exclude=__pycache__, venv, .venv, tests import-order-style=pycharm diff --git a/.gitignore b/.gitignore index 894a44cc..fb02eae0 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ venv.bak/ # mypy .mypy_cache/ + +# Editor files +.vscode diff --git a/Pipfile b/Pipfile index 72b70b6f..84792c17 100644 --- a/Pipfile +++ b/Pipfile @@ -5,11 +5,20 @@ verify_ssl = true [dev-packages] flake8 = "*" +pytest = "*" [packages] +more-itertools = "*" +pillow = "*" +pygame = "*" +aiohttp = "*" +configparser = "*" +requests = "*" [requires] python_version = "3.7" [scripts] -lint = "python -m flake8" \ No newline at end of file +lint = "python -m flake8" +start = "python -m src" +test = "python -m pytest" diff --git a/Pipfile.lock b/Pipfile.lock index 79354a3c..28367b84 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a376db0bd471e38a7080cd854c46349b46922db98afeaf83d17b84923fbe9710" + "sha256": "b62e2f48230c6f1b3beaf0eb5b0295f59f4b12be44370924bc5732dbba2a25aa" }, "pipfile-spec": 6, "requires": { @@ -15,8 +15,233 @@ } ] }, - "default": {}, + "default": { + "aiohttp": { + "hashes": [ + "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55", + "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed", + "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10", + "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5", + "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1", + "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939", + "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390", + "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa", + "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc", + "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5", + "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d", + "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf", + "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6", + "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72", + "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12", + "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366", + "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4", + "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300", + "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d", + "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303", + "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6", + "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889" + ], + "index": "pypi", + "version": "==3.5.4" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "configparser": { + "hashes": [ + "sha256:27594cf4fc279f321974061ac69164aaebd2749af962ac8686b20503ac0bcf2d", + "sha256:9d51fe0a382f05b6b117c5e601fc219fede4a8c71703324af3f7d883aef476a3" + ], + "index": "pypi", + "version": "==3.7.3" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "more-itertools": { + "hashes": [ + "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", + "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" + ], + "index": "pypi", + "version": "==6.0.0" + }, + "multidict": { + "hashes": [ + "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f", + "sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3", + "sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef", + "sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b", + "sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73", + "sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc", + "sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3", + "sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd", + "sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351", + "sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941", + "sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d", + "sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1", + "sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b", + "sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a", + "sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3", + "sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7", + "sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0", + "sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0", + "sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014", + "sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5", + "sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036", + "sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d", + "sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a", + "sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce", + "sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1", + "sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a", + "sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9", + "sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7", + "sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b" + ], + "version": "==4.5.2" + }, + "pillow": { + "hashes": [ + "sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e", + "sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7", + "sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a", + "sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3", + "sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1", + "sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1", + "sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7", + "sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1", + "sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3", + "sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055", + "sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf", + "sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f", + "sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f", + "sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239", + "sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe", + "sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c", + "sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697", + "sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494", + "sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356", + "sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6", + "sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000", + "sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f", + "sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c", + "sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca", + "sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8", + "sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3", + "sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad", + "sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9", + "sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc", + "sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e" + ], + "index": "pypi", + "version": "==5.4.1" + }, + "pygame": { + "hashes": [ + "sha256:06dc92ccfea33b85f209db3d49f99a2a30c88fe9fb80fa2564cee443ece787b5", + "sha256:0919a2ec5fcb0d00518c2a5fa99858ccf22d7fbcc0e12818b317062d11386984", + "sha256:0a8c92e700e0042faefa998fa064616f330201890d6ea1c993eb3ff30ab53e99", + "sha256:220a1048ebb3d11a4d48cc4219ec8f65ca62fcafd255239478677625e8ead2e9", + "sha256:315861d2b8428f7b4d56d2c98d6c1acc18f08c77af4b129211bc036774f64be2", + "sha256:3469e87867832fe5226396626a8a6a9dac9b2e21a7819dd8cd96cf0e08bbcd41", + "sha256:54c19960180626165512d596235d75dc022d38844467cec769a8d8153fd66645", + "sha256:5ba598736ab9716f53dc943a659a9578f62acfe00c0c9c5490f3aca61d078f75", + "sha256:60ddc4f361babb30ff2d554132b1f3296490f3149d6c1c77682213563f59937a", + "sha256:6a49ab8616a9de534f1bf62c98beabf0e0bb0b6ff8917576bba22820bba3fdad", + "sha256:6d4966eeba652df2fd9a757b3fc5b29b578b47b58f991ad714471661ea2141cb", + "sha256:700d1781c999af25d11bfd1f3e158ebb660f72ebccb2040ecafe5069d0b2c0b6", + "sha256:73f4c28e894e76797b8ccaf6eb1205b433efdb803c70f489ebc3db6ac9c097e6", + "sha256:786eca2bea11abd924f3f67eb2483bcb22acff08f28dbdbf67130abe54b23797", + "sha256:7bcf586a1c51a735361ca03561979eea3180de45e6165bcdfa12878b752544af", + "sha256:82a1e93d82c1babceeb278c55012a9f5140e77665d372a6d97ec67786856d254", + "sha256:9e03589bc80a21ae951fca7659a767b7cac668289937e3756c0ab3d753cf6d24", + "sha256:aa8926a4e34fb0943abe1a8bb04a0ad82265341bf20064c0862db0a521100dfc", + "sha256:aa90689b889c417d2ac571ef2bbb5f7e735ae30c7553c60fae7508404f46c101", + "sha256:c9f8cdefee267a2e690bf17d61a8f5670b620f25a981f24781b034363a8eedc9", + "sha256:d9177afb2f46103bfc28a51fbc49ce18987a857e5c934db47b4a7030cb30fbd0", + "sha256:deb0551d4bbfb8131e2463a7fe1943bfcec5beb11acdf9c4bfa27fa5a9758d62", + "sha256:e7edfe57a5972aa9130ce9a186020a0f097e7a8e4c25e292109bdae1432b77f9", + "sha256:f0ad32efb9e26160645d62ba6cf3e5a5828dc4e82e8f41f9badfe7b685b07295" + ], + "index": "pypi", + "version": "==1.9.4" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + }, + "yarl": { + "hashes": [ + "sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9", + "sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f", + "sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb", + "sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320", + "sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842", + "sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0", + "sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829", + "sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310", + "sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4", + "sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8", + "sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1" + ], + "version": "==1.3.0" + } + }, "develop": { + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, "entrypoints": { "hashes": [ "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", @@ -26,11 +251,11 @@ }, "flake8": { "hashes": [ - "sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048", - "sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383" + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" ], "index": "pypi", - "version": "==3.7.6" + "version": "==3.7.7" }, "mccabe": { "hashes": [ @@ -39,6 +264,28 @@ ], "version": "==0.6.1" }, + "more-itertools": { + "hashes": [ + "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", + "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" + ], + "index": "pypi", + "version": "==6.0.0" + }, + "pluggy": { + "hashes": [ + "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", + "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + ], + "version": "==0.9.0" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, "pycodestyle": { "hashes": [ "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", @@ -48,10 +295,25 @@ }, "pyflakes": { "hashes": [ - "sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d", - "sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd" + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "pytest": { + "hashes": [ + "sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c", + "sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4" + ], + "index": "pypi", + "version": "==4.3.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" ], - "version": "==2.1.0" + "version": "==1.12.0" } } } diff --git a/README.md b/README.md index 697c2bf7..b510898e 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ The theme for this code jam will be **This app hates you!**. You will be creating an application using a GUI library of your choice in Python. The application must serve a real purpose, but must also fit the theme. -You can use any GUI library that you wish to use, but you have to make _a desktop app_. For example, you may use frameworks like PySide, PyQt, tkinter, or wxPython. You can even use stuff like Kivy or PyGame, although we do not recommend that you do. You may not, however, use webframeworks like Django or Flask, and you may not use anything that turns HTML and CSS into a desktop app that runs as a browser. +You can use any GUI library that you wish to use, but you have to make _a desktop app_. For example, you may use frameworks like PySide, PyQt, tkinter, or wxPython. You can even use stuff like Kivy or PyGame, although we do not recommend that you do. You may not, however, use webframeworks like Django or Flask, and you may not use anything that turns HTML and CSS into a desktop app that runs as a browser. Here are a couple of examples of what we mean by an application that "serves a real purpose but also fits the theme": * A calculator app that calculates the right answers, but represents the answer in a way that's completely impractical. * An image resizer where you have to specify which part of the image to resize, specify how much force to apply to the resize operation in newtons, and then manually resize the image by turning a crank. * An alarm clock app that plays a very loud sound effect every 5 minutes reminding you that your alarm will ring in 6 hours. The closer it gets to the 6 hour mark, the lower the volume of the sound effect. When the time is up, the sound effect is virtually inaudible. -Remember that teamwork is not optional for our code jams - You must find a way to work together. For this jam, we've assigned a leader for each team based on their responses to the application form. Remember to listen to your leader, and communicate with the rest of your team! +Remember that teamwork is not optional for our code jams - You must find a way to work together. For this jam, we've assigned a leader for each team based on their responses to the application form. Remember to listen to your leader, and communicate with the rest of your team! **Remember to provide instructions on how to set up and run your app at the bottom of this README**. @@ -22,7 +22,7 @@ Remember that teamwork is not optional for our code jams - You must find a way t # Setting Up -You should be using [Pipenv](https://pipenv.readthedocs.io/en/latest/). Take a look +You should be using [Pipenv](https://pipenv.readthedocs.io/en/latest/). Take a look [at the documentation](https://pipenv.readthedocs.io/en/latest/) if you've never used it before. In short: * Setting up for development: `pipenv install --dev` @@ -30,16 +30,21 @@ You should be using [Pipenv](https://pipenv.readthedocs.io/en/latest/). Take a l # Project Information -`# TODO` +We didn't get to finish, but the basic gist of it was a personality quiz that focused on widget animations to make it difficult to use. + +`master` should be the repo judged on, however we were able to get a page working (somewhat) after the deadline, in the `animations` branch. That would be the best example of our vision. ## Description -`# TODO` +We have some neat features: concurrent caching, a nice loading screen (that works 85% of the time), and a pretty thorough animation lib. ## Setup & Installation - -`# TODO` - +**Additional Dependencies**: tcl/tk (tkinter) +```sh +pipenv install +``` ## How do I use this thing? -`# TODO` +```sh +pipenv run start +``` diff --git a/res/api.json b/res/api.json new file mode 100644 index 00000000..e07c113a --- /dev/null +++ b/res/api.json @@ -0,0 +1,5 @@ +{ + "image": "https://api.thecatapi.com/v1/images/search", + "info": "https://www.pawclub.com.au/assets/js/namesTemp.json", + "hobbies": "https://gist.githubusercontent.com/mbejda/453fdb77ef8d4d3b3a67/raw/e8334f09109dc212892406e25fdee03efdc23f56/hobbies.txt" +} \ No newline at end of file diff --git a/res/docs/intro.txt b/res/docs/intro.txt new file mode 100644 index 00000000..bb26b18f --- /dev/null +++ b/res/docs/intro.txt @@ -0,0 +1,5 @@ +Note: + +Through this quiz you will achieve new levels of spiritual insight, of not only yourself, but the very fabric of reality. + +Please answer these very important questions with the utmost honesty, they are crucial to achieving true enlightenment. \ No newline at end of file diff --git a/res/docs/questions.json b/res/docs/questions.json new file mode 100644 index 00000000..84e3a386 --- /dev/null +++ b/res/docs/questions.json @@ -0,0 +1,3 @@ +{ + "What is your least favorite color?": ["#0801d5", "#ee018d", "#03a130", "#db0013", "#fd9501"] +} \ No newline at end of file diff --git a/res/images/1.jpg b/res/images/1.jpg new file mode 100644 index 00000000..7222b021 Binary files /dev/null and b/res/images/1.jpg differ diff --git a/res/images/10.jpg b/res/images/10.jpg new file mode 100644 index 00000000..ddc4c3d7 Binary files /dev/null and b/res/images/10.jpg differ diff --git a/res/images/2.jpg b/res/images/2.jpg new file mode 100644 index 00000000..7c900c50 Binary files /dev/null and b/res/images/2.jpg differ diff --git a/res/images/3.jpg b/res/images/3.jpg new file mode 100644 index 00000000..661f3bc9 Binary files /dev/null and b/res/images/3.jpg differ diff --git a/res/images/4.jpg b/res/images/4.jpg new file mode 100644 index 00000000..ec2430e0 Binary files /dev/null and b/res/images/4.jpg differ diff --git a/res/images/5.jpg b/res/images/5.jpg new file mode 100644 index 00000000..b5185126 Binary files /dev/null and b/res/images/5.jpg differ diff --git a/res/images/6.jpg b/res/images/6.jpg new file mode 100644 index 00000000..1bbe9ce0 Binary files /dev/null and b/res/images/6.jpg differ diff --git a/res/images/7.jpg b/res/images/7.jpg new file mode 100644 index 00000000..eb54d229 Binary files /dev/null and b/res/images/7.jpg differ diff --git a/res/images/8.jpg b/res/images/8.jpg new file mode 100644 index 00000000..870ae743 Binary files /dev/null and b/res/images/8.jpg differ diff --git a/res/images/9.jpg b/res/images/9.jpg new file mode 100644 index 00000000..6b14d723 Binary files /dev/null and b/res/images/9.jpg differ diff --git a/res/images/checkbox.png b/res/images/checkbox.png new file mode 100644 index 00000000..fc443295 Binary files /dev/null and b/res/images/checkbox.png differ diff --git a/res/images/loading.gif b/res/images/loading.gif new file mode 100644 index 00000000..412b59a5 Binary files /dev/null and b/res/images/loading.gif differ diff --git a/res/settings.ini b/res/settings.ini new file mode 100644 index 00000000..c91a0a52 --- /dev/null +++ b/res/settings.ini @@ -0,0 +1,3 @@ +[APP] +title = The Always (One-Hundred Percent of the Time) Correct and Never Wrong (In Any Way), Completely Accurate Personality Quiz Adventure +geometry = 400x500 diff --git a/res/sounds/jumpscare.mp3 b/res/sounds/jumpscare.mp3 new file mode 100644 index 00000000..141c7f3a Binary files /dev/null and b/res/sounds/jumpscare.mp3 differ diff --git a/res/widgets.ini b/res/widgets.ini new file mode 100644 index 00000000..3b3e89c1 --- /dev/null +++ b/res/widgets.ini @@ -0,0 +1,7 @@ +[base] + +[primary] + + +[secondary] + diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 00000000..48521bb2 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,12 @@ +from pathlib import Path + +SRC: Path = Path(__file__).parent +ROOT: Path = SRC.parent +RES: Path = ROOT / 'res' + +SETTINGS: Path = RES / 'settings.ini' +THEME: Path = RES / 'widgets.ini' +IMAGES: Path = RES / 'images' +SOUNDS: Path = RES / 'sounds' +API: Path = RES / 'api.json' +DOCS: Path = RES / 'docs' diff --git a/src/__main__.py b/src/__main__.py new file mode 100644 index 00000000..d07ed62b --- /dev/null +++ b/src/__main__.py @@ -0,0 +1,6 @@ +from .main import App + +if __name__ == "__main__": + root = App() + root.protocol('WM_DELETE_WINDOW', root.cleanup) + root.mainloop() diff --git a/src/animate.py b/src/animate.py new file mode 100644 index 00000000..bd7023da --- /dev/null +++ b/src/animate.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +import tkinter as tk +import operator +import time +import math +import random +from typing import NamedTuple, Callable, TypeVar, Generator +from enum import Enum +from functools import partialmethod + + +class Animater: + """ + Manager for executing animations. + + example:: + + ``` + motion = Motion(...) + window = Animater(...) + window.add_motion(motion) + ``` + """ + _motions = set() + + def __init__(self, canvas: tk.Canvas): + self.canvas = canvas + + def start(self): + while self._motions: + self.run() + + def run(self): + for motion in self._motions.copy(): + try: + move = next(motion) + move() + self.canvas.update() + except StopIteration: + self._motions.remove(motion) + self.canvas.update() + # self.canvas.after(10, self.run) + + def add(self, motion: Motion): + self._motions.add(motion.start()) + + def add_motion(self, id: int, end: Coord, **kwargs): + motion = Motion(self.canvas, id, end, **kwargs) + self.add(motion) + + def clear(self): + self._motions.clear() + + @property + def running(self): + return bool(self._motions) + + +class Motion: + def __init__(self, canvas: tk.Canvas, id: str, end: Coord, speed: float = 1): + self.canvas = canvas + self.id = id + self.end = end + self.speed = speed ** 3 + + def __iter__(self): + return self.start() + + def __key(self): + return self.id + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self): + return isinstance(self, type(other)) and self.__key() == other.__key() + + def start(self) -> Generator[Callable]: + """ + The entry point for generating move commands. + """ + self.reset() + while self.current != self.end: + print(self.current, self.end) + yield self.move + + def reset(self): + self.time = time.time() + self.beg = self.current + self.distance = self.beg.distance(self.end) + + def move(self): + self.canvas.move(self.id, *self.increment) + self.canvas.update_idletasks() + + @property + def time(self): + return time.time() - self._time + + @time.setter + def time(self, val): + self._time = val + + @property + def increment(self): + future = self.future + if future.distance(self.end) > self.journey: + return self.end - self.current + else: + return future - self.current + + @property + def future(self): + mult = (self.time * self.speed) / self.distance + return (self.end - self.beg) * mult + self.beg + + @property + def current(self): + return Coord(*self.canvas.coords(self.id)) + + @property + def journey(self): + return self.current.distance(self.end) + + +class BounceBall(Motion): + + chaos = 3 + + def kick(self, direction: Point): + self.canvas.update() + + c1, c2 = -self.chaos, self.chaos + chaoticx, chaoticy = random.randint(c1, c2), random.randint(c1, c2) + self.direction = direction + Coord(chaoticx, chaoticy) + self.end = self.direction * self.canvas.winfo_height() + self.reset() + + @property + def increment(self): + bounce = self.get_bounce() + if bounce != Coord(0, 0): + self.kick(bounce) + return self.future - self.current + + def get_bounce(self): + x1, y1, x2, y2 = self.canvas.bbox(self.id) + bounce = Coord(0, 0) + if x1 <= self.bound_x1: + bounce += Direction.RIGHT + if y1 <= self.bound_y1: + bounce += Direction.DOWN + if x2 >= self.bound_x2: + bounce += Direction.LEFT + if y2 >= self.bound_y2: + bounce += Direction.UP + return bounce + + @property + def bound_x1(self): + return self.canvas.winfo_x() + + @property + def bound_y1(self): + return self.canvas.winfo_y() + + @property + def bound_x2(self): + return self.bound_x1 + self.canvas.winfo_width() + + @property + def bound_y2(self): + return self.bound_y1 + self.canvas.winfo_height() + + +class Coord(NamedTuple): + """ + Helper class for managing coordinate values. + + Coord overloads many of the numeric operators by mapping + it to the x and y values individually. + + param: + x: float -- X position. + y: float -- Y position. + + example:: + + ``` + c1 = c2 = Coord(1, 1) + c1 + c2 + >>> Coord(2, 2) + # For convenience, numbers are accepted as well + c1 = Coord(1, 1) + c1 + 1 # 1 is cast to Coord(1, 1) + >>> Coord(2, 2) + ``` + """ + + x: float + y: float + + Operand = TypeVar('Operand', 'Coord', float) + + def __apply(self, op: Callable, other: Coord.Operand) -> Coord: + if isinstance(other, Direction): + other = other.value + elif not isinstance(other, self.__class__): + other = self.__class__(other, other) + + x = op(self.x, other.x) + y = op(self.y, other.y) + return self.__class__(x, y) + + def midpoint(self, other: Coord) -> Coord: + """ + The Coord that is equal distance from `self` and `other`. + + param: + other: Coord -- The point to consider. + + return: + Coord -- The resulting coordinate. + """ + return (self + other) / 2 + + def distance(self, other: Coord) -> float: + """ + The distance between `self` and `other`. + + param: + other: Coord -- THe point to consider. + + return: + int -- A numeric representation of the distance between two points. + """ + diff = other - self + return math.hypot(*diff) + + def flip(self): + return Coord(0, 0) - self + + __add__ = partialmethod(__apply, operator.add) + __sub__ = partialmethod(__apply, operator.sub) + __mul__ = partialmethod(__apply, operator.mul) + __mod__ = partialmethod(__apply, operator.mod) + __pow__ = partialmethod(__apply, operator.pow) + __floordiv__ = partialmethod(__apply, operator.floordiv) + __truediv__ = partialmethod(__apply, operator.truediv) + + +class Direction(Enum): + """ + Defines base directions. Can be used to create Coords relative + to a direction. + + example:: + + ``` + start = Coord(1, 1) + end = start + (Direction.LEFT * 20) + end + >>> Coord(x=-19, y=1) + """ + LEFT = Coord(-1, 0) + RIGHT = Coord(1, 0) + UP = Coord(0, -1) + DOWN = Coord(0, 1) + + def __mul__(self, other: int) -> Coord: + return self.value * other + + def __add__(self, other: Direction) -> Coord: + if isinstance(other, self.__class__): + return self.value + other.value + else: + return self.value + other + + def flip(self): + return Direction(Coord(0, 0) - self.value) diff --git a/src/cache.py b/src/cache.py new file mode 100644 index 00000000..9ae919fd --- /dev/null +++ b/src/cache.py @@ -0,0 +1,90 @@ +import json +import requests +import time +import multiprocessing as mp + +from typing import List, Text +from random import randint, choice, sample + +from . import API + + +class ImageCache: + """Class used for caching images""" + + ratelimit = 0.05 + with API.open() as fp: + api = json.load(fp) + + api_cache = { + 'info': None, + 'hobbies': None + } + + def __init__(self, size): + self.queue = mp.Queue(size) + self.worker = None + + def __del__(self): + self.stop() + + def __get(self, url, **kwargs): + try: + return requests.get(url, **kwargs) + except requests.ConnectionError as e: + return e + + def __parse_image(self, data: List[dict]) -> dict: + url = data[0]['url'] + response = self.__get(url) + return {'image': response.content} + + def __parse_hobbies(self, data: Text): + return {'hobbies': sample(data, 5)} + + def __parse_info(self, data: dict): + letter = choice('acdefghijklmnopqrstuvwxyz') + data = choice(data[letter]) + return { + 'name': data['name'], + 'info': { + 'gender': data['gender'], + 'age': randint(1, 42), + 'location': f'{randint(1, 99)} miles away' + } + } + + def get_profile(self): + response = {k: self.__get(v) for k, v in self.api.items() if self.api_cache.get(k) is None} + if requests.ConnectionError not in response: # TODO Record connection errors + if 'info' in response: + self.api_cache['info'] = response['info'].json() + if 'hobbies' in response: + self.api_cache['hobbies'] = response['hobbies'].text.split('\n') + return { + **self.__parse_info(self.api_cache['info']), + **self.__parse_hobbies(self.api_cache['hobbies']), + **self.__parse_image(response['image'].json()) + } + + def mainloop(self, queue): + while True: + profile = self.get_profile() + if profile is not None: + queue.put(profile) + time.sleep(self.ratelimit) + + def next(self): + return self.queue.get() + + def ready(self): + return not self.queue.empty() + + def start(self): + if self.worker is not None and self.worker.is_alive(): + self.stop() + self.worker = mp.Process(target=self.mainloop, args=(self.queue,)) + self.worker.start() + + def stop(self): + self.worker.terminate() diff --git a/src/front.py b/src/front.py new file mode 100644 index 00000000..ac29d6c1 --- /dev/null +++ b/src/front.py @@ -0,0 +1,142 @@ +import io +from PIL import Image, ImageTk + +from . import widget +from .animate import Direction +from .view import Window, View +from .cache import ImageCache +from .loading import Loading + + +def process_image(image: bytes, width: int, height: int): + im = Image.open(io.BytesIO(image)) + im = im.resize((width, height), Image.NEAREST) + return ImageTk.PhotoImage(im) + + +class Front(widget.PrimaryFrame): + + cachesize = 20 + + # Quick fix to keep a reference count on the + # last image, making sure the garbage collector + # doesn't delete it before the animation ends + _last = None + + def __next(self, direction: Direction = None): + if self.window.active: # Spam protection + return + if self.cache.ready(): + data: dict = self.cache.next() + image = process_image( + data.pop('image'), + self.window.winfo_width(), + self.window.winfo_height() + ) + name = data.pop('name') + self.__load(name, image, data) + self.window.change_view(self.image, direction) + + else: + self.loadscreen = Loading( + self.window, + width=self.window.winfo_width(), + height=self.window.winfo_height() + ) + self.window.change_view(View(self.window, window=self.loadscreen), direction) + self.loadscreen.waitfor(self.cache.ready, cmd=self.__next, args=('up',)) + + def __load(self, name, image, data): + self.title.config(text=name) + self.image = View(self.window, image=image) + self.bio = View(self.window, window=Bio(self.window)) + + self._last = self.image + self.bio.data.load(data) + self.update() + + def init(self): + self.title = widget.PrimaryLabel(self) + self.window = Window(self) + self.commandbar = widget.SecondaryFrame(self) + + self.bio = None + self.image = None + self.loading = None + + self.btn_dislike = widget.PrimaryButton( + self.commandbar, text='Nope', bg='red', command=self.cmd_dislike + ) + self.btn_bio = widget.SecondaryButton( + self.commandbar, text='Bio', command=self.cmd_bio + ) + self.btn_like = widget.PrimaryButton( + self.commandbar, text='Yep', bg='green', command=self.cmd_like + ) + self.title.pack(fill='x', expand=True) + self.window.pack(fill='both', expand=True) + self.commandbar.pack(side='bottom', fill='both', expand=True) + + self.btn_dislike.pack(side='left', padx=10) + self.btn_like.pack(side='right', padx=10) + self.btn_bio.pack(pady=10) + + self.cache = ImageCache(self.cachesize) + self.cache.start() + self.start() + + def start(self): + self.after(0, self.__next) + + def cmd_dislike(self): + self.__next('left') + + def cmd_like(self): + self.__next('right') + + def cmd_bio(self): + if self.window.active: + return + if self.window.current != self.bio: + self.window.change_view(self.bio, 'up') + else: + self.window.change_view(self.image, 'down') + + def cleanup(self): + self.cache.stop() + + +class Bio(widget.PrimaryFrame): + + def init(self): + width = self.master.winfo_width() + height = self.master.winfo_height() + self.config(height=height, width=width) + self.pack_propagate(0) + + def __build_info(self, info: dict): + item = widget.PrimaryFrame(self) + info = [ + info['gender'].capitalize(), + f"Age: {info['age']}", + f'{"She" if info["gender"].startswith("f") else "He"} is {info["location"]}.' + ] + for val in info: + name = widget.PrimaryLabel(item, text=val, font=('sys', 15), fg='gray') + name.pack(fill='both') + return item + + def __build_hobbies(self, hobbies): + frame = widget.PrimaryFrame(self) + title = widget.PrimaryLabel(frame, text='Hobbies', font=('sys', 15), justify='left') + title.pack(fill='both') + for hobby in hobbies: + val = widget.SecondaryLabel(frame, text=hobby) + val.pack(fill='both') + return frame + + def load(self, data: dict): + info = self.__build_info(data['info']) + hobbies = self.__build_hobbies(data['hobbies']) + info.pack(expand=True, fill='both') + hobbies.pack(expand=True, fill='both') diff --git a/src/loading.py b/src/loading.py new file mode 100644 index 00000000..d05400e4 --- /dev/null +++ b/src/loading.py @@ -0,0 +1,36 @@ +from time import sleep +from PIL import Image, ImageTk +from contextlib import suppress +from itertools import cycle +from typing import Callable + +from .widget import PrimaryCanvas +from . import IMAGES + + +def generate_frames(im: Image): + with suppress(EOFError): + while True: + im.seek(im.tell()+1) + yield ImageTk.PhotoImage(im) + + +class Loading(PrimaryCanvas): + image = IMAGES / "loading.gif" + limit = 1 / 20 + active = True + + def init(self): + self.frames = generate_frames(Image.open(self.image)) + + def waitfor(self, condition: Callable, cmd: Callable = None, args=()): + for im in cycle(self.frames): + if not self.active: + break + if condition(): + if cmd is not None: + cmd(*args) + break + self.last = self.create_image(-40.5, 0, image=im, anchor='nw') + self.update_idletasks() + sleep(self.limit) diff --git a/src/main.py b/src/main.py new file mode 100644 index 00000000..e5e8eaec --- /dev/null +++ b/src/main.py @@ -0,0 +1,39 @@ +import configparser +import tkinter as tk +from contextlib import suppress + +from .front import Front +# from .splash import Splash +from . import SETTINGS, widget + + +parser = configparser.ConfigParser() +parser.read(SETTINGS) + + +class App(tk.Tk): + appconfig = parser['APP'] + + def __init__(self, *args, **kwds): + super().__init__(*args, **kwds) + self.resizable(False, False) + + for name, val in parser['APP'].items(): + getattr(self, name)(val) + + self.frame = widget.PrimaryFrame(self) + self.frame.pack(expand=True, fill='both') + + # self.splash = Splash(self.frame) + # self.splash.pack(expand=True, fill='both') + self.switch() + + def switch(self): + # self.splash.pack_forget() + self.front = Front(self.frame) + self.front.pack(fill='both', expand=True) + + + def cleanup(self): + self.front.cleanup() + self.destroy() \ No newline at end of file diff --git a/src/splash.py b/src/splash.py new file mode 100644 index 00000000..71c45560 --- /dev/null +++ b/src/splash.py @@ -0,0 +1,97 @@ +import json + +from .view import Window, View +from .animate import Direction, BounceBall +from . import widget, DOCS + + +class Splash(widget.PrimaryFrame): + + with (DOCS / 'questions.json').open() as fp: + questions = json.load(fp) + + def init(self): + self.intro = Intro(self, bg='gray') + + self.btn_confirm = widget.PrimaryButton( + self.intro.window, command=self.switch, text='Okay' + ) + self.update() + + def build(self): + self.update() + self.intro.pack(fill='both', expand=True) + self.intro.build() + self.bounce( + View(self.intro.window, window=self.btn_confirm) + ) + + def bounce(self, view): + self.update() + start = view.master.center + (Direction.LEFT * 175) + (Direction.DOWN * 100) + wid = view.master.set_view(view, start) + motion = BounceBall(view.master, wid, view.master.origin, speed=6) + motion.kick(Direction.UP) + self.after(0, view.master.run, motion) + + def switch(self): + self.master.master.switch() + + def cleanup(self): + self.intro.cleanup() + + +class Intro(widget.PrimaryFrame): + intro = (DOCS / 'intro.txt').read_text() + + def init(self): + self.window = Window(self) + self.window.pack(expand=True, fill='both') + self.update() + + width = self.winfo_reqwidth() + self.title = View( + self.window, + text=self.master.master.title(), # yikes + font=('Courier', 17), + width=width, justify='center' + ) + self.intro = View( + self.window, + text=self.intro, + width=width, + font=('sys', 12), justify='center' + ) + + def build(self): + self.update() + adjust = (Direction.LEFT * 175) + (Direction.DOWN * 100) + + self.window.set_view(self.title) + self.window.set_view(self.intro, self.window.center + adjust) + self.update() + + def cleanup(self): + self.window.animater.clear() + + +class Question(widget.PrimaryFrame): + + def init(self): + self.title = widget.PrimaryLabel(self) + self.choices = widget.SecondaryFrame(self) + + self.options = [] + + def load(self, choices): + for question in questions: + frame = widget.SecondaryFrame(self.choices) + check = widget.PrimaryCheckbutton(frame) + val = widget.SecondaryLabel(frame, text=question) + + frame.pack() + check.pack(side='left') + val.pack(side='left') + + self.title.pack(fill='both', expand=True) + self.choices.pack(fill='both', expand=True) diff --git a/project/__main__.py b/src/test/__init__.py similarity index 100% rename from project/__main__.py rename to src/test/__init__.py diff --git a/src/test/test_animate.py b/src/test/test_animate.py new file mode 100644 index 00000000..407e5035 --- /dev/null +++ b/src/test/test_animate.py @@ -0,0 +1,54 @@ +from ..animate import Coord, vector, Direction + +coord1 = Coord(1, 1) +coord2 = Coord(1, 1) + + +def test_add(): + assert coord1 + coord2 == Coord(2, 2) + assert coord1 + 1 == Coord(2, 2) + + +def test_sub(): + assert coord1 - coord2 == Coord(0, 0) + assert coord1 - 1 == Coord(0, 0) + + +def test_mul(): + assert coord1 * coord2 == Coord(1, 1) + assert coord1 * 1 == Coord(1, 1) + + +def test_mod(): + assert coord1 % coord2 == Coord(0, 0) + assert coord1 % 1 == Coord(0, 0) + + +def test_pow(): + assert coord1 ** coord2 == Coord(1, 1) + assert coord1 ** 1 == Coord(1, 1) + + +def test_truediv(): + assert coord1 / Coord(2, 2) == Coord(0.5, 0.5) + assert coord1 / 2 == Coord(0.5, 0.5) + + +def test_floordiv(): + assert coord1 // coord2 == Coord(1, 1) + assert coord1 // 1 == Coord(1, 1) + + +def test_direction(): + assert Direction.UP.value == Direction.UP + Coord(0, 0) + assert Direction.LEFT.value == Direction.LEFT + Coord(0, 0) + assert Direction.RIGHT.value == Direction.RIGHT + Coord(0, 0) + assert Direction.DOWN.value == Direction.DOWN + Coord(0, 0) + + +def test_vector(): + start = Coord(0, 0) + end = start + 50 + vec = vector(start, end) + assert vec[0] == start + assert vec[-1] == end diff --git a/src/view.py b/src/view.py new file mode 100644 index 00000000..32aee951 --- /dev/null +++ b/src/view.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from enum import Enum + +from . import widget +from .animate import Coord, Animater, Direction + + +class Window(widget.PrimaryCanvas): + animation_speed = 10 + views = {} + current = None + + def init(self): + self.animater = Animater(self) + + def __coord(self, id): + return Coord(*self.coords(id)) + + def __set(self, view: View, coord: Coord): + wid = view.draw(coord, anchor='nw') + self.views[view] = wid + return wid + + def set_view(self, view: View): + self.current = view + self.__set(self.current, self.origin) + + def move_view(self, view: View, end: Coord): + wid = self.views.get(view) + if wid is not None: + self.animater.add_motion( + wid, end, speed=self.animation_speed + ) + + def move_in(self, view: View, direction: Direction): + distance = self.get_distance(direction) + start = self.origin + distance + wid = self.__set(view, start) + self.move_view(view, self.origin) + return wid + + def move_out(self, view: View, direction: Direction): + distance = self.get_distance(direction) + end = self.origin + distance + self.move_view(view, end) + del self.views[view] + + def change_view(self, view: View, direction: Direction = None): + if direction is None: + self.set_view(view) + return + if not isinstance(direction, Direction): + direction = Direction[direction.upper()] # Cast string for convenience + + self.animater.clear() + + last = self.current + self.current = view + self.move_in(self.current, direction.flip()) + self.move_out(last, direction) + + self.animater.start() + + def get_distance(self, direction: Direction): + if not isinstance(direction, Direction): + direction = Direction[direction.upper()] # Cast string for convenience + + if direction in (Direction.UP, Direction.DOWN): + return direction * Coord(0, self.winfo_height()) + elif direction in (Direction.LEFT, Direction.RIGHT): + return direction * Coord(self.winfo_width(), 0) + else: + raise NotImplementedError + + @property + def active(self): + return self.animater.running + + @property + def origin(self): + return Coord(self.canvasx(0), self.canvasy(0)) + + +class DrawType(Enum): + image = 'create_image' + window = 'create_window' + text = 'create_text' + + +class View: + + def __init__(self, master: Window, **kwds): + self.master = master + self.kwds = kwds + self.drawtype = self.data = None + for k, v in self.kwds.items(): + if hasattr(DrawType, k): + self.drawtype = DrawType[k] + self.data = v + if self.drawtype is None: + raise NotImplementedError + + def draw(self, *args, **kwds): + fn = getattr(self.master, self.drawtype.value) + return fn(*args, **{**self.kwds, **kwds}) diff --git a/src/widget.py b/src/widget.py new file mode 100644 index 00000000..91a76915 --- /dev/null +++ b/src/widget.py @@ -0,0 +1,111 @@ +import tkinter as tk +from configparser import ConfigParser +from . import THEME, IMAGES + +parser = ConfigParser() +parser.read(THEME) + + +class SecondaryFrame(tk.Frame): + DEFAULT = { + 'bg': 'gray' + } + + def __init__(self, *args, **kwds): + super().__init__(*args, **{**self.DEFAULT, **kwds}) + if hasattr(self, 'init'): + self.init() + + +class SecondaryButton(tk.Button): + DEFAULT = { + 'height': 1, + 'width': 10 + } + + def __init__(self, *args, **kwds): + super().__init__(*args, **{**self.DEFAULT, **kwds}) + if hasattr(self, 'init'): + self.init() + + +class SecondaryLabel(tk.Label): + DEFAULT = { + 'justify': 'left', + 'width': 10, + 'bg': 'gray' + } + + def __init__(self, *args, **kwds): + super().__init__(*args, **{**self.DEFAULT, **kwds}) + if hasattr(self, 'init'): + self.init() + + +class SecondaryCanvas(tk.Canvas): + DEFAULT = {} + + def __init__(self, *args, **kwds): + super().__init__(*args, **{**self.DEFAULT, **kwds}) + if hasattr(self, 'init'): + self.init() + + +class PrimaryFrame(tk.Frame): + DEFAULT = { + 'bg': 'black' + } + + def __init__(self, *args, **kwds): + super().__init__(*args, **{**self.DEFAULT, **kwds}) + if hasattr(self, 'init'): + self.init() + + +class PrimaryButton(tk.Button): + DEFAULT = { + 'height': 3, + 'width': 10 + } + + def __init__(self, *args, **kwds): + super().__init__(*args, **{**self.DEFAULT, **kwds}) + if hasattr(self, 'init'): + self.init() + + +class PrimaryLabel(tk.Label): + DEFAULT = { + 'font': ('Courier', 25), + 'bg': 'black', + 'fg': 'gray' + } + + def __init__(self, *args, **kwds): + super().__init__(*args, **{**self.DEFAULT, **kwds}) + if hasattr(self, 'init'): + self.init() + + +class PrimaryCanvas(tk.Canvas): + DEFAULT = { + 'bg': 'black' + } + + def __init__(self, *args, **kwds): + super().__init__(*args, **{**self.DEFAULT, **kwds}) + if hasattr(self, 'init'): + self.init() + + +class PrimaryCheckbutton(tk.Checkbutton): + DEFAULT = { + 'bg': 'black' + } + img = IMAGES / 'checkbox.png' + + def __init__(self, *args, **kwds): + img = tk.PhotoImage(file=self.img) + super().__init__(*args, image=img, **{**self.DEFAULT, **kwds}) + if hasattr(self, 'init'): + self.init()